diff --git a/library/cloud/gce b/library/cloud/gce index 29d62d349b0..88460e8ede5 100755 --- a/library/cloud/gce +++ b/library/cloud/gce @@ -89,6 +89,13 @@ options: required: false default: "false" aliases: [] + disks: + description: + - a list of persistent disks to attach to the instance; a string value gives the name of the disk; alternatively, a dictionary value can define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry will be the boot disk (which must be READ_WRITE). + required: false + default: null + aliases: [] + version_added: "1.6" state: description: - desired state of the resource @@ -209,8 +216,16 @@ def get_instance_info(inst): netname = inst.extra['networkInterfaces'][0]['network'].split('/')[-1] except: netname = None + if 'disks' in inst.extra: + disk_names = [disk_info['source'].split('/')[-1] + for disk_info + in sorted(inst.extra['disks'], + key=lambda disk_info: disk_info['index'])] + else: + disk_names = [] return({ 'image': not inst.image is None and inst.image.split('/')[-1] or None, + 'disks': disk_names, 'machine_type': inst.size, 'metadata': metadata, 'name': inst.name, @@ -240,6 +255,7 @@ def create_instances(module, gce, instance_names): metadata = module.params.get('metadata') network = module.params.get('network') persistent_boot_disk = module.params.get('persistent_boot_disk') + disks = module.params.get('disks') state = module.params.get('state') tags = module.params.get('tags') zone = module.params.get('zone') @@ -248,6 +264,16 @@ def create_instances(module, gce, instance_names): changed = False lc_image = gce.ex_get_image(image) + lc_disks = [] + disk_modes = [] + for i, disk in enumerate(disks or []): + if isinstance(disk, dict): + lc_disks.append(gce.ex_get_volume(disk['name'])) + disk_modes.append(disk['mode']) + else: + lc_disks.append(gce.ex_get_volume(disk)) + # boot disk is implicitly READ_WRITE + disk_modes.append('READ_ONLY' if i > 0 else 'READ_WRITE') lc_network = gce.ex_get_network(network) lc_machine_type = gce.ex_get_size(machine_type) lc_zone = gce.ex_get_zone(zone) @@ -282,7 +308,9 @@ def create_instances(module, gce, instance_names): for name in instance_names: pd = None - if persistent_boot_disk: + if lc_disks: + pd = lc_disks[0] + elif persistent_boot_disk: try: pd = gce.create_volume(None, "%s" % name, image=lc_image) except ResourceExistsError: @@ -299,6 +327,28 @@ def create_instances(module, gce, instance_names): module.fail_json(msg='Unexpected error attempting to create ' + \ 'instance %s, error: %s' % (name, e.value)) + for i, lc_disk in enumerate(lc_disks): + # Check whether the disk is already attached + if (len(inst.extra['disks']) > i): + attached_disk = inst.extra['disks'][i] + if attached_disk['source'] != lc_disk.extra['selfLink']: + module.fail_json( + msg=("Disk at index %d does not match: requested=%s found=%s" % ( + i, lc_disk.extra['selfLink'], attached_disk['source']))) + elif attached_disk['mode'] != disk_modes[i]: + module.fail_json( + msg=("Disk at index %d is in the wrong mode: requested=%s found=%s" % ( + i, disk_modes[i], attached_disk['mode']))) + else: + continue + gce.attach_volume(inst, lc_disk, ex_mode=disk_modes[i]) + # Work around libcloud bug: attached volumes don't get added + # to the instance metadata. get_instance_info() only cares about + # source and index. + if len(inst.extra['disks']) != i+1: + inst.extra['disks'].append( + {'source': lc_disk.extra['selfLink'], 'index': i}) + if inst: new_instances.append(inst) @@ -351,6 +401,7 @@ def main(): name = dict(), network = dict(default='default'), persistent_boot_disk = dict(type='bool', default=False), + disks = dict(type='list'), state = dict(choices=['active', 'present', 'absent', 'deleted'], default='present'), tags = dict(type='list'), diff --git a/library/cloud/gce_pd b/library/cloud/gce_pd index e5ea6cc4ad8..8dd42d04fa4 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: @@ -62,6 +60,20 @@ options: required: false default: 10 aliases: [] + image: + description: + - the source image to use for the disk + required: false + default: null + aliases: [] + version_added: "1.6" + snapshot: + description: + - the source snapshot to use for the disk + required: false + default: null + aliases: [] + version_added: "1.6" state: description: - desired state of the persistent disk @@ -132,6 +144,8 @@ def main(): mode = dict(default='READ_ONLY', choices=['READ_WRITE', 'READ_ONLY']), 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(), @@ -147,6 +161,8 @@ def main(): mode = module.params.get('mode') 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') @@ -204,8 +220,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) + elif snapshot is not None: + lc_snapshot = gce.ex_get_snapshot(snapshot) try: - disk = gce.create_volume(size_gb, name, location=zone) + disk = gce.create_volume( + size_gb, name, location=zone, image=lc_image, + snapshot=lc_snapshot) except ResourceExistsError: pass except QuotaExceededError: @@ -214,6 +242,10 @@ def main(): except Exception, e: module.fail_json(msg=unexpected_error_msg(e), changed=False) json_output['size_gb'] = size_gb + 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 aefb27dbb83..7fdbf6ffd1d 100644 --- a/test/integration/Makefile +++ b/test/integration/Makefile @@ -56,6 +56,12 @@ 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)" + rackspace_cleanup: @echo "FIXME - cleanup_rax.py not yet implemented" @# python cleanup_rax.py -y --match="^$(CLOUD_RESOURCE_PREFIX)" @@ -70,6 +76,13 @@ amazon: $(CREDENTIALS_FILE) CLOUD_RESOURCE_PREFIX="$(CLOUD_RESOURCE_PREFIX)" make amazon_cleanup ; \ exit $$RC; +gce: $(CREDENTIALS_FILE) + 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; + rackspace: $(CREDENTIALS_FILE) ansible-playbook rackspace.yml -i $(INVENTORY) -e @$(VARS_FILE) $(CREDENTIALS_ARG) -e "resource_prefix=$(CLOUD_RESOURCE_PREFIX)" -v $(TEST_FLAGS) ; \ RC=$$? ; \ diff --git a/test/integration/cleanup_gce.py b/test/integration/cleanup_gce.py new file mode 100644 index 00000000000..e0cf0bc0431 --- /dev/null +++ b/test/integration/cleanup_gce.py @@ -0,0 +1,77 @@ +''' +Find and delete GCE resources matching the provided --match string. Unless +--yes|-y is provided, the prompt for confirmation prior to deleting resources. +Please use caution, you can easily delete your *ENTIRE* GCE infrastructure. +''' + +import os +import re +import sys +import optparse +import yaml + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceInUseError, ResourceNotFoundError + _ = Provider.GCE +except ImportError: + print("failed=True " + \ + "msg='libcloud with GCE support (0.13.3+) required for this module'") + sys.exit(1) + +import gce_credentials + + +def delete_gce_resources(get_func, attr, opts): + for item in get_func(): + val = getattr(item, attr) + if re.search(opts.match_re, val, re.IGNORECASE): + prompt_and_delete(item, "Delete matching %s? [y/n]: " % (item,), opts.assumeyes) + +def prompt_and_delete(item, prompt, assumeyes): + if not assumeyes: + assumeyes = raw_input(prompt).lower() == 'y' + assert hasattr(item, 'destroy'), "Class <%s> has no delete attribute" % item.__class__ + if assumeyes: + item.destroy() + print ("Deleted %s" % item) + +def parse_args(): + parser = optparse.OptionParser(usage="%s [options]" % (sys.argv[0],), + description=__doc__) + gce_credentials.add_credentials_options(parser) + parser.add_option("--yes", "-y", + action="store_true", dest="assumeyes", + default=False, + help="Don't prompt for confirmation") + parser.add_option("--match", + action="store", dest="match_re", + default="^ansible-testing-", + help="Regular expression used to find GCE resources (default: %default)") + + (opts, args) = parser.parse_args() + gce_credentials.check_required(opts, parser) + return (opts, args) + +if __name__ == '__main__': + + (opts, args) = parse_args() + + # Connect to GCE + gce = gce_credentials.get_gce_driver(opts) + + 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: + print "\nExiting on user command." diff --git a/test/integration/credentials.template b/test/integration/credentials.template index f21100405fc..12316254bbd 100644 --- a/test/integration/credentials.template +++ b/test/integration/credentials.template @@ -3,5 +3,10 @@ ec2_access_key: ec2_secret_key: +# GCE Credentials +service_account_email: +pem_file: +project_id: + # GITHUB SSH private key - a path to a SSH private key for use with github.com github_ssh_private_key: "{{ lookup('env','HOME') }}/.ssh/id_rsa" diff --git a/test/integration/gce.yml b/test/integration/gce.yml new file mode 100644 index 00000000000..6e314315c52 --- /dev/null +++ b/test/integration/gce.yml @@ -0,0 +1,6 @@ +- hosts: testhost + gather_facts: true + roles: + - { role: test_gce, tags: test_gce } + - { role: test_gce_pd, tags: test_gce_pd } + # TODO: tests for gce_lb, gce_net, gc_storage 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/defaults/main.yml b/test/integration/roles/test_gce/defaults/main.yml new file mode 100644 index 00000000000..1564808d792 --- /dev/null +++ b/test/integration/roles/test_gce/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# defaults file for test_gce +instance_name: "{{ resource_prefix|lower }}" +service_account_email: "{{ gce_service_account_email }}" +pem_file: "{{ gce_pem_file }}" +project_id: "{{ gce_project_id }}" diff --git a/test/integration/roles/test_gce/tasks/main.yml b/test/integration/roles/test_gce/tasks/main.yml new file mode 100644 index 00000000000..3308dede8bb --- /dev/null +++ b/test/integration/roles/test_gce/tasks/main.yml @@ -0,0 +1,211 @@ +# TODO: lots of attributes not covered: machine_type, zone, metadata, tags, etc. +# +# ============================================================ +- name: test with no parameters + gce: + register: result + ignore_errors: true + +- name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + - 'result.msg == "Missing GCE connection parameters in libcloud secrets file."' + +# ============================================================ +- name: test missing name + gce: + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + register: result + ignore_errors: true + +- name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + - 'result.msg == "Must specify a \"name\" or \"instance_names\""' + +# ============================================================ +- name: test state=present (expected changed=true) + gce: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert state=present (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "present"' + +# ============================================================ +- name: test state=present (expected changed=false) + gce: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert state=present (expected changed=false) + assert: + that: + - 'not result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "present"' + +# ============================================================ +- name: test state=absent (expected changed=true) + gce: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert state=absent (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test state=absent (expected changed=false) + gce: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert state=absent (expected changed=false) + assert: + that: + - 'not result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test disks given (expected changed=true) + gce: + name: "{{ instance_name }}" + disks: + - "{{ instance_name }}-base" + - "{{ instance_name }}-extra" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert disks given + assert: + that: + - 'result.changed' + - 'result.instance_data[0].disks == ["{{ instance_name }}-base", "{{ instance_name }}-extra"]' + - 'result.state == "present"' + +# ============================================================ +- name: test disks given (expected changed=false) + gce: + name: "{{ instance_name }}" + disks: + - "{{ instance_name }}-base" + - "{{ instance_name }}-extra" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert disks given + assert: + that: + - 'not result.changed' + - 'result.instance_data[0].disks == ["{{ instance_name }}-base", "{{ instance_name }}-extra"]' + - 'result.state == "present"' + +# ============================================================ +- name: test disks in the wrong order + gce: + name: "{{ instance_name }}" + disks: + - "{{ instance_name }}-extra" + - "{{ instance_name }}-base" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + register: result + ignore_errors: true + +- name: assert disks in the wrong order + assert: + that: + - 'result.failed' + - '{{ result.msg | match("Disk at index 0 does not match:.*") }}' + +# ============================================================ +- name: test disks given with name and mode + gce: + name: "{{ instance_name }}" + disks: + - { name: "{{ instance_name }}-base", mode: "READ_WRITE" } + - { name: "{{ instance_name }}-extra", mode: "READ_ONLY" } + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + register: result + +- name: assert disks given + assert: + that: + - 'not result.changed' + - 'result.state == "present"' + +# ============================================================ +- name: test disks given with name and wrong mode + gce: + name: "{{ instance_name }}" + disks: + - { name: "{{ instance_name }}-base", mode: "READ_ONLY" } + - "{{ instance_name }}-extra" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + register: result + ignore_errors: true + +- name: assert disks given + assert: + that: + - 'result.failed' + - '{{ result.msg | match("Disk at index 0 is in the wrong mode:.*") }}' + +# ============================================================ +- name: test disks given, state absent (expected changed=true) + gce: + name: "{{ instance_name }}" + disks: + - "{{ instance_name }}-base" + - "{{ instance_name }}-extra" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert disks given, state absent (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.state == "absent"' diff --git a/test/integration/roles/test_gce_pd/defaults/main.yml b/test/integration/roles/test_gce_pd/defaults/main.yml new file mode 100644 index 00000000000..1564808d792 --- /dev/null +++ b/test/integration/roles/test_gce_pd/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# defaults file for test_gce +instance_name: "{{ resource_prefix|lower }}" +service_account_email: "{{ gce_service_account_email }}" +pem_file: "{{ gce_pem_file }}" +project_id: "{{ gce_project_id }}" diff --git a/test/integration/roles/test_gce_pd/tasks/main.yml b/test/integration/roles/test_gce_pd/tasks/main.yml new file mode 100644 index 00000000000..70ee1ec10bd --- /dev/null +++ b/test/integration/roles/test_gce_pd/tasks/main.yml @@ -0,0 +1,220 @@ +# TODO: need tests for read/write mode. + +# ============================================================ +- name: test missing name + gce_pd: + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + register: result + ignore_errors: true + +- name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + - 'result.msg == "missing required arguments: name"' + +# ============================================================ +- name: test state=present (expected changed=true) + gce_pd: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert state=present (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.size_gb == 10' # default size + - 'result.zone == "us-central1-b"' # default zone + - 'result.state == "present"' + +# ============================================================ +- name: test state=present (expected changed=false) + gce_pd: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert state=present (expected changed=false) + assert: + that: + - 'not result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "present"' + +# ============================================================ +- name: test state=absent (expected changed=true) + gce_pd: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert state=absent (expected changed=true) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test state=absent (expected changed=false) + gce_pd: + name: "{{ instance_name }}" + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert state=absent (expected changed=false) + assert: + that: + - 'not result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test non-default size/zone + gce_pd: + name: "{{ instance_name }}" + size_gb: 5 + zone: us-central1-a + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: present + register: result + +- name: assert non-default size/zone + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.size_gb == 5' + - 'result.zone == "us-central1-a"' + - 'result.state == "present"' + +# ============================================================ +- name: test non-default size/zone (state=absent) + gce_pd: + name: "{{ instance_name }}" + size_gb: 5 + zone: us-central1-a + service_account_email: "{{ service_account_email }}" + pem_file: "{{ pem_file }}" + project_id: "{{ project_id }}" + state: absent + register: result + +- name: assert non-default size/zone (state=absent) + assert: + that: + - 'result.changed' + - 'result.name == "{{ instance_name }}"' + - 'result.state == "absent"' + +# ============================================================ +- name: test image given (state=present) + gce_pd: + name: "{{ instance_name }}" + image: debian-7 + 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.image == "debian-7"' + - 'result.state == "present"' + +# ============================================================ +- name: test image given (state=absent) + gce_pd: + name: "{{ instance_name }}" + image: debian-7 + 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 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..0248d7684dc --- /dev/null +++ b/test/integration/setup_gce.py @@ -0,0 +1,42 @@ +''' +Create GCE resources for use in integration tests. + +Takes a prefix as a command-line argument and creates two persistent disks named +${prefix}-base and ${prefix}-extra and a snapshot of the base disk 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') + gce.create_volume( + size=10, name=prefix+'-extra', location='us-central1-a') + except KeyboardInterrupt, e: + print "\nExiting on user command."