4 # (c) 2014, Hewlett-Packard Development Company, L.P.
6 # This module is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This software is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this software. If not, see <http://www.gnu.org/licenses/>.
19 ANSIBLE_METADATA = {'metadata_version': '1.1',
20 'status': ['preview'],
21 'supported_by': 'community'}
27 short_description: Create/Delete Bare Metal Resources from OpenStack
28 extends_documentation_fragment: openstack
29 author: "Monty Taylor (@emonty)"
32 - Create or Remove Ironic nodes from OpenStack.
36 - Indicates desired state of the resource
37 choices: ['present', 'absent']
41 - globally unique identifier (UUID) to be given to the resource. Will
42 be auto-generated if not specified, and name is specified.
43 - Definition of a UUID will always take precedence to a name value.
48 - unique name identifier to be given to the resource.
53 - The name of the Ironic Driver to use with this node.
58 - Associate the node with a pre-defined chassis.
63 - If noauth mode is utilized, this is required to be set to the
64 endpoint URL for the Ironic API. Use with "auth" and "auth_type"
70 - Information for this server's driver. Will vary based on which
71 driver is in use. Any sub-field which is populated will be validated
76 - Information necessary to turn this server on / off.
77 This often includes such things as IPMI username, password, and IP address.
81 - Information necessary to deploy this server directly, without using Nova. THIS IS NOT RECOMMENDED.
84 - Information necessary to connect to this server's serial console. Not all drivers support this.
87 - Information necessary to interact with this server's management interface. May be shared by power_info in some cases.
91 - 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"'
95 - Definition of the physical characteristics of this server, used for scheduling purposes
99 - CPU architecture (x86_64, i686, ...)
103 - Number of CPU cores this machine has
107 - amount of RAM this machine has, in MB
111 - size of first storage device in this machine (typically /dev/sda), in GB
113 skip_update_of_driver_password:
115 - Allows the code that would assert changes to nodes to skip the
116 update if the change is a single line consisting of the password
117 field. As of Kilo, by default, passwords are always masked to API
118 requests, which means the logic as a result always attempts to
119 re-assert the password field.
124 - Ignored. Present for backwards compatibility
127 requirements: ["shade", "jsonpatch"]
131 # Enroll a node with some basic properties and driver info
134 driver: "pxe_ipmitool"
135 uuid: "00000000-0000-0000-0000-000000000002"
142 - mac: "aa:bb:cc:aa:bb:cc"
143 - mac: "dd:ee:ff:dd:ee:ff"
146 ipmi_address: "1.2.3.4"
147 ipmi_username: "admin"
148 ipmi_password: "adminpass"
149 chassis_uuid: "00000000-0000-0000-0000-000000000001"
163 HAS_JSONPATCH = False
166 def _parse_properties(module):
167 p = module.params['properties']
169 cpu_arch=p.get('cpu_arch') if p.get('cpu_arch') else 'x86_64',
170 cpus=p.get('cpus') if p.get('cpus') else 1,
171 memory_mb=p.get('ram') if p.get('ram') else 1,
172 local_gb=p.get('disk_size') if p.get('disk_size') else 1,
173 capabilities=p.get('capabilities') if p.get('capabilities') else '',
174 root_device=p.get('root_device') if p.get('root_device') else '',
179 def _parse_driver_info(module):
180 p = module.params['driver_info']
181 info = p.get('power')
183 raise shade.OpenStackCloudException(
184 "driver_info['power'] is required")
186 info.update(p.get('console'))
187 if p.get('management'):
188 info.update(p.get('management'))
190 info.update(p.get('deploy'))
194 def _choose_id_value(module):
195 if module.params['uuid']:
196 return module.params['uuid']
197 if module.params['name']:
198 return module.params['name']
204 def _choose_if_password_only(module, patch):
206 if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']:
207 # Return false to abort update as the password appears
208 # to be the only element in the patch.
213 def _exit_node_not_updated(module, server):
216 result="Node not updated",
218 provision_state=server['provision_state']
223 argument_spec = openstack_full_argument_spec(
224 uuid=dict(required=False),
225 name=dict(required=False),
226 driver=dict(required=False),
227 driver_info=dict(type='dict', required=True),
228 nics=dict(type='list', required=True),
229 properties=dict(type='dict', default={}),
230 ironic_url=dict(required=False),
231 chassis_uuid=dict(required=False),
232 skip_update_of_masked_password=dict(required=False, type='bool'),
233 state=dict(required=False, default='present')
235 module_kwargs = openstack_module_kwargs()
236 module = AnsibleModule(argument_spec, **module_kwargs)
239 module.fail_json(msg='shade is required for this module')
240 if not HAS_JSONPATCH:
241 module.fail_json(msg='jsonpatch is required for this module')
242 if (module.params['auth_type'] in [None, 'None'] and
243 module.params['ironic_url'] is None):
244 module.fail_json(msg="Authentication appears to be disabled, "
245 "Please define an ironic_url parameter")
247 if (module.params['ironic_url'] and
248 module.params['auth_type'] in [None, 'None']):
249 module.params['auth'] = dict(
250 endpoint=module.params['ironic_url']
253 node_id = _choose_id_value(module)
256 cloud = shade.operator_cloud(**module.params)
257 server = cloud.get_machine(node_id)
258 if module.params['state'] == 'present':
259 if module.params['driver'] is None:
260 module.fail_json(msg="A driver must be defined in order "
261 "to set a node to present.")
263 properties = _parse_properties(module)
264 driver_info = _parse_driver_info(module)
266 driver=module.params['driver'],
267 properties=properties,
268 driver_info=driver_info,
269 name=module.params['name'],
272 if module.params['chassis_uuid']:
273 kwargs['chassis_uuid'] = module.params['chassis_uuid']
276 # Note(TheJulia): Add a specific UUID to the request if
277 # present in order to be able to re-use kwargs for if
278 # the node already exists logic, since uuid cannot be
280 if module.params['uuid']:
281 kwargs['uuid'] = module.params['uuid']
283 server = cloud.register_machine(module.params['nics'],
285 module.exit_json(changed=True, uuid=server['uuid'],
286 provision_state=server['provision_state'])
288 # TODO(TheJulia): Presently this does not support updating
289 # nics. Support needs to be added.
291 # Note(TheJulia): This message should never get logged
292 # however we cannot realistically proceed if neither a
293 # name or uuid was supplied to begin with.
295 module.fail_json(msg="A uuid or name value "
298 # Note(TheJulia): Constructing the configuration to compare
299 # against. The items listed in the server_config block can
300 # be updated via the API.
302 server_config = dict(
303 driver=server['driver'],
304 properties=server['properties'],
305 driver_info=server['driver_info'],
309 # Add the pre-existing chassis_uuid only if
310 # it is present in the server configuration.
311 if hasattr(server, 'chassis_uuid'):
312 server_config['chassis_uuid'] = server['chassis_uuid']
314 # Note(TheJulia): If a password is defined and concealed, a
315 # patch will always be generated and re-asserted.
316 patch = jsonpatch.JsonPatch.from_diff(server_config, kwargs)
319 _exit_node_not_updated(module, server)
320 elif _choose_if_password_only(module, list(patch)):
321 # Note(TheJulia): Normally we would allow the general
322 # exception catch below, however this allows a specific
325 server = cloud.patch_machine(
328 except Exception as e:
329 module.fail_json(msg="Failed to update node, "
330 "Error: %s" % e.message)
332 # Enumerate out a list of changed paths.
334 for change in list(patch):
335 change_list.append(change['path'])
336 module.exit_json(changed=True,
337 result="Node Updated",
340 provision_state=server['provision_state'])
342 # Return not updated by default as the conditions were not met
344 _exit_node_not_updated(module, server)
346 if module.params['state'] == 'absent':
348 module.fail_json(msg="A uuid or name value must be defined "
349 "in order to remove a node.")
351 if server is not None:
352 cloud.unregister_machine(module.params['nics'],
354 module.exit_json(changed=True, result="deleted")
356 module.exit_json(changed=False, result="Server not found")
358 except shade.OpenStackCloudException as e:
359 module.fail_json(msg=str(e))
362 # this is magic, see lib/ansible/module_common.py
363 from ansible.module_utils.basic import *
364 from ansible.module_utils.openstack import *
366 if __name__ == "__main__":