From d227330a55b87b2479252e7cabf615d14c0f3b91 Mon Sep 17 00:00:00 2001 From: Chris Conway Date: Fri, 11 Apr 2014 15:45:56 -0700 Subject: [PATCH] Adds support for creating GCE persistent disks from snapshots --- library/cloud/gce_pd | 30 ++++++++-- test/integration/Makefile | 6 +- test/integration/cleanup_gce.py | 6 ++ test/integration/gce_credentials.py | 51 +++++++++++++++++ .../roles/test_gce_pd/tasks/main.yml | 57 +++++++++++++++++++ test/integration/setup_gce.py | 40 +++++++++++++ 6 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 test/integration/gce_credentials.py create mode 100644 test/integration/setup_gce.py diff --git a/library/cloud/gce_pd b/library/cloud/gce_pd index 98e8e8d15e8..0032acd57e4 100644 --- a/library/cloud/gce_pd +++ b/library/cloud/gce_pd @@ -24,9 +24,7 @@ short_description: utilize GCE persistent disk resources description: - This module can create and destroy unformatted GCE persistent disks U(https://developers.google.com/compute/docs/disks#persistentdisks). - It also supports attaching and detaching disks from running instances - but does not support creating boot disks from images or snapshots. The - 'gce' module supports creating instances with boot disks. + It also supports attaching and detaching disks from running instances. Full install/configuration instructions for the gce* modules can be found in the comments of ansible/test/gce_tests.py. options: @@ -68,6 +66,12 @@ options: required: false default: null aliases: [] + snapshot: + description: + - the source snapshot to use for the disk + required: false + default: null + aliases: [] state: description: - desired state of the persistent disk @@ -139,6 +143,7 @@ def main(): name = dict(required=True), size_gb = dict(default=10), image = dict(), + snapshot = dict(), state = dict(default='present'), zone = dict(default='us-central1-b'), service_account_email = dict(), @@ -155,6 +160,7 @@ def main(): name = module.params.get('name') size_gb = module.params.get('size_gb') image = module.params.get('image') + snapshot = module.params.get('snapshot') state = module.params.get('state') zone = module.params.get('zone') @@ -212,11 +218,20 @@ def main(): instance_name, zone), changed=False) if not disk: + if image is not None and snapshot is not None: + module.fail_json( + msg='Cannot give both image (%s) and snapshot (%s)' % ( + image, snapshot), changed=False) lc_image = None + lc_snapshot = None if image is not None: - lc_image = gce.ex_get_image(image) + lc_image = gce.ex_get_image(image) + elif snapshot is not None: + lc_snapshot = gce.ex_get_snapshot(snapshot) try: - disk = gce.create_volume(size_gb, name, location=zone, image=lc_image) + disk = gce.create_volume( + size_gb, name, location=zone, image=lc_image, + snapshot=lc_snapshot) except ResourceExistsError: pass except QuotaExceededError: @@ -225,7 +240,10 @@ def main(): except Exception, e: module.fail_json(msg=unexpected_error_msg(e), changed=False) json_output['size_gb'] = size_gb - json_output['image'] = image + if image is not None: + json_output['image'] = image + if snapshot is not None: + json_output['snapshot'] = snapshot changed = True if inst and not is_attached: try: diff --git a/test/integration/Makefile b/test/integration/Makefile index d8ca0a10c24..0b0a02003ab 100644 --- a/test/integration/Makefile +++ b/test/integration/Makefile @@ -39,6 +39,9 @@ cloud_cleanup: amazon_cleanup rackspace_cleanup amazon_cleanup: python cleanup_ec2.py -y --match="^$(CLOUD_RESOURCE_PREFIX)" +gce_setup: + python setup_gce.py "$(CLOUD_RESOURCE_PREFIX)" + gce_cleanup: python cleanup_gce.py -y --match="^$(CLOUD_RESOURCE_PREFIX)" @@ -57,7 +60,8 @@ amazon: $(CREDENTIALS_FILE) exit $$RC; gce: $(CREDENTIALS_FILE) - ansible-playbook gce.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -e "resource_prefix=$(CLOUD_RESOURCE_PREFIX)" -v $(TEST_FLAGS) ; \ + CLOUD_RESOURCE_PREFIX="$(CLOUD_RESOURCE_PREFIX)" make gce_setup ; \ + ansible-playbook gce.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -e "resource_prefix=$(CLOUD_RESOURCE_PREFIX)" -v $(TEST_FLAGS) ; \ RC=$$? ; \ CLOUD_RESOURCE_PREFIX="$(CLOUD_RESOURCE_PREFIX)" make gce_cleanup ; \ exit $$RC; diff --git a/test/integration/cleanup_gce.py b/test/integration/cleanup_gce.py index 58b74ca324c..70fc65b65da 100644 --- a/test/integration/cleanup_gce.py +++ b/test/integration/cleanup_gce.py @@ -98,6 +98,12 @@ if __name__ == '__main__': try: # Delete matching instances delete_gce_resources(gce.list_nodes, 'name', opts) + # Delete matching snapshots + def get_snapshots(): + for volume in gce.list_volumes(): + for snapshot in gce.list_volume_snapshots(volume): + yield snapshot + delete_gce_resources(get_snapshots, 'name', opts) # Delete matching disks delete_gce_resources(gce.list_volumes, 'name', opts) except KeyboardInterrupt, e: diff --git a/test/integration/gce_credentials.py b/test/integration/gce_credentials.py new file mode 100644 index 00000000000..0d7ae81cae4 --- /dev/null +++ b/test/integration/gce_credentials.py @@ -0,0 +1,51 @@ +import collections +import os +import yaml + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + _ = Provider.GCE +except ImportError: + print("failed=True " + \ + "msg='libcloud with GCE support (0.13.3+) required for this module'") + sys.exit(1) + + +def add_credentials_options(parser): + default_service_account_email=None + default_pem_file=None + default_project_id=None + + # Load details from credentials.yml + if os.path.isfile('credentials.yml'): + credentials = yaml.load(open('credentials.yml', 'r')) + default_service_account_email = credentials['gce_service_account_email'] + default_pem_file = credentials['gce_pem_file'] + default_project_id = credentials['gce_project_id'] + + parser.add_option("--service_account_email", + action="store", dest="service_account_email", + default=default_service_account_email, + help="GCE service account email. Default is loaded from credentials.yml.") + parser.add_option("--pem_file", + action="store", dest="pem_file", + default=default_pem_file, + help="GCE client key. Default is loaded from credentials.yml.") + parser.add_option("--project_id", + action="store", dest="project_id", + default=default_project_id, + help="Google Cloud project ID. Default is loaded from credentials.yml.") + + +def check_required(opts, parser): + for required in ['service_account_email', 'pem_file', 'project_id']: + if getattr(opts, required) is None: + parser.error("Missing required parameter: --%s" % required) + + +def get_gce_driver(opts): + # Connect to GCE + gce_cls = get_driver(Provider.GCE) + return gce_cls( + opts.service_account_email, opts.pem_file, project=opts.project_id) diff --git a/test/integration/roles/test_gce_pd/tasks/main.yml b/test/integration/roles/test_gce_pd/tasks/main.yml index 1a756e1c48a..70ee1ec10bd 100644 --- a/test/integration/roles/test_gce_pd/tasks/main.yml +++ b/test/integration/roles/test_gce_pd/tasks/main.yml @@ -161,3 +161,60 @@ - 'result.changed' - 'result.name == "{{ instance_name }}"' - 'result.state == "absent"' + +# ============================================================ +- name: test snapshot given (state=present) + gce_pd: + name: "{{ instance_name }}" + snapshot: "{{ instance_name }}-snapshot" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert image given (state=present) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.snapshot == "{{ instance_name }}-snapshot"' + - 'result.state == "present"' + +# ============================================================ +- name: test snapshot given (state=absent) + gce_pd: + name: "{{ instance_name }}" + snapshot: "{{ instance_name }}-snapshot" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert image given (state=absent) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test both image and snapshot given + gce_pd: + name: "{{ instance_name }}" + image: "debian-7" + snapshot: "{{ instance_name }}-snapshot" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + ignore_errors: true + +- name: assert image given (state=present) + assert: + that: + - 'result.failed' + - 'result.msg == "Cannot give both image (debian-7) and snapshot ({{ instance_name }}-snapshot)"' + diff --git a/test/integration/setup_gce.py b/test/integration/setup_gce.py new file mode 100644 index 00000000000..4020b59979a --- /dev/null +++ b/test/integration/setup_gce.py @@ -0,0 +1,40 @@ +''' +Create GCE resources for use in integration tests. + +Takes a prefix as a command-line argument and creates a persistent disk +named ${prefix}-base and a snapshot of it named ${prefix}-snapshot. +prefix will be forced to lowercase, to ensure the names are legal GCE +resource names. +''' + +import sys +import optparse + +import gce_credentials + + +def parse_args(): + parser = optparse.OptionParser( + usage="%s [options] " % (sys.argv[0],), description=__doc__) + gce_credentials.add_credentials_options(parser) + parser.add_option("--prefix", + action="store", dest="prefix", + help="String used to prefix GCE resource names (default: %default)") + + (opts, args) = parser.parse_args() + gce_credentials.check_required(opts, parser) + if not args: + parser.error("Missing required argument: name prefix") + return (opts, args) + +if __name__ == '__main__': + + (opts, args) = parse_args() + gce = gce_credentials.get_gce_driver(opts) + prefix = args[0].lower() + try: + base_volume = gce.create_volume( + size=10, name=prefix+'-base', location='us-central1-a') + gce.create_volume_snapshot(base_volume, name=prefix+'-snapshot') + except KeyboardInterrupt, e: + print "\nExiting on user command."