From 99074ccd128d35a3ccfc49f5f1625eae23104eea Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 28 Sep 2022 11:26:31 +0200 Subject: [PATCH] Refactored baremetal_port and baremetal_port_info modules Sorted argument specs and documentation of both modules. Refactored both modules to be subclasses of OpenStackModule class. Renamed baremetal_port's module attributes 'uuid' to 'id' and 'portgroup' to 'port_group' to match openstacksdk. Added the previous attribute names as aliases to keep backward compatibility. Added alias 'pxe_enabled' for 'is_pxe_enabled' which was previously set programmatically. Changed baremetal_port module to return attribute 'port' only when state is present. It will return no values (except Ansible's default values) when state is absent. Previous return value 'id' can be retrieved from port's dictionary entry 'id'. The non-standard return value 'result' has been dropped because its content can easily be reconstructed with Ansible's is changed check. The non-standard return value 'changes' has been dropped because its content was only returned on updates, has no known uses and can easily be reconstructed in Ansible by comparing the updated port dictionary with a copy of the pre-updated port dictionary. Module baremetal_port_info will no longer fail when no port with a matching id or name or address could be found. Instead it will return an empty list like other *_info modules. baremetal_port_info's return attribute 'baremetal_ports' has been renamed to 'ports' to be consistent with other modules. The former name will keep to be available for now to keep backward compatibility. Both modules convert their return values into dictionaries without computed (redundant) values. They do not drop values such as links anymore though, because we do not withhold information from users. Updated DOCUMENTATION, EXAMPLES and RETURN docstrings in both modules. Added integration tests for both modules. They will not run in CI atm, because we do not have Ironic enabled in our DevStack environment. Change-Id: I54b3ea9917fbbbdf381ef934a0d92e2857f6d51b --- ci/roles/baremetal_port/defaults/main.yml | 14 + ci/roles/baremetal_port/tasks/main.yml | 112 ++++++++ plugins/modules/baremetal_port.py | 319 +++++++++------------- plugins/modules/baremetal_port_info.py | 166 ++++++----- 4 files changed, 326 insertions(+), 285 deletions(-) create mode 100644 ci/roles/baremetal_port/defaults/main.yml create mode 100644 ci/roles/baremetal_port/tasks/main.yml diff --git a/ci/roles/baremetal_port/defaults/main.yml b/ci/roles/baremetal_port/defaults/main.yml new file mode 100644 index 00000000..874dcc4f --- /dev/null +++ b/ci/roles/baremetal_port/defaults/main.yml @@ -0,0 +1,14 @@ +expected_fields: + - address + - created_at + - extra + - id + - internal_info + - is_pxe_enabled + - links + - local_link_connection + - name + - node_id + - physical_network + - port_group_id + - updated_at diff --git a/ci/roles/baremetal_port/tasks/main.yml b/ci/roles/baremetal_port/tasks/main.yml new file mode 100644 index 00000000..97c7ad23 --- /dev/null +++ b/ci/roles/baremetal_port/tasks/main.yml @@ -0,0 +1,112 @@ +--- +# TODO: Actually run this role in CI. Atm we do not have DevStack's ironic plugin enabled. +- name: Create baremetal node + openstack.cloud.baremetal_node: + cloud: "{{ cloud }}" + driver_info: + ipmi_address: "1.2.3.4" + ipmi_username: "admin" + ipmi_password: "secret" + name: ansible_baremetal_node + nics: + - mac: "aa:bb:cc:aa:bb:cc" + state: present + register: node + +- name: Create baremetal port + openstack.cloud.baremetal_port: + cloud: "{{ cloud }}" + state: present + node: ansible_baremetal_node + address: fa:16:3e:aa:aa:aa + is_pxe_enabled: False + register: port + +- debug: var=port + +- name: Assert return values of baremetal_port module + assert: + that: + - not port.port.is_pxe_enabled + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(port.port.keys())|length == 0 + +- name: Fetch baremetal ports + openstack.cloud.baremetal_port_info: + cloud: "{{ cloud }}" + register: ports + +- name: Assert module results of baremetal_port_info module + assert: + that: + - ports.ports|list|length > 0 + +- name: assert return values of baremetal_port_info module + assert: + that: + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(ports.ports.0.keys())|length == 0 + +- name: Fetch baremetal port by id + openstack.cloud.baremetal_port_info: + cloud: "{{ cloud }}" + id: "{{ port.port.id }}" + register: ports + +- name: assert module results of baremetal_port_info module + assert: + that: + - ports.ports|list|length == 1 + - ports.ports.0.id == port.port.id + +- name: Update baremetal port + openstack.cloud.baremetal_port: + cloud: "{{ cloud }}" + state: present + id: "{{ port.port.id }}" + is_pxe_enabled: True + register: updated_port + +- name: Assert return values of updated baremetal port + assert: + that: + - update_port is changed + - update_port.port.id == port.port.id + - update_port.port.address == port.port.address + - update_port.port.is_pxe_enabled + +- name: Update baremetal port again + openstack.cloud.baremetal_port: + cloud: "{{ cloud }}" + state: present + id: "{{ port.port.id }}" + is_pxe_enabled: True + register: updated_port + +- name: Assert return values of updated baremetal port + assert: + that: + - update_port is not changed + - update_port.port.id == port.port.id + +- name: Delete Bare Metal port + openstack.cloud.baremetal_port: + cloud: "{{ cloud }}" + state: absent + id: "{{ port.port.id }}" + +- name: Fetch baremetal ports + openstack.cloud.baremetal_port_info: + cloud: "{{ cloud }}" + register: ports + +- name: Assert no baremetal port is left + assert: + that: + - ports.ports|list|length == 0 + +- name: Delete baremetal node + openstack.cloud.baremetal_node: + cloud: "{{ cloud }}" + name: ansible_baremetal_node + state: absent diff --git a/plugins/modules/baremetal_port.py b/plugins/modules/baremetal_port.py index 13c7a15c..31515c70 100644 --- a/plugins/modules/baremetal_port.py +++ b/plugins/modules/baremetal_port.py @@ -4,37 +4,33 @@ # Copyright (c) 2021 by Red Hat, Inc. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' module: baremetal_port short_description: Create/Delete Bare Metal port Resources from OpenStack author: OpenStack Ansible SIG description: - Create, Update and Remove ironic ports from OpenStack. options: - state: - description: - - Indicates desired state of the resource - choices: ['present', 'absent'] - default: present - type: str - uuid: - description: - - globally unique identifier (UUID) to be given to the resource. Will - be auto-generated if not specified. - type: str - node: - description: - - UUID or Name of the Node this resource belongs to. - type: str address: description: - Physical hardware address of this network Port, typically the hardware MAC address. type: str - portgroup: + extra: description: - - UUID or Name of the Portgroup this resource belongs to. + - A set of one or more arbitrary metadata key and value pairs. + type: dict + id: + description: + - ID of the Port. + - Will be auto-generated if not specified. type: str + aliases: ['uuid'] + is_pxe_enabled: + description: + - Whether PXE should be enabled or disabled on the Port. + type: bool + aliases: ['pxe_enabled'] local_link_connection: description: - The Port binding profile. @@ -54,25 +50,25 @@ options: - An optional string field to be used to store any vendor-specific information. type: str - is_pxe_enabled: + node: description: - - Whether PXE should be enabled or disabled on the Port. - type: bool + - ID or Name of the Node this resource belongs to. + type: str physical_network: description: - The name of the physical network to which a port is connected. type: str - extra: + port_group: description: - - A set of one or more arbitrary metadata key and value pairs. - type: dict - ironic_url: - description: - - If noauth mode is utilized, this is required to be set to the - endpoint URL for the Ironic API. Use with "auth" and "auth_type" - settings set to None. + - ID or Name of the portgroup this resource belongs to. + type: str + aliases: ['portgroup'] + state: + description: + - Indicates desired state of the resource + choices: ['present', 'absent'] + default: present type: str - requirements: - "python >= 3.6" - "openstacksdk" @@ -80,15 +76,14 @@ extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Create Bare Metal port +EXAMPLES = r''' - name: Create Bare Metal port openstack.cloud.baremetal_port: cloud: devstack state: present node: bm-0 address: fa:16:3e:aa:aa:aa - pxe_enabled: True + is_pxe_enabled: True local_link_connection: switch_id: 0a:1b:2c:3d:4e:5f port_id: Ethernet3/1 @@ -97,45 +92,32 @@ EXAMPLES = ''' something: extra physical_network: datacenter register: result -# Delete Bare Metal port + - name: Delete Bare Metal port openstack.cloud.baremetal_port: cloud: devstack state: absent address: fa:16:3e:aa:aa:aa register: result -# Update Bare Metal port + - name: Update Bare Metal port openstack.cloud.baremetal_port: cloud: devstack state: present - uuid: 1a85ebca-22bf-42eb-ad9e-f640789b8098 - pxe_enabled: False + id: 1a85ebca-22bf-42eb-ad9e-f640789b8098 + is_pxe_enabled: False local_link_connection: switch_id: a0:b1:c2:d3:e4:f5 port_id: Ethernet4/12 switch_info: switch2 ''' -RETURN = ''' -id: - description: Unique UUID of the port. - returned: always, but can be null - type: str -result: - description: A short text describing the result. - returned: success - type: str -changes: - description: Map showing from -> to values for properties that was changed - after port update. - returned: success - type: dict +RETURN = r''' port: description: A port dictionary, subset of the dictionary keys listed below may be returned, depending on your cloud provider. returned: success - type: complex + type: dict contains: address: description: Physical hardware address of this network Port, @@ -164,6 +146,11 @@ port: description: Whether PXE is enabled or disabled on the Port. returned: success type: bool + links: + description: A list of relative links, including the self and + bookmark links. + returned: success + type: list local_link_connection: description: The Port binding profile. If specified, must contain switch_id (only a MAC address or an OpenFlow based @@ -202,171 +189,113 @@ port: type: str ''' -from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( - IronicModule, - ironic_argument_spec, -) from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( - openstack_module_kwargs, - openstack_cloud_from_module + OpenStackModule ) -_PROP_TO_ATTR_MAP = { - 'pxe_enabled': 'is_pxe_enabled', - 'address': 'address', - 'extra': 'extra', - 'local_link_connection': 'local_link_connection', - 'physical_network': 'physical_network', - 'node_uuid': 'node_id', - 'portgroup_uuid': 'port_group_id', - 'uuid': 'id', -} +class BaremetalPortModule(OpenStackModule): + argument_spec = dict( + address=dict(), + extra=dict(type='dict'), + id=dict(aliases=['uuid']), + is_pxe_enabled=dict(type='bool', aliases=['pxe_enabled']), + local_link_connection=dict(type='dict'), + node=dict(), + physical_network=dict(), + port_group=dict(aliases=['portgroup']), + state=dict(default='present', choices=['present', 'absent']), + ) -def find_port(module, cloud): - port = None - if module.params['uuid']: - port = cloud.baremetal.find_port(module.params['uuid']) - elif module.params['address']: - ports = list(cloud.baremetal.ports(address=module.params['address'], - details=True)) - if ports and len(ports) == 1: - port = ports[0] - elif len(ports) > 1: - module.fail_json( - msg="Multiple ports with address {address} found. A uuid must " - "be defined in order to identify the correct port" - .format(address=module.params['address'])) + module_kwargs = dict( + required_one_of=[ + ('id', 'address'), + ], + required_if=[ + ('state', 'present', ('node', 'address',), False), + ], + ) - return port + def run(self): + port = self._find_port() + state = self.params['state'] + if state == 'present': + # create or update port + kwargs = {} + id = self.params['id'] + if id: + kwargs['id'] = id -def add_port(module, cloud): - port = find_port(module, cloud) - if port: - update_port(module, cloud, port=port) + node_name_or_id = self.params['node'] + # assert node_name_or_id + node = self.conn.baremetal.find_node(node_name_or_id, + ignore_missing=False) + kwargs['node_id'] = node['id'] - if not module.params['node'] or not module.params['address']: - module.fail_json( - msg="A Bare Metal node (name or uuid) and an address is required " - "to create a port") + port_group_name_or_id = self.params['port_group'] + if port_group_name_or_id: + port_group = self.conn.baremetal.find_port_group( + port_group_name_or_id, ignore_missing=False) + kwargs['port_group_id'] = port_group['id'] - machine = cloud.get_machine(module.params['node']) - if not machine: - module.fail_json( - msg="Bare Metal node {node} could not be found".format( - node=module.params['node'])) + for k in ['address', 'extra', 'is_pxe_enabled', + 'local_link_connection', 'physical_network']: + if self.params[k] is not None: + kwargs[k] = self.params[k] - module.params['node_uuid'] = machine.id - props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() - if k in module.params} - port = cloud.baremetal.create_port(**props) - port_dict = port.to_dict() - port_dict.pop('links', None) - module.exit_json( - changed=True, - result="Port successfully created", - changes=None, - port=port_dict, - id=port_dict['id']) + changed = True + if not port: + # create port + port = self.conn.baremetal.create_port(**kwargs) + else: + # update port + updates = dict((k, v) + for k, v in kwargs.items() + if v != port[k]) + if updates: + port = \ + self.conn.baremetal.update_port(port['id'], **updates) + else: + changed = False -def update_port(module, cloud, port=None): - if not port: - port = find_port(module, cloud) + self.exit_json(changed=changed, port=port.to_dict(computed=False)) - if module.params['node']: - machine = cloud.get_machine(module.params['node']) - if machine: - module.params['node_uuid'] = machine.id + if state == 'absent': + # remove port + if not port: + self.exit_json(changed=False) - old_props = {k: port[v] for k, v in _PROP_TO_ATTR_MAP.items()} - new_props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() - if k in module.params and module.params[k] is not None} - prop_diff = {k: new_props[k] for k in _PROP_TO_ATTR_MAP.keys() - if k in new_props and old_props[k] != new_props[k]} + port = self.conn.baremetal.delete_port(port['id']) + self.exit_json(changed=True) - if not prop_diff: - port_dict = port.to_dict() - port_dict.pop('links', None) - module.exit_json( - changed=False, - result="No port update required", - changes=None, - port=port_dict, - id=port_dict['id']) + def _find_port(self): + id = self.params['id'] + if id: + return self.conn.baremetal.get_port(id) - port = cloud.baremetal.update_port(port.id, **prop_diff) - port_dict = port.to_dict() - port_dict.pop('links', None) - module.exit_json( - changed=True, - result="Port successfully updated", - changes={k: {'to': new_props[k], 'from': old_props[k]} - for k in prop_diff}, - port=port_dict, - id=port_dict['id']) + address = self.params['address'] + if address: + ports = list(self.conn.baremetal.ports(address=address, + details=True)) + if len(ports) == 1: + return ports[0] + elif len(ports) > 1: + raise ValueError( + 'Multiple ports with address {address} found. A ID' + ' must be defined in order to identify a unique' + ' port.'.format(address=address)) + else: + return None -def remove_port(module, cloud): - if not module.params['uuid'] and not module.params['address']: - module.fail_json( - msg="A uuid or an address value must be defined in order to " - "remove a port.") - if module.params['uuid']: - port = cloud.baremetal.delete_port(module.params['uuid']) - if not port: - module.exit_json( - changed=False, - result="Port not found", - changes=None, - id=module.params['uuid']) - else: - port = find_port(module, cloud) - if not port: - module.exit_json( - changed=False, - result="Port not found", - changes=None, - id=None) - port = cloud.baremetal.delete_port(port.id) - - module.exit_json( - changed=True, - result="Port successfully removed", - changes=None, - id=port.id) + raise AssertionError("id or address must be specified") def main(): - argument_spec = ironic_argument_spec( - uuid=dict(), - node=dict(), - address=dict(), - portgroup=dict(), - local_link_connection=dict(type='dict'), - is_pxe_enabled=dict(type='bool'), - physical_network=dict(), - extra=dict(type='dict'), - state=dict(default='present', - choices=['present', 'absent']) - ) - - module_kwargs = openstack_module_kwargs() - module = IronicModule(argument_spec, **module_kwargs) - - module.params['pxe_enabled'] = module.params.pop('is_pxe_enabled', None) - - sdk, cloud = openstack_cloud_from_module(module) - try: - if module.params['state'] == 'present': - add_port(module, cloud) - - if module.params['state'] == 'absent': - remove_port(module, cloud) - - except sdk.exceptions.OpenStackCloudException as e: - module.fail_json(msg=str(e)) + module = BaremetalPortModule() + module() if __name__ == "__main__": diff --git a/plugins/modules/baremetal_port_info.py b/plugins/modules/baremetal_port_info.py index 2c538407..9fb070d0 100644 --- a/plugins/modules/baremetal_port_info.py +++ b/plugins/modules/baremetal_port_info.py @@ -4,32 +4,26 @@ # Copyright (c) 2021 by Red Hat, Inc. # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -DOCUMENTATION = ''' +DOCUMENTATION = r''' module: baremetal_port_info short_description: Retrieve information about Bare Metal ports from OpenStack author: OpenStack Ansible SIG description: - Retrieve information about Bare Metal ports from OpenStack. options: - uuid: - description: - - Name or globally unique identifier (UUID) to identify the port. - type: str address: description: - Physical hardware address of this network Port, typically the hardware MAC address. type: str + name: + description: + - Name or ID of the Bare Metal port. + type: str + aliases: ['uuid'] node: description: - - Name or globally unique identifier (UUID) to identify a Baremetal - Node. - type: str - ironic_url: - description: - - If noauth mode is utilized, this is required to be set to the - endpoint URL for the Ironic API. Use with "auth" and "auth_type" - settings set to None. + - Name or ID of a Bare Metal node. type: str requirements: - "python >= 3.6" @@ -38,34 +32,31 @@ extends_documentation_fragment: - openstack.cloud.openstack ''' -EXAMPLES = ''' -# Gather information about all baremetal ports -- openstack.cloud.baremetal_port_info: +EXAMPLES = r''' +- name: Gather information about all baremetal ports + openstack.cloud.baremetal_port_info: cloud: devstack - register: result -# Gather information about a baremetal port by address -- openstack.cloud.baremetal_port_info: + +- name: Gather information about a baremetal port by address + openstack.cloud.baremetal_port_info: cloud: devstack address: fa:16:3e:aa:aa:aa - register: result -# Gather information about a baremetal port by address -- openstack.cloud.baremetal_port_info: + +- name: Gather information about a baremetal port by address + openstack.cloud.baremetal_port_info: cloud: devstack - uuid: a2b6bd99-77b9-43f0-9ddc-826568e68dec - register: result -# Gather information about a baremetal ports associated with a baremetal node -- openstack.cloud.baremetal_port_info: + name: a2b6bd99-77b9-43f0-9ddc-826568e68dec + +- name: Gather information about a baremetal ports associated with a node + openstack.cloud.baremetal_port_info: cloud: devstack node: bm-0 - register: result ''' -RETURN = ''' -baremetal_ports: - description: Bare Metal port list. A subset of the dictionary keys - listed below may be returned, depending on your cloud - provider. - returned: always, but can be null +RETURN = r''' +ports: + description: Bare Metal port list. + returned: always type: list elements: dict contains: @@ -96,6 +87,11 @@ baremetal_ports: description: Whether PXE is enabled or disabled on the Port. returned: success type: bool + links: + description: A list of relative links, including the self and + bookmark links. + returned: success + type: list local_link_connection: description: The Port binding profile. returned: success @@ -141,68 +137,58 @@ baremetal_ports: type: str ''' - -from ansible_collections.openstack.cloud.plugins.module_utils.ironic import ( - IronicModule, - ironic_argument_spec, -) from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( - openstack_module_kwargs, - openstack_cloud_from_module + OpenStackModule ) +class BaremetalPortInfoModule(OpenStackModule): + argument_spec = dict( + address=dict(), + name=dict(aliases=['uuid']), + node=dict(), + ) + + module_kwargs = dict( + supports_check_mode=True, + ) + + def _fetch_ports(self): + name_or_id = self.params['name'] + + if name_or_id: + port = self.conn.baremetal.find_port(name_or_id) + return [port] if port else [] + + kwargs = {} + address = self.params['address'] + if address: + kwargs['address'] = address + + node_name_or_id = self.params['node'] + if node_name_or_id: + node = self.conn.baremetal.find_node(node_name_or_id) + if node: + kwargs['node_uuid'] = node['id'] + else: + # node does not exist so no port could possibly be found + return [] + + return self.conn.baremetal.ports(details=True, **kwargs) + + def run(self): + ports = [port.to_dict(computed=False) + for port in self._fetch_ports()] + + self.exit_json(changed=False, + ports=ports, + # keep for backward compatibility + baremetal_ports=ports) + + def main(): - argument_spec = ironic_argument_spec( - uuid=dict(), - address=dict(), - node=dict(), - ) - module_kwargs = openstack_module_kwargs() - module_kwargs['supports_check_mode'] = True - module = IronicModule(argument_spec, **module_kwargs) - - ports = list() - sdk, cloud = openstack_cloud_from_module(module) - try: - if module.params['uuid']: - port = cloud.baremetal.find_port(module.params['uuid']) - if not port: - module.fail_json( - msg='Baremetal port with uuid {uuid} was not found' - .format(uuid=module.params['uuid'])) - ports.append(port) - - elif module.params['address']: - ports = list( - cloud.baremetal.ports(address=module.params['address'], - details=True)) - if not ports: - module.fail_json( - msg='Baremetal port with address {address} was not found' - .format(address=module.params['address'])) - - elif module.params['node']: - machine = cloud.get_machine(module.params['node']) - if not machine: - module.fail_json( - msg='Baremetal node {node} was not found' - .format(node=module.params['node'])) - ports = list( - cloud.baremetal.ports(node_uuid=machine.uuid, details=True)) - - else: - ports = list(cloud.baremetal.ports(details=True)) - - # Convert ports to dictionaries and cleanup properties - ports = [port.to_dict() for port in ports] - for port in ports: - # links are not useful - port.pop('links', None) - - module.exit_json(changed=False, baremetal_ports=ports) - except sdk.exceptions.OpenStackCloudException as e: - module.fail_json(msg=str(e)) + module = BaremetalPortInfoModule() + module() if __name__ == "__main__":