--- /dev/null
+#!/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 <http://www.gnu.org/licenses/>.
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = '''
+---
+module: os_ironic
+short_description: Create/Delete Bare Metal Resources from OpenStack
+extends_documentation_fragment: openstack
+author: "Monty Taylor (@emonty)"
+version_added: "2.0"
+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, 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:
+ description:
+ - 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_type"
+ 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.
+ suboptions:
+ power:
+ description:
+ - Information necessary to turn this server on / off.
+ This often includes such things as IPMI username, password, and IP address.
+ required: true
+ deploy:
+ description:
+ - Information necessary to deploy this server directly, without using Nova. THIS IS NOT RECOMMENDED.
+ console:
+ description:
+ - Information necessary to connect to this server's serial console. Not all drivers support this.
+ management:
+ description:
+ - 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
+ suboptions:
+ 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
+ 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
+ availability_zone:
+ description:
+ - Ignored. Present for backwards compatibility
+ required: false
+
+requirements: ["shade", "jsonpatch"]
+'''
+
+EXAMPLES = '''
+# Enroll a node with some basic properties and driver info
+- os_ironic:
+ cloud: "devstack"
+ driver: "pxe_ipmitool"
+ uuid: "00000000-0000-0000-0000-000000000002"
+ 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"
+ chassis_uuid: "00000000-0000-0000-0000-000000000001"
+
+'''
+
+try:
+ import shade
+ HAS_SHADE = True
+except ImportError:
+ HAS_SHADE = False
+
+try:
+ import jsonpatch
+ HAS_JSONPATCH = True
+except ImportError:
+ HAS_JSONPATCH = False
+
+
+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,
+ capabilities=p.get('capabilities') if p.get('capabilities') else '',
+ root_device=p.get('root_device') if p.get('root_device') else '',
+ )
+ 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 _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) is 1:
+ if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']:
+ # Return false to abort update as the password appears
+ # to be the only element in the patch.
+ return False
+ return True
+
+
+def _exit_node_not_updated(module, server):
+ module.exit_json(
+ changed=False,
+ result="Node not updated",
+ uuid=server['uuid'],
+ provision_state=server['provision_state']
+ )
+
+
+def main():
+ argument_spec = openstack_full_argument_spec(
+ uuid=dict(required=False),
+ 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, type='bool'),
+ 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 not HAS_JSONPATCH:
+ module.fail_json(msg='jsonpatch is required for this module')
+ if (module.params['auth_type'] in [None, 'None'] and
+ module.params['ironic_url'] is None):
+ 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)
+
+ try:
+ cloud = shade.operator_cloud(**module.params)
+ 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(
+ 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'],
+ 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 shade.OpenStackCloudException as e:
+ module.fail_json(msg=str(e))
+
+
+# this is magic, see lib/ansible/module_common.py
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+
+if __name__ == "__main__":
+ main()