From 4e6f879febf771fa81af8782367ca9eae277a452 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Mar 2015 19:28:02 -0400 Subject: [PATCH 1/3] Add Ironic module --- cloud/openstack/os_ironic.py | 212 +++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 cloud/openstack/os_ironic.py diff --git a/cloud/openstack/os_ironic.py b/cloud/openstack/os_ironic.py new file mode 100644 index 00000000000..3f28a5b78dc --- /dev/null +++ b/cloud/openstack/os_ironic.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2014, Hewlett-Packard Development Company, L.P. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +try: + import shade + HAS_SHADE = True +except ImportError: + HAS_SHADE = False + +# TODO FIX UUID/Add node support +DOCUMENTATION = ''' +--- +module: os_ironic +short_description: Create/Delete Bare Metal Resources from OpenStack +version_added: "1.10" +extends_documentation_fragment: openstack +description: + - Create or Remove Ironic nodes from OpenStack. +options: + state: + description: + - Indicates desired state of the resource + choices: ['present', 'absent'] + default: present + uuid: + description: + - globally unique identifier (UUID) to be given to the resource. Will + be auto-generated if not specified. + required: false + default: None + driver: + description: + - The name of the Ironic Driver to use with this node. + required: true + default: None + ironic_url: + description: + - If noauth mode is utilized, this is required to be set to the + endpoint URL for the Ironic API. Use with "auth" and "auth_plugin" + settings set to None. + required: false + default: None + 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. + power: + - Information necessary to turn this server on / off. This often + includes such things as IPMI username, password, and IP address. + required: true + deploy: + - Information necessary to deploy this server directly, without + using Nova. THIS IS NOT RECOMMENDED. + console: + - Information necessary to connect to this server's serial console. + Not all drivers support this. + management: + - Information necessary to interact with this server's management + interface. May be shared by power_info in some cases. + required: true + nics: + description: + - A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc" + required: true + properties: + description: + - Definition of the physical characteristics of this server, used for + scheduling purposes + cpu_arch: + description: + - CPU architecture (x86_64, i686, ...) + default: x86_64 + cpus: + description: + - Number of CPU cores this machine has + default: 1 + ram: + description: + - amount of RAM this machine has, in MB + default: 1 + disk_size: + description: + - size of first storage device in this machine (typically + /dev/sda), in GB + default: 1 + +requirements: ["shade"] +''' + +EXAMPLES = ''' +# Enroll a node with some basic properties and driver info +- os_ironic: + cloud: "devstack" + driver: "pxe_ipmitool" + uuid: "a8cb6624-0d9f-4882-affc-046ebb96ec92" + properties: + cpus: 2 + cpu_arch: "x86_64" + ram: 8192 + disk_size: 64 + nics: + - mac: "aa:bb:cc:aa:bb:cc" + - mac: "dd:ee:ff:dd:ee:ff" + driver_info: + power: + ipmi_address: "1.2.3.4" + ipmi_username: "admin" + ipmi_password: "adminpass" + +''' + + +def _parse_properties(module): + p = module.params['properties'] + props = dict( + cpu_arch=p.get('cpu_arch') if p.get('cpu_arch') else 'x86_64', + cpus=p.get('cpus') if p.get('cpus') else 1, + memory_mb=p.get('ram') if p.get('ram') else 1, + local_gb=p.get('disk_size') if p.get('disk_size') else 1, + ) + return props + + +def _parse_driver_info(module): + p = module.params['driver_info'] + info = p.get('power') + if not info: + raise shade.OpenStackCloudException( + "driver_info['power'] is required") + if p.get('console'): + info.update(p.get('console')) + if p.get('management'): + info.update(p.get('management')) + if p.get('deploy'): + info.update(p.get('deploy')) + return info + + +def main(): + argument_spec = openstack_full_argument_spec( + uuid=dict(required=False), + driver=dict(required=True), + driver_info=dict(type='dict', required=True), + nics=dict(type='list', required=True), + properties=dict(type='dict', default={}), + ironic_url=dict(required=False), + ) + module_kwargs = openstack_module_kwargs() + module = AnsibleModule(argument_spec, **module_kwargs) + + if not HAS_SHADE: + module.fail_json(msg='shade is required for this module') + if (module.params['auth_plugin'] == 'None' and + module.params['ironic_url'] is None): + module.fail_json(msg="Authentication appears disabled, Please " + "define an ironic_url parameter") + + if module.params['ironic_url'] and module.params['auth_plugin'] == 'None': + module.params['auth'] = dict(endpoint=module.params['ironic_url']) + try: + cloud = shade.operator_cloud(**module.params) + server = cloud.get_machine_by_uuid(module.params['uuid']) + + if module.params['state'] == 'present': + properties = _parse_properties(module) + driver_info = _parse_driver_info(module) + kwargs = dict( + uuid=module.params['uuid'], + driver=module.params['driver'], + properties=properties, + driver_info=driver_info, + ) + if server is None: + server = cloud.register_machine(module.params['nics'], + **kwargs) + module.exit_json(changed=True, uuid=server.uuid) + else: + # TODO: compare properties here and update if necessary + # ... but the interface for that is terrible! + module.exit_json(changed=False, + result="Server already present") + if module.params['state'] == 'absent': + if server is not None: + cloud.unregister_machine(module.params['nics'], + module.params['uuid']) + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="Server not found") + except shade.OpenStackCloudException as e: + module.fail_json(msg=e.message) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +from ansible.module_utils.openstack import * +main() From c040ae5374cfddab58f21fda3949c6eaf31268b9 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 1 May 2015 09:47:42 -0400 Subject: [PATCH 2/3] Updating os_ironic module Updating os_ironic module to the most recent version accounting for changes in Ansible devel branch and the shade library since the original creation of the module. --- cloud/openstack/os_ironic.py | 180 +++++++++++++++++++++++++++++++---- 1 file changed, 159 insertions(+), 21 deletions(-) diff --git a/cloud/openstack/os_ironic.py b/cloud/openstack/os_ironic.py index 3f28a5b78dc..e74376a3983 100644 --- a/cloud/openstack/os_ironic.py +++ b/cloud/openstack/os_ironic.py @@ -22,12 +22,11 @@ try: except ImportError: HAS_SHADE = False -# TODO FIX UUID/Add node support +import jsonpatch DOCUMENTATION = ''' --- module: os_ironic short_description: Create/Delete Bare Metal Resources from OpenStack -version_added: "1.10" extends_documentation_fragment: openstack description: - Create or Remove Ironic nodes from OpenStack. @@ -40,7 +39,13 @@ options: uuid: description: - globally unique identifier (UUID) to be given to the resource. Will - be auto-generated if not specified. + be auto-generated if not specified, and name is specified. + - Definition of a UUID will always take precedence to a name value. + required: false + default: None + name: + description: + - unique name identifier to be given to the resource. required: false default: None driver: @@ -48,10 +53,15 @@ options: - The name of the Ironic Driver to use with this node. required: true default: None + chassis_uuid: + description: + - Associate the node with a pre-defined chassis. + required: false + default: None ironic_url: description: - If noauth mode is utilized, this is required to be set to the - endpoint URL for the Ironic API. Use with "auth" and "auth_plugin" + endpoint URL for the Ironic API. Use with "auth" and "auth_type" settings set to None. required: false default: None @@ -99,8 +109,17 @@ options: - size of first storage device in this machine (typically /dev/sda), in GB default: 1 + skip_update_of_driver_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. + required: false + default: false -requirements: ["shade"] +requirements: ["shade", "jsonpatch"] ''' EXAMPLES = ''' @@ -108,7 +127,7 @@ EXAMPLES = ''' - os_ironic: cloud: "devstack" driver: "pxe_ipmitool" - uuid: "a8cb6624-0d9f-4882-affc-046ebb96ec92" + uuid: "00000000-0000-0000-0000-000000000002" properties: cpus: 2 cpu_arch: "x86_64" @@ -122,6 +141,7 @@ EXAMPLES = ''' ipmi_address: "1.2.3.4" ipmi_username: "admin" ipmi_password: "adminpass" + chassis_uuid: "00000000-0000-0000-0000-000000000001" ''' @@ -152,56 +172,174 @@ def _parse_driver_info(module): return info +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +def _is_value_true(value): + true_values = [True, 'yes', 'Yes', 'True', 'true'] + if value in true_values: + return True + return False + + +def _choose_if_password_only(module, patch): + if len(patch) is 1: + if 'password' in patch[0]['path'] and _is_value_true( + module.params['skip_update_of_masked_password']): + # Return false to aabort update as the password appears + # to be the only element in the patch. + return False + return True + + +def _exit_node_not_updated(module, server): + module.exit_json( + changed=False, + result="Node not updated", + uuid=server['uuid'], + provision_state=server['provision_state'] + ) + + def main(): argument_spec = openstack_full_argument_spec( uuid=dict(required=False), - driver=dict(required=True), + name=dict(required=False), + driver=dict(required=False), driver_info=dict(type='dict', required=True), nics=dict(type='list', required=True), properties=dict(type='dict', default={}), ironic_url=dict(required=False), + chassis_uuid=dict(required=False), + skip_update_of_masked_password=dict(required=False, choices=BOOLEANS), + state=dict(required=False, default='present') ) module_kwargs = openstack_module_kwargs() module = AnsibleModule(argument_spec, **module_kwargs) if not HAS_SHADE: module.fail_json(msg='shade is required for this module') - if (module.params['auth_plugin'] == 'None' and + if (module.params['auth_type'] in [None, 'None'] and module.params['ironic_url'] is None): - module.fail_json(msg="Authentication appears disabled, Please " - "define an ironic_url parameter") + module.fail_json(msg="Authentication appears to be disabled, " + "Please define an ironic_url parameter") + + if (module.params['ironic_url'] and + module.params['auth_type'] in [None, 'None']): + module.params['auth'] = dict( + endpoint=module.params['ironic_url'] + ) + + node_id = _choose_id_value(module) - if module.params['ironic_url'] and module.params['auth_plugin'] == 'None': - module.params['auth'] = dict(endpoint=module.params['ironic_url']) try: cloud = shade.operator_cloud(**module.params) - server = cloud.get_machine_by_uuid(module.params['uuid']) - + 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 = _parse_driver_info(module) kwargs = dict( - uuid=module.params['uuid'], driver=module.params['driver'], properties=properties, driver_info=driver_info, + name=module.params['name'], ) + + 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'], **kwargs) - module.exit_json(changed=True, uuid=server.uuid) + module.exit_json(changed=True, uuid=server['uuid'], + provision_state=server['provision_state']) else: - # TODO: compare properties here and update if necessary - # ... but the interface for that is terrible! - module.exit_json(changed=False, - result="Server already present") + # 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'], - module.params['uuid']) + server['uuid']) module.exit_json(changed=True, result="deleted") else: module.exit_json(changed=False, result="Server not found") + except shade.OpenStackCloudException as e: module.fail_json(msg=e.message) From 62073565e1bec66b22450d0495b476fd8d8419e6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 9 Jun 2015 19:43:27 -0400 Subject: [PATCH 3/3] Update version added --- cloud/openstack/os_ironic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/openstack/os_ironic.py b/cloud/openstack/os_ironic.py index e74376a3983..137effe6073 100644 --- a/cloud/openstack/os_ironic.py +++ b/cloud/openstack/os_ironic.py @@ -28,6 +28,7 @@ DOCUMENTATION = ''' module: os_ironic short_description: Create/Delete Bare Metal Resources from OpenStack extends_documentation_fragment: openstack +version_added: "2.0" description: - Create or Remove Ironic nodes from OpenStack. options: