Refactored baremetal_node and baremetal_node_info 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.

Sorted argument specs and documentation of both modules.

Refactored both modules to be subclasses of OpenStackModule class.

Renamed baremetal_node_info's module attribute 'node' to 'name' and
added the former as an alias to be consistent with other *_info
modules.

baremetal_node_info will no longer fetch port and portgroup details
because this requires extra api calls for each node. Users can use
the baremetal_port module to retrieve ports for each node on demand.

Refactored code for constructing node updates in baremetal_node module
which allowed us to drop the dependency on Python module jsonpatch.

Deprecated baremetal_node's skip_update_of_masked_password attribute.
Updating or even specificing passwords for nodes has not been
supported for a while now, rendering the attribute useless.

Renamed baremetal_node's module attributes 'chassis_uuid' to
'chassis_id', 'uuid' to 'id' as well as suboptions of
'properties' to match openstacksdk. Added the previous attribute
names as aliases to keep backward compatibility.
Marked nics attribute in baremetal_node as not updatable.

Changed baremetal_node module to return attribute 'node' only when
state is present. It will return no values (except Ansible's default
values) when state is absent. Previous return value 'uuid' can be
retrieved from node'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 it
was only returned on updates, has no known uses and can easily be
reconstructed in Ansible by comparing the returned node dictionary
with a copy of a previous node dictionary.

Module baremetal_node_info will no longer fail when no node with a
matching id or name or mac could be found. Instead it will return
an empty list like other *_info modules.

baremetal_node_info's return attribute 'baremetal_nodes' has been
renamed to 'nodes' 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.

Dropped deprecated ironic_url attribute from DOCUMENTATION docstring
in baremetal_info. Dropped wait attribute from DOCUMENTATION because
its docstring will be added via documentation fragment.

Kept timeout attribute in DOCUMENTATION and argument_spec because
it has a high(er) default value, to account for long provisioning
times, than what e.g. the generic doc fragment specifies.

Change-Id: If3044acf672295e9b61fa60d0969f47cd06dfdeb
This commit is contained in:
Jakob Meng 2022-09-28 09:12:08 +02:00
parent f51898bd2f
commit 902b2f8147
4 changed files with 807 additions and 624 deletions

View File

@ -0,0 +1,56 @@
expected_fields:
- allocation_id
- bios_interface
- boot_interface
- boot_mode
- chassis_id
- clean_step
- conductor
- conductor_group
- console_interface
- created_at
- deploy_interface
- deploy_step
- driver
- driver_info
- driver_internal_info
- extra
- fault
- id
- inspect_interface
- instance_id
- instance_info
- is_automated_clean_enabled
- is_console_enabled
- is_maintenance
- is_protected
- is_retired
- is_secure_boot
- last_error
- links
- maintenance_reason
- management_interface
- name
- network_interface
- owner
- port_groups
- ports
- power_interface
- power_state
- properties
- protected_reason
- provision_state
- raid_config
- raid_interface
- rescue_interface
- reservation
- resource_class
- retired_reason
- states
- storage_interface
- target_power_state
- target_provision_state
- target_raid_config
- traits
- updated_at
- vendor_interface

View File

@ -0,0 +1,74 @@
---
# 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
- debug: var=node
- name: assert return values of baremetal_node module
assert:
that:
# allow new fields to be introduced but prevent fields from being removed
- expected_fields|difference(node.node.keys())|length == 0
- name: Fetch baremetal nodes
openstack.cloud.baremetal_node_info:
cloud: "{{ cloud }}"
register: nodes
- name: assert module results of baremetal_node_info module
assert:
that:
- nodes.nodes|list|length > 0
- name: assert return values of baremetal_node_info module
assert:
that:
# allow new fields to be introduced but prevent fields from being removed
- expected_fields|difference(nodes.nodes.0.keys())|length == 0
- name: Fetch baremetal node by name
openstack.cloud.baremetal_node_info:
cloud: "{{ cloud }}"
name: ansible_baremetal_node
register: nodes
- name: assert module results of baremetal_node_info module
assert:
that:
- nodes.nodes|list|length == 1
- nodes.nodes.0.id == node.node.id
- nodes.nodes.0.name == "ansible_baremetal_node"
- name: Delete 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: absent
- name: Fetch baremetal node by name
openstack.cloud.baremetal_node_info:
cloud: "{{ cloud }}"
name: ansible_baremetal_node
register: nodes
- name: Assert that baremetal node has been deleted
assert:
that:
- nodes.nodes|list|length == 0

View File

@ -4,7 +4,7 @@
# (c) 2014, Hewlett-Packard Development Company, L.P.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
DOCUMENTATION = r'''
---
module: baremetal_node
short_description: Create/Delete Bare Metal Resources from OpenStack
@ -12,105 +12,69 @@ author: OpenStack Ansible SIG
description:
- Create or Remove Ironic nodes from OpenStack.
options:
state:
bios_interface:
description:
- Indicates desired state of the resource
choices: ['present', 'absent']
default: present
- The bios interface for this node, e.g. C(no-bios).
type: str
uuid:
boot_interface:
description:
- globally unique identifier (UUID) to be given to the resource. Will
be auto-generated if not specified, and name is specified.
- Definition of a UUID will always take precedence to a name value.
- The boot interface for this node, e.g. C(pxe).
type: str
name:
chassis_id:
description:
- unique name identifier to be given to the resource.
- Associate the node with a pre-defined chassis.
type: str
aliases: ['chassis_uuid']
console_interface:
description:
- The console interface for this node, e.g. C(no-console).
type: str
deploy_interface:
description:
- The deploy interface for this node, e.g. C(iscsi).
type: str
driver:
description:
- The name of the Ironic Driver to use with this node.
- Required when I(state=present)
- Required when I(state) is C(present)
type: str
chassis_uuid:
driver_info:
description:
- Associate the node with a pre-defined chassis.
type: str
ironic_url:
- Information for this node's driver. Will vary based on which
driver is in use. Any sub-field which is populated will be validated
during creation. For compatibility reasons sub-fields `power`,
`deploy`, `management` and `console` are flattened.
required: true
type: dict
id:
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
resource_class:
description:
- The specific resource type to which this node belongs.
type: str
bios_interface:
description:
- The bios interface for this node, e.g. "no-bios".
type: str
boot_interface:
description:
- The boot interface for this node, e.g. "pxe".
type: str
console_interface:
description:
- The console interface for this node, e.g. "no-console".
type: str
deploy_interface:
description:
- The deploy interface for this node, e.g. "iscsi".
- ID to be given to the baremetal node. Will be auto-generated on
creation if not specified, and I(name) is specified.
- Definition of I(id) will always take precedence over I(name).
type: str
aliases: ['uuid']
inspect_interface:
description:
- The interface used for node inspection, e.g. "no-inspect".
- The interface used for node inspection, e.g. C(no-inspect).
type: str
management_interface:
description:
- The interface for out-of-band management of this node, e.g.
"ipmitool".
type: str
name:
description:
- unique name identifier to be given to the resource.
type: str
network_interface:
description:
- The network interface provider to use when describing
connections for this node.
type: str
power_interface:
description:
- The interface used to manage power actions on this node, e.g.
"ipmitool".
type: str
raid_interface:
description:
- Interface used for configuring raid on this node.
type: str
rescue_interface:
description:
- Interface used for node rescue, e.g. "no-rescue".
type: str
storage_interface:
description:
- Interface used for attaching and detaching volumes on this node, e.g.
"cinder".
type: str
vendor_interface:
description:
- Interface for all vendor-specific actions on this node, e.g.
"no-vendor".
type: str
driver_info:
description:
- Information for this server's driver. Will vary based on which
driver is in use. Any sub-field which is populated will be validated
during creation. For compatibility reasons sub-fields `power`,
`deploy`, `management` and `console` are flattened.
required: true
type: dict
nics:
description:
- 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"'
- 'A list of network interface cards, eg, C( - mac: aa:bb:cc:aa:bb:cc)'
- This node attribute cannot be updated.
required: true
type: list
elements: dict
@ -119,316 +83,609 @@ options:
description: The MAC address of the network interface card.
type: str
required: true
power_interface:
description:
- The interface used to manage power actions on this node, e.g.
C(ipmitool).
type: str
properties:
description:
- Definition of the physical characteristics of this server, used for scheduling purposes
- Definition of the physical characteristics of this node
- Used for scheduling purposes
type: dict
suboptions:
cpu_arch:
description:
- CPU architecture (x86_64, i686, ...)
default: x86_64
type: str
cpus:
description:
- Number of CPU cores this machine has
default: 1
ram:
type: str
memory_mb:
description:
- amount of RAM this machine has, in MB
default: 1
disk_size:
- Amount of RAM in MB this machine has
aliases: ['ram']
type: str
local_gb:
description:
- size of first storage device in this machine (typically /dev/sda), in GB
default: 1
- Size in GB of first storage device in this machine (typically
/dev/sda)
aliases: ['disk_size']
type: str
capabilities:
description:
- special capabilities for the node, such as boot_option, node_role etc
(see U(https://docs.openstack.org/ironic/latest/install/advanced.html)
for more information)
default: ""
- Special capabilities for this node such as boot_option etc.
- For more information refer to
U(https://docs.openstack.org/ironic/latest/install/advanced.html).
type: str
root_device:
description:
- Root disk device hints for deployment.
- See U(https://docs.openstack.org/ironic/latest/install/advanced.html#specifying-the-disk-for-deployment-root-device-hints)
for allowed hints.
default: ""
- For allowed hints refer to
U(https://docs.openstack.org/ironic/latest/install/advanced.html).
type: dict
raid_interface:
description:
- Interface used for configuring raid on this node.
type: str
rescue_interface:
description:
- Interface used for node rescue, e.g. C(no-rescue).
type: str
resource_class:
description:
- The specific resource type to which this node belongs.
type: str
skip_update_of_masked_password:
description:
- Allows the code that would assert changes to nodes to skip the
update if the change is a single line consisting of the password
field.
- As of Kilo, by default, passwords are always masked to API
requests, which means the logic as a result always attempts to
re-assert the password field.
- Deprecated, no longer used.
- Updating or specifing a password has not been supported for a while.
type: bool
wait:
state:
description:
- A boolean value instructing the module to wait for the newly created
node to reach the available state.
type: bool
default: 'no'
- Indicates desired state of the resource
choices: ['present', 'absent']
default: present
type: str
storage_interface:
description:
- Interface used for attaching and detaching volumes on this node, e.g.
C(cinder).
type: str
timeout:
description:
- An integer value representing the number of seconds to
wait for the newly created node to reach the available state.
default: 1800
- Number of seconds to wait for the newly created node to reach the
available state.
type: int
default: 1800
vendor_interface:
description:
- Interface for all vendor-specific actions on this node, e.g.
C(no-vendor).
type: str
requirements:
- "python >= 3.6"
- "openstacksdk"
- "jsonpatch"
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = '''
# Enroll a node with some basic properties and driver info
- openstack.cloud.baremetal_node:
EXAMPLES = r'''
- name: Enroll a node with some basic properties and driver info
openstack.cloud.baremetal_node:
chassis_id: "00000000-0000-0000-0000-000000000001"
cloud: "devstack"
driver: "pxe_ipmitool"
uuid: "00000000-0000-0000-0000-000000000002"
properties:
cpus: 2
cpu_arch: "x86_64"
ram: 8192
disk_size: 64
capabilities: "boot_option:local"
root_device:
wwn: "0x4000cca77fc4dba1"
nics:
- mac: "aa:bb:cc:aa:bb:cc"
- mac: "dd:ee:ff:dd:ee:ff"
driver_info:
ipmi_address: "1.2.3.4"
ipmi_username: "admin"
ipmi_password: "adminpass"
chassis_uuid: "00000000-0000-0000-0000-000000000001"
id: "00000000-0000-0000-0000-000000000002"
nics:
- mac: "aa:bb:cc:aa:bb:cc"
- mac: "dd:ee:ff:dd:ee:ff"
properties:
capabilities: "boot_option:local"
cpu_arch: "x86_64"
cpus: 2
local_gb: 64
memory_mb: 8192
root_device:
wwn: "0x4000cca77fc4dba1"
'''
try:
import jsonpatch
HAS_JSONPATCH = True
except ImportError:
HAS_JSONPATCH = False
RETURN = r'''
node:
description: Dictionary describing the Bare Metal node.
type: dict
returned: On success when I(state) is 'present'.
contains:
allocation_id:
description: The UUID of the allocation associated with the node.
If not null, will be the same as instance_id (the
opposite is not always true). Unlike instance_id,
this field is read-only. Please use the Allocation API
to remove allocations.
returned: success
type: str
bios_interface:
description: The bios interface to be used for this node.
returned: success
type: str
boot_interface:
description: The boot interface for a node, e.g. "pxe".
returned: success
type: str
boot_mode:
description: The boot mode for a node, either "uefi" or "bios"
returned: success
type: str
chassis_id:
description: UUID of the chassis associated with this node. May be
empty or None.
returned: success
type: str
clean_step:
description: The current clean step.
returned: success
type: str
conductor:
description: |
The conductor currently servicing a node.
returned: success
type: str
conductor_group:
description: The conductor group for a node.
returned: success
type: str
console_interface:
description: The console interface for a node, e.g. "no-console".
returned: success
type: str
created_at:
description: Bare Metal node created at timestamp.
returned: success
type: str
deploy_interface:
description: The deploy interface for a node, e.g. "direct".
returned: success
type: str
deploy_step:
description: The current deploy step.
returned: success
type: str
driver:
description: The name of the driver.
returned: success
type: str
driver_info:
description: All the metadata required by the driver to manage this
node. List of fields varies between drivers, and can
be retrieved from the
/v1/drivers/<DRIVER_NAME>/properties resource.
returned: success
type: dict
driver_internal_info:
description: Internal metadata set and stored by the node's driver.
returned: success
type: dict
extra:
description: A set of one or more arbitrary metadata key and value
pairs.
returned: success
type: dict
fault:
description: The fault indicates the active fault detected by
ironic, typically the node is in "maintenance mode".
None means no fault has been detected by ironic.
"power failure" indicates ironic failed to retrieve
power state from this node. There are other possible
types, e.g., "clean failure" and "rescue abort
failure".
returned: success
type: str
id:
description: The UUID for the resource.
returned: success
type: str
inspect_interface:
description: The interface used for node inspection.
returned: success
type: str
instance_id:
description: UUID of the Nova instance associated with this node.
returned: success
type: str
instance_info:
description: Information used to customize the deployed image. May
include root partition size, a base 64 encoded config
drive, and other metadata. Note that this field is
erased automatically when the instance is deleted
(this is done by requesting the node provision state
be changed to DELETED).
returned: success
type: dict
is_automated_clean_enabled:
description: Indicates whether the node will perform automated
clean or not.
returned: success
type: bool
is_console_enabled:
description: Indicates whether console access is enabled or
disabled on this node.
returned: success
type: bool
is_maintenance:
description: Whether or not this node is currently in "maintenance
mode". Setting a node into maintenance mode removes it
from the available resource pool and halts some
internal automation. This can happen manually (eg, via
an API request) or automatically when Ironic detects a
hardware fault that prevents communication with the
machine.
returned: success
type: bool
is_protected:
description: Whether the node is protected from undeploying,
rebuilding and deletion.
returned: success
type: bool
is_retired:
description: Whether the node is retired and can hence no longer be
provided, i.e. move from manageable to available, and
will end up in manageable after cleaning (rather than
available).
returned: success
type: bool
is_secure_boot:
description: Indicates whether node is currently booted with
secure_boot turned on.
returned: success
type: bool
last_error:
description: Any error from the most recent (last) transaction that
started but failed to finish.
returned: success
type: str
links:
description: A list of relative links, including self and bookmark
links.
returned: success
type: list
maintenance_reason:
description: User-settable description of the reason why this node
was placed into maintenance mode
returned: success
type: str
management_interface:
description: Interface for out-of-band node management.
returned: success
type: str
name:
description: Human-readable identifier for the node resource. May
be undefined. Certain words are reserved.
returned: success
type: str
network_interface:
description: Which Network Interface provider to use when plumbing
the network connections for this node.
returned: success
type: str
owner:
description: A string or UUID of the tenant who owns the object.
returned: success
type: str
ports:
description: List of ironic ports on this node.
returned: success
type: list
port_groups:
description: List of ironic port groups on this node.
returned: success
type: list
power_interface:
description: Interface used for performing power actions on the
node, e.g. "ipmitool".
returned: success
type: str
power_state:
description: The current power state of this node. Usually, "power
on" or "power off", but may be "None" if Ironic is
unable to determine the power state (eg, due to
hardware failure).
returned: success
type: str
properties:
description: Physical characteristics of this node. Populated by
ironic-inspector during inspection. May be edited via
the REST API at any time.
returned: success
type: dict
protected_reason:
description: The reason the node is marked as protected.
returned: success
type: str
provision_state:
description: The current provisioning state of this node.
returned: success
type: str
raid_config:
description: Represents the current RAID configuration of the node.
Introduced with the cleaning feature.
returned: success
type: dict
raid_interface:
description: Interface used for configuring RAID on this node.
returned: success
type: str
rescue_interface:
description: The interface used for node rescue, e.g. "no-rescue".
returned: success
type: str
reservation:
description: The name of an Ironic Conductor host which is holding
a lock on this node, if a lock is held. Usually
"null", but this field can be useful for debugging.
returned: success
type: str
resource_class:
description: A string which can be used by external schedulers to
identify this node as a unit of a specific type of
resource. For more details, see
https://docs.openstack.org/ironic/latest/install/configure-nova-flavors.html
returned: success
type: str
retired_reason:
description: The reason the node is marked as retired.
returned: success
type: str
states:
description: Links to the collection of states.
returned: success
type: list
storage_interface:
description: Interface used for attaching and detaching volumes on
this node, e.g. "cinder".
returned: success
type: str
target_power_state:
description: If a power state transition has been requested, this
field represents the requested (ie, "target") state,
either "power on" or "power off".
returned: success
type: str
target_provision_state:
description: If a provisioning action has been requested, this
field represents the requested (ie, "target") state.
Note that a node may go through several states during
its transition to this target state. For instance,
when requesting an instance be deployed to an
AVAILABLE node, the node may go through the following
state change progression, AVAILABLE -> DEPLOYING ->
DEPLOYWAIT -> DEPLOYING -> ACTIVE
returned: success
type: str
target_raid_config:
description: Represents the requested RAID configuration of the
node, which will be applied when the node next
transitions through the CLEANING state. Introduced
with the cleaning feature.
returned: success
type: dict
traits:
description: List of traits for this node.
returned: success
type: list
updated_at:
description: Bare Metal node updated at timestamp.
returned: success
type: str
vendor_interface:
description: Interface for vendor-specific functionality on this
node, e.g. "no-vendor".
returned: success
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
)
_PROPERTIES = {
'cpu_arch': 'cpu_arch',
'cpus': 'cpus',
'ram': 'memory_mb',
'disk_size': 'local_gb',
'capabilities': 'capabilities',
'root_device': 'root_device',
}
def _parse_properties(module):
"""Convert ansible properties into native ironic values.
Also filter out any properties that are not set.
"""
p = module.params['properties']
return {to_key: p[from_key] for (from_key, to_key) in _PROPERTIES.items()
if p.get(from_key) is not None}
def _choose_id_value(module):
if module.params['uuid']:
return module.params['uuid']
if module.params['name']:
return module.params['name']
return None
def _choose_if_password_only(module, patch):
if len(patch) == 1:
if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']:
# Return false to abort update as the password appears
# to be the only element in the patch.
return False
return True
def _exit_node_not_updated(module, server):
module.exit_json(
changed=False,
result="Node not updated",
uuid=server['uuid'],
provision_state=server['provision_state']
)
def main():
argument_spec = ironic_argument_spec(
uuid=dict(),
name=dict(),
driver=dict(),
resource_class=dict(),
class BaremetalNodeModule(OpenStackModule):
argument_spec = dict(
bios_interface=dict(),
boot_interface=dict(),
chassis_id=dict(aliases=['chassis_uuid']),
console_interface=dict(),
deploy_interface=dict(),
driver=dict(),
driver_info=dict(type='dict', required=True),
id=dict(aliases=['uuid']),
inspect_interface=dict(),
management_interface=dict(),
name=dict(),
network_interface=dict(),
nics=dict(type='list', required=True, elements='dict'),
power_interface=dict(),
properties=dict(
type='dict',
options=dict(
cpu_arch=dict(),
cpus=dict(),
memory_mb=dict(aliases=['ram']),
local_gb=dict(aliases=['disk_size']),
capabilities=dict(),
root_device=dict(type='dict'),
),
),
raid_interface=dict(),
rescue_interface=dict(),
storage_interface=dict(),
vendor_interface=dict(),
driver_info=dict(type='dict', required=True),
nics=dict(type='list', required=True, elements="dict"),
properties=dict(type='dict', default={}),
chassis_uuid=dict(),
skip_update_of_masked_password=dict(type='bool'),
resource_class=dict(),
skip_update_of_masked_password=dict(
type='bool',
removed_in_version='3.0.0',
removed_from_collection='openstack.cloud',
),
state=dict(default='present', choices=['present', 'absent']),
wait=dict(type='bool', default=False),
timeout=dict(type='int', default=1800),
storage_interface=dict(),
timeout=dict(default=1800, type='int'), # increased default value
vendor_interface=dict(),
)
module_kwargs = openstack_module_kwargs()
module = IronicModule(argument_spec, **module_kwargs)
if not HAS_JSONPATCH:
module.fail_json(msg='jsonpatch is required for this module')
node_id = _choose_id_value(module)
sdk, cloud = openstack_cloud_from_module(module)
try:
server = cloud.get_machine(node_id)
if module.params['state'] == 'present':
if module.params['driver'] is None:
module.fail_json(msg="A driver must be defined in order "
"to set a node to present.")
properties = _parse_properties(module)
driver_info = module.params['driver_info']
kwargs = dict(
driver=module.params['driver'],
properties=properties,
driver_info=driver_info,
name=module.params['name'],
module_kwargs = dict(
required_if=[
('state', 'present', ('driver',)),
],
required_one_of=[
('id', 'name'),
],
supports_check_mode=True,
)
optional_field_names = ('resource_class',
def run(self):
name_or_id = \
self.params['id'] if self.params['id'] else self.params['name']
node = self.conn.baremetal.find_node(name_or_id)
state = self.params['state']
if self.ansible.check_mode:
self.exit_json(changed=self._will_change(state, node))
if state == 'present' and not node:
node = self._create()
self.exit_json(changed=True,
node=node.to_dict(computed=False))
elif state == 'present' and node:
update = self._build_update(node)
if update:
node = self._update(node, update)
self.exit_json(changed=bool(update),
node=node.to_dict(computed=False))
elif state == 'absent' and node:
self._delete(node)
self.exit_json(changed=True)
elif state == 'absent' and not node:
self.exit_json(changed=False)
def _build_update(self, node):
update = {}
# TODO(TheJulia): Presently this does not support updating nics.
# Support needs to be added.
# Update all known updateable attributes
node_attributes = dict(
(k, self.params[k])
for k in [
'bios_interface',
'boot_interface',
'chassis_id',
'console_interface',
'deploy_interface',
'driver',
'driver_info',
'inspect_interface',
'management_interface',
'name',
'network_interface',
'power_interface',
'raid_interface',
'rescue_interface',
'resource_class',
'storage_interface',
'vendor_interface')
for i in optional_field_names:
if module.params[i]:
kwargs[i] = module.params[i]
'vendor_interface',
]
if k in self.params and self.params[k] is not None
and self.params[k] != node[k])
if module.params['chassis_uuid']:
kwargs['chassis_uuid'] = module.params['chassis_uuid']
properties = self.params['properties']
if properties is not None:
properties = dict(
(k, v) for k, v in properties.items() if v is not None)
if properties and properties != node['properties']:
node_attributes['properties'] = properties
if server is None:
# Note(TheJulia): Add a specific UUID to the request if
# present in order to be able to re-use kwargs for if
# the node already exists logic, since uuid cannot be
# updated.
if module.params['uuid']:
kwargs['uuid'] = module.params['uuid']
# name can only be updated if id is given
if self.params['id'] is None and 'name' in node_attributes:
self.fail_json(msg='The name of a node cannot be updated without'
' specifying an id')
server = cloud.register_machine(
module.params['nics'],
wait=module.params['wait'],
timeout=module.params['timeout'],
if node_attributes:
update['node_attributes'] = node_attributes
return update
def _create(self):
kwargs = {}
for k in ('bios_interface',
'boot_interface',
'chassis_id',
'console_interface',
'deploy_interface',
'driver',
'driver_info',
'id',
'inspect_interface',
'management_interface',
'name',
'network_interface',
'power_interface',
'raid_interface',
'rescue_interface',
'resource_class',
'storage_interface',
'vendor_interface'):
if self.params[k] is not None:
kwargs[k] = self.params[k]
properties = self.params['properties']
if properties is not None:
properties = dict(
(k, v) for k, v in properties.items() if v is not None)
if properties:
kwargs['properties'] = properties
node = self.conn.register_machine(
nics=self.params['nics'],
wait=self.params['wait'],
timeout=self.params['timeout'],
**kwargs)
module.exit_json(changed=True, uuid=server['uuid'],
provision_state=server['provision_state'])
self.exit_json(changed=True, node=node.to_dict(computed=False))
def _delete(self, node):
self.conn.unregister_machine(
nics=self.params['nics'], uuid=node['id'])
def _update(self, node, update):
node_attributes = update.get('node_attributes')
if node_attributes:
node = self.conn.baremetal.update_node(
node['id'], **node_attributes)
return node
def _will_change(self, state, node):
if state == 'present' and not node:
return True
elif state == 'present' and node:
return bool(self._build_update(node))
elif state == 'absent' and node:
return False
else:
# TODO(TheJulia): Presently this does not support updating
# nics. Support needs to be added.
#
# Note(TheJulia): This message should never get logged
# however we cannot realistically proceed if neither a
# name or uuid was supplied to begin with.
if not node_id:
module.fail_json(msg="A uuid or name value "
"must be defined")
# state == 'absent' and not node:
return True
# Note(TheJulia): Constructing the configuration to compare
# against. The items listed in the server_config block can
# be updated via the API.
server_config = dict(
driver=server['driver'],
properties=server['properties'],
driver_info=server['driver_info'],
name=server['name'],
)
# Add the pre-existing chassis_uuid only if
# it is present in the server configuration.
if hasattr(server, 'chassis_uuid'):
server_config['chassis_uuid'] = server['chassis_uuid']
# Note(TheJulia): If a password is defined and concealed, a
# patch will always be generated and re-asserted.
patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs)
if not patch:
_exit_node_not_updated(module, server)
elif _choose_if_password_only(module, list(patch)):
# Note(TheJulia): Normally we would allow the general
# exception catch below, however this allows a specific
# message.
try:
server = cloud.patch_machine(
server['uuid'],
list(patch))
except Exception as e:
module.fail_json(msg="Failed to update node, "
"Error: %s" % e.message)
# Enumerate out a list of changed paths.
change_list = []
for change in list(patch):
change_list.append(change['path'])
module.exit_json(changed=True,
result="Node Updated",
changes=change_list,
uuid=server['uuid'],
provision_state=server['provision_state'])
# Return not updated by default as the conditions were not met
# to update.
_exit_node_not_updated(module, server)
if module.params['state'] == 'absent':
if not node_id:
module.fail_json(msg="A uuid or name value must be defined "
"in order to remove a node.")
if server is not None:
cloud.unregister_machine(module.params['nics'],
server['uuid'])
module.exit_json(changed=True, result="deleted")
else:
module.exit_json(changed=False, result="Server not found")
except sdk.exceptions.OpenStackCloudException as e:
module.fail_json(msg=str(e))
def main():
module = BaremetalNodeModule()
module()
if __name__ == "__main__":

View File

@ -5,27 +5,22 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
DOCUMENTATION = r'''
module: baremetal_node_info
short_description: Retrieve information about Bare Metal nodes from OpenStack
author: OpenStack Ansible SIG
description:
- Retrieve information about Bare Metal nodes from OpenStack.
options:
node:
description:
- Name or globally unique identifier (UUID) to identify the host.
type: str
mac:
description:
- Unique mac address that is used to attempt to identify the host.
- MAC address that is used to attempt to identify the host.
type: str
ironic_url:
name:
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 the baremetal node.
type: str
aliases: ['node']
requirements:
- "python >= 3.6"
- "openstacksdk"
@ -34,34 +29,36 @@ extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = '''
# Gather information about all baremeal nodes
- openstack.cloud.baremetal_node_info:
EXAMPLES = r'''
- name: Gather information about all baremeal nodes
openstack.cloud.baremetal_node_info:
cloud: "devstack"
register: result
- debug:
msg: "{{ result.baremetal_nodes }}"
# Gather information about a baremeal node
- openstack.cloud.baremetal_node_info:
register: nodes
- debug: var=nodes
- name: Gather information about a baremeal node
openstack.cloud.baremetal_node_info:
cloud: "devstack"
node: "00000000-0000-0000-0000-000000000002"
register: result
- debug:
msg: "{{ result.baremetal_nodes }}"
name: "00000000-0000-0000-0000-000000000002"
register: nodes
- debug: var=nodes
'''
RETURN = '''
baremetal_nodes:
description: Bare Metal node list. A subset of the dictionary keys
listed below may be returned, depending on your cloud
provider.
returned: always, but can be null
type: complex
RETURN = r'''
nodes:
description: |
Bare Metal node list. A subset of the dictionary keys listed below may
be returned, depending on your cloud provider.
returned: always
type: list
elements: dict
contains:
allocation_id:
description: The UUID of the allocation associated with the node.
If not null, will be the same as instance_uuid (the
opposite is not always true). Unlike instance_uuid,
If not null, will be the same as instance_id (the
opposite is not always true). Unlike instance_id,
this field is read-only. Please use the Allocation API
to remove allocations.
returned: success
@ -88,14 +85,11 @@ baremetal_nodes:
returned: success
type: str
conductor:
description: The conductor currently servicing a node. This field
is read-only.
description: The conductor currently servicing a node.
returned: success
type: str
conductor_group:
description: The conductor group for a node. Case-insensitive
string up to 255 characters, containing a-z, 0-9, _,
-, and ..
description: The conductor group for a node.
returned: success
type: str
console_interface:
@ -239,209 +233,10 @@ baremetal_nodes:
description: List of ironic ports on this node.
returned: success
type: list
elements: dict
contains:
address:
description: Physical hardware address of this network port,
typically the hardware MAC address.
returned: success
type: str
created_at:
description: The UTC date and time when the resource was
created, ISO 8601 format.
returned: success
type: str
extra:
description: A set of one or more arbitrary metadata key and
value pairs.
returned: success
type: dict
id:
description: The UUID for the resource.
returned: success
type: str
internal_info:
description: Internal metadata set and stored by the port. This
field is read-only.
returned: success
type: dict
is_pxe_enabled:
description: Indicates whether PXE is enabled or disabled on
the port.
returned: success
type: str
links:
description: A list of relative links, including 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 datapath_id of the switch are
accepted in this field) and port_id (identifier of
the physical port on the switch to which node's
port is connected to) fields. switch_info is an
optional string field to be used to store any
vendor-specific information.
returned: success
type: dict
node_id:
description: UUID of the node this resource belongs to.
returned: success
type: str
physical_network:
description: The name of the physical network to which a port
is connected. May be empty.
returned: success
type: str
port_group_id:
description: UUID of the port group this resource belongs to.
returned: success
type: str
updated_at:
description: The UTC date and time when the resource was
updated, ISO 8601 format. May be "null".
returned: success
type: str
port_groups:
description: List of ironic port groups on this node.
returned: success
type: list
elements: dict
contains:
address:
description: Physical hardware address of this port group,
typically the hardware MAC address.
returned: success
type: str
created_at:
description: The UTC date and time when the resource was
created, ISO 8601 format.
returned: success
type: str
extra:
description: A set of one or more arbitrary metadata key and
value pairs.
returned: success
type: dict
id:
description: The UUID for the resource.
returned: success
type: str
internal_info:
description: Internal metadata set and stored by the port group.
This field is read-only.
returned: success
type: dict
is_standalone_ports_supported:
description: Indicates whether ports that are members of this
port group can be used as stand-alone ports.
returned: success
type: bool
links:
description: A list of relative links, including self and bookmark
links.
returned: success
type: list
mode:
description: Mode of the port group. For possible values, refer
to https://www.kernel.org/doc/Documentation/networking/bonding.txt.
If not specified in a request to create a port
group, it will be set to the value of the
[DEFAULT]default_portgroup_mode configuration
option. When set, can not be removed from the port
group.
returned: success
type: str
name:
description: Human-readable identifier for the port group
resource. May be undefined.
returned: success
type: str
node_id:
description: UUID of the node this resource belongs to.
returned: success
type: str
ports:
description: List of ports belonging to this port group.
returned: success
type: list
elements: dict
contains:
address:
description: Physical hardware address of this network port,
typically the hardware MAC address.
returned: success
type: str
created_at:
description: The UTC date and time when the resource was
created, ISO 8601 format.
returned: success
type: str
extra:
description: A set of one or more arbitrary metadata key and
value pairs.
returned: success
type: dict
id:
description: The UUID for the resource.
returned: success
type: str
internal_info:
description: Internal metadata set and stored by the port. This
field is read-only.
returned: success
type: dict
is_pxe_enabled:
description: Indicates whether PXE is enabled or disabled on
the port.
returned: success
type: str
links:
description: A list of relative links, including 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 datapath_id of the switch are
accepted in this field) and port_id (identifier of
the physical port on the switch to which node's
port is connected to) fields. switch_info is an
optional string field to be used to store any
vendor-specific information.
returned: success
type: dict
node_id:
description: UUID of the node this resource belongs to.
returned: success
type: str
physical_network:
description: The name of the physical network to which a port
is connected. May be empty.
returned: success
type: str
port_group_id:
description: UUID of the port group this resource belongs to.
returned: success
type: str
updated_at:
description: The UTC date and time when the resource was
updated, ISO 8601 format. May be "null".
returned: success
type: str
properties:
description: Key/value properties related to the port group's
configuration.
returned: success
type: dict
updated_at:
description: The UTC date and time when the resource was
updated, ISO 8601 format. May be "null".
returned: success
type: str
power_interface:
description: Interface used for performing power actions on the
node, e.g. "ipmitool".
@ -544,73 +339,74 @@ baremetal_nodes:
node, e.g. "no-vendor".
returned: success
type: str
baremetal_nodes:
description: Same as C(nodes), kept for backward compatibility.
returned: always
type: list
elements: dict
'''
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
)
def get_ports_and_portgroups(cloud, machine):
machine['ports'] = [nic.to_dict(computed=False)
for nic in cloud.baremetal.ports(
details=True, node_id=machine['id'])]
class BaremetalNodeInfoModule(OpenStackModule):
argument_spec = dict(
mac=dict(),
name=dict(aliases=['node']),
)
machine['port_groups'] = [grp.to_dict(computed=False) for grp in
cloud.baremetal.port_groups(node=machine['id'],
details=True)]
module_kwargs = dict(
mutually_exclusive=[
('mac', 'name'),
],
supports_check_mode=True,
)
# links to ports are not useful, replace with list of ports
for port_group in machine['port_groups']:
port_group['ports'] = [port for port in machine['ports']
if port['port_group_id'] == port_group['id']]
def run(self):
name_or_id = self.params['name']
mac = self.params['mac']
node_id = None
if name_or_id:
# self.conn.baremetal.nodes() does not support searching by name or
# id which we want to provide for backward compatibility
node = self.conn.baremetal.find_node(name_or_id)
if node:
node_id = node['id']
elif mac:
# self.conn.get_machine_by_mac(mac) is not necessary
# because nodes can be filtered by instance_id
baremetal_port = self.conn.get_nic_by_mac(mac)
if baremetal_port:
node_id = baremetal_port['node_id']
if name_or_id or mac:
if node_id:
# fetch node details with self.conn.baremetal.get_node()
# because self.conn.baremetal.nodes() does not provide a
# query parameter to filter by a node's id
node = self.conn.baremetal.get_node(node_id)
nodes = [node.to_dict(computed=False)]
else: # not node_id
# return empty list when no matching node could be found
# because *_info modules do not raise errors on missing
# resources
nodes = []
else: # not name_or_id and not mac
nodes = [node.to_dict(computed=False) for node in
self.conn.baremetal.nodes(details=True)]
self.exit_json(changed=False,
nodes=nodes,
# keep for backward compatibility
baremetal_nodes=nodes)
def main():
argument_spec = ironic_argument_spec(
node=dict(),
mac=dict(),
)
module_kwargs = openstack_module_kwargs()
module_kwargs['supports_check_mode'] = True
module = IronicModule(argument_spec, **module_kwargs)
machine = None
machines = list()
sdk, cloud = openstack_cloud_from_module(module)
try:
if module.params['node']:
machine = cloud.baremetal.find_node(module.params['node'])
elif module.params['mac']:
nic = next(cloud.baremetal.ports(address=module.params['mac'],
fields=['node_id']), None)
if nic:
machine = cloud.baremetal.find_node(nic['node_id'])
# Fail if node not found
if (module.params['node'] or module.params['mac']) and not machine:
module.fail_json(msg='The baremetal node was not found')
if machine:
machines.append(machine.to_dict(computed=False))
else:
machines = [machine.to_dict(computed=False)
for machine in cloud.baremetal.nodes(details=True)]
for machine in machines:
get_ports_and_portgroups(cloud, machine)
module.exit_json(changed=False, baremetal_nodes=machines)
except sdk.exceptions.OpenStackCloudException as e:
module.fail_json(msg=str(e))
module = BaremetalNodeInfoModule()
module()
if __name__ == "__main__":