blob: d18a6d2a8b7fcfff8ae8d489157b0fb09df64709 [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 Saienkoaad112d2017-06-19 16:45:37 +030070 ironic_interface = _get_ironic_session(
71 endpoint=endpoint,
72 token = token,
73 api_version=api_version)
Vasyl Saienko8403d172017-04-27 14:21:46 +030074
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030075 return func_name(ironic_interface, *args, **nkwargs)
76 return decorator_method
77 return _auth
Vasyl Saienko8403d172017-04-27 14:21:46 +030078
79
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030080@_autheticate()
81def list_nodes(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +030082 '''
83 list all ironic nodes
84 CLI Example:
85 .. code-block:: bash
86 salt '*' ironic.list_nodes
87 '''
88 return {'nodes': [x.to_dict() for x
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030089 in ironic_interface.node.list(*args, **kwargs)]}
Vasyl Saienko8403d172017-04-27 14:21:46 +030090
91
Vasyl Saienkoaad112d2017-06-19 16:45:37 +030092@_autheticate()
93def create_node(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +030094 '''
95 create ironic node
96 CLI Example:
97 .. code-block:: bash
98 salt '*' ironic.create_node
99 '''
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300100 return ironic_interface.node.create(*args, **kwargs).to_dict()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300101
102
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300103@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300104def delete_node(ironic_interface, node_id):
105 '''
106 delete ironic node
107
108 :param node_id: UUID or Name of the node.
109 CLI Example:
110 .. code-block:: bash
111 salt '*' ironic.delete_node
112 '''
113 ironic_interface.node.delete(node_id)
114
115
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300116@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300117def show_node(ironic_interface, node_id):
118 '''
119 show info about ironic node
120 :param node_id: UUID or Name of the node.
121 CLI Example:
122 .. code-block:: bash
123 salt '*' ironic.show_node
124 '''
125 return ironic_interface.node.get(node_id).to_dict()
126
127
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300128@_autheticate()
Vasyl Saienko8403d172017-04-27 14:21:46 +0300129def create_port(ironic_interface, address, node_name=None,
130 node_uuid=None, **kwargs):
131 '''
132 create ironic port
133 CLI Example:
134 .. code-block:: bash
135 salt '*' ironic.crate_port
136 '''
137 node_uuid = node_uuid or ironic_interface.node.get(
138 node_name).to_dict()['uuid']
139 return ironic_interface.port.create(
140 address=address, node_uuid=node_uuid, **kwargs).to_dict()
141
142
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300143@_autheticate()
144def list_ports(ironic_interface, *args, **kwargs):
Vasyl Saienko8403d172017-04-27 14:21:46 +0300145 '''
146 list all ironic ports
147 CLI Example:
148 .. code-block:: bash
149 salt '*' ironic.list_ports
150 '''
151
152 return {'ports': [x.to_dict() for x
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300153 in ironic_interface.port.list(*args, **kwargs)]}
154
155@_autheticate()
156def node_set_provision_state(ironic_interface, *args, **kwargs):
157 '''Set the provision state for the node.
158
159 CLI Example:
160 .. code-block:: bash
161 salt '*' ironic.node_set_provision_state node_uuid=node-1 state=active profile=admin_identity
162 '''
163
164 ironic_interface.node.set_provision_state(*args, **kwargs)
165
166@_autheticate(api_version='1.28')
167def vif_attach(ironic_interface, *args, **kwargs):
168 '''Attach vif to a given node.
169
170 CLI Example:
171 .. code-block:: bash
172 salt '*' ironic.vif_attach node_ident=node-1 vif_id=vif1 profile=admin_identity
173 '''
174
175 ironic_interface.node.vif_attach(*args, **kwargs)
176
177@_autheticate(api_version='1.28')
178def vif_detach(ironic_interface, *args, **kwargs):
179 '''Detach vif from a given node.
180
181 CLI Example:
182 .. code-block:: bash
183 salt '*' ironic.vif_detach node_ident=node-1 vif_id=vif1 profile=admin_identity
184 '''
185
186 ironic_interface.node.vif_detach(*args, **kwargs)
187
188@_autheticate(api_version='1.28')
189def vif_list(ironic_interface, *args, **kwargs):
190 '''List vifs for a given node.
191
192 CLI Example:
193 .. code-block:: bash
194 salt '*' ironic.vif_list node_ident=node-1 profile=admin_identity
195 '''
196
197 return [vif.to_dict() for vif in ironic_interface.node.vif_list(*args, **kwargs)]
198
199def _merge_profiles(a, b, path=None):
200 """Merge b into a"""
201 if path is None: path = []
202 for key in b:
203 if key in a:
204 if isinstance(a[key], dict) and isinstance(b[key], dict):
205 _merge_profiles(a[key], b[key], path + [str(key)])
206 elif a[key] == b[key]:
207 pass # same leaf value
208 else:
209 raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
210 else:
211 a[key] = b[key]
212 return a
213
214def _get_node_deployment_profile(node_id, profile):
215 dp = {}
216 nodes = __salt__['pillar.get'](
217 'ironic:client:nodes:%s' % profile)
218
219 for node in nodes:
220 if node['name'] == node_id:
221 return node.get('deployment_profile')
Vasyl Saienko8403d172017-04-27 14:21:46 +0300222
223
Vasyl Saienkoaad112d2017-06-19 16:45:37 +0300224def deploy_node(node_id, image_source=None, root_gb=None,
225 image_checksum=None, configdrive=None, vif_id=None,
226 deployment_profile=None, partition_profile=None,
227 profile=None, **kwargs):
228 '''Deploy user image to ironic node
229
230 Deploy node with provided data. If deployment_profile is set,
231 try to get deploy data from pillar:
232
233 ironic:
234 client:
235 deployment_profiles:
236 profile1:
237 image_source:
238 image_checksum:
239 ...
240
241 :param node_id: UUID or Name of the node
242 :param image_source: URL/glance image uuid to deploy
243 :param root_gb: Size of root partition
244 :param image_checksum: md5 summ of image, only when image_source
245 is URL
246 :param configdrive: URL to or base64 gzipped iso config drive
247 :param vif_id: UUID of VIF to attach to node.
248 :param deployment_profile: id of the profile to look nodes in.
249 :param partition_profile: id of the partition profile to apply.
250 :param profile: auth profile to use.
251
252 CLI Example:
253 .. code-block:: bash
254 salt '*' ironicng.deploy_node node_id=node01 image_source=aaa-bbb-ccc-ddd-eee-fff
255 '''
256 deploy_params = [image_source, image_checksum, configdrive, vif_id]
257 if deployment_profile and any(deploy_params):
258 err_msg = ("deployment_profile can' be specified with any "
259 "of %s" % ', '.join(deploy_params))
260 LOG.error(err_msg)
261 return _deploy_failed(name, err_msg)
262
263 if partition_profile:
264 partition_profile = __salt__['pillar.get'](
265 'ironic:client:partition_profiles:%s' % partition_profile)
266
267 if deployment_profile:
268 deployment_profile = __salt__['pillar.get'](
269 'ironic:client:deployment_profiles:%s' % deployment_profile)
270 node_deployment_profile = _get_node_deployment_profile(
271 node_id, profile=profile) or {}
272 if partition_profile:
273 image_properties = deployment_profile['instance_info'].get('image_properties', {})
274 image_properties.update(partition_profile)
275 deployment_profile['instance_info']['image_properties'] = image_properties
276 _merge_profiles(deployment_profile, node_deployment_profile)
277 else:
278 deployment_profile = {
279 'instance_info': {
280 'image_source': image_source,
281 'image_checksum': image_checksum,
282 'root_gb': root_gb,
283 },
284 'configdrive': configdrive,
285 'network': {
286 'vif_id': vif_id,
287 }
288 }
289
290 connection_args, nkwargs = _get_function_attrs(profile=profile, **kwargs)
291
292 endpoint, token = _get_keystone_endpoint_and_token(**connection_args)
293 ironic_interface = _get_ironic_session(
294 endpoint=endpoint,
295 token = token)
296
297 def _convert_to_uuid(resource, name, **connection_args):
298 resources = __salt__['neutronng.list_%s' % resource](
299 name=name, **connection_args)
300
301 err_msg = None
302 if len(resources) == 0:
303 err_msg = "{0} with name {1} not found".format(
304 resource, network_name)
305 elif len(resources) > 1:
306 err_msg = "Multiple {0} with name {1} found.".format(
307 resource, network_name)
308 else:
309 return resources[resource][0]['id']
310
311 LOG.err(err_msg)
312 return _deploy_failed(name, err_msg)
313
314
315 def _prepare_node_for_deploy(ironic_interface,
316 node_id,
317 deployment_profile):
318
319 instance_info = deployment_profile.get('instance_info')
320 node_attr = []
321 for k,v in instance_info.iteritems():
322 node_attr.append('instance_info/%s=%s' % (k, json.dumps(v)))
323
324 net = deployment_profile.get('network')
325 vif_id = net.get('vif_id')
326 network_id = net.get('id')
327 network_name = net.get('name')
328 if (vif_id and any([network_name, network_id]) or
329 (network_name and network_id)):
330 err_msg = ("Only one of network:name or network:id or vif_id should be specified.")
331 LOG.error(err_msg)
332 return _deploy_failed(name, err_msg)
333
334 if network_name:
335 network_id = _convert_to_uuid('networks', network_name, **connection_args)
336
337 if network_id:
338 create_port_args = {
339 'name': '%s_port' % node_id,
340 'network_id': network_id,
341 }
342 fixed_ips = []
343 for fixed_ip in net.get('fixed_ips', []):
344 subnet_name = fixed_ip.get('subnet_name')
345 subnet_id = fixed_ip.get('subnet_id')
346 if subnet_name and subnet_id:
347 err_msg = ("Only one of subnet_name or subnet_id should be specified.")
348 LOG.error(err_msg)
349 return _deploy_failed(name, err_msg)
350 if subnet_name:
351 subnet_id = _convert_to_uuid('subnets', subnet_name, **connection_args)
352 if subnet_id:
353 fixed_ips.append({'ip_address': fixed_ip['ip_address'],
354 'subnet_id': subnet_id})
355 if fixed_ips:
356 create_port_args['fixed_ips'] = fixed_ips
357 create_port_args.update(connection_args)
358
359 vif_id = __salt__['neutronng.create_port'](**create_port_args)
360
361 if vif_id:
362 __salt__['ironicng.vif_attach'](node_ident=node_id, vif_id=vif_id, **connection_args)
363
364 configdrive = deployment_profile.get('configdrive')
365 if not configdrive:
366 metadata = deployment_profile.get('metadata')
367 if metadata:
368 configdrive_args = {}
369 userdata = metadata.get('userdata')
370 instance = metadata.get('instance')
371 hostname = instance.pop('hostname', node_id)
372 if userdata:
373 configdrive_args['user_data'] = userdata
374 if instance:
375 configdrive_args.update(instance)
376 configdrive = __salt__['configdrive.generate'](
377 dst='/tmp/%s' % node_id, hostname=hostname, ironic_format=True,
378 **configdrive_args)['base64_gzip']
379
380 if configdrive:
381 node_attr.append('instance_info/configdrive=%s' % configdrive)
382
383 if node_attr:
384 patch = utils.args_array_to_patch('add', node_attr)
385 ironic_interface.node.update(node_id, patch).to_dict()
386
387
388 _prepare_node_for_deploy(ironic_interface, node_id, deployment_profile)
389
390 provision_args = {
391 'node_uuid': node_id,
392 'state': 'active'
393 }
394 provision_args.update(connection_args)
395
396 __salt__['ironicng.node_set_provision_state'](**provision_args)
397 return _deploy_started(node_id)
398
399
400def _deploy_failed(name, reason):
401 changes_dict = {'name': name,
402 'comment': 'Deployment of node {0} failed to start due to {1}.'.format(name, reason),
403 'result': False}
404 return changes_dict
405
406
407def _deploy_started(name):
408 changes_dict = {'name': name,
409 'comment': 'Deployment of node {0} has been started.'.format(name),
410 'result': True}
411 return changes_dict