|  | # -*- coding: utf-8 -*- | 
|  |  | 
|  | import logging | 
|  | import json | 
|  | from functools import wraps | 
|  | LOG = logging.getLogger(__name__) | 
|  |  | 
|  | # Import third party libs | 
|  | HAS_IRONIC = False | 
|  | try: | 
|  | from ironicclient import client | 
|  | from ironicclient.common import utils | 
|  | HAS_IRONIC = True | 
|  | except ImportError: | 
|  | pass | 
|  |  | 
|  | __opts__ = {} | 
|  |  | 
|  |  | 
|  | def __virtual__(): | 
|  | ''' | 
|  | Only load this module if ironic is installed on this minion. | 
|  | ''' | 
|  | if HAS_IRONIC: | 
|  | return 'ironicng' | 
|  | return False | 
|  |  | 
|  |  | 
|  | def _get_keystone_endpoint_and_token(**connection_args): | 
|  | if connection_args.get('connection_endpoint_type') == None: | 
|  | endpoint_type = 'internalURL' | 
|  | else: | 
|  | endpoint_type = connection_args.get('connection_endpoint_type') | 
|  |  | 
|  | kstone = __salt__['keystone.auth'](**connection_args) | 
|  | endpoint = kstone.service_catalog.url_for( | 
|  | service_type='baremetal', endpoint_type=endpoint_type) | 
|  | token = kstone.auth_token | 
|  | return endpoint, token | 
|  |  | 
|  |  | 
|  | def _get_ironic_session(endpoint, token, api_version=None): | 
|  | return client.get_client(1, ironic_url=endpoint, | 
|  | os_auth_token=token, | 
|  | os_ironic_api_version=api_version) | 
|  |  | 
|  |  | 
|  | def _get_function_attrs(**kwargs): | 
|  | connection_args = {'profile': kwargs.pop('profile', None)} | 
|  | nkwargs = {} | 
|  | for kwarg in kwargs: | 
|  | if 'connection_' in kwarg: | 
|  | connection_args.update({kwarg: kwargs[kwarg]}) | 
|  | elif '__' not in kwarg: | 
|  | nkwargs.update({kwarg: kwargs[kwarg]}) | 
|  | return connection_args, nkwargs | 
|  |  | 
|  |  | 
|  | def _autheticate(api_version=None): | 
|  | def _auth(func_name): | 
|  | ''' | 
|  | Authenticate requests with the salt keystone module and format return data | 
|  | ''' | 
|  | @wraps(func_name) | 
|  | def decorator_method(*args, **kwargs): | 
|  | '''Authenticate request and format return data''' | 
|  | connection_args, nkwargs = _get_function_attrs(**kwargs) | 
|  | endpoint, token = _get_keystone_endpoint_and_token(**connection_args) | 
|  |  | 
|  | ironic_api_version = api_version or connection_args.get( | 
|  | 'connection_ironic_api_version', None) | 
|  |  | 
|  | ironic_interface = _get_ironic_session( | 
|  | endpoint=endpoint, | 
|  | token = token, | 
|  | api_version=ironic_api_version) | 
|  |  | 
|  | return func_name(ironic_interface, *args, **nkwargs) | 
|  | return decorator_method | 
|  | return _auth | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def list_nodes(ironic_interface, *args, **kwargs): | 
|  | ''' | 
|  | list all ironic nodes | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.list_nodes | 
|  | ''' | 
|  | return {'nodes': [x.to_dict() for x | 
|  | in ironic_interface.node.list(*args, **kwargs)]} | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def create_node(ironic_interface, *args, **kwargs): | 
|  | ''' | 
|  | create ironic node | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.create_node | 
|  | ''' | 
|  | return ironic_interface.node.create(*args, **kwargs).to_dict() | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def delete_node(ironic_interface, node_id): | 
|  | ''' | 
|  | delete ironic node | 
|  |  | 
|  | :param node_id: UUID or Name of the node. | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.delete_node | 
|  | ''' | 
|  | ironic_interface.node.delete(node_id) | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def show_node(ironic_interface, node_id): | 
|  | ''' | 
|  | show info about ironic node | 
|  | :param node_id: UUID or Name of the node. | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.show_node | 
|  | ''' | 
|  | return ironic_interface.node.get(node_id).to_dict() | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def create_port(ironic_interface, address, node_name=None, | 
|  | node_uuid=None, **kwargs): | 
|  | ''' | 
|  | create ironic port | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.crate_port | 
|  | ''' | 
|  | node_uuid = node_uuid or ironic_interface.node.get( | 
|  | node_name).to_dict()['uuid'] | 
|  | return ironic_interface.port.create( | 
|  | address=address, node_uuid=node_uuid, **kwargs).to_dict() | 
|  |  | 
|  |  | 
|  | @_autheticate() | 
|  | def list_ports(ironic_interface, *args, **kwargs): | 
|  | ''' | 
|  | list all ironic ports | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.list_ports | 
|  | ''' | 
|  |  | 
|  | return {'ports': [x.to_dict() for x | 
|  | in ironic_interface.port.list(*args, **kwargs)]} | 
|  |  | 
|  | @_autheticate() | 
|  | def node_set_provision_state(ironic_interface, *args, **kwargs): | 
|  | '''Set the provision state for the node. | 
|  |  | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.node_set_provision_state node_uuid=node-1 state=active profile=admin_identity | 
|  | ''' | 
|  |  | 
|  | ironic_interface.node.set_provision_state(*args, **kwargs) | 
|  |  | 
|  | @_autheticate(api_version='1.28') | 
|  | def vif_attach(ironic_interface, *args, **kwargs): | 
|  | '''Attach vif to a given node. | 
|  |  | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.vif_attach node_ident=node-1 vif_id=vif1 profile=admin_identity | 
|  | ''' | 
|  |  | 
|  | ironic_interface.node.vif_attach(*args, **kwargs) | 
|  |  | 
|  | @_autheticate(api_version='1.28') | 
|  | def vif_detach(ironic_interface, *args, **kwargs): | 
|  | '''Detach vif from a given node. | 
|  |  | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.vif_detach node_ident=node-1 vif_id=vif1 profile=admin_identity | 
|  | ''' | 
|  |  | 
|  | ironic_interface.node.vif_detach(*args, **kwargs) | 
|  |  | 
|  | @_autheticate(api_version='1.28') | 
|  | def vif_list(ironic_interface, *args, **kwargs): | 
|  | '''List vifs for a given node. | 
|  |  | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironic.vif_list node_ident=node-1 profile=admin_identity | 
|  | ''' | 
|  |  | 
|  | return [vif.to_dict() for vif in ironic_interface.node.vif_list(*args, **kwargs)] | 
|  |  | 
|  | def _merge_profiles(a, b, path=None): | 
|  | """Merge b into a""" | 
|  | if path is None: path = [] | 
|  | for key in b: | 
|  | if key in a: | 
|  | if isinstance(a[key], dict) and isinstance(b[key], dict): | 
|  | _merge_profiles(a[key], b[key], path + [str(key)]) | 
|  | elif a[key] == b[key]: | 
|  | pass # same leaf value | 
|  | else: | 
|  | raise Exception('Conflict at %s' % '.'.join(path + [str(key)])) | 
|  | else: | 
|  | a[key] = b[key] | 
|  | return a | 
|  |  | 
|  | def _get_node_deployment_profile(node_id, profile): | 
|  | dp = {} | 
|  | nodes = __salt__['pillar.get']( | 
|  | 'ironic:client:nodes:%s' % profile) | 
|  |  | 
|  | for node in nodes: | 
|  | if node['name'] == node_id: | 
|  | return node.get('deployment_profile') | 
|  |  | 
|  |  | 
|  | def deploy_node(node_id, image_source=None, root_gb=None, | 
|  | image_checksum=None, configdrive=None, vif_id=None, | 
|  | deployment_profile=None, partition_profile=None, | 
|  | profile=None, **kwargs): | 
|  | '''Deploy user image to ironic node | 
|  |  | 
|  | Deploy node with provided data. If deployment_profile is set, | 
|  | try to get deploy data from pillar: | 
|  |  | 
|  | ironic: | 
|  | client: | 
|  | deployment_profiles: | 
|  | profile1: | 
|  | image_source: | 
|  | image_checksum: | 
|  | ... | 
|  |  | 
|  | :param node_id: UUID or Name of the node | 
|  | :param image_source: URL/glance image uuid to deploy | 
|  | :param root_gb: Size of root partition | 
|  | :param image_checksum: md5 summ of image, only when image_source | 
|  | is URL | 
|  | :param configdrive: URL to or base64 gzipped iso config drive | 
|  | :param vif_id: UUID of VIF to attach to node. | 
|  | :param deployment_profile: id of the profile to look nodes in. | 
|  | :param partition_profile: id of the partition profile to apply. | 
|  | :param profile: auth profile to use. | 
|  |  | 
|  | CLI Example: | 
|  | .. code-block:: bash | 
|  | salt '*' ironicng.deploy_node node_id=node01 image_source=aaa-bbb-ccc-ddd-eee-fff | 
|  | ''' | 
|  | deploy_params = [image_source, image_checksum, configdrive, vif_id] | 
|  | if deployment_profile and any(deploy_params): | 
|  | err_msg = ("deployment_profile can' be specified with any " | 
|  | "of %s" % ', '.join(deploy_params)) | 
|  | LOG.error(err_msg) | 
|  | return  _deploy_failed(name, err_msg) | 
|  |  | 
|  | if partition_profile: | 
|  | partition_profile = __salt__['pillar.get']( | 
|  | 'ironic:client:partition_profiles:%s' % partition_profile) | 
|  |  | 
|  | if deployment_profile: | 
|  | deployment_profile = __salt__['pillar.get']( | 
|  | 'ironic:client:deployment_profiles:%s' % deployment_profile) | 
|  | node_deployment_profile = _get_node_deployment_profile( | 
|  | node_id, profile=profile) or {} | 
|  | if partition_profile: | 
|  | image_properties = deployment_profile['instance_info'].get('image_properties', {}) | 
|  | image_properties.update(partition_profile) | 
|  | deployment_profile['instance_info']['image_properties'] = image_properties | 
|  | _merge_profiles(deployment_profile, node_deployment_profile) | 
|  | else: | 
|  | deployment_profile = { | 
|  | 'instance_info': { | 
|  | 'image_source': image_source, | 
|  | 'image_checksum': image_checksum, | 
|  | 'root_gb': root_gb, | 
|  | }, | 
|  | 'configdrive': configdrive, | 
|  | 'network': { | 
|  | 'vif_id': vif_id, | 
|  | } | 
|  | } | 
|  |  | 
|  | connection_args, nkwargs = _get_function_attrs(profile=profile, **kwargs) | 
|  |  | 
|  | endpoint, token = _get_keystone_endpoint_and_token(**connection_args) | 
|  | ironic_interface = _get_ironic_session( | 
|  | endpoint=endpoint, | 
|  | token = token) | 
|  |  | 
|  | def _convert_to_uuid(resource, name, **connection_args): | 
|  | resources =  __salt__['neutronng.list_%s' % resource]( | 
|  | name=name, **connection_args) | 
|  |  | 
|  | err_msg = None | 
|  | if len(resources) == 0: | 
|  | err_msg = "{0} with name {1} not found".format( | 
|  | resource, network_name) | 
|  | elif len(resources) > 1: | 
|  | err_msg = "Multiple {0} with name {1} found.".format( | 
|  | resource, network_name) | 
|  | else: | 
|  | return resources[resource][0]['id'] | 
|  |  | 
|  | LOG.err(err_msg) | 
|  | return _deploy_failed(name, err_msg) | 
|  |  | 
|  |  | 
|  | def _prepare_node_for_deploy(ironic_interface, | 
|  | node_id, | 
|  | deployment_profile): | 
|  |  | 
|  | instance_info = deployment_profile.get('instance_info') | 
|  | node_attr = [] | 
|  | for k,v in instance_info.iteritems(): | 
|  | node_attr.append('instance_info/%s=%s' % (k, json.dumps(v))) | 
|  |  | 
|  | net = deployment_profile.get('network') | 
|  | vif_id = net.get('vif_id') | 
|  | network_id = net.get('id') | 
|  | network_name = net.get('name') | 
|  | if (vif_id and any([network_name, network_id]) or | 
|  | (network_name and network_id)): | 
|  | err_msg = ("Only one of network:name or network:id or vif_id should be specified.") | 
|  | LOG.error(err_msg) | 
|  | return  _deploy_failed(name, err_msg) | 
|  |  | 
|  | if network_name: | 
|  | network_id = _convert_to_uuid('networks', network_name, **connection_args) | 
|  |  | 
|  | if network_id: | 
|  | create_port_args = { | 
|  | 'name': '%s_port' % node_id, | 
|  | 'network_id': network_id, | 
|  | } | 
|  | fixed_ips = [] | 
|  | for fixed_ip in net.get('fixed_ips', []): | 
|  | subnet_name = fixed_ip.get('subnet_name') | 
|  | subnet_id = fixed_ip.get('subnet_id') | 
|  | if subnet_name and subnet_id: | 
|  | err_msg = ("Only one of subnet_name or subnet_id should be specified.") | 
|  | LOG.error(err_msg) | 
|  | return  _deploy_failed(name, err_msg) | 
|  | if subnet_name: | 
|  | subnet_id = _convert_to_uuid('subnets', subnet_name, **connection_args) | 
|  | if subnet_id: | 
|  | fixed_ips.append({'ip_address': fixed_ip['ip_address'], | 
|  | 'subnet_id': subnet_id}) | 
|  | if fixed_ips: | 
|  | create_port_args['fixed_ips'] = fixed_ips | 
|  | create_port_args.update(connection_args) | 
|  |  | 
|  | vif_id = __salt__['neutronng.create_port'](**create_port_args) | 
|  |  | 
|  | if vif_id: | 
|  | __salt__['ironicng.vif_attach'](node_ident=node_id, vif_id=vif_id, **connection_args) | 
|  |  | 
|  | configdrive = deployment_profile.get('configdrive') | 
|  | if not configdrive: | 
|  | metadata = deployment_profile.get('metadata') | 
|  | if metadata: | 
|  | configdrive_args = {} | 
|  | userdata = metadata.get('userdata') | 
|  | instance = metadata.get('instance') | 
|  | hostname = instance.pop('hostname', node_id) | 
|  | if userdata: | 
|  | configdrive_args['user_data'] = userdata | 
|  | if instance: | 
|  | configdrive_args.update(instance) | 
|  | configdrive = __salt__['configdrive.generate']( | 
|  | dst='/tmp/%s' % node_id, hostname=hostname, ironic_format=True, | 
|  | **configdrive_args)['base64_gzip'] | 
|  |  | 
|  | if configdrive: | 
|  | node_attr.append('instance_info/configdrive=%s' % configdrive) | 
|  |  | 
|  | if node_attr: | 
|  | patch = utils.args_array_to_patch('add', node_attr) | 
|  | ironic_interface.node.update(node_id, patch).to_dict() | 
|  |  | 
|  |  | 
|  | _prepare_node_for_deploy(ironic_interface, node_id, deployment_profile) | 
|  |  | 
|  | provision_args = { | 
|  | 'node_uuid': node_id, | 
|  | 'state': 'active' | 
|  | } | 
|  | provision_args.update(connection_args) | 
|  |  | 
|  | __salt__['ironicng.node_set_provision_state'](**provision_args) | 
|  | return _deploy_started(node_id) | 
|  |  | 
|  |  | 
|  | def _deploy_failed(name, reason): | 
|  | changes_dict = {'name': name, | 
|  | 'comment': 'Deployment of node {0} failed to start due to {1}.'.format(name, reason), | 
|  | 'result': False} | 
|  | return changes_dict | 
|  |  | 
|  |  | 
|  | def _deploy_started(name): | 
|  | changes_dict = {'name': name, | 
|  | 'comment': 'Deployment of node {0} has been started.'.format(name), | 
|  | 'result': True} | 
|  | return changes_dict |