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
This commit is contained in:
Jakob Meng
2022-09-28 11:26:31 +02:00
parent 65a7e74b2b
commit 99074ccd12
4 changed files with 326 additions and 285 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -4,37 +4,33 @@
# Copyright (c) 2021 by Red Hat, Inc. # 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) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = ''' DOCUMENTATION = r'''
module: baremetal_port module: baremetal_port
short_description: Create/Delete Bare Metal port Resources from OpenStack short_description: Create/Delete Bare Metal port Resources from OpenStack
author: OpenStack Ansible SIG author: OpenStack Ansible SIG
description: description:
- Create, Update and Remove ironic ports from OpenStack. - Create, Update and Remove ironic ports from OpenStack.
options: 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: address:
description: description:
- Physical hardware address of this network Port, typically the - Physical hardware address of this network Port, typically the
hardware MAC address. hardware MAC address.
type: str type: str
portgroup: extra:
description: 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 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: local_link_connection:
description: description:
- The Port binding profile. - The Port binding profile.
@@ -54,25 +50,25 @@ options:
- An optional string field to be used to store any vendor-specific - An optional string field to be used to store any vendor-specific
information. information.
type: str type: str
is_pxe_enabled: node:
description: description:
- Whether PXE should be enabled or disabled on the Port. - ID or Name of the Node this resource belongs to.
type: bool type: str
physical_network: physical_network:
description: description:
- The name of the physical network to which a port is connected. - The name of the physical network to which a port is connected.
type: str type: str
extra: port_group:
description: description:
- A set of one or more arbitrary metadata key and value pairs. - ID or Name of the portgroup this resource belongs to.
type: dict type: str
ironic_url: aliases: ['portgroup']
description: state:
- If noauth mode is utilized, this is required to be set to the description:
endpoint URL for the Ironic API. Use with "auth" and "auth_type" - Indicates desired state of the resource
settings set to None. choices: ['present', 'absent']
default: present
type: str type: str
requirements: requirements:
- "python >= 3.6" - "python >= 3.6"
- "openstacksdk" - "openstacksdk"
@@ -80,15 +76,14 @@ extends_documentation_fragment:
- openstack.cloud.openstack - openstack.cloud.openstack
''' '''
EXAMPLES = ''' EXAMPLES = r'''
# Create Bare Metal port
- name: Create Bare Metal port - name: Create Bare Metal port
openstack.cloud.baremetal_port: openstack.cloud.baremetal_port:
cloud: devstack cloud: devstack
state: present state: present
node: bm-0 node: bm-0
address: fa:16:3e:aa:aa:aa address: fa:16:3e:aa:aa:aa
pxe_enabled: True is_pxe_enabled: True
local_link_connection: local_link_connection:
switch_id: 0a:1b:2c:3d:4e:5f switch_id: 0a:1b:2c:3d:4e:5f
port_id: Ethernet3/1 port_id: Ethernet3/1
@@ -97,45 +92,32 @@ EXAMPLES = '''
something: extra something: extra
physical_network: datacenter physical_network: datacenter
register: result register: result
# Delete Bare Metal port
- name: Delete Bare Metal port - name: Delete Bare Metal port
openstack.cloud.baremetal_port: openstack.cloud.baremetal_port:
cloud: devstack cloud: devstack
state: absent state: absent
address: fa:16:3e:aa:aa:aa address: fa:16:3e:aa:aa:aa
register: result register: result
# Update Bare Metal port
- name: Update Bare Metal port - name: Update Bare Metal port
openstack.cloud.baremetal_port: openstack.cloud.baremetal_port:
cloud: devstack cloud: devstack
state: present state: present
uuid: 1a85ebca-22bf-42eb-ad9e-f640789b8098 id: 1a85ebca-22bf-42eb-ad9e-f640789b8098
pxe_enabled: False is_pxe_enabled: False
local_link_connection: local_link_connection:
switch_id: a0:b1:c2:d3:e4:f5 switch_id: a0:b1:c2:d3:e4:f5
port_id: Ethernet4/12 port_id: Ethernet4/12
switch_info: switch2 switch_info: switch2
''' '''
RETURN = ''' RETURN = r'''
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
port: port:
description: A port dictionary, subset of the dictionary keys listed below description: A port dictionary, subset of the dictionary keys listed below
may be returned, depending on your cloud provider. may be returned, depending on your cloud provider.
returned: success returned: success
type: complex type: dict
contains: contains:
address: address:
description: Physical hardware address of this network Port, description: Physical hardware address of this network Port,
@@ -164,6 +146,11 @@ port:
description: Whether PXE is enabled or disabled on the Port. description: Whether PXE is enabled or disabled on the Port.
returned: success returned: success
type: bool type: bool
links:
description: A list of relative links, including the self and
bookmark links.
returned: success
type: list
local_link_connection: local_link_connection:
description: The Port binding profile. If specified, must contain description: The Port binding profile. If specified, must contain
switch_id (only a MAC address or an OpenFlow based switch_id (only a MAC address or an OpenFlow based
@@ -202,171 +189,113 @@ port:
type: str 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 ( from ansible_collections.openstack.cloud.plugins.module_utils.openstack import (
openstack_module_kwargs, OpenStackModule
openstack_cloud_from_module
) )
_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): module_kwargs = dict(
port = None required_one_of=[
if module.params['uuid']: ('id', 'address'),
port = cloud.baremetal.find_port(module.params['uuid']) ],
elif module.params['address']: required_if=[
ports = list(cloud.baremetal.ports(address=module.params['address'], ('state', 'present', ('node', 'address',), False),
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']))
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): node_name_or_id = self.params['node']
port = find_port(module, cloud) # assert node_name_or_id
if port: node = self.conn.baremetal.find_node(node_name_or_id,
update_port(module, cloud, port=port) ignore_missing=False)
kwargs['node_id'] = node['id']
if not module.params['node'] or not module.params['address']: port_group_name_or_id = self.params['port_group']
module.fail_json( if port_group_name_or_id:
msg="A Bare Metal node (name or uuid) and an address is required " port_group = self.conn.baremetal.find_port_group(
"to create a port") port_group_name_or_id, ignore_missing=False)
kwargs['port_group_id'] = port_group['id']
machine = cloud.get_machine(module.params['node']) for k in ['address', 'extra', 'is_pxe_enabled',
if not machine: 'local_link_connection', 'physical_network']:
module.fail_json( if self.params[k] is not None:
msg="Bare Metal node {node} could not be found".format( kwargs[k] = self.params[k]
node=module.params['node']))
module.params['node_uuid'] = machine.id changed = True
props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() if not port:
if k in module.params} # create port
port = cloud.baremetal.create_port(**props) port = self.conn.baremetal.create_port(**kwargs)
port_dict = port.to_dict() else:
port_dict.pop('links', None) # update port
module.exit_json( updates = dict((k, v)
changed=True, for k, v in kwargs.items()
result="Port successfully created", if v != port[k])
changes=None,
port=port_dict,
id=port_dict['id'])
if updates:
port = \
self.conn.baremetal.update_port(port['id'], **updates)
else:
changed = False
def update_port(module, cloud, port=None): self.exit_json(changed=changed, port=port.to_dict(computed=False))
if not port:
port = find_port(module, cloud)
if module.params['node']: if state == 'absent':
machine = cloud.get_machine(module.params['node']) # remove port
if machine: if not port:
module.params['node_uuid'] = machine.id self.exit_json(changed=False)
old_props = {k: port[v] for k, v in _PROP_TO_ATTR_MAP.items()} port = self.conn.baremetal.delete_port(port['id'])
new_props = {k: module.params[k] for k in _PROP_TO_ATTR_MAP.keys() self.exit_json(changed=True)
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]}
if not prop_diff: def _find_port(self):
port_dict = port.to_dict() id = self.params['id']
port_dict.pop('links', None) if id:
module.exit_json( return self.conn.baremetal.get_port(id)
changed=False,
result="No port update required",
changes=None,
port=port_dict,
id=port_dict['id'])
port = cloud.baremetal.update_port(port.id, **prop_diff) address = self.params['address']
port_dict = port.to_dict() if address:
port_dict.pop('links', None) ports = list(self.conn.baremetal.ports(address=address,
module.exit_json( details=True))
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'])
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): raise AssertionError("id or address must be specified")
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)
def main(): def main():
argument_spec = ironic_argument_spec( module = BaremetalPortModule()
uuid=dict(), module()
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))
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -4,32 +4,26 @@
# Copyright (c) 2021 by Red Hat, Inc. # 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) # 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 module: baremetal_port_info
short_description: Retrieve information about Bare Metal ports from OpenStack short_description: Retrieve information about Bare Metal ports from OpenStack
author: OpenStack Ansible SIG author: OpenStack Ansible SIG
description: description:
- Retrieve information about Bare Metal ports from OpenStack. - Retrieve information about Bare Metal ports from OpenStack.
options: options:
uuid:
description:
- Name or globally unique identifier (UUID) to identify the port.
type: str
address: address:
description: description:
- Physical hardware address of this network Port, typically the - Physical hardware address of this network Port, typically the
hardware MAC address. hardware MAC address.
type: str type: str
name:
description:
- Name or ID of the Bare Metal port.
type: str
aliases: ['uuid']
node: node:
description: description:
- Name or globally unique identifier (UUID) to identify a Baremetal - Name or ID of a Bare Metal node.
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.
type: str type: str
requirements: requirements:
- "python >= 3.6" - "python >= 3.6"
@@ -38,34 +32,31 @@ extends_documentation_fragment:
- openstack.cloud.openstack - openstack.cloud.openstack
''' '''
EXAMPLES = ''' EXAMPLES = r'''
# Gather information about all baremetal ports - name: Gather information about all baremetal ports
- openstack.cloud.baremetal_port_info: openstack.cloud.baremetal_port_info:
cloud: devstack cloud: devstack
register: result
# Gather information about a baremetal port by address - name: Gather information about a baremetal port by address
- openstack.cloud.baremetal_port_info: openstack.cloud.baremetal_port_info:
cloud: devstack cloud: devstack
address: fa:16:3e:aa:aa:aa address: fa:16:3e:aa:aa:aa
register: result
# Gather information about a baremetal port by address - name: Gather information about a baremetal port by address
- openstack.cloud.baremetal_port_info: openstack.cloud.baremetal_port_info:
cloud: devstack cloud: devstack
uuid: a2b6bd99-77b9-43f0-9ddc-826568e68dec name: a2b6bd99-77b9-43f0-9ddc-826568e68dec
register: result
# Gather information about a baremetal ports associated with a baremetal node - name: Gather information about a baremetal ports associated with a node
- openstack.cloud.baremetal_port_info: openstack.cloud.baremetal_port_info:
cloud: devstack cloud: devstack
node: bm-0 node: bm-0
register: result
''' '''
RETURN = ''' RETURN = r'''
baremetal_ports: ports:
description: Bare Metal port list. A subset of the dictionary keys description: Bare Metal port list.
listed below may be returned, depending on your cloud returned: always
provider.
returned: always, but can be null
type: list type: list
elements: dict elements: dict
contains: contains:
@@ -96,6 +87,11 @@ baremetal_ports:
description: Whether PXE is enabled or disabled on the Port. description: Whether PXE is enabled or disabled on the Port.
returned: success returned: success
type: bool type: bool
links:
description: A list of relative links, including the self and
bookmark links.
returned: success
type: list
local_link_connection: local_link_connection:
description: The Port binding profile. description: The Port binding profile.
returned: success returned: success
@@ -141,68 +137,58 @@ baremetal_ports:
type: str 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 ( from ansible_collections.openstack.cloud.plugins.module_utils.openstack import (
openstack_module_kwargs, OpenStackModule
openstack_cloud_from_module
) )
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(): def main():
argument_spec = ironic_argument_spec( module = BaremetalPortInfoModule()
uuid=dict(), module()
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))
if __name__ == "__main__": if __name__ == "__main__":