Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | import logging |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 4 | import json |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 5 | from functools import wraps |
| 6 | LOG = logging.getLogger(__name__) |
| 7 | |
| 8 | # Import third party libs |
| 9 | HAS_IRONIC = False |
| 10 | try: |
| 11 | from ironicclient import client |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 12 | from ironicclient.common import utils |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 13 | HAS_IRONIC = True |
| 14 | except ImportError: |
| 15 | pass |
| 16 | |
| 17 | __opts__ = {} |
| 18 | |
| 19 | |
| 20 | def __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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 29 | def _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 | |
| 42 | def _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 | |
| 48 | def _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 | |
| 59 | def _autheticate(api_version=None): |
| 60 | def _auth(func_name): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 61 | ''' |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 62 | Authenticate requests with the salt keystone module and format return data |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 63 | ''' |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 64 | @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 Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 69 | |
Vasyl Saienko | 4f92500 | 2017-09-19 14:05:19 +0300 | [diff] [blame] | 70 | ironic_api_version = api_version or connection_args.get( |
| 71 | 'connection_ironic_api_version', None) |
| 72 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 73 | ironic_interface = _get_ironic_session( |
| 74 | endpoint=endpoint, |
| 75 | token = token, |
Vasyl Saienko | 4f92500 | 2017-09-19 14:05:19 +0300 | [diff] [blame] | 76 | api_version=ironic_api_version) |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 77 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 78 | return func_name(ironic_interface, *args, **nkwargs) |
| 79 | return decorator_method |
| 80 | return _auth |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 81 | |
| 82 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 83 | @_autheticate() |
| 84 | def list_nodes(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 85 | ''' |
| 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 92 | in ironic_interface.node.list(*args, **kwargs)]} |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 93 | |
| 94 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 95 | @_autheticate() |
| 96 | def create_node(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 97 | ''' |
| 98 | create ironic node |
| 99 | CLI Example: |
| 100 | .. code-block:: bash |
| 101 | salt '*' ironic.create_node |
| 102 | ''' |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 103 | return ironic_interface.node.create(*args, **kwargs).to_dict() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 104 | |
| 105 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 106 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 107 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 119 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 120 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 131 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 132 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 146 | @_autheticate() |
| 147 | def list_ports(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 148 | ''' |
| 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 156 | in ironic_interface.port.list(*args, **kwargs)]} |
| 157 | |
| 158 | @_autheticate() |
| 159 | def 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') |
| 170 | def 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') |
| 181 | def 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') |
| 192 | def 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 | |
| 202 | def _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 | |
| 217 | def _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 Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 225 | |
| 226 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 227 | def 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 | |
| 403 | def _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 | |
| 410 | def _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 |