From b3c2e8f1ce9f04565ed7259cd966567d62c911af Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Thu, 3 Nov 2022 11:29:01 +0100 Subject: [PATCH] Refactored volume_snapshot{,_info} modules Change-Id: I70fc744f786a9de654592c97188af48ddbe8751d --- .zuul.yaml | 1 + ci/roles/volume/tasks/main.yml | 41 +--- ci/roles/volume_snapshot/defaults/main.yml | 13 + ci/roles/volume_snapshot/tasks/main.yml | 96 ++++++++ ci/run-collection.yml | 1 + plugins/modules/volume_snapshot.py | 272 ++++++++++++--------- plugins/modules/volume_snapshot_info.py | 100 ++++---- 7 files changed, 327 insertions(+), 197 deletions(-) create mode 100644 ci/roles/volume_snapshot/defaults/main.yml create mode 100644 ci/roles/volume_snapshot/tasks/main.yml diff --git a/.zuul.yaml b/.zuul.yaml index b48a96c4..7002d002 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -112,6 +112,7 @@ user_role volume volume_backup + volume_snapshot # failing tags # neutron_rbac diff --git a/ci/roles/volume/tasks/main.yml b/ci/roles/volume/tasks/main.yml index fa4a54ef..604cdd28 100644 --- a/ci/roles/volume/tasks/main.yml +++ b/ci/roles/volume/tasks/main.yml @@ -20,38 +20,12 @@ volume: "{{ vol.volume.id }}" name: ansible_volume1 description: Test volume - register: vol -- name: Create volume snapshot - openstack.cloud.volume_snapshot: - cloud: "{{ cloud }}" - state: present - name: ansible_volume_snapshot - volume: ansible_volume - register: vol_snap - -- name: Get snapshot info - openstack.cloud.volume_snapshot_info: - cloud: "{{ cloud }}" - name: ansible_volume_snapshot - register: snap_info - -- name: Create volume from snapshot +- name: Delete volume openstack.cloud.volume: cloud: "{{ cloud }}" - state: present - size: 1 - snapshot: ansible_volume_snapshot - name: ansible_volume2 - description: Test volume - register: vol - -- name: Delete volume snapshot - openstack.cloud.volume_snapshot: - cloud: "{{ cloud }}" - name: ansible_volume_snapshot - volume: ansible_volume - state: absent + state: absent + name: ansible_volume1 - name: Delete volume openstack.cloud.volume: @@ -59,15 +33,6 @@ state: absent name: ansible_volume -- name: Clean up - openstack.cloud.volume: - cloud: "{{ cloud }}" - state: absent - name: "{{ item }}" - loop: - - ansible_volume1 - - ansible_volume2 - - name: Test images block: - name: Ensure clean environment diff --git a/ci/roles/volume_snapshot/defaults/main.yml b/ci/roles/volume_snapshot/defaults/main.yml new file mode 100644 index 00000000..f2628168 --- /dev/null +++ b/ci/roles/volume_snapshot/defaults/main.yml @@ -0,0 +1,13 @@ +expected_fields: + - created_at + - description + - id + - is_forced + - metadata + - name + - progress + - project_id + - size + - status + - updated_at + - volume_id diff --git a/ci/roles/volume_snapshot/tasks/main.yml b/ci/roles/volume_snapshot/tasks/main.yml new file mode 100644 index 00000000..05c10685 --- /dev/null +++ b/ci/roles/volume_snapshot/tasks/main.yml @@ -0,0 +1,96 @@ +--- +- name: Get existing snapshots + openstack.cloud.volume_snapshot_info: + cloud: "{{ cloud }}" + register: info + +- name: Assert volume_snapshot_info + assert: + that: + - info.volume_snapshots|length == 0 + +- name: Get non-existing snapshot + openstack.cloud.volume_snapshot_info: + cloud: "{{ cloud }}" + name: non-existing-snapshot + register: info + +- name: Assert volume_snapshot_info + assert: + that: + - info.volume_snapshots|length == 0 + +- name: Create volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: present + size: 1 + name: ansible_volume + description: Test volume + register: volume + +- name: Create volume snapshot + openstack.cloud.volume_snapshot: + cloud: "{{ cloud }}" + state: present + name: ansible_volume_snapshot + volume: ansible_volume + register: snapshot + +- name: Assert volume_snapshot + assert: + that: + - snapshot.volume_snapshot.name == "ansible_volume_snapshot" + +- name: Assert return values of volume_snapshot module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(snapshot.volume_snapshot.keys())|length == 0 + +- name: Get snapshot info + openstack.cloud.volume_snapshot_info: + cloud: "{{ cloud }}" + name: ansible_volume_snapshot + register: info + +- name: Assert volume_snapshot_info + assert: + that: + - info.volume_snapshots|length == 1 + - info.volume_snapshots[0].id == snapshot.volume_snapshot.id + - info.volume_snapshots[0].volume_id == volume.volume.id + +- name: Assert return values of volume_info module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(info.volume_snapshots[0].keys())|length == 0 + +- name: Create volume from snapshot + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: present + size: 1 + snapshot: ansible_volume_snapshot + name: ansible_volume2 + description: Test volume + +- name: Delete volume snapshot + openstack.cloud.volume_snapshot: + cloud: "{{ cloud }}" + name: ansible_volume_snapshot + volume: ansible_volume + state: absent + +- name: Delete volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: absent + name: ansible_volume2 + +- name: Delete volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: absent + name: ansible_volume diff --git a/ci/run-collection.yml b/ci/run-collection.yml index d02bc480..508cedcc 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -66,6 +66,7 @@ - { role: user_role, tags: user_role } - { role: volume, tags: volume } - { role: volume_backup, tags: volume_backup } + - { role: volume_snapshot, tags: volume_snapshot } - role: loadbalancer tags: loadbalancer - { role: quota, tags: quota } diff --git a/plugins/modules/volume_snapshot.py b/plugins/modules/volume_snapshot.py index 1f434995..9837f0f8 100644 --- a/plugins/modules/volume_snapshot.py +++ b/plugins/modules/volume_snapshot.py @@ -4,7 +4,7 @@ # Copyright (c) 2016, Mario Santos # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: volume_snapshot short_description: Create/Delete Cinder Volume Snapshots @@ -12,75 +12,109 @@ author: OpenStack Ansible SIG description: - Create or Delete cinder block storage volume snapshots options: - display_name: - description: - - Name of the snapshot - required: true - aliases: ['name'] - type: str - display_description: - description: - - String describing the snapshot - aliases: ['description'] - type: str - volume: - description: - - The volume name or id to create/delete the snapshot - required: True - type: str - force: - description: - - Allows or disallows snapshot of a volume to be created when the volume - is attached to an instance. - type: bool - default: 'no' - state: - description: - - Should the resource be present or absent. - choices: [present, absent] - default: present - type: str + description: + description: + - String describing the snapshot + aliases: ['display_description'] + type: str + force: + description: + - Allows or disallows snapshot of a volume to be created, + when the volume is attached to an instance. + type: bool + default: 'no' + name: + description: + - Name of the snapshot + required: true + aliases: ['display_name'] + type: str + state: + description: + - Should the snapshot be C(present) or C(absent). + choices: [present, absent] + default: present + type: str + volume: + description: + - Volume name or ID to create the snapshot from. + - Required when I(state) is C(present). + type: str requirements: - "python >= 3.6" - "openstacksdk" +notes: + - Updating existing volume snapshots has not been implemented yet. + extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Creates a snapshot on volume 'test_volume' -- name: create and delete snapshot - hosts: localhost - tasks: - - name: create snapshot - openstack.cloud.volume_snapshot: - state: present - cloud: mordred - availability_zone: az2 - display_name: test_snapshot - volume: test_volume - - name: delete snapshot - openstack.cloud.volume_snapshot: - state: absent - cloud: mordred - availability_zone: az2 - display_name: test_snapshot - volume: test_volume +EXAMPLES = r''' +- name: create snapshot + openstack.cloud.volume_snapshot: + state: present + cloud: mordred + name: test_snapshot + volume: test_volume +- name: delete snapshot + openstack.cloud.volume_snapshot: + state: absent + cloud: mordred + name: test_snapshot + volume: test_volume ''' -RETURN = ''' +RETURN = r''' snapshot: - description: The snapshot instance after the change + description: Same as C(volume_snapshot), kept for backward compatibility. + returned: On success when C(state=present) + type: dict +volume_snapshot: + description: The snapshot instance returned: success type: dict - sample: - id: 837aca54-c0ee-47a2-bf9a-35e1b4fdac0c - name: test_snapshot - volume_id: ec646a7c-6a35-4857-b38b-808105a24be6 - size: 2 - status: available - display_name: test_snapshot + contains: + created_at: + description: Snapshot creation time. + type: str + description: + description: Snapshot desciption. + type: str + id: + description: Unique UUID. + type: str + sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + is_forced: + description: Indicate whether to create snapshot, + even if the volume is attached. + type: bool + metadata: + description: Snapshot metadata. + type: dict + name: + description: Snapshot Name. + type: str + progress: + description: The percentage of completeness the snapshot is + currently at. + type: str + project_id: + description: The project ID this snapshot is associated with. + type: str + size: + description: The size of the volume, in GBs. + type: int + status: + description: Snapshot status. + type: str + updated_at: + description: Snapshot update time. + type: str + volume_id: + description: Volume ID. + type: str ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule @@ -88,74 +122,86 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class VolumeSnapshotModule(OpenStackModule): argument_spec = dict( - display_name=dict(required=True, aliases=['name']), - display_description=dict(aliases=['description']), - volume=dict(required=True), + description=dict(aliases=['display_description']), + name=dict(required=True, aliases=['display_name']), force=dict(default=False, type='bool'), state=dict(default='present', choices=['absent', 'present']), + volume=dict(), ) module_kwargs = dict( + required_if=[ + ('state', 'present', ['volume']) + ], supports_check_mode=True ) - def _present_volume_snapshot(self): - volume = self.conn.get_volume(self.params['volume']) - snapshot = self.conn.get_volume_snapshot( - self.params['display_name'], filters={'volume_id': volume.id}) - if not snapshot: - snapshot = self.conn.create_volume_snapshot( - volume.id, - force=self.params['force'], - wait=self.params['wait'], - timeout=self.params['timeout'], - name=self.params['display_name'], - description=self.params.get('display_description') - ) - self.exit_json(changed=True, snapshot=snapshot) - else: - self.exit_json(changed=False, snapshot=snapshot) - - def _absent_volume_snapshot(self): - volume = self.conn.get_volume(self.params['volume']) - snapshot = self.conn.get_volume_snapshot( - self.params['display_name'], filters={'volume_id': volume.id}) - if not snapshot: - self.exit_json(changed=False) - else: - self.conn.delete_volume_snapshot( - name_or_id=snapshot.id, - wait=self.params['wait'], - timeout=self.params['timeout'], - ) - self.exit_json(changed=True, snapshot_id=snapshot.id) - - def _system_state_change(self): - volume = self.conn.get_volume(self.params['volume']) - snapshot = self.conn.get_volume_snapshot( - self.params['display_name'], - filters={'volume_id': volume.id}) - state = self.params['state'] - - if state == 'present': - return snapshot is None - if state == 'absent': - return snapshot is not None - def run(self): + name = self.params['name'] state = self.params['state'] - if self.conn.volume_exists(self.params['volume']): - if self.ansible.check_mode: - self.exit_json(changed=self._system_state_change()) - if state == 'present': - self._present_volume_snapshot() - if state == 'absent': - self._absent_volume_snapshot() + snapshot = self.conn.block_storage.find_snapshot(name) + + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, snapshot)) + + if state == 'present' and not snapshot: + snapshot = self._create() + self.exit_json(changed=True, + snapshot=snapshot.to_dict(computed=False), + volume_snapshot=snapshot.to_dict(computed=False)) + + elif state == 'present' and snapshot: + # We do not support snapshot updates yet + # TODO: Implement module updates + self.exit_json(changed=False, + snapshot=snapshot.to_dict(computed=False), + volume_snapshot=snapshot.to_dict(computed=False)) + + elif state == 'absent' and snapshot: + self._delete(snapshot) + self.exit_json(changed=True) + + else: # state == 'absent' and not snapshot + self.exit_json(changed=False) + + def _create(self): + args = dict() + for k in ['description', 'force', 'name']: + if self.params[k] is not None: + args[k] = self.params[k] + + volume_name_or_id = self.params['volume'] + volume = self.conn.block_storage.find_volume(volume_name_or_id, + ignore_missing=False) + args['volume_id'] = volume.id + + snapshot = self.conn.block_storage.create_snapshot(**args) + + if self.params['wait']: + snapshot = self.conn.block_storage.wait_for_status( + snapshot, wait=self.params['timeout']) + + return snapshot + + def _delete(self, snapshot): + self.conn.block_storage.delete_snapshot(snapshot) + if self.params['wait']: + self.conn.block_storage.wait_for_delete( + snapshot, wait=self.params['timeout']) + + def _will_change(self, state, snapshot): + if state == 'present' and not snapshot: + return True + elif state == 'present' and snapshot: + # We do not support snapshot updates yet + # TODO: Implement module updates + return False + elif state == 'absent' and snapshot: + return True else: - self.fail_json( - msg="No volume with name or id '{0}' was found.".format( - self.params['volume'])) + # state == 'absent' and not snapshot: + return False def main(): diff --git a/plugins/modules/volume_snapshot_info.py b/plugins/modules/volume_snapshot_info.py index 8710dd19..db9a8106 100644 --- a/plugins/modules/volume_snapshot_info.py +++ b/plugins/modules/volume_snapshot_info.py @@ -5,7 +5,7 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: volume_snapshot_info short_description: Get volume snapshots @@ -16,32 +16,35 @@ options: details: description: More detailed output type: bool - default: True name: description: - Name of the Snapshot. type: str - volume: - description: - - Name of the volume. - type: str status: description: - Specifies the snapshot status. - choices: [creating, available, error, deleting, - error_deleting, rollbacking, backing-up] + choices: ['available', 'backing-up', 'creating', 'deleted', 'deleting', + 'error', 'error_deleting', 'restoring', 'unmanaging'] type: str -requirements: ["openstacksdk"] + volume: + description: + - Name or ID of the volume. + type: str + +requirements: + - "python >= 3.6" + - "openstacksdk" + extends_documentation_fragment: -- openstack.cloud.openstack + - openstack.cloud.openstack ''' -RETURN = ''' +RETURN = r''' volume_snapshots: description: List of dictionaries describing volume snapshots. type: list elements: dict - returned: always. + returned: always contains: created_at: description: Snapshot creation time. @@ -53,12 +56,26 @@ volume_snapshots: description: Unique UUID. type: str sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + is_forced: + description: Indicate whether to create snapshot, + even if the volume is attached. + type: bool metadata: description: Snapshot metadata. type: dict name: description: Snapshot Name. type: str + progress: + description: The percentage of completeness the snapshot is + currently at. + type: str + project_id: + description: The project ID this snapshot is associated with. + type: str + size: + description: The size of the volume, in GBs. + type: int status: description: Snapshot status. type: str @@ -68,62 +85,53 @@ volume_snapshots: volume_id: description: Volume ID. type: str - ''' -EXAMPLES = ''' -# Get snapshots. -- openstack.cloud.volume_snapshot_info: - register: snapshots +EXAMPLES = r''' +- name: List all snapshots + openstack.cloud.volume_snapshot_info: -- openstack.cloud.volume_snapshotbackup_info: +- name: Fetch data about a single snapshot + openstack.cloud.volume_snapshot_info: name: my_fake_snapshot - register: snapshot ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule class VolumeSnapshotInfoModule(OpenStackModule): - module_min_sdk_version = '0.49.0' - argument_spec = dict( - details=dict(default=True, type='bool'), + details=dict(type='bool'), name=dict(), + status=dict(choices=['available', 'backing-up', 'creating', 'deleted', + 'deleting', 'error', 'error_deleting', + 'restoring', 'unmanaging']), volume=dict(), - status=dict(choices=['creating', 'available', 'error', - 'deleting', 'error_deleting', 'rollbacking', - 'backing-up']), ) + module_kwargs = dict( supports_check_mode=True ) def run(self): + kwargs = dict((k, self.params[k]) + for k in ['details', 'name', 'status'] + if self.params[k] is not None) - details_filter = self.params['details'] - name_filter = self.params['name'] - volume_filter = self.params['volume'] - status_filter = self.params['status'] + volume_name_or_id = self.params['volume'] + volume = None + if volume_name_or_id: + volume = self.conn.block_storage.find_volume(volume_name_or_id) + if volume: + kwargs['volume_id'] = volume.id - data = [] - query = {} - if name_filter: - query['name'] = name_filter - if volume_filter: - query['volume_id'] = self.conn.block_storage.find_volume(volume_filter) - if status_filter: - query['status'] = status_filter.lower() + if volume_name_or_id and not volume: + snapshots = [] + else: + snapshots = [b.to_dict(computed=False) + for b in self.conn.block_storage.snapshots(**kwargs)] - for raw in self.conn.block_storage.snapshots(details_filter, **query): - dt = raw.to_dict() - dt.pop('location') - data.append(dt) - - self.exit_json( - changed=False, - volume_snapshots=data - ) + self.exit_json(changed=False, volume_snapshots=snapshots) def main():