From 902b2f81471d7776bb3b0e170f48bf6951689232 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Wed, 28 Sep 2022 09:12:08 +0200 Subject: [PATCH] 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 --- ci/roles/baremetal_node/defaults/main.yml | 56 ++ ci/roles/baremetal_node/tasks/main.yml | 74 ++ plugins/modules/baremetal_node.py | 923 ++++++++++++++-------- plugins/modules/baremetal_node_info.py | 378 ++------- 4 files changed, 807 insertions(+), 624 deletions(-) create mode 100644 ci/roles/baremetal_node/defaults/main.yml create mode 100644 ci/roles/baremetal_node/tasks/main.yml diff --git a/ci/roles/baremetal_node/defaults/main.yml b/ci/roles/baremetal_node/defaults/main.yml new file mode 100644 index 00000000..d993fcdb --- /dev/null +++ b/ci/roles/baremetal_node/defaults/main.yml @@ -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 diff --git a/ci/roles/baremetal_node/tasks/main.yml b/ci/roles/baremetal_node/tasks/main.yml new file mode 100644 index 00000000..367feca5 --- /dev/null +++ b/ci/roles/baremetal_node/tasks/main.yml @@ -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 diff --git a/plugins/modules/baremetal_node.py b/plugins/modules/baremetal_node.py index a1d51bf0..245f9ef7 100644 --- a/plugins/modules/baremetal_node.py +++ b/plugins/modules/baremetal_node.py @@ -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//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'] +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(), + 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']), + storage_interface=dict(), + timeout=dict(default=1800, type='int'), # increased default value + vendor_interface=dict(), ) + module_kwargs = dict( + required_if=[ + ('state', 'present', ('driver',)), + ], + required_one_of=[ + ('id', 'name'), + ], + supports_check_mode=True, + ) + + 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', + ] + if k in self.params and self.params[k] is not None + and self.params[k] != node[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 and properties != node['properties']: + node_attributes['properties'] = properties + + # 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') + + 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) + + 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: + # state == 'absent' and not node: + return True + def main(): - argument_spec = ironic_argument_spec( - uuid=dict(), - name=dict(), - driver=dict(), - resource_class=dict(), - bios_interface=dict(), - boot_interface=dict(), - console_interface=dict(), - deploy_interface=dict(), - inspect_interface=dict(), - management_interface=dict(), - network_interface=dict(), - power_interface=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'), - state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', default=False), - timeout=dict(type='int', default=1800), - ) - 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'], - ) - optional_field_names = ('resource_class', - 'bios_interface', - 'boot_interface', - 'console_interface', - 'deploy_interface', - 'inspect_interface', - 'management_interface', - 'network_interface', - 'power_interface', - 'raid_interface', - 'rescue_interface', - 'storage_interface', - 'vendor_interface') - for i in optional_field_names: - if module.params[i]: - kwargs[i] = module.params[i] - - if module.params['chassis_uuid']: - kwargs['chassis_uuid'] = module.params['chassis_uuid'] - - 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'] - - server = cloud.register_machine( - module.params['nics'], - wait=module.params['wait'], - timeout=module.params['timeout'], - **kwargs) - module.exit_json(changed=True, uuid=server['uuid'], - provision_state=server['provision_state']) - 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") - - # 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)) + module = BaremetalNodeModule() + module() if __name__ == "__main__": diff --git a/plugins/modules/baremetal_node_info.py b/plugins/modules/baremetal_node_info.py index 4f73cf00..97ce5598 100644 --- a/plugins/modules/baremetal_node_info.py +++ b/plugins/modules/baremetal_node_info.py @@ -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__":