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__":