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 | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 70 | ironic_interface = _get_ironic_session( |
| 71 | endpoint=endpoint, |
| 72 | token = token, |
| 73 | api_version=api_version) |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 74 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 75 | return func_name(ironic_interface, *args, **nkwargs) |
| 76 | return decorator_method |
| 77 | return _auth |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 78 | |
| 79 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 80 | @_autheticate() |
| 81 | def list_nodes(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 82 | ''' |
| 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 89 | in ironic_interface.node.list(*args, **kwargs)]} |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 90 | |
| 91 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 92 | @_autheticate() |
| 93 | def create_node(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 94 | ''' |
| 95 | create ironic node |
| 96 | CLI Example: |
| 97 | .. code-block:: bash |
| 98 | salt '*' ironic.create_node |
| 99 | ''' |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 100 | return ironic_interface.node.create(*args, **kwargs).to_dict() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 101 | |
| 102 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 103 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 104 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 116 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 117 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 128 | @_autheticate() |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 129 | def 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 143 | @_autheticate() |
| 144 | def list_ports(ironic_interface, *args, **kwargs): |
Vasyl Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 145 | ''' |
| 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 Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 153 | in ironic_interface.port.list(*args, **kwargs)]} |
| 154 | |
| 155 | @_autheticate() |
| 156 | def 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') |
| 167 | def 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') |
| 178 | def 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') |
| 189 | def 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 | |
| 199 | def _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 | |
| 214 | def _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 Saienko | 8403d17 | 2017-04-27 14:21:46 +0300 | [diff] [blame] | 222 | |
| 223 | |
Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame^] | 224 | def 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 | |
| 400 | def _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 | |
| 407 | def _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 |