From 2257a6972335bb7e4f8ac2f145c29c39be9d9ad2 Mon Sep 17 00:00:00 2001 From: Chris Conway Date: Sat, 12 Apr 2014 15:21:26 -0700 Subject: [PATCH] Adds support for attaching persistent disks to GCE instances --- library/cloud/gce | 52 +++++++- .../integration/roles/test_gce/tasks/main.yml | 114 ++++++++++++++++++ test/integration/setup_gce.py | 10 +- 3 files changed, 171 insertions(+), 5 deletions(-) diff --git a/library/cloud/gce b/library/cloud/gce index 2d95c8143b..3484990526 100755 --- a/library/cloud/gce +++ b/library/cloud/gce @@ -89,6 +89,12 @@ 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: [] state: description: - desired state of the resource @@ -210,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, @@ -241,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') @@ -249,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) @@ -283,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: @@ -300,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) @@ -352,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/test/integration/roles/test_gce/tasks/main.yml b/test/integration/roles/test_gce/tasks/main.yml index 8791c281a5..3308dede8b 100644 --- a/test/integration/roles/test_gce/tasks/main.yml +++ b/test/integration/roles/test_gce/tasks/main.yml @@ -95,3 +95,117 @@ - '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/setup_gce.py b/test/integration/setup_gce.py index 4020b59979..0248d7684d 100644 --- a/test/integration/setup_gce.py +++ b/test/integration/setup_gce.py @@ -1,10 +1,10 @@ ''' 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. +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 @@ -36,5 +36,7 @@ if __name__ == '__main__': 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."