Add volume_type related plugins/modules

Added 2 new modules to manipulate volume types in OpenStack
* volume_type is used to create, delete and modify volume type
* volume_type_info is used to show volume_type details, including
  encryption information

ci tests extended with additional role to test basic module behaviour

It is currently impossible to update is_public volume type attribute
as it is being changed to "os-volume-type-access:is_public" which is not
expected by api. Which expects just "is_public"
https://docs.openstack.org/api-ref/block-storage/v3/?expanded=update-a-volume-type-detail#update-a-volume-type
Which results in "'os-volume-type-access:is_public' was unexpected"
reply. I guess the change is required by openstacksdk or on the API side

Change-Id: Idc26a5240b5f3314c8384c7326d8a82dcc8c6171
This commit is contained in:
Denys Mishchenko 2023-08-04 15:02:50 +02:00 committed by arddennis
parent 407369da6e
commit 147ad6c452
5 changed files with 511 additions and 0 deletions

View File

@ -0,0 +1,12 @@
---
volume_backend_name: LVM_iSCSI
volume_type_name: test_type
volume_type_description: Test volume type
volume_type_alt_name: changed_type
volume_type_alt_description: Changed test volume type
enc_provider_name: nova.volume.encryptors.luks.LuksEncryptor
enc_cipher: aes-xts-plain64
enc_control_location: front-end
enc_control_alt_location: back-end
enc_key_size: 256

View File

@ -0,0 +1,82 @@
---
- name: Create volume type
openstack.cloud.volume_type:
name: "{{ volume_type_name }}"
cloud: "{{ cloud }}"
state: present
extra_specs:
volume_backend_name: "{{ volume_backend_name }}"
description: "{{ volume_type_description }}"
is_public: true
register: the_result
- name: Check created volume type
vars:
the_volume: "{{ the_result.volume_type }}"
ansible.builtin.assert:
that:
- "'id' in the_result.volume_type"
- the_volume.description == volume_type_description
- the_volume.is_public == True
- the_volume.name == volume_type_name
- the_volume.extra_specs['volume_backend_name'] == volume_backend_name
success_msg: >-
Created volume: {{ the_result.volume_type.id }},
Name: {{ the_result.volume_type.name }},
Description: {{ the_result.volume_type.description }}
- name: Test, check idempotency
openstack.cloud.volume_type:
name: "{{ volume_type_name }}"
cloud: "{{ cloud }}"
state: present
extra_specs:
volume_backend_name: "{{ volume_backend_name }}"
description: "{{ volume_type_description }}"
is_public: true
register: the_result
- name: Check result.changed is false
ansible.builtin.assert:
that:
- the_result.changed == false
success_msg: "Request with the same details lead to no changes"
- name: Add extra spec
openstack.cloud.volume_type:
cloud: "{{ cloud }}"
name: "{{ volume_type_name }}"
state: present
extra_specs:
volume_backend_name: "{{ volume_backend_name }}"
some_spec: fake_spec
description: "{{ volume_type_alt_description }}"
is_public: true
register: the_result
- name: Check volume type extra spec
ansible.builtin.assert:
that:
- "'some_spec' in the_result.volume_type.extra_specs"
- the_result.volume_type.extra_specs["some_spec"] == "fake_spec"
success_msg: >-
New extra specs: {{ the_result.volume_type.extra_specs }}
# is_public update attempt using openstacksdk result in unexpected attribute
# error... TODO: Find solution
#
# - name: Make volume type private
# openstack.cloud.volume_type:
# cloud: "{{ cloud }}"
# name: "{{ volume_type_alt_name }}"
# state: present
# extra_specs:
# volume_backend_name: "{{ volume_backend_name }}"
# # some_other_spec: test
# description: Changed 3rd time test volume type
# is_public: true
# register: the_result
- name: Delete volume type
openstack.cloud.volume_type:
cloud: "{{ cloud }}"
name: "{{ volume_type_name }}"
state: absent
register: the_result

View File

@ -53,6 +53,7 @@
- { role: subnet, tags: subnet }
- { role: subnet_pool, tags: subnet_pool }
- { role: volume, tags: volume }
- { role: volume_type, tags: volume_type }
- { role: volume_backup, tags: volume_backup }
- { role: volume_snapshot, tags: volume_snapshot }
- { role: volume_type_access, tags: volume_type_access }

View File

@ -0,0 +1,241 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Cleura AB
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = r'''
---
module: volume_type
short_description: Manage OpenStack volume type
author: OpenStack Ansible SIG
description:
- Add, remove or update volume types in OpenStack.
options:
name:
description:
- Volume type name or id.
required: true
type: str
description:
description:
- Description of the volume type.
type: str
extra_specs:
description:
- List of volume type properties
type: dict
is_public:
description:
- Make volume type accessible to the public.
- Can be set only during creation
type: bool
state:
description:
- Indicate desired state of the resource.
- When I(state) is C(present), then I(is_public) is required.
choices: ['present', 'absent']
default: present
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = r'''
- name: Delete volume type by name
openstack.cloud.volume_type:
name: test_type
state: absent
- name: Delete volume type by id
openstack.cloud.volume_type:
name: fbadfa6b-5f17-4c26-948e-73b94de57b42
state: absent
- name: Create volume type
openstack.cloud.volume_type:
name: unencrypted_volume_type
state: present
extra_specs:
volume_backend_name: LVM_iSCSI
description: Unencrypted volume type
is_public: True
'''
RETURN = '''
volume_type:
description: Dictionary describing volume type
returned: On success when I(state) is 'present'
type: dict
contains:
name:
description: volume type name
returned: success
type: str
sample: test_type
extra_specs:
description: volume type extra parameters
returned: success
type: dict
sample: null
is_public:
description: whether the volume type is public
returned: success
type: bool
sample: True
description:
description: volume type description
returned: success
type: str
sample: Unencrypted volume type
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class VolumeTypeModule(OpenStackModule):
argument_spec = dict(
name=dict(type='str', required=True),
description=dict(type='str', required=False),
extra_specs=dict(type='dict', required=False),
is_public=dict(type='bool'),
state=dict(
type='str', default='present', choices=['absent', 'present']),
)
module_kwargs = dict(
required_if=[('state', 'present', ['is_public'])],
supports_check_mode=True,
)
@staticmethod
def _extract_result(details):
if details is not None:
return details.to_dict(computed=False)
return {}
def run(self):
state = self.params['state']
name_or_id = self.params['name']
volume_type = self.conn.block_storage.find_type(name_or_id)
if self.ansible.check_mode:
self.exit_json(
changed=self._will_change(state, volume_type))
if state == 'present' and not volume_type:
# Create type
create_result = self._create()
volume_type = self._extract_result(create_result)
self.exit_json(changed=True, volume_type=volume_type)
elif state == 'present' and volume_type:
# Update type
update = self._build_update(volume_type)
update_result = self._update(volume_type, update)
volume_type = self._extract_result(update_result)
self.exit_json(changed=bool(update), volume_type=volume_type)
elif state == 'absent' and volume_type:
# Delete type
self._delete(volume_type)
self.exit_json(changed=True)
def _build_update(self, volume_type):
return {
**self._build_update_extra_specs(volume_type),
**self._build_update_volume_type(volume_type)}
def _build_update_extra_specs(self, volume_type):
update = {}
old_extra_specs = volume_type['extra_specs']
new_extra_specs = self.params['extra_specs'] or {}
delete_extra_specs_keys = \
set(old_extra_specs.keys()) - set(new_extra_specs.keys())
if delete_extra_specs_keys:
update['delete_extra_specs_keys'] = delete_extra_specs_keys
stringified = {k: str(v) for k, v in new_extra_specs.items()}
if old_extra_specs != stringified:
update['create_extra_specs'] = new_extra_specs
return update
def _build_update_volume_type(self, volume_type):
update = {}
allowed_attributes = [
'is_public', 'description', 'name']
type_attributes = {
k: self.params[k]
for k in allowed_attributes
if k in self.params and self.params.get(k) is not None
and self.params.get(k) != volume_type.get(k)}
if type_attributes:
update['type_attributes'] = type_attributes
return update
def _create(self):
kwargs = {k: self.params[k]
for k in ['name', 'is_public', 'description', 'extra_specs']
if self.params.get(k) is not None}
volume_type = self.conn.block_storage.create_type(**kwargs)
return volume_type
def _delete(self, volume_type):
self.conn.block_storage.delete_type(volume_type.id)
def _update(self, volume_type, update):
if not update:
return volume_type
volume_type = self._update_volume_type(volume_type, update)
volume_type = self._update_extra_specs(volume_type, update)
return volume_type
def _update_extra_specs(self, volume_type, update):
delete_extra_specs_keys = update.get('delete_extra_specs_keys')
if delete_extra_specs_keys:
self.conn.block_storage.delete_type_extra_specs(
volume_type, delete_extra_specs_keys)
# refresh volume_type information
volume_type = self.conn.block_storage.find_type(volume_type.id)
create_extra_specs = update.get('create_extra_specs')
if create_extra_specs:
self.conn.block_storage.update_type_extra_specs(
volume_type, **create_extra_specs)
# refresh volume_type information
volume_type = self.conn.block_storage.find_type(volume_type.id)
return volume_type
def _update_volume_type(self, volume_type, update):
type_attributes = update.get('type_attributes')
if type_attributes:
updated_type = self.conn.block_storage.update_type(
volume_type, **type_attributes)
return updated_type
return volume_type
def _will_change(self, state, volume_type):
if state == 'present' and not volume_type:
return True
if state == 'present' and volume_type:
return bool(self._build_update(volume_type))
if state == 'absent' and volume_type:
return True
return False
def main():
module = VolumeTypeModule()
module()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,175 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Cleura AB
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = r'''
---
module: volume_type_info
short_description: Get OpenStack volume type details
author: OpenStack Ansible SIG
description:
- Get volume type details in OpenStack.
- Get volume type encryption details in OpenStack
options:
name:
description:
- Volume type name or id.
required: true
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = r'''
- name: Get volume type details
openstack.cloud.volume_type_info:
name: test_type
- name: Get volume type details by id
openstack.cloud.volume_type_info:
name: fbadfa6b-5f17-4c26-948e-73b94de57b42
'''
RETURN = '''
access_project_ids:
description:
- List of project IDs allowed to access volume type
- Public volume types returns 'null' value as it is not applicable
returned: On success when I(state) is 'present'
type: list
elements: str
volume_type:
description: Dictionary describing volume type
returned: On success when I(state) is 'present'
type: dict
contains:
id:
description: volume_type uuid
returned: success
type: str
sample: b75d8c5c-a6d8-4a5d-8c86-ef4f1298525d
name:
description: volume type name
returned: success
type: str
sample: test_type
extra_specs:
description: volume type extra parameters
returned: success
type: dict
sample: null
is_public:
description: whether the volume type is public
returned: success
type: bool
sample: True
description:
description: volume type description
returned: success
type: str
sample: Unencrypted volume type
encryption:
description: Dictionary describing volume type encryption
returned: On success when I(state) is 'present'
type: dict
contains:
cipher:
description: encryption cipher
returned: success
type: str
sample: aes-xts-plain64
control_location:
description: encryption location
returned: success
type: str
sample: front-end
created_at:
description: Resource creation date and time
returned: success
type: str
sample: "2023-08-04T10:23:03.000000"
deleted:
description: Boolean if the resource was deleted
returned: success
type: str
sample: false
deleted_at:
description: Resource delete date and time
returned: success
type: str
sample: null
encryption_id:
description: UUID of the volume type encryption
returned: success
type: str
sample: b75d8c5c-a6d8-4a5d-8c86-ef4f1298525d
id:
description: Alias to encryption_id
returned: success
type: str
sample: b75d8c5c-a6d8-4a5d-8c86-ef4f1298525d
key_size:
description: Size of the key
returned: success
type: str
sample: 256
provider:
description: Encryption provider
returned: success
type: str
sample: "nova.volume.encryptors.luks.LuksEncryptor"
updated_at:
description: Resource last update date and time
returned: success
type: str
sample: null
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class VolumeTypeModule(OpenStackModule):
argument_spec = dict(
name=dict(type='str', required=True)
)
module_kwargs = dict(
supports_check_mode=True,
)
@staticmethod
def _extract_result(details):
if details is not None:
return details.to_dict(computed=False)
return {}
def run(self):
name_or_id = self.params['name']
volume_type = self.conn.block_storage.find_type(name_or_id)
type_encryption = self.conn.block_storage.get_type_encryption(
volume_type.id)
if volume_type.is_public:
type_access = None
else:
type_access = [
proj['project_id']
for proj in self.conn.block_storage.get_type_access(
volume_type.id)]
self.exit_json(
changed=False,
volume_type=self._extract_result(volume_type),
encryption=self._extract_result(type_encryption),
access_project_ids=type_access)
def main():
module = VolumeTypeModule()
module()
if __name__ == '__main__':
main()