From 5862d91615388f27a1be4df1b0790aa8a1fb578d Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Thu, 3 Nov 2022 14:37:00 +0100 Subject: [PATCH] Split project_access into {compute_flavor,volume_type}_access modules Change-Id: I33fa4b3a08392feac702f45a2c47f8b04799ac0b --- .zuul.yaml | 2 + ci/roles/compute_flavor_access/tasks/main.yml | 96 ++++++++ ci/roles/volume_type_access/tasks/main.yml | 96 ++++++++ ci/run-collection.yml | 2 + meta/runtime.yml | 3 +- plugins/modules/compute_flavor_access.py | 209 ++++++++++++++++++ plugins/modules/project_access.py | 194 ---------------- plugins/modules/volume_type_access.py | 177 +++++++++++++++ 8 files changed, 584 insertions(+), 195 deletions(-) create mode 100644 ci/roles/compute_flavor_access/tasks/main.yml create mode 100644 ci/roles/volume_type_access/tasks/main.yml create mode 100644 plugins/modules/compute_flavor_access.py delete mode 100644 plugins/modules/project_access.py create mode 100644 plugins/modules/volume_type_access.py diff --git a/.zuul.yaml b/.zuul.yaml index bdc07f4a..cb53d006 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -67,6 +67,7 @@ auth catalog_service compute_flavor + compute_flavor_access config dns dns_zone_info @@ -114,6 +115,7 @@ volume volume_backup volume_snapshot + volume_type_access # failing tags # neutron_rbac diff --git a/ci/roles/compute_flavor_access/tasks/main.yml b/ci/roles/compute_flavor_access/tasks/main.yml new file mode 100644 index 00000000..996f65b1 --- /dev/null +++ b/ci/roles/compute_flavor_access/tasks/main.yml @@ -0,0 +1,96 @@ +--- +- name: Create flavor + openstack.cloud.compute_flavor: + cloud: devstack-admin + state: present + name: ansible_flavor + is_public: False + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + swap: 1 + register: flavor + +- name: Fetch demo project + openstack.cloud.project_info: + cloud: devstack-admin + name: demo + register: projects + +- name: Verify demo project + assert: + that: + - projects.openstack_projects|length == 1 + - projects.openstack_projects.0.name == "demo" + +- name: Grant access to flavor + openstack.cloud.compute_flavor_access: + cloud: devstack-admin + name: ansible_flavor + project: demo + state: present + register: access + +- name: Verify access + assert: + that: + - access is changed + - access.flavor.id == flavor.flavor.id + +# TODO: Replace with appropriate Ansible module once available +- name: Get compute flavor + command: openstack --os-cloud=devstack-admin flavor show ansible_flavor -f json + register: flavor_show + +- name: Verify volume type access + assert: + that: + - (flavor_show.stdout | from_json).name == 'ansible_flavor' + - projects.openstack_projects.0.id in (flavor_show.stdout | from_json).access_project_ids + +- name: Grant access to flavor again + openstack.cloud.compute_flavor_access: + cloud: devstack-admin + name: ansible_flavor + project: demo + state: present + register: access + +- name: Verify access did not change + assert: + that: + - access is not changed + +- name: Revoke access to flavor + openstack.cloud.compute_flavor_access: + cloud: devstack-admin + name: ansible_flavor + project: demo + state: absent + register: access + +- name: Verify revoked access + assert: + that: + - access is changed + - access.flavor.id == flavor.flavor.id + +- name: Revoke access to flavor again + openstack.cloud.compute_flavor_access: + cloud: devstack-admin + name: ansible_flavor + project: demo + state: absent + register: access + +- name: Verify access did not change + assert: + that: + - access is not changed + +- name: Delete flavor + openstack.cloud.compute_flavor: + cloud: devstack-admin + state: absent + name: ansible_flavor diff --git a/ci/roles/volume_type_access/tasks/main.yml b/ci/roles/volume_type_access/tasks/main.yml new file mode 100644 index 00000000..1c774d9b --- /dev/null +++ b/ci/roles/volume_type_access/tasks/main.yml @@ -0,0 +1,96 @@ +--- +# TODO: Replace with appropriate Ansible module once available +- name: Get volume types + command: openstack --os-cloud=devstack-admin volume type list -f json + register: volume_types + +# TODO: Replace with appropriate Ansible module once available +- name: Create volume type + command: openstack --os-cloud=devstack-admin volume type create ansible_volume_type --private + when: "'ansible_volume_type' not in (volume_types.stdout | from_json) | map(attribute='Name') | list" + +# TODO: Replace with appropriate Ansible module once available +- name: Get volume types + command: openstack --os-cloud=devstack-admin volume type show ansible_volume_type -f json + register: volume_type + +- name: Fetch demo project + openstack.cloud.project_info: + cloud: devstack-admin + name: demo + register: projects + +- name: Verify demo project + assert: + that: + - projects.openstack_projects|length == 1 + - projects.openstack_projects.0.name == "demo" + +- name: Grant access to volume type + openstack.cloud.volume_type_access: + cloud: devstack-admin + name: ansible_volume_type + project: demo + state: present + register: access + +- name: Verify access + assert: + that: + - access is changed + - access.volume_type.id == (volume_type.stdout | from_json).id + +# TODO: Replace with appropriate Ansible module once available +- name: Get volume types + command: openstack --os-cloud=devstack-admin volume type show ansible_volume_type -f json + register: volume_type + +- name: Verify volume type access + assert: + that: + - (volume_type.stdout | from_json).name == 'ansible_volume_type' + - projects.openstack_projects.0.id in (volume_type.stdout | from_json).access_project_ids + +- name: Grant access to volume type again + openstack.cloud.volume_type_access: + cloud: devstack-admin + name: ansible_volume_type + project: demo + state: present + register: access + +- name: Verify access did not change + assert: + that: + - access is not changed + +- name: Revoke access to volume type + openstack.cloud.volume_type_access: + cloud: devstack-admin + name: ansible_volume_type + project: demo + state: absent + register: access + +- name: Verify revoked access + assert: + that: + - access is changed + - access.volume_type.id == (volume_type.stdout | from_json).id + +- name: Revoke access to volume type again + openstack.cloud.volume_type_access: + cloud: devstack-admin + name: ansible_volume_type + project: demo + state: absent + register: access + +- name: Verify access did not change + assert: + that: + - access is not changed + +# TODO: Replace with appropriate Ansible module once available +- name: Delete volume type + command: openstack --os-cloud=devstack-admin volume type delete ansible_volume_type diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 335d7221..15ad6fba 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -8,6 +8,7 @@ - { role: auth, tags: auth } - { role: catalog_service, tags: catalog_service } - { role: compute_flavor, tags: compute_flavor } + - { role: compute_flavor_access, tags: compute_flavor_access } - { role: config, tags: config } - { role: dns_zone_info, tags: dns_zone_info } - role: object_container @@ -68,6 +69,7 @@ - { role: volume, tags: volume } - { role: volume_backup, tags: volume_backup } - { role: volume_snapshot, tags: volume_snapshot } + - { role: volume_type_access, tags: volume_type_access } - role: loadbalancer tags: loadbalancer - { role: quota, tags: quota } diff --git a/meta/runtime.yml b/meta/runtime.yml index b01d9294..b538b1a4 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -13,6 +13,7 @@ action_groups: - coe_cluster - coe_cluster_template - compute_flavor + - compute_flavor_access - compute_flavor_info - compute_service_info - config @@ -55,7 +56,6 @@ action_groups: - port - port_info - project - - project_access - project_info - quota - recordset @@ -83,3 +83,4 @@ action_groups: - volume_info - volume_snapshot - volume_snapshot_info + - volume_type_access diff --git a/plugins/modules/compute_flavor_access.py b/plugins/modules/compute_flavor_access.py new file mode 100644 index 00000000..4ca54218 --- /dev/null +++ b/plugins/modules/compute_flavor_access.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: compute_flavor_access +short_description: Manage access to OpenStack compute flavors +author: OpenStack Ansible SIG +description: + - Add or remove access to OpenStack compute flavor +options: + name: + description: + - Name or ID of the compute flavor. + required: true + type: str + project: + description: + - ID or Name of project to grant. + - Allow I(project) to access private flavor (name or ID). + type: str + required: true + project_domain: + description: + - Domain the project belongs to (name or ID). + - This can be used in case collisions between project names exist. + type: str + state: + description: + - Indicate whether project should have access to compute flavor or not. + default: present + type: str + choices: ['present', 'absent'] +notes: + - A compute flavor must not be private to manage project access. +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = r''' +- name: Grant access to tiny flavor + openstack.cloud.compute_flavor_access: + cloud: devstack + name: tiny + project: demo + state: present + +- name: Revoke access to compute flavor + openstack.cloud.compute_flavor_access: + cloud: devstack + name: tiny + project: demo + state: absent +''' + +RETURN = ''' +flavor: + description: Dictionary describing the flavor. + returned: On success when I(state) is 'present' + type: dict + contains: + description: + description: Description attached to flavor + returned: success + type: str + sample: Example description + disk: + description: Size of local disk, in GB. + returned: success + type: int + sample: 10 + ephemeral: + description: Ephemeral space size, in GB. + returned: success + type: int + sample: 10 + extra_specs: + description: Flavor metadata + returned: success + type: dict + sample: + "quota:disk_read_iops_sec": 5000 + "aggregate_instance_extra_specs:pinned": false + id: + description: Flavor ID. + returned: success + type: str + sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" + is_disabled: + description: Whether the flavor is disabled + returned: success + type: bool + sample: true + is_public: + description: Make flavor accessible to the public. + returned: success + type: bool + sample: true + name: + description: Flavor name. + returned: success + type: str + sample: "tiny" + original_name: + description: The name of this flavor when returned by server list/show + type: str + returned: success + ram: + description: Amount of memory, in MB. + returned: success + type: int + sample: 1024 + rxtx_factor: + description: | + The bandwidth scaling factor this flavor receives on the network + returned: success + type: int + sample: 100 + swap: + description: Swap space size, in MB. + returned: success + type: int + sample: 100 + vcpus: + description: Number of virtual CPUs. + returned: success + type: int + sample: 2 +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeFlavorAccess(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + project=dict(required=True), + project_domain=dict(), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + # TODO: Merge with equal function from volume_type_access module. + def _project_and_project_domain(self): + project_name_or_id = self.params['project'] + project_domain_name_or_id = self.params['project_domain'] + + if project_domain_name_or_id: + domain_id = self.conn.identity.find_domain( + project_domain_name_or_id, ignore_missing=False).id + else: + domain_id = None + + kwargs = dict() if domain_id is None else dict(domain_id=domain_id) + + if project_name_or_id: + project_id = self.conn.identity.find_project( + project_name_or_id, ignore_missing=False, *kwargs).id + else: + project_id = None + + return project_id, domain_id + + def run(self): + name_or_id = self.params['name'] + flavor = self.conn.compute.find_flavor(name_or_id, + ignore_missing=False) + + state = self.params['state'] + if state == 'present' and flavor.is_public: + raise ValueError('access can only be granted to private flavors') + + project_id, domain_id = self._project_and_project_domain() + + flavor_access = self.conn.compute.get_flavor_access(flavor.id) + project_ids = [access.get('tenant_id') for access in flavor_access] + + if (project_id in project_ids and state == 'present') \ + or (project_id not in project_ids and state == 'absent'): + self.exit_json(changed=False, + flavor=flavor.to_dict(computed=False)) + + if self.ansible.check_mode: + self.exit_json(changed=True, flavor=flavor.to_dict(computed=False)) + + if project_id in project_ids: # and state == 'absent' + self.conn.compute.flavor_remove_tenant_access(flavor.id, + project_id) + else: # project_id not in project_ids and state == 'present' + self.conn.compute.flavor_add_tenant_access(flavor.id, project_id) + + self.exit_json(changed=True, flavor=flavor.to_dict(computed=False)) + + +def main(): + module = ComputeFlavorAccess() + module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/project_access.py b/plugins/modules/project_access.py deleted file mode 100644 index 5fe8e0a9..00000000 --- a/plugins/modules/project_access.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# This module is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software. If not, see . - -DOCUMENTATION = ''' ---- -module: project_access -short_description: Manage OpenStack compute flavors access -author: OpenStack Ansible SIG -description: - - Add or remove flavor, volume_type or other resources access - from OpenStack. -options: - state: - description: - - Indicate desired state of the resource. - choices: ['present', 'absent'] - required: false - default: present - type: str - target_project_id: - description: - - Project id. - required: true - type: str - resource_type: - description: - - The resource type (eg. nova_flavor, cinder_volume_type). - required: true - type: str - resource_name: - description: - - The resource name (eg. tiny). - required: true - type: str -requirements: - - "python >= 3.6" - - "openstacksdk" - - -extends_documentation_fragment: -- openstack.cloud.openstack -''' - -EXAMPLES = ''' -- name: "Enable access to tiny flavor to your tenant." - openstack.cloud.project_access: - cloud: mycloud - state: present - target_project_id: f0f1f2f3f4f5f67f8f9e0e1 - resource_name: tiny - resource_type: nova_flavor - - -- name: "Disable access to the given flavor to project" - openstack.cloud.project_access: - cloud: mycloud - state: absent - target_project_id: f0f1f2f3f4f5f67f8f9e0e1 - resource_name: tiny - resource_type: nova_flavor -''' - -RETURN = ''' -flavor: - description: Dictionary describing the flavor. - returned: On success when I(state) is 'present' - type: complex - contains: - id: - description: Flavor ID. - returned: success - type: str - sample: "515256b8-7027-4d73-aa54-4e30a4a4a339" - name: - description: Flavor name. - returned: success - type: str - sample: "tiny" - -''' - -from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule - - -class IdentityProjectAccess(OpenStackModule): - argument_spec = dict( - state=dict(default='present', - choices=['absent', 'present']), - target_project_id=dict(required=True), - resource_type=dict(required=True), - resource_name=dict(required=True), - ) - - module_kwargs = dict( - supports_check_mode=True, - required_if=[ - ('state', 'present', ['target_project_id']) - ] - ) - - def run(self): - state = self.params['state'] - resource_name = self.params['resource_name'] - resource_type = self.params['resource_type'] - target_project_id = self.params['target_project_id'] - - if resource_type == 'nova_flavor': - # returns Munch({'NAME_ATTR': 'name', - # 'tenant_id': u'37e55da59ec842649d84230f3a24eed5', - # 'HUMAN_ID': False, - # 'flavor_id': u'6d4d37b9-0480-4a8c-b8c9-f77deaad73f9', - # 'request_ids': [], 'human_id': None}), - _get_resource = self.conn.get_flavor - _list_resource_access = self.conn.list_flavor_access - _add_resource_access = self.conn.add_flavor_access - _remove_resource_access = self.conn.remove_flavor_access - elif resource_type == 'cinder_volume_type': - # returns [Munch({ - # 'project_id': u'178cdb9955b047eea7afbe582038dc94', - # 'properties': {'request_ids': [], 'NAME_ATTR': 'name', - # 'human_id': None, - # 'HUMAN_ID': False}, - # 'id': u'd5573023-b290-42c8-b232-7c5ca493667f'}), - _get_resource = self.conn.get_volume_type - _list_resource_access = self.conn.get_volume_type_access - _add_resource_access = self.conn.add_volume_type_access - _remove_resource_access = self.conn.remove_volume_type_access - else: - self.exit_json( - changed=False, - resource_name=resource_name, - resource_type=resource_type, - error="Not implemented.") - - resource = _get_resource(resource_name) - if not resource: - self.exit_json( - changed=False, - resource_name=resource_name, - resource_type=resource_type, - error="Not found.") - resource_id = getattr(resource, 'id', resource['id']) - # _list_resource_access returns a list of dicts containing 'project_id' - acls = _list_resource_access(resource_id) - - if not all(acl.get('project_id') for acl in acls): - self.exit_json( - changed=False, - resource_name=resource_name, - resource_type=resource_type, - error="Missing project_id in resource output.") - allowed_tenants = [acl['project_id'] for acl in acls] - - changed_access = any(( - state == 'present' and target_project_id not in allowed_tenants, - state == 'absent' and target_project_id in allowed_tenants - )) - if self.ansible.check_mode or not changed_access: - self.exit_json( - changed=changed_access, resource=resource, id=resource_id) - - if state == 'present': - _add_resource_access( - resource_id, target_project_id - ) - elif state == 'absent': - _remove_resource_access( - resource_id, target_project_id - ) - - self.exit_json( - changed=True, resource=resource, id=resource_id) - - -def main(): - module = IdentityProjectAccess() - module() - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/volume_type_access.py b/plugins/modules/volume_type_access.py new file mode 100644 index 00000000..2d1e60ec --- /dev/null +++ b/plugins/modules/volume_type_access.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: volume_type_access +short_description: Manage access to OpenStack block-storage volume type +author: OpenStack Ansible SIG +description: + - Add or remove access to OpenStack block-storage volume type +options: + name: + description: + - Name or ID of the block-storage volume type. + required: true + type: str + project: + description: + - ID or Name of project to grant. + - Allow I(project) to access private volume type (name or ID). + type: str + required: true + project_domain: + description: + - Domain the project belongs to (name or ID). + - This can be used in case collisions between project names exist. + type: str + state: + description: + - Indicate whether project should have access to volume type or not. + default: present + type: str + choices: ['present', 'absent'] +notes: + - A volume type must not be private to manage project access. +requirements: + - "python >= 3.6" + - "openstacksdk" +extends_documentation_fragment: +- openstack.cloud.openstack +''' + +EXAMPLES = r''' +- name: Grant access to volume type vol-type-001 + openstack.cloud.volume_type_access: + cloud: devstack + name: vol-type-001 + project: demo + state: present + +- name: Revoke access to volume type + openstack.cloud.volume_type_access: + cloud: devstack + name: vol-type-001 + project: demo + state: absent +''' + +RETURN = ''' +volume_type: + description: Dictionary describing the volume type. + returned: success + type: dict + contains: + description: + description: Description of the type. + returned: success + type: str + extra_specs: + description: A dict of extra specifications. + "capabilities" is a usual key. + returned: success + type: dict + id: + description: Volume type ID. + returned: success + type: str + is_public: + description: Volume type is accessible to the public. + returned: success + type: bool + name: + description: Volume type name. + returned: success + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class VolumeTypeAccess(OpenStackModule): + argument_spec = dict( + name=dict(required=True), + project=dict(required=True), + project_domain=dict(), + state=dict(default='present', choices=['absent', 'present']), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + # TODO: Merge with equal function from compute_flavor_access module. + def _project_and_project_domain(self): + project_name_or_id = self.params['project'] + project_domain_name_or_id = self.params['project_domain'] + + if project_domain_name_or_id: + domain_id = self.conn.identity.find_domain( + project_domain_name_or_id, ignore_missing=False).id + else: + domain_id = None + + kwargs = dict() if domain_id is None else dict(domain_id=domain_id) + + if project_name_or_id: + project_id = self.conn.identity.find_project( + project_name_or_id, ignore_missing=False, *kwargs).id + else: + project_id = None + + return project_id, domain_id + + def run(self): + name_or_id = self.params['name'] + + # Workaround for an issue in openstacksdk where + # self.conn.block_storage.find_type() will not + # find private volume types. + volume_types = \ + list(self.conn.block_storage.types(is_public=False)) \ + + list(self.conn.block_storage.types(is_public=True)) + + volume_type = [volume_type for volume_type in volume_types + if volume_type.id == name_or_id + or volume_type.name == name_or_id][0] + + state = self.params['state'] + if state == 'present' and volume_type.is_public: + raise ValueError('access can only be granted to private types') + + project_id, domain_id = self._project_and_project_domain() + + volume_type_access = \ + self.conn.block_storage.get_type_access(volume_type.id) + project_ids = [access.get('project_id') + for access in volume_type_access] + + if (project_id in project_ids and state == 'present') \ + or (project_id not in project_ids and state == 'absent'): + self.exit_json(changed=False, + volume_type=volume_type.to_dict(computed=False)) + + if self.ansible.check_mode: + self.exit_json(changed=True, + volume_type=volume_type.to_dict(computed=False)) + + if project_id in project_ids: # and state == 'absent' + self.conn.block_storage.remove_type_access(volume_type.id, + project_id) + else: # project_id not in project_ids and state == 'present' + self.conn.block_storage.add_type_access(volume_type.id, + project_id) + + self.exit_json(changed=True, + volume_type=volume_type.to_dict(computed=False)) + + +def main(): + module = VolumeTypeAccess() + module() + + +if __name__ == '__main__': + main()