blob: 5364ea52d003c80beccd812248845e4dcf407676 [file] [log] [blame]
Vasyl Saienko8403d172017-04-27 14:21:46 +03001# -*- coding: utf-8 -*-
2
3import logging
Vasyl Saienkoaad112d2017-06-19 16:45:37 +03004import json
Vasyl Saienko8403d172017-04-27 14:21:46 +03005from functools import wraps
6LOG = logging.getLogger(__name__)
7
8# Import third party libs
9HAS_IRONIC = False
10try:
11 from ironicclient import client
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030012 from ironicclient.common import utils
Vasyl Saienko8403d172017-04-27 14:21:46 +030013 HAS_IRONIC = True
14except ImportError:
15 pass
16
17__opts__ = {}
18
19
20def __virtual__():
21 '''
22 Only load this module if ironic is installed on this minion.
23 '''
24 if HAS_IRONIC:
25 return 'ironicng'
26 return False
27
28
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030029def _get_keystone_endpoint_and_token(**connection_args):
30 if connection_args.get('connection_endpoint_type') == None:
31 endpoint_type = 'internalURL'
32 else:
33 endpoint_type = connection_args.get('connection_endpoint_type')
34
35 kstone = __salt__['keystone.auth'](**connection_args)
36 endpoint = kstone.service_catalog.url_for(
37 service_type='baremetal', endpoint_type=endpoint_type)
38 token = kstone.auth_token
39 return endpoint, token
40
41
42def _get_ironic_session(endpoint, token, api_version=None):
43 return client.get_client(1, ironic_url=endpoint,
44 os_auth_token=token,
45 os_ironic_api_version=api_version)
46
47
48def _get_function_attrs(**kwargs):
49 connection_args = {'profile': kwargs.pop('profile', None)}
50 nkwargs = {}
51 for kwarg in kwargs:
52 if 'connection_' in kwarg:
53 connection_args.update({kwarg: kwargs[kwarg]})
54 elif '__' not in kwarg:
55 nkwargs.update({kwarg: kwargs[kwarg]})
56 return connection_args, nkwargs
57
58
59def _autheticate(api_version=None):
60 def _auth(func_name):
Vasyl Saienko8403d172017-04-27 14:21:46 +030061 '''
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030062 Authenticate requests with the salt keystone module and format return data
Vasyl Saienko8403d172017-04-27 14:21:46 +030063 '''
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030064 @wraps(func_name)
65 def decorator_method(*args, **kwargs):
66 '''Authenticate request and format return data'''
67 connection_args, nkwargs = _get_function_attrs(**kwargs)
68 endpoint, token = _get_keystone_endpoint_and_token(**connection_args)
Vasyl Saienko8403d172017-04-27 14:21:46 +030069
Vasyl Saienko4f925002017-09-19 14:05:19 +030070 ironic_api_version = api_version or connection_args.get(
71 'connection_ironic_api_version', None)
72
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030073 ironic_interface = _get_ironic_session(
74 endpoint=endpoint,
75 token = token,
Vasyl Saienko4f925002017-09-19 14:05:19 +030076 api_version=ironic_api_version)
Vasyl Saienko8403d172017-04-27 14:21:46 +030077
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030078 return func_name(ironic_interface, *args, **nkwargs)
79 return decorator_method
80 return _auth
Vasyl Saienko8403d172017-04-27 14:21:46 +030081
82
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030083@_autheticate()
84def list_nodes(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +030085 '''
86 list all ironic nodes
87 CLI Example:
88 .. code-block:: bash
89 salt '*' ironic.list_nodes
90 '''
91 return {'nodes': [x.to_dict() for x
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030092 in ironic_interface.node.list(*args, **kwargs)]}
Vasyl Saienko8403d172017-04-27 14:21:46 +030093
94
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030095@_autheticate()
96def create_node(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +030097 '''
98 create ironic node
99 CLI Example:
100 .. code-block:: bash
101 salt '*' ironic.create_node
102 '''
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300103 return ironic_interface.node.create(*args, **kwargs).to_dict()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300104
105
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300106@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300107def delete_node(ironic_interface, node_id):
108 '''
109 delete ironic node
110
111 :param node_id: UUID or Name of the node.
112 CLI Example:
113 .. code-block:: bash
114 salt '*' ironic.delete_node
115 '''
116 ironic_interface.node.delete(node_id)
117
118
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300119@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300120def show_node(ironic_interface, node_id):
121 '''
122 show info about ironic node
123 :param node_id: UUID or Name of the node.
124 CLI Example:
125 .. code-block:: bash
126 salt '*' ironic.show_node
127 '''
128 return ironic_interface.node.get(node_id).to_dict()
129
130
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300131@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300132def create_port(ironic_interface, address, node_name=None,
133 node_uuid=None, **kwargs):
134 '''
135 create ironic port
136 CLI Example:
137 .. code-block:: bash
138 salt '*' ironic.crate_port
139 '''
140 node_uuid = node_uuid or ironic_interface.node.get(
141 node_name).to_dict()['uuid']
142 return ironic_interface.port.create(
143 address=address, node_uuid=node_uuid, **kwargs).to_dict()
144
145
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300146@_autheticate()
147def list_ports(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +0300148 '''
149 list all ironic ports
150 CLI Example:
151 .. code-block:: bash
152 salt '*' ironic.list_ports
153 '''
154
155 return {'ports': [x.to_dict() for x
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300156 in ironic_interface.port.list(*args, **kwargs)]}
157
158@_autheticate()
159def node_set_provision_state(ironic_interface, *args, **kwargs):
160 '''Set the provision state for the node.
161
162 CLI Example:
163 .. code-block:: bash
164 salt '*' ironic.node_set_provision_state node_uuid=node-1 state=active profile=admin_identity
165 '''
166
167 ironic_interface.node.set_provision_state(*args, **kwargs)
168
169@_autheticate(api_version='1.28')
170def vif_attach(ironic_interface, *args, **kwargs):
171 '''Attach vif to a given node.
172
173 CLI Example:
174 .. code-block:: bash
175 salt '*' ironic.vif_attach node_ident=node-1 vif_id=vif1 profile=admin_identity
176 '''
177
178 ironic_interface.node.vif_attach(*args, **kwargs)
179
180@_autheticate(api_version='1.28')
181def vif_detach(ironic_interface, *args, **kwargs):
182 '''Detach vif from a given node.
183
184 CLI Example:
185 .. code-block:: bash
186 salt '*' ironic.vif_detach node_ident=node-1 vif_id=vif1 profile=admin_identity
187 '''
188
189 ironic_interface.node.vif_detach(*args, **kwargs)
190
191@_autheticate(api_version='1.28')
192def vif_list(ironic_interface, *args, **kwargs):
193 '''List vifs for a given node.
194
195 CLI Example:
196 .. code-block:: bash
197 salt '*' ironic.vif_list node_ident=node-1 profile=admin_identity
198 '''
199
200 return [vif.to_dict() for vif in ironic_interface.node.vif_list(*args, **kwargs)]
201
202def _merge_profiles(a, b, path=None):
203 """Merge b into a"""
204 if path is None: path = []
205 for key in b:
206 if key in a:
207 if isinstance(a[key], dict) and isinstance(b[key], dict):
208 _merge_profiles(a[key], b[key], path + [str(key)])
209 elif a[key] == b[key]:
210 pass # same leaf value
211 else:
212 raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
213 else:
214 a[key] = b[key]
215 return a
216
217def _get_node_deployment_profile(node_id, profile):
218 dp = {}
219 nodes = __salt__['pillar.get'](
220 'ironic:client:nodes:%s' % profile)
221
222 for node in nodes:
223 if node['name'] == node_id:
224 return node.get('deployment_profile')
Vasyl Saienko8403d172017-04-27 14:21:46 +0300225
226
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300227def deploy_node(node_id, image_source=None, root_gb=None,
228 image_checksum=None, configdrive=None, vif_id=None,
229 deployment_profile=None, partition_profile=None,
230 profile=None, **kwargs):
231 '''Deploy user image to ironic node
232
233 Deploy node with provided data. If deployment_profile is set,
234 try to get deploy data from pillar:
235
236 ironic:
237 client:
238 deployment_profiles:
239 profile1:
240 image_source:
241 image_checksum:
242 ...
243
244 :param node_id: UUID or Name of the node
245 :param image_source: URL/glance image uuid to deploy
246 :param root_gb: Size of root partition
247 :param image_checksum: md5 summ of image, only when image_source
248 is URL
249 :param configdrive: URL to or base64 gzipped iso config drive
250 :param vif_id: UUID of VIF to attach to node.
251 :param deployment_profile: id of the profile to look nodes in.
252 :param partition_profile: id of the partition profile to apply.
253 :param profile: auth profile to use.
254
255 CLI Example:
256 .. code-block:: bash
257 salt '*' ironicng.deploy_node node_id=node01 image_source=aaa-bbb-ccc-ddd-eee-fff
258 '''
259 deploy_params = [image_source, image_checksum, configdrive, vif_id]
260 if deployment_profile and any(deploy_params):
261 err_msg = ("deployment_profile can' be specified with any "
262 "of %s" % ', '.join(deploy_params))
263 LOG.error(err_msg)
264 return _deploy_failed(name, err_msg)
265
266 if partition_profile:
267 partition_profile = __salt__['pillar.get'](
268 'ironic:client:partition_profiles:%s' % partition_profile)
269
270 if deployment_profile:
271 deployment_profile = __salt__['pillar.get'](
272 'ironic:client:deployment_profiles:%s' % deployment_profile)
273 node_deployment_profile = _get_node_deployment_profile(
274 node_id, profile=profile) or {}
275 if partition_profile:
276 image_properties = deployment_profile['instance_info'].get('image_properties', {})
277 image_properties.update(partition_profile)
278 deployment_profile['instance_info']['image_properties'] = image_properties
279 _merge_profiles(deployment_profile, node_deployment_profile)
280 else:
281 deployment_profile = {
282 'instance_info': {
283 'image_source': image_source,
284 'image_checksum': image_checksum,
285 'root_gb': root_gb,
286 },
287 'configdrive': configdrive,
288 'network': {
289 'vif_id': vif_id,
290 }
291 }
292
293 connection_args, nkwargs = _get_function_attrs(profile=profile, **kwargs)
294
295 endpoint, token = _get_keystone_endpoint_and_token(**connection_args)
296 ironic_interface = _get_ironic_session(
297 endpoint=endpoint,
298 token = token)
299
300 def _convert_to_uuid(resource, name, **connection_args):
301 resources = __salt__['neutronng.list_%s' % resource](
302 name=name, **connection_args)
303
304 err_msg = None
305 if len(resources) == 0:
306 err_msg = "{0} with name {1} not found".format(
307 resource, network_name)
308 elif len(resources) > 1:
309 err_msg = "Multiple {0} with name {1} found.".format(
310 resource, network_name)
311 else:
312 return resources[resource][0]['id']
313
314 LOG.err(err_msg)
315 return _deploy_failed(name, err_msg)
316
317
318 def _prepare_node_for_deploy(ironic_interface,
319 node_id,
320 deployment_profile):
321
322 instance_info = deployment_profile.get('instance_info')
323 node_attr = []
324 for k,v in instance_info.iteritems():
325 node_attr.append('instance_info/%s=%s' % (k, json.dumps(v)))
326
327 net = deployment_profile.get('network')
328 vif_id = net.get('vif_id')
329 network_id = net.get('id')
330 network_name = net.get('name')
331 if (vif_id and any([network_name, network_id]) or
332 (network_name and network_id)):
333 err_msg = ("Only one of network:name or network:id or vif_id should be specified.")
334 LOG.error(err_msg)
335 return _deploy_failed(name, err_msg)
336
337 if network_name:
338 network_id = _convert_to_uuid('networks', network_name, **connection_args)
339
340 if network_id:
341 create_port_args = {
342 'name': '%s_port' % node_id,
343 'network_id': network_id,
344 }
345 fixed_ips = []
346 for fixed_ip in net.get('fixed_ips', []):
347 subnet_name = fixed_ip.get('subnet_name')
348 subnet_id = fixed_ip.get('subnet_id')
349 if subnet_name and subnet_id:
350 err_msg = ("Only one of subnet_name or subnet_id should be specified.")
351 LOG.error(err_msg)
352 return _deploy_failed(name, err_msg)
353 if subnet_name:
354 subnet_id = _convert_to_uuid('subnets', subnet_name, **connection_args)
355 if subnet_id:
356 fixed_ips.append({'ip_address': fixed_ip['ip_address'],
357 'subnet_id': subnet_id})
358 if fixed_ips:
359 create_port_args['fixed_ips'] = fixed_ips
360 create_port_args.update(connection_args)
361
362 vif_id = __salt__['neutronng.create_port'](**create_port_args)
363
364 if vif_id:
365 __salt__['ironicng.vif_attach'](node_ident=node_id, vif_id=vif_id, **connection_args)
366
367 configdrive = deployment_profile.get('configdrive')
368 if not configdrive:
369 metadata = deployment_profile.get('metadata')
370 if metadata:
371 configdrive_args = {}
372 userdata = metadata.get('userdata')
373 instance = metadata.get('instance')
374 hostname = instance.pop('hostname', node_id)
375 if userdata:
376 configdrive_args['user_data'] = userdata
377 if instance:
378 configdrive_args.update(instance)
379 configdrive = __salt__['configdrive.generate'](
380 dst='/tmp/%s' % node_id, hostname=hostname, ironic_format=True,
381 **configdrive_args)['base64_gzip']
382
383 if configdrive:
384 node_attr.append('instance_info/configdrive=%s' % configdrive)
385
386 if node_attr:
387 patch = utils.args_array_to_patch('add', node_attr)
388 ironic_interface.node.update(node_id, patch).to_dict()
389
390
391 _prepare_node_for_deploy(ironic_interface, node_id, deployment_profile)
392
393 provision_args = {
394 'node_uuid': node_id,
395 'state': 'active'
396 }
397 provision_args.update(connection_args)
398
399 __salt__['ironicng.node_set_provision_state'](**provision_args)
400 return _deploy_started(node_id)
401
402
403def _deploy_failed(name, reason):
404 changes_dict = {'name': name,
405 'comment': 'Deployment of node {0} failed to start due to {1}.'.format(name, reason),
406 'result': False}
407 return changes_dict
408
409
410def _deploy_started(name):
411 changes_dict = {'name': name,
412 'comment': 'Deployment of node {0} has been started.'.format(name),
413 'result': True}
414 return changes_dict