diff --git a/.zuul.yaml b/.zuul.yaml index 6f481a1408d3a4f0d35ce29c12b42ae39917287d..b48a96c45d762e2165b74427fbef35bdf7377a89 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -111,6 +111,7 @@ user_group user_role volume + volume_backup # failing tags # neutron_rbac diff --git a/ci/roles/volume/tasks/main.yml b/ci/roles/volume/tasks/main.yml index de0686a1be494561d3dd1699ccbf6218d329d4fe..fa4a54efd04d23311b5032cefa5cbefd83a2235c 100644 --- a/ci/roles/volume/tasks/main.yml +++ b/ci/roles/volume/tasks/main.yml @@ -46,35 +46,6 @@ description: Test volume register: vol -- name: Create volume backup - openstack.cloud.volume_backup: - cloud: "{{ cloud }}" - state: present - name: ansible_volume_backup - volume: ansible_volume - register: vol_backup - -- name: Get backup info - openstack.cloud.volume_backup_info: - cloud: "{{ cloud }}" - name: ansible_volume_backup - register: backup_info - -- debug: var=vol - -- debug: var=vol_backup - -- debug: var=backup_info - -- debug: var=snap_info - -- name: Delete volume backup - openstack.cloud.volume_backup: - cloud: "{{ cloud }}" - name: ansible_volume_backup - wait: false - state: absent - - name: Delete volume snapshot openstack.cloud.volume_snapshot: cloud: "{{ cloud }}" diff --git a/ci/roles/volume_backup/defaults/main.yml b/ci/roles/volume_backup/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..7091e286e5b566b8a99751bf138cab5fe4f7d399 --- /dev/null +++ b/ci/roles/volume_backup/defaults/main.yml @@ -0,0 +1,22 @@ +expected_fields: + - availability_zone + - container + - created_at + - data_timestamp + - description + - fail_reason + - force + - has_dependent_backups + - id + - is_incremental + - links + - metadata + - name + - object_count + - project_id + - size + - snapshot_id + - status + - updated_at + - user_id + - volume_id diff --git a/ci/roles/volume_backup/tasks/main.yml b/ci/roles/volume_backup/tasks/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..bc1b319544e3e84d56337779f64b87226683cfd3 --- /dev/null +++ b/ci/roles/volume_backup/tasks/main.yml @@ -0,0 +1,89 @@ +--- +- name: Get existing backups + openstack.cloud.volume_backup_info: + cloud: "{{ cloud }}" + register: info + +- name: Assert volume_backup_info + assert: + that: + - info.volume_backups|length == 0 + +- name: Get non-existing backup + openstack.cloud.volume_backup_info: + cloud: "{{ cloud }}" + name: non-existing-backup + register: info + +- name: Assert volume_backup_info + assert: + that: + - info.volume_backups|length == 0 + +- name: Create volume + openstack.cloud.volume: + cloud: "{{ cloud }}" + state: present + size: 1 + name: ansible_volume + register: volume + +- name: Create volume backup + openstack.cloud.volume_backup: + cloud: "{{ cloud }}" + state: present + name: ansible_volume_backup + volume: ansible_volume + # TODO: Uncomment code when https://storyboard.openstack.org/#!/story/2010395 has been solved. + #metadata: + # key1: value1 + # key2: value2 + register: backup + +- name: Assert volume_backup + assert: + that: + - backup.volume_backup.name == "ansible_volume_backup" + - backup.volume_backup.volume_id == volume.volume.id + # TODO: Uncomment code when https://storyboard.openstack.org/#!/story/2010395 has been solved. + #- backup.volume_backup.metadata.keys()|sort == ['key1', 'key2'] + #- backup.volume_backup.metadata['key1'] == 'value1' + #- backup.volume_backup.metadata['key2'] == 'value2' + +- name: Assert return values of volume_backup module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(backup.volume_backup.keys())|length == 0 + +- name: Get backup info + openstack.cloud.volume_backup_info: + cloud: "{{ cloud }}" + name: ansible_volume_backup + register: info + +- name: Assert volume_backup_info + assert: + that: + - info.volume_backups|length == 1 + - info.volume_backups[0].id == backup.backup.id + - info.volume_backups[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_backups[0].keys())|length == 0 + +- name: Delete volume backup + openstack.cloud.volume_backup: + cloud: "{{ cloud }}" + name: ansible_volume_backup + wait: false + state: absent + +- 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 70c37784445c920e95f816ef8d9a6024397d7c72..d02bc480cdfe4398d27508f86dcde5e4b67df10a 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -65,6 +65,7 @@ - { role: user_group, tags: user_group } - { role: user_role, tags: user_role } - { role: volume, tags: volume } + - { role: volume_backup, tags: volume_backup } - role: loadbalancer tags: loadbalancer - { role: quota, tags: quota } diff --git a/plugins/modules/volume_backup.py b/plugins/modules/volume_backup.py index ec6ae7e61ad870d6b2073a7dc35d793f06ce5257..709e2b47058f5e5d5de11c3651fe9e438c2f98be 100644 --- a/plugins/modules/volume_backup.py +++ b/plugins/modules/volume_backup.py @@ -4,28 +4,41 @@ # Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: volume_backup short_description: Add/Delete Volume backup -extends_documentation_fragment: openstack.cloud.openstack author: OpenStack Ansible SIG description: - - Add or Remove Volume Backup in OTC. + - Add or Remove Volume Backup in OpenStack. options: - display_name: + description: + description: + - String describing the backup + type: str + aliases: ['display_description'] + force: + description: + - Indicates whether to backup, even if the volume is attached. + type: bool + default: False + is_incremental: + description: The backup mode + type: bool + default: False + aliases: ['incremental'] + metadata: + description: Metadata for the backup + type: dict + name: description: - Name that has to be given to the backup required: true type: str - aliases: ['name'] - display_description: - description: - - String describing the backup - required: false + aliases: ['display_name'] + snapshot: + description: Name or ID of the Snapshot to take backup of. type: str - aliases: ['description'] state: description: - Should the resource be present or absent. @@ -34,63 +47,117 @@ options: type: str volume: description: - - Name or ID of the volume. Required when state is True. - type: str - required: False - snapshot: - description: Name or ID of the Snapshot to take backup of + - Name or ID of the volume. + - Required when I(state) is C(present). type: str - force: - description: - - Indicates whether to backup, even if the volume is attached. - type: bool - default: False - metadata: - description: Metadata for the backup - type: dict - incremental: - description: The backup mode - type: bool - default: False -requirements: ["openstacksdk"] + +notes: + - This module does not support updates to existing backups. + +requirements: + - "python >= 3.6" + - "openstacksdk" + +extends_documentation_fragment: +- openstack.cloud.openstack ''' -RETURN = ''' -id: - description: The Volume backup ID. - returned: On success when C(state=present) - type: str - sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" +RETURN = r''' backup: - description: Dictionary describing the Cluster. + description: Same as C(volume_backup), kept for backward compatibility. + returned: On success when C(state=present) + type: dict +volume_backup: + description: Dictionary describing the volume backup. returned: On success when C(state=present) - type: complex + type: dict contains: + availability_zone: + description: Backup availability zone. + type: str + container: + description: The container name. + type: str + created_at: + description: Backup creation time. + type: str + data_timestamp: + description: The time when the data on the volume was first saved. + If it is a backup from volume, it will be the same as + C(created_at) for a backup. If it is a backup from a + snapshot, it will be the same as created_at for the + snapshot. + type: str + description: + description: Backup desciption. + type: str + fail_reason: + description: Backup fail reason. + type: str + force: + description: Force backup. + type: bool + has_dependent_backups: + description: If this value is true, there are other backups + depending on this backup. + type: bool id: description: Unique UUID. type: str sample: "39007a7e-ee4f-4d13-8283-b4da2e037c69" + is_incremental: + description: Backup incremental property. + type: bool + links: + description: A list of links associated with this volume. + type: list + metadata: + description: Backup metadata. + type: dict name: - description: Name given to the load balancer. + description: Backup Name. + type: str + object_count: + description: backup object count. + type: int + project_id: + description: The UUID of the owning project. + type: str + size: + description: The size of the volume, in gibibytes (GiB). + type: int + snapshot_id: + description: Snapshot ID. + type: str + status: + description: Backup status. + type: str + updated_at: + description: Backup update time. + type: str + user_id: + description: The UUID of the project owner. + type: str + volume_id: + description: Volume ID. type: str - sample: "elb_test" ''' -EXAMPLES = ''' +EXAMPLES = r''' - name: Create backup openstack.cloud.volume_backup: - display_name: test_volume_backup + name: test_volume_backup volume: "test_volume" - name: Create backup from snapshot openstack.cloud.volume_backup: - display_name: test_volume_backup - volume: "test_volume" + name: test_volume_backup snapshot: "test_snapshot" + volume: "test_volume" - name: Delete volume backup openstack.cloud.volume_backup: - display_name: test_volume_backup + name: test_volume_backup state: absent ''' @@ -98,18 +165,20 @@ from ansible_collections.openstack.cloud.plugins.module_utils.openstack import O class VolumeBackupModule(OpenStackModule): - module_min_sdk_version = '0.49.0' argument_spec = dict( - display_name=dict(required=True, aliases=['name']), - display_description=dict(aliases=['description']), - volume=dict(), - snapshot=dict(), - state=dict(default='present', choices=['absent', 'present']), + description=dict(aliases=['display_description']), force=dict(default=False, type='bool'), + is_incremental=dict(default=False, + type='bool', + aliases=['incremental']), metadata=dict(type='dict'), - incremental=dict(default=False, type='bool') + name=dict(required=True, aliases=['display_name']), + snapshot=dict(), + state=dict(default='present', choices=['absent', 'present']), + volume=dict(), ) + module_kwargs = dict( required_if=[ ('state', 'present', ['volume']) @@ -117,98 +186,79 @@ class VolumeBackupModule(OpenStackModule): supports_check_mode=True ) - def _create_backup(self): - if self.ansible.check_mode: - self.exit_json(changed=True) - - name = self.params['display_name'] - description = self.params['display_description'] - volume = self.params['volume'] - snapshot = self.params['snapshot'] - force = self.params['force'] - is_incremental = self.params['incremental'] - metadata = self.params['metadata'] + def run(self): + name = self.params['name'] + state = self.params['state'] - changed = False + backup = self.conn.block_storage.find_backup(name) - cloud_volume = self.conn.block_storage.find_volume(volume) - cloud_snapshot_id = None + if self.ansible.check_mode: + self.exit_json(changed=self._will_change(state, backup)) + + if state == 'present' and not backup: + backup = self._create() + self.exit_json(changed=True, + backup=backup.to_dict(computed=False), + volume_backup=backup.to_dict(computed=False)) + + elif state == 'present' and backup: + # We do not support backup updates, because + # openstacksdk does not support it either + self.exit_json(changed=False, + backup=backup.to_dict(computed=False), + volume_backup=backup.to_dict(computed=False)) + + elif state == 'absent' and backup: + self._delete(backup) + self.exit_json(changed=True) - attrs = { - 'name': name, - 'volume_id': cloud_volume.id, - 'force': force, - 'is_incremental': is_incremental - } + else: # state == 'absent' and not backup + self.exit_json(changed=False) - if snapshot: - cloud_snapshot_id = self.conn.block_storage.find_snapshot( - snapshot, ignore_missing=False).id - attrs['snapshot_id'] = cloud_snapshot_id + def _create(self): + args = dict() + for k in ['description', 'is_incremental', 'force', 'metadata', + 'name']: + if self.params[k] is not None: + args[k] = self.params[k] - if metadata: - attrs['metadata'] = metadata + 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 - if description: - attrs['description'] = description + snapshot_name_or_id = self.params['snapshot'] + if snapshot_name_or_id: + snapshot = self.conn.block_storage.find_snapshot( + snapshot_name_or_id, ignore_missing=False) + args['snapshot_id'] = snapshot.id - backup = self.conn.block_storage.create_backup(**attrs) - changed = True + backup = self.conn.block_storage.create_backup(**args) if self.params['wait']: - try: - backup = self.conn.block_storage.wait_for_status( - backup, - status='available', - wait=self.params['timeout']) - self.exit_json( - changed=True, volume_backup=backup.to_dict(), id=backup.id - ) - except self.sdk.exceptions.ResourceTimeout: - self.fail_json( - msg='Timeout failure waiting for backup ' - 'to complete' - ) - - self.exit_json( - changed=changed, volume_backup=backup.to_dict(), id=backup.id - ) - - def _delete_backup(self, backup): - if self.ansible.check_mode: - self.exit_json(changed=True) + backup = self.conn.block_storage.wait_for_status( + backup, status='available', wait=self.params['timeout']) - if backup: - self.conn.block_storage.delete_backup(backup) - if self.params['wait']: - try: - self.conn.block_storage.wait_for_delete( - backup, - interval=2, - wait=self.params['timeout']) - except self.sdk.exceptions.ResourceTimeout: - self.fail_json( - msg='Timeout failure waiting for backup ' - 'to be deleted' - ) - - self.exit_json(changed=True) - - def run(self): - name = self.params['display_name'] + return backup - backup = self.conn.block_storage.find_backup(name) - - if self.params['state'] == 'present': - if not backup: - self._create_backup() - else: - # For the moment we do not support backup update, since SDK - # doesn't support it either => do nothing - self.exit_json(changed=False) - - elif self.params['state'] == 'absent': - self._delete_backup(backup) + def _delete(self, backup): + self.conn.block_storage.delete_backup(backup) + if self.params['wait']: + self.conn.block_storage.wait_for_delete( + backup, wait=self.params['timeout']) + + def _will_change(self, state, backup): + if state == 'present' and not backup: + return True + elif state == 'present' and backup: + # We do not support backup updates, because + # openstacksdk does not support it either + return False + elif state == 'absent' and backup: + return False + else: + # state == 'absent' and not backup: + return True def main(): diff --git a/plugins/modules/volume_backup_info.py b/plugins/modules/volume_backup_info.py index 1fb549fd3b16a753774fe622b862689a69055d93..57f01fed0976f931027098e8fb5b9650c92b4ab2 100644 --- a/plugins/modules/volume_backup_info.py +++ b/plugins/modules/volume_backup_info.py @@ -4,8 +4,7 @@ # Copyright (c) 2020 by Open Telekom Cloud, operated by T-Systems International GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: volume_backup_info short_description: Get Backups @@ -19,14 +18,18 @@ options: type: str volume: description: - - Name of the volume. + - Name or ID of the volume. type: str -requirements: ["openstacksdk"] + +requirements: + - "python >= 3.6" + - "openstacksdk" + extends_documentation_fragment: - openstack.cloud.openstack ''' -RETURN = ''' +RETURN = r''' volume_backups: description: List of dictionaries describing volume backups. type: list @@ -36,12 +39,32 @@ volume_backups: availability_zone: description: Backup availability zone. type: str + container: + description: The container name. + type: str created_at: description: Backup creation time. type: str + data_timestamp: + description: The time when the data on the volume was first saved. + If it is a backup from volume, it will be the same as + C(created_at) for a backup. If it is a backup from a + snapshot, it will be the same as created_at for the + snapshot. + type: str description: description: Backup desciption. type: str + fail_reason: + description: Backup fail reason. + type: str + force: + description: Force backup. + type: bool + has_dependent_backups: + description: If this value is true, there are other backups + depending on this backup. + type: bool id: description: Unique UUID. type: str @@ -49,12 +72,24 @@ volume_backups: is_incremental: description: Backup incremental property. type: bool + links: + description: A list of links associated with this volume. + type: list metadata: description: Backup metadata. type: dict name: description: Backup Name. type: str + object_count: + description: backup object count. + type: int + project_id: + description: The UUID of the owning project. + type: str + size: + description: The size of the volume, in gibibytes (GiB). + type: int snapshot_id: description: Snapshot ID. type: str @@ -64,57 +99,56 @@ volume_backups: updated_at: description: Backup update time. type: str + user_id: + description: The UUID of the project owner. + type: str volume_id: description: Volume ID. type: str - ''' -EXAMPLES = ''' -# Get backups. -- openstack.cloud.volume_backup_info: - register: backup +EXAMPLES = r''' +- name: Get all backups + openstack.cloud.volume_backup_info: -- openstack.cloud.volume_backup_info: +- name: Get backup 'my_fake_backup' + openstack.cloud.volume_backup_info: name: my_fake_backup - register: backup ''' from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule class VolumeBackupInfoModule(OpenStackModule): - module_min_sdk_version = '0.49.0' argument_spec = dict( name=dict(), volume=dict() ) + module_kwargs = dict( supports_check_mode=True ) def run(self): - name_filter = self.params['name'] - volume = self.params['volume'] - - data = [] - attrs = {} - - if name_filter: - attrs['name'] = name_filter - if volume: - attrs['volume_id'] = self.conn.block_storage.find_volume(volume) - - for raw in self.conn.block_storage.backups(**attrs): - dt = raw.to_dict() - dt.pop('location') - data.append(dt) - - self.exit_json( - changed=False, - volume_backups=data - ) + kwargs = dict((k, self.params[k]) + for k in ['name'] + if self.params[k] is not None) + + 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 + + if volume_name_or_id and not volume: + backups = [] + else: + backups = [b.to_dict(computed=False) + for b in self.conn.block_storage.backups(**kwargs)] + + self.exit_json(changed=False, volume_backups=backups) def main():