| import collections |
| import logging |
| import time |
| |
| log = logging.getLogger(__name__) |
| |
| |
| def __virtual__(): |
| return 'ironicv1' if 'ironicv1.node_list' in __salt__ else False |
| |
| |
| def _ironicv1_call(fname, *args, **kwargs): |
| return __salt__['ironicv1.{}'.format(fname)](*args, **kwargs) |
| |
| |
| def node_present(name, cloud_name, driver, **kwargs): |
| resource = 'node' |
| microversion = kwargs.pop('microversion', '1.16') |
| try: |
| method_name = '{}_get_details'.format(resource) |
| exact_resource = _ironicv1_call( |
| method_name, name, cloud_name=cloud_name, |
| microversion=microversion |
| ) |
| except Exception as e: |
| if 'Not Found' in str(e): |
| try: |
| method_name = '{}_create'.format(resource) |
| resp = _ironicv1_call( |
| method_name, driver, name=name, cloud_name=cloud_name, |
| microversion=microversion, |
| **kwargs |
| ) |
| except Exception as e: |
| log.exception('Ironic {0} create failed with {1}'. |
| format('node', e)) |
| return _failed('create', name, resource) |
| return _succeeded('create', name, resource, resp) |
| raise |
| |
| to_change = [] |
| for prop in kwargs: |
| path = prop.replace('~', '~0').replace('/', '~1') |
| if prop in exact_resource: |
| if exact_resource[prop] != kwargs[prop]: |
| to_change.append({ |
| 'op': 'replace', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| else: |
| to_change.append({ |
| 'op': 'add', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| if to_change: |
| try: |
| method_name = '{}_update'.format(resource) |
| resp = _ironicv1_call( |
| method_name, name, properties=to_change, |
| microversion=microversion, cloud_name=cloud_name, |
| ) |
| except Exception as e: |
| log.exception( |
| 'Ironic {0} update failed with {1}'.format(resource, e)) |
| return _failed('update', name, resource) |
| return _succeeded('update', name, resource, resp) |
| return _succeeded('no_changes', name, resource) |
| |
| |
| def node_absent(name, cloud_name, **kwargs): |
| resource = 'node' |
| microversion = kwargs.pop('microversion', '1.16') |
| try: |
| method_name = '{}_get_details'.format(resource) |
| _ironicv1_call( |
| method_name, name, cloud_name=cloud_name, |
| microversion=microversion |
| ) |
| except Exception as e: |
| if 'Not Found' in str(e): |
| return _succeeded('absent', name, resource) |
| try: |
| method_name = '{}_delete'.format(resource) |
| _ironicv1_call( |
| method_name, name, cloud_name=cloud_name, microversion=microversion |
| ) |
| except Exception as e: |
| log.error('Ironic delete {0} failed with {1}'.format(resource, e)) |
| return _failed('delete', name, resource) |
| return _succeeded('delete', name, resource) |
| |
| |
| def port_present(name, cloud_name, node, address, **kwargs): |
| resource = 'port' |
| microversion = kwargs.pop('microversion', '1.16') |
| method_name = '{}_list'.format(resource) |
| exact_resource = _ironicv1_call( |
| method_name, node=node, address=address, |
| cloud_name=cloud_name, microversion=microversion |
| )['ports'] |
| if len(exact_resource) == 0: |
| try: |
| node_uuid = _ironicv1_call( |
| 'node_get_details', node, cloud_name=cloud_name, |
| microversion=microversion |
| )['uuid'] |
| except Exception as e: |
| return _failed('create', node, "port's node") |
| try: |
| method_name = '{}_create'.format(resource) |
| resp = _ironicv1_call( |
| method_name, node_uuid, address, cloud_name=cloud_name, |
| microversion=microversion, **kwargs) |
| except Exception as e: |
| log.exception('Ironic {0} create failed with {1}'. |
| format('node', e)) |
| return _failed('create', name, resource) |
| return _succeeded('create', name, resource, resp) |
| if len(exact_resource) == 1: |
| exact_resource = exact_resource[0] |
| to_change = [] |
| for prop in kwargs: |
| path = prop.replace('~', '~0').replace('/', '~1') |
| if prop in exact_resource: |
| if exact_resource[prop] != kwargs[prop]: |
| to_change.append({ |
| 'op': 'replace', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| else: |
| to_change.append({ |
| 'op': 'add', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| if to_change: |
| try: |
| method_name = '{}_update'.format(resource) |
| resp = _ironicv1_call( |
| method_name, exact_resource['uuid'], properties=to_change, |
| microversion=microversion, cloud_name=cloud_name, |
| ) |
| except Exception as e: |
| log.exception( |
| 'Ironic {0} update failed with {1}'.format(resource, e)) |
| return _failed('update', name, resource) |
| return _succeeded('update', name, resource, resp) |
| return _succeeded('no_changes', name, resource) |
| else: |
| return _failed('find', name, resource) |
| |
| |
| def port_absent(name, cloud_name, node, address, **kwargs): |
| resource = 'port' |
| microversion = kwargs.pop('microversion', '1.16') |
| method_name = '{}_list'.format(resource) |
| exact_resource = _ironicv1_call( |
| method_name, node=node, address=address, |
| cloud_name=cloud_name, microversion=microversion |
| )['ports'] |
| if len(exact_resource) == 0: |
| return _succeeded('absent', name, resource) |
| elif len(exact_resource) == 1: |
| port_id = exact_resource[0]['uuid'] |
| try: |
| method_name = '{}_delete'.format(resource) |
| _ironicv1_call( |
| method_name, port_id, cloud_name=cloud_name, |
| microversion=microversion |
| ) |
| except Exception as e: |
| log.error('Ironic delete {0} failed with {1}'.format(resource, e)) |
| return _failed('delete', name, resource) |
| return _succeeded('delete', name, resource) |
| else: |
| return _failed('find', name, resource) |
| |
| |
| def volume_connector_present(name, node, volume_type, cloud_name, |
| **kwargs): |
| """ |
| |
| :param name: alias for connector_id because of how salt works |
| :param node: node_ident |
| :param volume_type: type of volume |
| """ |
| resource = 'volume_connector' |
| microversion = kwargs.pop('microversion', '1.32') |
| method_name = '{}_list'.format(resource) |
| exact_resource = filter( |
| lambda data: data['connector_id'] == name, |
| _ironicv1_call(method_name, node=node, |
| cloud_name=cloud_name, |
| microversion=microversion)['connectors']) |
| if len(exact_resource) == 0: |
| try: |
| method_name = 'node_get_details' |
| node_uuid = _ironicv1_call( |
| method_name, node, cloud_name=cloud_name, |
| microversion=microversion |
| )['uuid'] |
| except Exception as e: |
| if 'Not Found' in str(e): |
| return _failed('not_found', node, 'node') |
| raise |
| try: |
| method_name = '{}_create'.format(resource) |
| resp = _ironicv1_call( |
| method_name, node_uuid, volume_type, name, |
| cloud_name=cloud_name, microversion=microversion, **kwargs) |
| except Exception as e: |
| log.exception('Ironic {0} create failed with {1}'. |
| format('node', e)) |
| return _failed('create', name, resource) |
| return _succeeded('create', name, resource, resp) |
| elif len(exact_resource) == 1: |
| exact_resource = exact_resource[0] |
| to_change = [] |
| for prop in kwargs: |
| path = prop.replace('~', '~0').replace('/', '~1') |
| if prop in exact_resource: |
| if exact_resource[prop] != kwargs[prop]: |
| to_change.append({ |
| 'op': 'replace', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| else: |
| to_change.append({ |
| 'op': 'add', |
| 'path': '/{}'.format(path), |
| 'value': kwargs[prop], |
| }) |
| if to_change: |
| try: |
| method_name = '{}_update'.format(resource) |
| resp = _ironicv1_call( |
| method_name, exact_resource['uuid'], properties=to_change, |
| microversion=microversion, cloud_name=cloud_name, |
| ) |
| except Exception as e: |
| log.exception( |
| 'Ironic {0} update failed with {1}'.format(resource, |
| e)) |
| return _failed('update', name, resource) |
| return _succeeded('update', name, resource, resp) |
| return _succeeded('no_changes', name, resource) |
| else: |
| return _failed('find', name, resource) |
| |
| |
| def volume_connector_absent(name, cloud_name, node, **kwargs): |
| """ |
| |
| :param name: alias for connector_id because of how salt works |
| :param node: node ident |
| """ |
| resource = 'volume_connector' |
| microversion = kwargs.pop('microversion', '1.32') |
| method_name = '{}_list'.format(resource) |
| exact_resource = filter( |
| lambda data: data['connector_id'] == name, |
| _ironicv1_call(method_name, node=node, |
| cloud_name=cloud_name, |
| microversion=microversion)['connectors']) |
| if len(exact_resource) == 0: |
| return _succeeded('absent', name, resource) |
| elif len(exact_resource) == 1: |
| connector_uuid = exact_resource[0]['uuid'] |
| try: |
| method_name = '{}_delete'.format(resource) |
| _ironicv1_call( |
| method_name, connector_uuid, cloud_name=cloud_name, |
| microversion=microversion |
| ) |
| except Exception as e: |
| log.error('Ironic delete {0} failed with {1}'.format(resource, e)) |
| return _failed('delete', name, resource) |
| return _succeeded('delete', name, resource) |
| else: |
| return _failed('find', name, resource) |
| |
| |
| def ensure_target_state(name, cloud_name, node_names=None, |
| provision_state=None, pool_size=3, sleep_time=5, |
| timeout=600, **kwargs): |
| """ |
| Ensures nodes are moved to target state. As node distinguisher might take |
| either list of nodes specified in node names param or provision state. |
| Is designed to move nodes from enroll to available state for now. |
| |
| :param name: name of target state |
| :param node_names: list of node names |
| :param provision_state: current provision_state to filter nodes by. |
| :param cloud_name: the mane of cloud in clouds.yml |
| :param pool_size: max size of nodes to change state in one moment |
| :param sleep_time: time between checking states |
| :param timeout: global timeout |
| """ |
| |
| microversion = kwargs.pop('microversion', '1.32') |
| |
| if node_names is None: |
| nodes = _ironicv1_call('node_list', provision_state=provision_state, |
| cloud_name=cloud_name, |
| fields='name', |
| microversion=microversion)['nodes'] |
| node_names = [n['name'] for n in nodes] |
| |
| Transition = collections.namedtuple('Transition', |
| ('action', 'success', 'failures')) |
| transition_map = { |
| 'enroll': Transition('manage', 'manageable', ('enroll',)), |
| 'manageable': Transition('provide', 'available', ('clean failed',)), |
| 'available': Transition('active', 'active', ('deploy failed',)), |
| } |
| nodes = [ |
| {'name': node, 'status': 'new', 'result': None, |
| 'current_state': provision_state or 'enroll'} |
| for node in node_names |
| ] |
| counter = 0 |
| while nodes and timeout > 0: |
| for node in nodes: |
| api_node = _ironicv1_call('node_get_details', node['name'], |
| cloud_name=cloud_name, |
| microversion=microversion) |
| if api_node['provision_state'] == name: |
| node['status'] = 'done' |
| node['result'] = 'success' |
| counter -= 1 |
| elif (api_node['provision_state'] |
| == transition_map[node['current_state']].success): |
| new_state = transition_map[node['current_state']].success |
| _ironicv1_call('node_provision_state_change', node['name'], |
| transition_map[new_state].action, |
| cloud_name=cloud_name, |
| microversion=microversion) |
| node['current_state'] = new_state |
| elif (node['status'] == 'processing' |
| and not api_node['target_provision_state'] |
| and (api_node['provision_state'] |
| in transition_map[api_node['provision_state']].failures) |
| ): |
| node['status'] = 'done' |
| node['result'] = 'failure' |
| counter -= 1 |
| elif counter < pool_size: |
| if node['status'] == 'new': |
| _ironicv1_call( |
| 'node_provision_state_change', node['name'], |
| transition_map[node['current_state']].action, |
| cloud_name=cloud_name, microversion=microversion) |
| node['status'] = 'processing' |
| counter += 1 |
| else: |
| continue |
| else: |
| break |
| nodes = filter( |
| lambda node: node['status'] in ['new', 'processing'], nodes) |
| time.sleep(sleep_time) |
| timeout -= sleep_time |
| return _succeeded('update', name, 'node_states', |
| {'result': filter( |
| lambda node: node['name'] in node_names, |
| _ironicv1_call('node_list', cloud_name=cloud_name, |
| microversion=microversion)['nodes'])}) |
| |
| |
| def _succeeded(op, name, resource, changes=None): |
| msg_map = { |
| 'create': '{0} {1} created', |
| 'delete': '{0} {1} removed', |
| 'update': '{0} {1} updated', |
| 'no_changes': '{0} {1} is in desired state', |
| 'absent': '{0} {1} not present' |
| } |
| changes_dict = { |
| 'name': name, |
| 'result': True, |
| 'comment': msg_map[op].format(resource, name), |
| 'changes': changes or {}, |
| } |
| return changes_dict |
| |
| |
| def _failed(op, name, resource): |
| msg_map = { |
| 'create': '{0} {1} failed to create', |
| 'delete': '{0} {1} failed to delete', |
| 'update': '{0} {1} failed to update', |
| 'find': '{0} {1} found multiple {0}', |
| 'not found': '{0} {1} not found', |
| } |
| changes_dict = { |
| 'name': name, |
| 'result': False, |
| 'comment': msg_map[op].format(resource, name), |
| 'changes': {}, |
| } |
| return changes_dict |