From 4656797389f2162a304ba24873eab66471143b41 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Tue, 31 Mar 2015 19:28:02 -0400 Subject: [PATCH 1/2] Add Ironic Node module --- cloud/openstack/os_ironic_node.py | 155 ++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 cloud/openstack/os_ironic_node.py diff --git a/cloud/openstack/os_ironic_node.py b/cloud/openstack/os_ironic_node.py new file mode 100644 index 00000000000..386a5f9fe84 --- /dev/null +++ b/cloud/openstack/os_ironic_node.py @@ -0,0 +1,155 @@ +#!/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 + +DOCUMENTATION = ''' +--- +module: os_ironic_node +version_added: "1.10" +short_description: Activate/Deactivate Bare Metal Resources from OpenStack +extends_documentation_fragment: openstack +description: + - Deploy to nodes controlled by Ironic. +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. + 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" + settings set to None. + required: false + default: None + config_drive: + description: + - A configdrive file or HTTP(S) URL that will be passed along to the + node. + required: false + default: None + instance_info: + description: + - Definition of the instance information which is used to deploy + the node. + image_source: + description: + - An HTTP(S) URL where the image can be retrieved from. + image_checksum: + description: + - The checksum of image_source. + image_disk_format: + description: + - The type of image that has been requested to be deployed. +requirements: ["shade"] +''' + +EXAMPLES = ''' +# Activate a node by booting an image with a configdrive attached +os_ironic_node: + cloud: "openstack" + uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" + state: present + config_drive: "http://192.168.1.1/host-configdrive.iso" + instance_info: + image_source: "http://192.168.1.1/deploy_image.img" + image_checksum: "356a6b55ecc511a20c33c946c4e678af" + image_disk_format: "qcow" + delegate_to: localhost +''' + + +def _prepare_instance_info_patch(instance_info): + patch = [] + patch.append({ + 'op': 'replace', + 'path': '/instance_info', + 'value': instance_info + }) + return patch + + +def main(): + argument_spec = openstack_full_argument_spec( + uuid=dict(required=True), + instance_info=dict(type='dict', required=True), + config_drive=dict(required=False), + 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']) + instance_info = module.params['instance_info'] + uuid = module.params['uuid'] + if module.params['state'] == 'present': + if server is None: + module.fail_json(msg="node not found") + else: + # TODO: compare properties here and update if necessary + # ... but the interface for that is terrible! + if server.provision_state is "active": + module.exit_json( + changed=False, + result="Node already in an active state" + ) + + patch = _prepare_instance_info_patch(instance_info) + cloud.set_node_instance_info(uuid, patch) + cloud.validate_node(uuid) + cloud.activate_node(uuid, module.params['config_drive']) + # TODO: Add more error checking and a wait option. + module.exit_json(changed=False, result="node activated") + + if module.params['state'] == 'absent': + if server.provision_state is not "deleted": + cloud.purge_node_instance_info(uuid) + cloud.deactivate_node(uuid) + module.exit_json(changed=True, result="deleted") + else: + module.exit_json(changed=False, result="node 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 47113727eed38741c9a0e79324e4495dc20db161 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Fri, 1 May 2015 09:51:59 -0400 Subject: [PATCH 2/2] Updating os_ironic_node module Updating the os_ironic_node module to the most recent version including support for power and maintenance states. --- cloud/openstack/os_ironic_node.py | 241 ++++++++++++++++++++++++++---- 1 file changed, 209 insertions(+), 32 deletions(-) diff --git a/cloud/openstack/os_ironic_node.py b/cloud/openstack/os_ironic_node.py index 386a5f9fe84..a50d6897e5c 100644 --- a/cloud/openstack/os_ironic_node.py +++ b/cloud/openstack/os_ironic_node.py @@ -1,7 +1,7 @@ #!/usr/bin/python # coding: utf-8 -*- -# (c) 2014, Hewlett-Packard Development Company, L.P. +# (c) 2015, 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 @@ -25,7 +25,6 @@ except ImportError: DOCUMENTATION = ''' --- module: os_ironic_node -version_added: "1.10" short_description: Activate/Deactivate Bare Metal Resources from OpenStack extends_documentation_fragment: openstack description: @@ -36,6 +35,13 @@ options: - Indicates desired state of the resource choices: ['present', 'absent'] default: present + deploy: + description: + - Indicates if the resource should be deployed. Allows for deployment + logic to be disengaged and control of the node power or maintenance + state to be changed. + choices: ['true', 'false'] + default: true uuid: description: - globally unique identifier (UUID) to be given to the resource. @@ -44,7 +50,7 @@ options: 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 @@ -57,7 +63,8 @@ options: instance_info: description: - Definition of the instance information which is used to deploy - the node. + the node. This information is only required when an instance is + set to present. image_source: description: - An HTTP(S) URL where the image can be retrieved from. @@ -67,6 +74,26 @@ options: image_disk_format: description: - The type of image that has been requested to be deployed. + power: + description: + - A setting to allow power state to be asserted allowing nodes + that are not yet deployed to be powered on, and nodes that + are deployed to be powered off. + choices: ['present', 'absent'] + default: present + maintenance: + description: + - A setting to allow the direct control if a node is in + maintenance mode. + required: false + default: false + maintenance_reason: + description: + - A string expression regarding the reason a node is in a + maintenance mode. + required: false + default: None + requirements: ["shade"] ''' @@ -76,6 +103,9 @@ os_ironic_node: cloud: "openstack" uuid: "d44666e1-35b3-4f6b-acb0-88ab7052da69" state: present + power: present + deploy: True + maintenance: False config_drive: "http://192.168.1.1/host-configdrive.iso" instance_info: image_source: "http://192.168.1.1/deploy_image.img" @@ -85,6 +115,16 @@ os_ironic_node: ''' +def _choose_id_value(module): + if module.params['uuid']: + return module.params['uuid'] + if module.params['name']: + return module.params['name'] + return None + + +# TODO(TheJulia): Change this over to use the machine patch method +# in shade once it is available. def _prepare_instance_info_patch(instance_info): patch = [] patch.append({ @@ -95,56 +135,193 @@ def _prepare_instance_info_patch(instance_info): return patch +def _is_true(value): + true_values = [True, 'yes', 'Yes', 'True', 'true', 'present', 'on'] + if value in true_values: + return True + return False + + +def _is_false(value): + false_values = [False, None, 'no', 'No', 'False', 'false', 'absent', 'off'] + if value in false_values: + return True + return False + + +def _check_set_maintenance(module, cloud, node): + if _is_true(module.params['maintenance']): + if _is_false(node['maintenance']): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node has been set into " + "maintenance mode") + else: + # User has requested maintenance state, node is already in the + # desired state, checking to see if the reason has changed. + if (str(node['maintenance_reason']) not in + str(module.params['maintenance_reason'])): + cloud.set_machine_maintenance_state( + node['uuid'], + True, + reason=module.params['maintenance_reason']) + module.exit_json(changed=True, msg="Node maintenance reason " + "updated, cannot take any " + "additional action.") + elif _is_false(module.params['maintenance']): + if node['maintenance'] is True: + cloud.remove_machine_from_maintenance(node['uuid']) + return True + else: + module.fail_json(msg="maintenance parameter was set but a valid " + "the value was not recognized.") + return False + + +def _check_set_power_state(module, cloud, node): + if 'power on' in str(node['power_state']): + if _is_false(module.params['power']): + # User has requested the node be powered off. + cloud.set_machine_power_off(node['uuid']) + module.exit_json(changed=True, msg="Power requested off") + if 'power off' in str(node['power_state']): + if (_is_false(module.params['power']) and + _is_false(module.params['state'])): + return False + if (_is_false(module.params['power']) and + _is_false(module.params['state'])): + module.exit_json( + changed=False, + msg="Power for node is %s, node must be reactivated " + "OR set to state absent" + ) + # In the event the power has been toggled on and + # deployment has been requested, we need to skip this + # step. + if (_is_true(module.params['power']) and + _is_false(module.params['deploy'])): + # Node is powered down when it is not awaiting to be provisioned + cloud.set_machine_power_on(node['uuid']) + return True + # Default False if no action has been taken. + return False + + def main(): argument_spec = openstack_full_argument_spec( - uuid=dict(required=True), - instance_info=dict(type='dict', required=True), + uuid=dict(required=False), + name=dict(required=False), + instance_info=dict(type='dict', required=False), config_drive=dict(required=False), ironic_url=dict(required=False), + state=dict(required=False, default='present'), + maintenance=dict(required=False), + maintenance_reason=dict(required=False), + power=dict(required=False, default='present'), + deploy=dict(required=False, default=True), ) 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") - if module.params['ironic_url'] and module.params['auth_plugin'] == 'None': - module.params['auth'] = dict(endpoint=module.params['ironic_url']) + 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 not node_id: + module.fail_json(msg="A uuid or name value must be defined " + "to use this module.") try: cloud = shade.operator_cloud(**module.params) - server = cloud.get_machine_by_uuid(module.params['uuid']) + node = cloud.get_machine(node_id) + + if node is None: + module.fail_json(msg="node not found") + + uuid = node['uuid'] instance_info = module.params['instance_info'] - uuid = module.params['uuid'] - if module.params['state'] == 'present': - if server is None: - module.fail_json(msg="node not found") - else: - # TODO: compare properties here and update if necessary - # ... but the interface for that is terrible! - if server.provision_state is "active": - module.exit_json( - changed=False, - result="Node already in an active state" - ) - - patch = _prepare_instance_info_patch(instance_info) - cloud.set_node_instance_info(uuid, patch) - cloud.validate_node(uuid) - cloud.activate_node(uuid, module.params['config_drive']) - # TODO: Add more error checking and a wait option. - module.exit_json(changed=False, result="node activated") - - if module.params['state'] == 'absent': - if server.provision_state is not "deleted": + changed = False + + # User has reqeusted desired state to be in maintenance state. + if module.params['state'] is 'maintenance': + module.params['maintenance'] = True + + if node['provision_state'] in [ + 'cleaning', + 'deleting', + 'wait call-back']: + module.fail_json(msg="Node is in %s state, cannot act upon the " + "request as the node is in a transition " + "state" % node['provision_state']) + # TODO(TheJulia) This is in-development code, that requires + # code in the shade library that is still in development. + if _check_set_maintenance(module, cloud, node): + if node['provision_state'] in 'active': + module.exit_json(changed=True, + result="Maintenance state changed") + changed = True + node = cloud.get_machine(node_id) + + if _check_set_power_state(module, cloud, node): + changed = True + node = cloud.get_machine(node_id) + + if _is_true(module.params['state']): + if _is_false(module.params['deploy']): + module.exit_json( + changed=changed, + result="User request has explicitly disabled " + "deployment logic" + ) + + if 'active' in node['provision_state']: + module.exit_json( + changed=changed, + result="Node already in an active state." + ) + + if instance_info is None: + module.fail_json( + changed=changed, + msg="When setting an instance to present, " + "instance_info is a required variable.") + + # TODO(TheJulia): Update instance info, however info is + # deployment specific. Perhaps consider adding rebuild + # support, although there is a known desire to remove + # rebuild support from Ironic at some point in the future. + patch = _prepare_instance_info_patch(instance_info) + cloud.set_node_instance_info(uuid, patch) + cloud.validate_node(uuid) + cloud.activate_node(uuid, module.params['config_drive']) + # TODO(TheJulia): Add more error checking and a wait option. + # We will need to loop, or just add the logic to shade, + # although this could be a very long running process as + # baremetal deployments are not a "quick" task. + module.exit_json(changed=changed, result="node activated") + + elif _is_false(module.params['state']): + if node['provision_state'] not in "deleted": cloud.purge_node_instance_info(uuid) cloud.deactivate_node(uuid) module.exit_json(changed=True, result="deleted") else: module.exit_json(changed=False, result="node not found") + else: + module.fail_json(msg="State must be present, absent, " + "maintenance, off") + except shade.OpenStackCloudException as e: module.fail_json(msg=e.message)