def validate(self, params, **kwargs): """Validate params passed during creation. Default implementation checks for presence of fields from REQUIRED_PARAMS and fails for unexpected fields (not from REQUIRED_PARAMS + OPTIONAL_PARAMS). :param params: params as a dictionary :param kwargs: used for extensibility without breaking existing plugins :raises: ValueError on validation failure """ passed = {k for k, v in params.items() if v is not None} missing = self.REQUIRED_PARAMS - passed unexpected = passed - self.REQUIRED_PARAMS - self.OPTIONAL_PARAMS msg = [] if missing: msg.append( _('missing required parameter(s): %s') % ', '.join(missing)) if unexpected: msg.append( _('unexpected parameter(s): %s') % ', '.join(unexpected)) if msg: raise ValueError('; '.join(msg))
def _process_root_device_hints(self, introspection_data, node_info, inventory): """Detect root disk from root device hints and IPA inventory.""" hints = node_info.node().properties.get('root_device') if not hints: LOG.debug('Root device hints are not provided', node_info=node_info, data=introspection_data) return try: device = il_utils.match_root_device_hints(inventory['disks'], hints) except (TypeError, ValueError) as e: raise utils.Error( _('No disks could be found using the root device hints ' '%(hints)s because they failed to validate. ' 'Error: %(error)s') % {'hints': hints, 'error': e}, node_info=node_info, data=introspection_data) if not device: raise utils.Error(_('No disks satisfied root device hints'), node_info=node_info, data=introspection_data) LOG.debug('Disk %(disk)s of size %(size)s satisfies ' 'root device hints', {'disk': device.get('name'), 'size': device['size']}, node_info=node_info, data=introspection_data) introspection_data['root_disk'] = device
def before_update(self, introspection_data, node_info, **kwargs): """Update node with scheduler properties.""" inventory = introspection_data.get('inventory') errors = [] root_disk = introspection_data.get('root_disk') if root_disk: introspection_data['local_gb'] = root_disk['size'] // units.Gi if CONF.processing.disk_partitioning_spacing: introspection_data['local_gb'] -= 1 elif inventory: errors.append(_('root disk is not supplied by the ramdisk and ' 'root_disk_selection hook is not enabled')) if inventory: try: introspection_data['cpus'] = int(inventory['cpu']['count']) introspection_data['cpu_arch'] = six.text_type( inventory['cpu']['architecture']) except (KeyError, ValueError, TypeError): errors.append(_('malformed or missing CPU information: %s') % inventory.get('cpu')) try: introspection_data['memory_mb'] = int( inventory['memory']['physical_mb']) except (KeyError, ValueError, TypeError): errors.append(_('malformed or missing memory information: %s; ' 'introspection requires physical memory size ' 'from dmidecode') % inventory.get('memory')) else: LOG.warning(_LW('No inventory provided: using old bash ramdisk ' 'is deprecated, please switch to ' 'ironic-python-agent'), node_info=node_info, data=introspection_data) missing = [key for key in self.KEYS if not introspection_data.get(key)] if missing: raise utils.Error( _('The following required parameters are missing: %s') % missing, node_info=node_info, data=introspection_data) if errors: raise utils.Error(_('The following problems encountered: %s') % '; '.join(errors), node_info=node_info, data=introspection_data) LOG.info(_LI('Discovered data: CPUs: %(cpus)s %(cpu_arch)s, ' 'memory %(memory_mb)s MiB, disk %(local_gb)s GiB'), {key: introspection_data.get(key) for key in self.KEYS}, node_info=node_info, data=introspection_data) overwrite = CONF.processing.overwrite_existing properties = {key: str(introspection_data[key]) for key in self.KEYS if overwrite or not node_info.node().properties.get(key)} node_info.update_properties(**properties)
def _validate_ipmi_credentials(node, new_ipmi_credentials): if not CONF.processing.enable_setting_ipmi_credentials: raise utils.Error( _('IPMI credentials setup is disabled in configuration')) new_username, new_password = new_ipmi_credentials if not new_username: new_username = node.driver_info.get('ipmi_username') if not new_username: raise utils.Error(_('Setting IPMI credentials requested, but neither ' 'new user name nor driver_info[ipmi_username] ' 'are provided'), node_info=node) wrong_chars = {c for c in new_password if c not in PASSWORD_ACCEPTED_CHARS} if wrong_chars: raise utils.Error(_('Forbidden characters encountered in new IPMI ' 'password: "******"; use only letters and numbers') % ''.join(wrong_chars), node_info=node) if not 0 < len(new_password) <= PASSWORD_MAX_LENGTH: raise utils.Error(_('IPMI password length should be > 0 and <= %d') % PASSWORD_MAX_LENGTH, node_info=node) return new_username, new_password
def create_object(self, object, data, container=CONF.swift.container, headers=None): """Uploads a given string to Swift. :param object: The name of the object in Swift :param data: string data to put in the object :param container: The name of the container for the object. :param headers: the headers for the object to pass to Swift :returns: The Swift UUID of the object :raises: utils.Error, if any operation with Swift fails. """ try: self.connection.put_container(container) except swift_exceptions.ClientException as e: err_msg = (_('Swift failed to create container %(container)s. ' 'Error was: %(error)s') % {'container': container, 'error': e}) raise utils.Error(err_msg) if CONF.swift.delete_after > 0: headers = headers or {} headers['X-Delete-After'] = CONF.swift.delete_after try: obj_uuid = self.connection.put_object(container, object, data, headers=headers) except swift_exceptions.ClientException as e: err_msg = (_('Swift failed to create object %(object)s in ' 'container %(container)s. Error was: %(error)s') % {'object': object, 'container': container, 'error': e}) raise utils.Error(err_msg) return obj_uuid
def validate(self, params, **kwargs): """Validate params passed during creation. Default implementation checks for presence of fields from REQUIRED_PARAMS and fails for unexpected fields (not from REQUIRED_PARAMS + OPTIONAL_PARAMS). :param params: params as a dictionary :param kwargs: used for extensibility without breaking existing plugins :raises: ValueError on validation failure """ passed = {k for k, v in params.items() if v is not None} missing = self.REQUIRED_PARAMS - passed unexpected = passed - self.REQUIRED_PARAMS - self.OPTIONAL_PARAMS msg = [] if missing: msg.append(_('missing required parameter(s): %s') % ', '.join(missing)) if unexpected: msg.append(_('unexpected parameter(s): %s') % ', '.join(unexpected)) if msg: raise ValueError('; '.join(msg))
def processing_logger_prefix(data=None, node_info=None): """Calculate prefix for logging. Tries to use: * node UUID, * node PXE MAC, * node BMC address :param data: introspection data :param node_info: NodeInfo or ironic node object :return: logging prefix as a string """ # TODO(dtantsur): try to get MAC and BMC address for node_info as well parts = [] data = data or {} if node_info is not None: parts.append(str(node_info.uuid)) pxe_mac = get_pxe_mac(data) if pxe_mac: parts.append('MAC %s' % pxe_mac) if CONF.processing.log_bmc_address: bmc_address = get_ipmi_address_from_data(data) if data else None if bmc_address: parts.append('BMC %s' % bmc_address) if parts: return _('[node: %s]') % ' '.join(parts) else: return _('[unidentified node]')
def lookup_node_by_bmc_addresses(addresses, introspection_data=None, ironic=None, fail=False): """Find a node by its BMC address.""" if ironic is None: ironic = get_client() # FIXME(aarefiev): it's not effective to fetch all nodes, and may # impact on performance on big clusters nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0) found = set() for node in nodes: bmc_address, bmc_ipv4, bmc_ipv6 = get_ipmi_address(node) for addr in addresses: if addr not in (bmc_ipv4, bmc_ipv6): continue elif fail: raise utils.Error( _('Node %(uuid)s already has BMC address %(addr)s') % {'addr': addr, 'uuid': node.uuid}, data=introspection_data) else: found.add(node.uuid) if len(found) > 1: raise utils.Error(_('BMC addresses %(addr)s correspond to more than ' 'one node: %(nodes)s') % {'addr': ', '.join(addresses), 'nodes': ', '.join(found)}, data=introspection_data) elif found: return found.pop()
def processing_logger_prefix(data=None, node_info=None): """Calculate prefix for logging. Tries to use: * node UUID, node._state * node PXE MAC, * node BMC address :param data: introspection data :param node_info: NodeInfo or ironic node object :return: logging prefix as a string """ # TODO(dtantsur): try to get MAC and BMC address for node_info as well parts = [] data = data or {} if node_info is not None: if isinstance(node_info, node.Node): parts.append(str(node_info.uuid)) else: parts.append(str(node_info)) pxe_mac = get_pxe_mac(data) if pxe_mac: parts.append('MAC %s' % pxe_mac) bmc_address = get_ipmi_address_from_data(data) if data else None if bmc_address: parts.append('BMC %s' % bmc_address) if parts: return _('[node: %s]') % ' '.join(parts) else: return _('[unidentified node]')
def get_inventory(data, node_info=None): """Get and validate the hardware inventory from introspection data.""" inventory = data.get('inventory') # TODO(dtantsur): validate inventory using JSON schema if not inventory: raise Error(_('Hardware inventory is empty or missing'), data=data, node_info=node_info) for key in _INVENTORY_MANDATORY_KEYS: if not inventory.get(key): raise Error(_('Invalid hardware inventory: %s key is missing ' 'or empty') % key, data=data, node_info=node_info) if not inventory.get('disks'): LOG.info( 'No disks were detected in the inventory, assuming this ' 'is a disk-less node', data=data, node_info=node_info) # Make sure the code iterating over it does not fail with a TypeError inventory['disks'] = [] return inventory
def find_node(**attributes): """Find node in cache. :param attributes: attributes known about this node (like macs, BMC etc) also ironic client instance may be passed under 'ironic' :returns: structure NodeInfo with attributes ``uuid`` and ``created_at`` :raises: Error if node is not found """ ironic = attributes.pop('ironic', None) # NOTE(dtantsur): sorting is not required, but gives us predictability found = set() for (name, value) in sorted(attributes.items()): if not value: LOG.debug('Empty value for attribute %s', name) continue if not isinstance(value, list): value = [value] LOG.debug('Trying to use %s of value %s for node look up' % (name, value)) value_list = [] for v in value: value_list.append('name="%s" AND value="%s"' % (name, v)) stmt = ('select distinct uuid from attributes where ' + ' OR '.join(value_list)) rows = (db.model_query(db.Attribute.uuid).from_statement( text(stmt)).all()) if rows: found.update(item.uuid for item in rows) if not found: raise utils.NotFoundInCacheError( _('Could not find a node for attributes %s') % attributes) elif len(found) > 1: raise utils.Error(_('Multiple matching nodes found for attributes ' '%(attr)s: %(found)s') % { 'attr': attributes, 'found': list(found) }, code=404) uuid = found.pop() row = (db.model_query(db.Node.started_at, db.Node.finished_at).filter_by(uuid=uuid).first()) if not row: raise utils.Error(_('Could not find node %s in introspection cache, ' 'probably it\'s not on introspection now') % uuid, code=404) if row.finished_at: raise utils.Error( _('Introspection for node %(node)s already finished on ' '%(finish)s') % { 'node': uuid, 'finish': row.finished_at }) return NodeInfo(uuid=uuid, started_at=row.started_at, ironic=ironic)
def api_introspection(node_id): if flask.request.method == 'POST': args = flask.request.args manage_boot = args.get('manage_boot', 'True') try: manage_boot = strutils.bool_from_string(manage_boot, strict=True) except ValueError: raise utils.Error(_('Invalid boolean value for manage_boot: %s') % manage_boot, code=400) if manage_boot and not CONF.can_manage_boot: raise utils.Error(_('Managed boot is requested, but this ' 'installation cannot manage boot (' '(can_manage_boot set to False)'), code=400) client = rpc.get_client() client.call({}, 'do_introspection', node_id=node_id, manage_boot=manage_boot, token=flask.request.headers.get('X-Auth-Token')) return '', 202 else: node_info = node_cache.get_node(node_id) return flask.json.jsonify(generate_introspection_status(node_info))
def _reapply(node_info, introspection_data=None): # runs in background node_info.started_at = timeutils.utcnow() node_info.commit() try: ironic = ir_utils.get_client() except Exception as exc: msg = _('Encountered an exception while getting the Ironic client: ' '%s') % exc LOG.error(msg, node_info=node_info, data=introspection_data) node_info.finished(istate.Events.error, error=msg) return try: _reapply_with_data(node_info, introspection_data) except Exception as exc: msg = (_('Failed reapply for node %(node)s, Error: ' '%(exc)s') % { 'node': node_info.uuid, 'exc': exc }) LOG.error(msg, node_info=node_info, data=introspection_data) return _finish(node_info, ironic, introspection_data, power_off=False) LOG.info('Successfully reapplied introspection on stored ' 'data', node_info=node_info, data=introspection_data)
def _check_existing_nodes(introspection_data, node_driver_info, ironic): macs = utils.get_valid_macs(introspection_data) if macs: # verify existing ports for mac in macs: ports = ironic.port.list(address=mac) if not ports: continue raise utils.Error( _('Port %(mac)s already exists, uuid: %(uuid)s') % { 'mac': mac, 'uuid': ports[0].uuid }, data=introspection_data) else: LOG.warning('No suitable interfaces found for discovered node. ' 'Check that validate_interfaces hook is listed in ' '[processing]default_processing_hooks config option') # verify existing node with discovered ipmi address ipmi_address = node_driver_info.get('ipmi_address') if ipmi_address: # FIXME(aarefiev): it's not effective to fetch all nodes, and may # impact on performance on big clusters nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0) for node in nodes: bmc_address, bmc_ipv4, bmc_ipv6 = ir_utils.get_ipmi_address(node) if ipmi_address in (bmc_ipv4, bmc_ipv6): raise utils.Error(_('Node %(uuid)s already has BMC address ' '%(ipmi_address)s, not enrolling') % { 'ipmi_address': ipmi_address, 'uuid': node.uuid }, data=introspection_data)
def _validate_actions(actions_json): """Validates actions from jsonschema. :returns: a list of actions. """ try: jsonschema.validate(actions_json, actions_schema()) except jsonschema.ValidationError as exc: raise utils.Error(_('Validation failed for actions: %s') % exc) act_mgr = plugins_base.rule_actions_manager() actions = [] for action_json in actions_json: plugin = act_mgr[action_json['action']].obj params = {k: v for k, v in action_json.items() if k != 'action'} try: plugin.validate(params) except ValueError as exc: raise utils.Error( _('Invalid parameters for action %(act)s: ' '%(error)s') % { 'act': action_json['action'], 'error': exc }) actions.append((action_json['action'], params)) return actions
def api_rules(): if flask.request.method == 'GET': res = [rule_repr(rule, short=True) for rule in rules.get_all()] return flask.jsonify(rules=res) elif flask.request.method == 'DELETE': rules.delete_all() return _generate_empty_response(204) else: body = flask.request.get_json(force=True) if body.get('uuid') and not uuidutils.is_uuid_like(body['uuid']): raise utils.Error(_('Invalid UUID value'), code=400) if body.get('scope') and len(body.get('scope')) > 255: raise utils.Error(_( "Invalid scope: the length of the scope should be within " "255 characters"), code=400) rule = rules.create(conditions_json=body.get('conditions', []), actions_json=body.get('actions', []), uuid=body.get('uuid'), description=body.get('description'), scope=body.get('scope')) response_code = (200 if _get_version() < (1, 6) else 201) return flask.make_response(flask.jsonify(rule_repr(rule, short=False)), response_code)
def lookup_node_by_macs(macs, introspection_data=None, ironic=None, fail=False): """Find a node by its MACs.""" if ironic is None: ironic = get_client() nodes = set() for mac in macs: ports = ironic.port.list(address=mac) if not ports: continue elif fail: raise utils.Error( _('Port %(mac)s already exists, uuid: %(uuid)s') % {'mac': mac, 'uuid': ports[0].uuid}, data=introspection_data) else: nodes.update(p.node_uuid for p in ports) if len(nodes) > 1: raise utils.Error(_('MAC addresses %(macs)s correspond to more than ' 'one node: %(nodes)s') % {'macs': ', '.join(macs), 'nodes': ', '.join(nodes)}, data=introspection_data) elif nodes: return nodes.pop()
def _find_node_info(introspection_data, failures): try: return node_cache.find_node( bmc_address=introspection_data.get('ipmi_address'), mac=utils.get_valid_macs(introspection_data)) except utils.NotFoundInCacheError as exc: not_found_hook = plugins_base.node_not_found_hook_manager() if not_found_hook is None: failures.append(_('Look up error: %s') % exc) return LOG.debug('Running node_not_found_hook %s', CONF.processing.node_not_found_hook, data=introspection_data) # NOTE(sambetts): If not_found_hook is not none it means that we were # unable to find the node in the node cache and there is a node not # found hook defined so we should try to send the introspection data # to that hook to generate the node info before bubbling up the error. try: node_info = not_found_hook.driver(introspection_data) if node_info: return node_info failures.append(_("Node not found hook returned nothing")) except Exception as exc: failures.append(_("Node not found hook failed: %s") % exc) except utils.Error as exc: failures.append(_('Look up error: %s') % exc)
def _validate_ipmi_credentials(node, new_ipmi_credentials): if not CONF.processing.enable_setting_ipmi_credentials: raise utils.Error( _('IPMI credentials setup is disabled in configuration')) new_username, new_password = new_ipmi_credentials if not new_username: new_username = node.driver_info.get('ipmi_username') if not new_username: raise utils.Error(_('Setting IPMI credentials requested for node %s,' ' but neither new user name nor' ' driver_info[ipmi_username] are provided') % node.uuid) wrong_chars = {c for c in new_password if c not in PASSWORD_ACCEPTED_CHARS} if wrong_chars: raise utils.Error(_('Forbidden characters encountered in new IPMI ' 'password for node %(node)s: "%(chars)s"; ' 'use only letters and numbers') % {'node': node.uuid, 'chars': ''.join(wrong_chars)}) if not 0 < len(new_password) <= PASSWORD_MAX_LENGTH: raise utils.Error(_('IPMI password length should be > 0 and <= %d') % PASSWORD_MAX_LENGTH) return new_username, new_password
def __init__(self): """Constructor for creating a SwiftAPI object. Authentification is loaded from config file. """ global SWIFT_SESSION adapter_opts = dict() # TODO(pas-ha): remove handling deprecated options in Rocky if CONF.swift.os_region and not CONF.swift.region_name: adapter_opts['region_name'] = CONF.swift.os_region try: if not SWIFT_SESSION: SWIFT_SESSION = keystone.get_session('swift') adapter = keystone.get_adapter('swift', session=SWIFT_SESSION, **adapter_opts) except Exception as exc: raise utils.Error( _("Could not create an adapter to connect to " "the object storage service: %s") % exc) # TODO(pas-ha) reverse-construct SSL-related session options here params = {'os_options': {'object_storage_url': adapter.get_endpoint()}} try: self.connection = swift_client.Connection(session=SWIFT_SESSION, **params) except Exception as exc: raise utils.Error( _("Could not connect to the object storage " "service: %s") % exc)
def _check_existing_nodes(introspection_data, node_driver_info, ironic): macs = utils.get_valid_macs(introspection_data) if macs: # verify existing ports for mac in macs: ports = ironic.port.list(address=mac) if not ports: continue raise utils.Error( _('Port %(mac)s already exists, uuid: %(uuid)s') % {'mac': mac, 'uuid': ports[0].uuid}, data=introspection_data) else: LOG.warning('No suitable interfaces found for discovered node. ' 'Check that validate_interfaces hook is listed in ' '[processing]default_processing_hooks config option') # verify existing node with discovered ipmi address ipmi_address = node_driver_info.get('ipmi_address') if ipmi_address: # FIXME(aarefiev): it's not effective to fetch all nodes, and may # impact on performance on big clusters nodes = ironic.node.list(fields=('uuid', 'driver_info'), limit=0) for node in nodes: bmc_address, bmc_ipv4, bmc_ipv6 = ir_utils.get_ipmi_address(node) if ipmi_address in (bmc_ipv4, bmc_ipv6): raise utils.Error( _('Node %(uuid)s already has BMC address ' '%(ipmi_address)s, not enrolling') % {'ipmi_address': ipmi_address, 'uuid': node.uuid}, data=introspection_data)
def _reapply(node_info): # runs in background try: introspection_data = _get_unprocessed_data(node_info.uuid) except Exception as exc: LOG.exception(_LE('Encountered exception while fetching ' 'stored introspection data'), node_info=node_info) msg = (_('Unexpected exception %(exc_class)s while fetching ' 'unprocessed introspection data from Swift: %(error)s') % {'exc_class': exc.__class__.__name__, 'error': exc}) node_info.finished(error=msg) return try: ironic = ir_utils.get_client() except Exception as exc: msg = _('Encountered an exception while getting the Ironic client: ' '%s') % exc LOG.error(msg, node_info=node_info, data=introspection_data) node_info.fsm_event(istate.Events.error) node_info.finished(error=msg) return try: _reapply_with_data(node_info, introspection_data) except Exception as exc: node_info.finished(error=str(exc)) return _finish(node_info, ironic, introspection_data, power_off=False) LOG.info(_LI('Successfully reapplied introspection on stored ' 'data'), node_info=node_info, data=introspection_data)
def introspect(node_id, manage_boot=True, token=None): """Initiate hardware properties introspection for a given node. :param node_id: node UUID or name :param manage_boot: whether to manage boot for this node :param token: authentication token :raises: Error """ ironic = ir_utils.get_client(token) node = ir_utils.get_node(node_id, ironic=ironic) ir_utils.check_provision_state(node) if manage_boot: try: ironic.validate_node(node.id, required='power') except os_exc.ValidationException as exc: msg = _('Failed validation of power interface: %s') raise utils.Error(msg % exc, node_info=node) bmc_address, bmc_ipv4, bmc_ipv6 = ir_utils.get_ipmi_address(node) lookup_attrs = list(filter(None, [bmc_ipv4, bmc_ipv6])) node_info = node_cache.start_introspection(node.id, bmc_address=lookup_attrs, manage_boot=manage_boot, ironic=ironic) if manage_boot: try: utils.executor().submit(_do_introspect, node_info, ironic) except Exception as exc: msg = _('Failed to submit introspection job: %s') raise utils.Error(msg % exc, node_info=node) else: _do_introspect(node_info, ironic)
def before_update(self, introspection_data, node_info, **kwargs): """Detect root disk from root device hints and IPA inventory.""" hints = node_info.node().properties.get('root_device') if not hints: LOG.debug('Root device hints are not provided', node_info=node_info, data=introspection_data) return inventory = utils.get_inventory(introspection_data, node_info=node_info) try: device = il_utils.match_root_device_hints(inventory['disks'], hints) except (TypeError, ValueError) as e: raise utils.Error( _('No disks could be found using the root device hints ' '%(hints)s because they failed to validate. ' 'Error: %(error)s') % {'hints': hints, 'error': e}, node_info=node_info, data=introspection_data) if not device: raise utils.Error(_('No disks satisfied root device hints'), node_info=node_info, data=introspection_data) LOG.debug('Disk %(disk)s of size %(size)s satisfies ' 'root device hints', {'disk': device.get('name'), 'size': device['size']}, node_info=node_info, data=introspection_data) introspection_data['root_disk'] = device
def _background_introspect_locked(ironic, node_info): # TODO(dtantsur): pagination macs = list(node_info.ports()) if macs: node_info.add_attribute(node_cache.MACS_ATTRIBUTE, macs) LOG.info( _LI('Whitelisting MAC\'s %(macs)s for node %(node)s on the' ' firewall') % { 'macs': macs, 'node': node_info.uuid }) firewall.update_filters(ironic) attrs = node_info.attributes if CONF.processing.node_not_found_hook is None and not attrs: raise utils.Error( _('No lookup attributes were found for node %s, inspector won\'t ' 'be able to find it after introspection. Consider creating ' 'ironic ports or providing an IPMI address.') % node_info.uuid) LOG.info( _LI('The following attributes will be used for looking up ' 'node %(uuid)s: %(attrs)s'), { 'attrs': attrs, 'uuid': node_info.uuid }) if not node_info.options.get('new_ipmi_credentials'): try: ironic.node.set_boot_device(node_info.uuid, 'pxe', persistent=False) except Exception as exc: LOG.warning( _LW('Failed to set boot device to PXE for' ' node %(node)s: %(exc)s') % { 'node': node_info.uuid, 'exc': exc }) try: ironic.node.set_power_state(node_info.uuid, 'reboot') except Exception as exc: raise utils.Error( _('Failed to power on node %(node)s,' ' check it\'s power ' 'management configuration:\n%(exc)s') % { 'node': node_info.uuid, 'exc': exc }) LOG.info(_LI('Introspection started successfully for node %s'), node_info.uuid) else: LOG.info( _LI('Introspection environment is ready for node %(node)s, ' 'manual power on is required within %(timeout)d seconds') % { 'node': node_info.uuid, 'timeout': CONF.timeout })
def before_update(self, introspection_data, node_info, **kwargs): """Detect root disk from root device hints and IPA inventory.""" hints = node_info.node().properties.get('root_device') if not hints: LOG.debug('Root device hints are not provided', node_info=node_info, data=introspection_data) return inventory = introspection_data.get('inventory') if not inventory: raise utils.Error(_( 'Root device selection requires ironic-python-agent ' 'as an inspection ramdisk'), node_info=node_info, data=introspection_data) disks = inventory.get('disks', []) if not disks: raise utils.Error(_('No disks found'), node_info=node_info, data=introspection_data) for disk in disks: properties = disk.copy() # Root device hints are in GiB, data from IPA is in bytes properties['size'] //= units.Gi for name, value in hints.items(): actual = properties.get(name) if actual != value: LOG.debug( 'Disk %(disk)s does not satisfy hint ' '%(name)s=%(value)s, actual value is %(actual)s', { 'disk': disk.get('name'), 'name': name, 'value': value, 'actual': actual }, node_info=node_info, data=introspection_data) break else: LOG.debug( 'Disk %(disk)s of size %(size)s satisfies ' 'root device hints', { 'disk': disk.get('name'), 'size': disk['size'] }, node_info=node_info, data=introspection_data) introspection_data['root_disk'] = disk return raise utils.Error(_('No disks satisfied root device hints'), node_info=node_info, data=introspection_data)
def _background_introspect(ironic, node_info): global _LAST_INTROSPECTION_TIME # TODO(dtantsur): pagination macs = list(node_info.ports()) if macs: node_info.add_attribute(node_cache.MACS_ATTRIBUTE, macs) LOG.info(_LI('Whitelisting MAC\'s %(macs)s for node %(node)s on the' ' firewall') % {'macs': macs, 'node': node_info.uuid}) firewall.update_filters(ironic) attrs = node_info.attributes if CONF.processing.node_not_found_hook is None and not attrs: raise utils.Error( _('No lookup attributes were found for node %s, inspector won\'t ' 'be able to find it after introspection. Consider creating ' 'ironic ports or providing an IPMI address.') % node_info.uuid) LOG.info(_LI('The following attributes will be used for looking up ' 'node %(uuid)s: %(attrs)s'), {'attrs': attrs, 'uuid': node_info.uuid}) if not node_info.options.get('new_ipmi_credentials'): try: ironic.node.set_boot_device(node_info.uuid, 'pxe', persistent=False) except Exception as exc: LOG.warning(_LW('Failed to set boot device to PXE for' ' node %(node)s: %(exc)s') % {'node': node_info.uuid, 'exc': exc}) if re.match(CONF.introspection_delay_drivers, node_info.node().driver): LOG.debug('Attempting to acquire lock on last introspection time') with _LAST_INTROSPECTION_LOCK: delay = (_LAST_INTROSPECTION_TIME - time.time() + CONF.introspection_delay) if delay > 0: LOG.debug('Waiting %d seconds before sending the next ' 'node on introspection', delay) time.sleep(delay) _LAST_INTROSPECTION_TIME = time.time() try: ironic.node.set_power_state(node_info.uuid, 'reboot') except Exception as exc: raise utils.Error(_('Failed to power on node %(node)s,' ' check it\'s power ' 'management configuration:\n%(exc)s') % {'node': node_info.uuid, 'exc': exc}) LOG.info(_LI('Introspection started successfully for node %s'), node_info.uuid) else: LOG.info(_LI('Introspection environment is ready for node %(node)s, ' 'manual power on is required within %(timeout)d seconds') % {'node': node_info.uuid, 'timeout': CONF.timeout})
def process(introspection_data): """Process data from the ramdisk. This function heavily relies on the hooks to do the actual data processing. """ unprocessed_data = copy.deepcopy(introspection_data) failures = [] _run_pre_hooks(introspection_data, failures) node_info = _find_node_info(introspection_data, failures) if node_info: # Locking is already done in find_node() but may be not done in a # node_not_found hook node_info.acquire_lock() if failures or node_info is None: msg = _('The following failures happened during running ' 'pre-processing hooks:\n%s') % '\n'.join(failures) if node_info is not None: node_info.finished(error='\n'.join(failures)) raise utils.Error(msg, node_info=node_info, data=introspection_data) LOG.info(_LI('Matching node is %s'), node_info.uuid, node_info=node_info, data=introspection_data) if node_info.finished_at is not None: # race condition or introspection canceled raise utils.Error(_('Node processing already finished with ' 'error: %s') % node_info.error, node_info=node_info, code=400) # Note(mkovacik): store data now when we're sure that a background # thread won't race with other process() or introspect.abort() # call utils.executor().submit(_store_unprocessed_data, node_info, unprocessed_data) try: node = node_info.node() except exceptions.NotFound: msg = _('Node was found in cache, but is not found in Ironic') node_info.finished(error=msg) raise utils.Error(msg, code=404, node_info=node_info, data=introspection_data) try: return _process_node(node, introspection_data, node_info) except utils.Error as exc: node_info.finished(error=str(exc)) raise except Exception as exc: LOG.exception(_LE('Unexpected exception during processing')) msg = _('Unexpected exception %(exc_class)s during processing: ' '%(error)s') % {'exc_class': exc.__class__.__name__, 'error': exc} node_info.finished(error=msg) raise utils.Error(msg, node_info=node_info, data=introspection_data)
def limit_field(value): """Fetch the pagination limit field from flask.request.args. :returns: the limit """ # limit of zero means the default limit value = int(value) or CONF.api_max_limit assert value >= 0, _('Limit cannot be negative') assert value <= CONF.api_max_limit, _('Limit over %s') % CONF.api_max_limit return value
def before_update(self, introspection_data, node_info, **kwargs): """Detect root disk from root device hints and IPA inventory.""" hints = node_info.node().properties.get('root_device') if not hints: LOG.debug('Root device hints are not provided', node_info=node_info, data=introspection_data) return if 'size' in hints: # Special case to match IPA behaviour try: hints['size'] = int(hints['size']) except (TypeError, ValueError): raise utils.Error(_('Invalid root device size hint, expected ' 'an integer, got %s') % hints['size'], node_info=node_info, data=introspection_data) inventory = utils.get_inventory(introspection_data, node_info=node_info) for disk in inventory['disks']: properties = disk.copy() # Root device hints are in GiB, data from IPA is in bytes properties['size'] //= units.Gi for name, value in hints.items(): actual = properties.get(name) if actual != value: LOG.debug( 'Disk %(disk)s does not satisfy hint ' '%(name)s=%(value)s, actual value is %(actual)s', { 'disk': disk.get('name'), 'name': name, 'value': value, 'actual': actual }, node_info=node_info, data=introspection_data) break else: LOG.debug( 'Disk %(disk)s of size %(size)s satisfies ' 'root device hints', { 'disk': disk.get('name'), 'size': disk['size'] }, node_info=node_info, data=introspection_data) introspection_data['root_disk'] = disk return raise utils.Error(_('No disks satisfied root device hints'), node_info=node_info, data=introspection_data)
def _validate_interfaces(self, interfaces, data=None): """Validate interfaces on correctness and suitability. :return: dict interface name -> dict with keys 'mac' and 'ip' """ if not interfaces: raise utils.Error(_('No interfaces supplied by the ramdisk'), data=data) pxe_mac = utils.get_pxe_mac(data) if not pxe_mac and CONF.processing.add_ports == 'pxe': LOG.warning(_LW('No boot interface provided in the introspection ' 'data, will add all ports with IP addresses')) result = {} for name, iface in interfaces.items(): mac = iface.get('mac') ip = iface.get('ip') if not mac: LOG.debug('Skipping interface %s without link information', name, data=data) continue if not utils.is_valid_mac(mac): LOG.warning(_LW('MAC %(mac)s for interface %(name)s is not ' 'valid, skipping'), {'mac': mac, 'name': name}, data=data) continue mac = mac.lower() if name == 'lo' or (ip and netaddr.IPAddress(ip).is_loopback()): LOG.debug('Skipping local interface %s', name, data=data) continue if (CONF.processing.add_ports == 'pxe' and pxe_mac and mac != pxe_mac): LOG.debug('Skipping interface %s as it was not PXE booting', name, data=data) continue elif CONF.processing.add_ports != 'all' and not ip: LOG.debug('Skipping interface %s as it did not have ' 'an IP address assigned during the ramdisk run', name, data=data) continue result[name] = {'ip': ip, 'mac': mac.lower()} if not result: raise utils.Error(_('No suitable interfaces found in %s') % interfaces, data=data) return result
def before_update(self, introspection_data, node_info, node_patches, ports_patches, **kwargs): """Detect root disk from root device hints and IPA inventory.""" hints = node_info.node().properties.get('root_device') if not hints: LOG.debug('Root device hints are not provided for node %s', node_info.uuid) return inventory = introspection_data.get('inventory') if not inventory: LOG.error( _LW('Root device selection require ironic-python-agent ' 'as an inspection ramdisk')) # TODO(dtantsur): make it a real error in Mitaka cycle return disks = inventory.get('disks', []) if not disks: raise utils.Error( _('No disks found on a node %s') % node_info.uuid) for disk in disks: properties = disk.copy() # Root device hints are in GiB, data from IPA is in bytes properties['size'] //= units.Gi for name, value in hints.items(): actual = properties.get(name) if actual != value: LOG.debug( 'Disk %(disk)s does not satisfy hint ' '%(name)s=%(value)s for node %(node)s, ' 'actual value is %(actual)s', { 'disk': disk.get('name'), 'name': name, 'value': value, 'node': node_info.uuid, 'actual': actual }) break else: LOG.debug( 'Disk %(disk)s of size %(size)s satisfies ' 'root device hints for node %(node)s', { 'disk': disk.get('name'), 'node': node_info.uuid, 'size': disk['size'] }) introspection_data['root_disk'] = disk return raise utils.Error( _('No disks satisfied root device hints for node %s') % node_info.uuid)
def find_node(**attributes): """Find node in cache. :param attributes: attributes known about this node (like macs, BMC etc) also ironic client instance may be passed under 'ironic' :returns: structure NodeInfo with attributes ``uuid`` and ``created_at`` :raises: Error if node is not found """ ironic = attributes.pop('ironic', None) # NOTE(dtantsur): sorting is not required, but gives us predictability found = set() for (name, value) in sorted(attributes.items()): if not value: LOG.debug('Empty value for attribute %s', name) continue if not isinstance(value, list): value = [value] LOG.debug('Trying to use %s of value %s for node look up' % (name, value)) value_list = [] for v in value: value_list.append('name="%s" AND value="%s"' % (name, v)) stmt = ('select distinct uuid from attributes where ' + ' OR '.join(value_list)) rows = (db.model_query(db.Attribute.uuid).from_statement( text(stmt)).all()) if rows: found.update(item.uuid for item in rows) if not found: raise utils.NotFoundInCacheError(_( 'Could not find a node for attributes %s') % attributes) elif len(found) > 1: raise utils.Error(_( 'Multiple matching nodes found for attributes ' '%(attr)s: %(found)s') % {'attr': attributes, 'found': list(found)}, code=404) uuid = found.pop() row = (db.model_query(db.Node.started_at, db.Node.finished_at). filter_by(uuid=uuid).first()) if not row: raise utils.Error(_( 'Could not find node %s in introspection cache, ' 'probably it\'s not on introspection now') % uuid, code=404) if row.finished_at: raise utils.Error(_( 'Introspection for node %(node)s already finished on ' '%(finish)s') % {'node': uuid, 'finish': row.finished_at}) return NodeInfo(uuid=uuid, started_at=row.started_at, ironic=ironic)
def before_processing(self, introspection_data, **kwargs): """Validate information about network interfaces.""" bmc_address = introspection_data.get('ipmi_address') if not introspection_data.get('interfaces'): raise utils.Error(_('No interfaces supplied by the ramdisk')) valid_interfaces = { n: iface for n, iface in introspection_data['interfaces'].items() if utils.is_valid_mac(iface.get('mac')) } pxe_mac = introspection_data.get('boot_interface') if CONF.processing.add_ports == 'pxe' and not pxe_mac: LOG.warning(_LW('No boot interface provided in the introspection ' 'data, will add all ports with IP addresses')) if CONF.processing.add_ports == 'pxe' and pxe_mac: LOG.info(_LI('PXE boot interface was %s'), pxe_mac) if '-' in pxe_mac: # pxelinux format: 01-aa-bb-cc-dd-ee-ff pxe_mac = pxe_mac.split('-', 1)[1] pxe_mac = pxe_mac.replace('-', ':').lower() valid_interfaces = { n: iface for n, iface in valid_interfaces.items() if iface['mac'].lower() == pxe_mac } elif CONF.processing.add_ports != 'all': valid_interfaces = { n: iface for n, iface in valid_interfaces.items() if iface.get('ip') } if not valid_interfaces: raise utils.Error(_('No valid interfaces found for node with ' 'BMC %(ipmi_address)s, got %(interfaces)s') % {'ipmi_address': bmc_address, 'interfaces': introspection_data['interfaces']}) elif valid_interfaces != introspection_data['interfaces']: invalid = {n: iface for n, iface in introspection_data['interfaces'].items() if n not in valid_interfaces} LOG.warning(_LW( 'The following interfaces were invalid or not eligible in ' 'introspection data for node with BMC %(ipmi_address)s and ' 'were excluded: %(invalid)s'), {'invalid': invalid, 'ipmi_address': bmc_address}) LOG.info(_LI('Eligible interfaces are %s'), valid_interfaces) introspection_data['all_interfaces'] = introspection_data['interfaces'] introspection_data['interfaces'] = valid_interfaces valid_macs = [iface['mac'] for iface in valid_interfaces.values()] introspection_data['macs'] = valid_macs
def before_update(self, introspection_data, node_info, **kwargs): """Update node with scheduler properties.""" inventory = utils.get_inventory(introspection_data, node_info=node_info) errors = [] root_disk = introspection_data.get('root_disk') if root_disk: introspection_data['local_gb'] = root_disk['size'] // units.Gi if CONF.processing.disk_partitioning_spacing: introspection_data['local_gb'] -= 1 else: errors.append( _('root disk is not supplied by the ramdisk and ' 'root_disk_selection hook is not enabled')) try: introspection_data['cpus'] = int(inventory['cpu']['count']) introspection_data['cpu_arch'] = six.text_type( inventory['cpu']['architecture']) except (KeyError, ValueError, TypeError): errors.append( _('malformed or missing CPU information: %s') % inventory.get('cpu')) try: introspection_data['memory_mb'] = int( inventory['memory']['physical_mb']) except (KeyError, ValueError, TypeError): errors.append( _('malformed or missing memory information: %s; ' 'introspection requires physical memory size ' 'from dmidecode') % inventory.get('memory')) if errors: raise utils.Error(_('The following problems encountered: %s') % '; '.join(errors), node_info=node_info, data=introspection_data) LOG.info(_LI('Discovered data: CPUs: %(cpus)s %(cpu_arch)s, ' 'memory %(memory_mb)s MiB, disk %(local_gb)s GiB'), {key: introspection_data.get(key) for key in self.KEYS}, node_info=node_info, data=introspection_data) overwrite = CONF.processing.overwrite_existing properties = { key: str(introspection_data[key]) for key in self.KEYS if overwrite or not node_info.node().properties.get(key) } node_info.update_properties(**properties)
def _validate_conditions(conditions_json): """Validates conditions from jsonschema. :returns: a list of conditions. """ try: jsonschema.validate(conditions_json, conditions_schema()) except jsonschema.ValidationError as exc: raise utils.Error(_('Validation failed for conditions: %s') % exc) cond_mgr = plugins_base.rule_conditions_manager() conditions = [] reserved_params = {'op', 'field', 'multiple', 'invert'} for cond_json in conditions_json: field = cond_json['field'] scheme, path = _parse_path(field) if scheme not in ('node', 'data'): raise utils.Error( _('Unsupported scheme for field: %s, valid ' 'values are node:// or data://') % scheme) # verify field as JSON path try: jsonpath.parse(path) except Exception as exc: raise utils.Error( _('Unable to parse field JSON path %(field)s: ' '%(error)s') % { 'field': field, 'error': exc }) plugin = cond_mgr[cond_json['op']].obj params = { k: v for k, v in cond_json.items() if k not in reserved_params } try: plugin.validate(params) except ValueError as exc: raise utils.Error( _('Invalid parameters for operator %(op)s: ' '%(error)s') % { 'op': cond_json['op'], 'error': exc }) conditions.append( (cond_json['field'], cond_json['op'], cond_json.get('multiple', 'any'), cond_json.get('invert', False), params)) return conditions
def check_provision_state(node, with_credentials=False): state = node.provision_state.lower() if with_credentials and state not in SET_CREDENTIALS_VALID_STATES: msg = _('Invalid provision state "%(state)s" for setting IPMI ' 'credentials on node %(node)s, valid states are %(valid)s') raise Error(msg % {'node': node.uuid, 'state': state, 'valid': list(SET_CREDENTIALS_VALID_STATES)}) elif not with_credentials and state not in VALID_STATES: msg = _('Invalid provision state "%(state)s" for introspection of ' 'node %(node)s, valid states are "%(valid)s"') raise Error(msg % {'node': node.uuid, 'state': state, 'valid': list(VALID_STATES)})
def introspect(uuid, new_ipmi_credentials=None, token=None): """Initiate hardware properties introspection for a given node. :param uuid: node uuid :param new_ipmi_credentials: tuple (new username, new password) or None :param token: authentication token :raises: Error """ ironic = utils.get_client(token) try: node = ironic.node.get(uuid) except exceptions.NotFound: raise utils.Error(_("Cannot find node %s") % uuid, code=404) except exceptions.HttpError as exc: raise utils.Error( _("Cannot get node %(node)s: %(exc)s") % { 'node': uuid, 'exc': exc }) utils.check_provision_state(node, with_credentials=new_ipmi_credentials) if new_ipmi_credentials: new_ipmi_credentials = (_validate_ipmi_credentials( node, new_ipmi_credentials)) else: validation = ironic.node.validate(node.uuid) if not validation.power['result']: msg = _('Failed validation of power interface for node %(node)s, ' 'reason: %(reason)s') raise utils.Error(msg % { 'node': node.uuid, 'reason': validation.power['reason'] }) node_info = node_cache.add_node(node.uuid, bmc_address=utils.get_ipmi_address(node), ironic=ironic) node_info.set_option('new_ipmi_credentials', new_ipmi_credentials) def _handle_exceptions(): try: _background_introspect(ironic, node_info) except utils.Error as exc: node_info.finished(error=str(exc)) except Exception as exc: msg = _('Unexpected exception in background introspection thread') LOG.exception(msg) node_info.finished(error=msg) utils.spawn_n(_handle_exceptions)
def check_provision_state(node, with_credentials=False): state = node.provision_state.lower() if with_credentials and state not in SET_CREDENTIALS_VALID_STATES: msg = _('Invalid provision state for setting IPMI credentials: ' '"%(state)s", valid states are %(valid)s') raise utils.Error(msg % {'state': state, 'valid': list(SET_CREDENTIALS_VALID_STATES)}, node_info=node) elif not with_credentials and state not in VALID_STATES: msg = _('Invalid provision state for introspection: ' '"%(state)s", valid states are "%(valid)s"') raise utils.Error(msg % {'state': state, 'valid': list(VALID_STATES)}, node_info=node)
def api_introspection_reapply(node_id): if flask.request.content_length: return error_response(_('User data processing is not ' 'supported yet'), code=400) if CONF.processing.store_data == 'swift': process.reapply(node_id) return '', 202 else: return error_response(_('Inspector is not configured to store' ' data. Set the [processing] ' 'store_data configuration option to ' 'change this.'), code=400)
def introspect(uuid, new_ipmi_credentials=None, token=None): """Initiate hardware properties introspection for a given node. :param uuid: node uuid :param new_ipmi_credentials: tuple (new username, new password) or None :param token: authentication token :raises: Error """ ironic = ir_utils.get_client(token) try: node = ironic.node.get(uuid) except exceptions.NotFound: raise utils.Error(_("Cannot find node %s") % uuid, code=404) except exceptions.HttpError as exc: raise utils.Error( _("Cannot get node %(node)s: %(exc)s") % { 'node': uuid, 'exc': exc }) ir_utils.check_provision_state(node, with_credentials=new_ipmi_credentials) if new_ipmi_credentials: new_ipmi_credentials = (_validate_ipmi_credentials( node, new_ipmi_credentials)) else: validation = ironic.node.validate(node.uuid) if not validation.power['result']: msg = _('Failed validation of power interface, reason: %s') raise utils.Error(msg % validation.power['reason'], node_info=node) bmc_address = ir_utils.get_ipmi_address(node) node_info = node_cache.add_node(node.uuid, bmc_address=bmc_address, ironic=ironic) node_info.set_option('new_ipmi_credentials', new_ipmi_credentials) def _handle_exceptions(fut): try: fut.result() except utils.Error as exc: # Logging has already happened in Error.__init__ node_info.finished(error=str(exc)) except Exception as exc: msg = _('Unexpected exception in background introspection thread') LOG.exception(msg, node_info=node_info) node_info.finished(error=msg) future = utils.executor().submit(_background_introspect, ironic, node_info) future.add_done_callback(_handle_exceptions)
def check_auth(request): """Check authentication on request. :param request: Flask request :raises: utils.Error if access is denied """ if get_auth_strategy() == 'noauth': return if request.headers.get('X-Identity-Status').lower() == 'invalid': raise Error(_('Authentication required'), code=401) roles = (request.headers.get('X-Roles') or '').split(',') if 'admin' not in roles: LOG.error(_LE('Role "admin" not in user role list %s'), roles) raise Error(_('Access denied'), code=403)
def introspect(uuid, new_ipmi_credentials=None, token=None): """Initiate hardware properties introspection for a given node. :param uuid: node uuid :param new_ipmi_credentials: tuple (new username, new password) or None :param token: authentication token :raises: Error """ ironic = ir_utils.get_client(token) try: node = ironic.node.get(uuid) except exceptions.NotFound: raise utils.Error(_("Cannot find node %s") % uuid, code=404) except exceptions.HttpError as exc: raise utils.Error(_("Cannot get node %(node)s: %(exc)s") % {'node': uuid, 'exc': exc}) ir_utils.check_provision_state(node, with_credentials=new_ipmi_credentials) if new_ipmi_credentials: new_ipmi_credentials = ( _validate_ipmi_credentials(node, new_ipmi_credentials)) else: validation = ironic.node.validate(node.uuid) if not validation.power['result']: msg = _('Failed validation of power interface, reason: %s') raise utils.Error(msg % validation.power['reason'], node_info=node) bmc_address = ir_utils.get_ipmi_address(node) node_info = node_cache.add_node(node.uuid, bmc_address=bmc_address, ironic=ironic) node_info.set_option('new_ipmi_credentials', new_ipmi_credentials) def _handle_exceptions(fut): try: fut.result() except utils.Error as exc: # Logging has already happened in Error.__init__ node_info.finished(error=str(exc)) except Exception as exc: msg = _('Unexpected exception in background introspection thread') LOG.exception(msg, node_info=node_info) node_info.finished(error=msg) future = utils.executor().submit(_background_introspect, ironic, node_info) future.add_done_callback(_handle_exceptions)
def api_introspection_reapply(uuid): utils.check_auth(flask.request) if flask.request.content_length: return error_response(_('User data processing is not ' 'supported yet'), code=400) if CONF.processing.store_data == 'swift': process.reapply(uuid) return '', 202 else: return error_response(_('Inspector is not configured to store' ' data. Set the [processing] ' 'store_data configuration option to ' 'change this.'), code=400)
def check_auth(request, rule=None, target=None): """Check authentication on request. :param request: Flask request :param rule: policy rule to check the request against :raises: utils.Error if access is denied """ if CONF.auth_strategy == 'noauth': return if not request.context.is_public_api: if request.headers.get('X-Identity-Status', '').lower() == 'invalid': raise Error(_('Authentication required'), code=401) target = {} if target is None else target if not policy.authorize(rule, target, request.context.to_policy_values()): raise Error(_("Access denied by policy"), code=403)
def _abort(node_info, ironic): # runs in background if node_info.finished_at is not None: # introspection already finished; nothing to do LOG.info(_LI('Cannot abort introspection as it is already ' 'finished'), node_info=node_info) node_info.release_lock() return # block this node from PXE Booting the introspection image try: firewall.update_filters(ironic) except Exception as exc: # Note(mkovacik): this will be retried in firewall update # periodic task; we continue aborting LOG.warning(_LW('Failed to update firewall filters: %s'), exc, node_info=node_info) # finish the introspection LOG.debug('Forcing power-off', node_info=node_info) try: ironic.node.set_power_state(node_info.uuid, 'off') except Exception as exc: LOG.warning(_LW('Failed to power off node: %s'), exc, node_info=node_info) node_info.finished(error=_('Canceled by operator')) LOG.info(_LI('Introspection aborted'), node_info=node_info)
def marker_field(value): """Fetch the pagination marker field from flask.request.args. :returns: an uuid """ assert uuidutils.is_uuid_like(value), _('Marker not UUID-like') return value
def api_introspection(node_id): utils.check_auth(flask.request) if flask.request.method == 'POST': new_ipmi_password = flask.request.args.get('new_ipmi_password', type=str, default=None) if new_ipmi_password: new_ipmi_username = flask.request.args.get('new_ipmi_username', type=str, default=None) new_ipmi_credentials = (new_ipmi_username, new_ipmi_password) else: new_ipmi_credentials = None if new_ipmi_credentials and _get_version() >= (1, 9): return _('Setting IPMI credentials is deprecated and not allowed ' 'starting with API version 1.9'), 400 introspect.introspect(node_id, new_ipmi_credentials=new_ipmi_credentials, token=flask.request.headers.get('X-Auth-Token')) return '', 202 else: node_info = node_cache.get_node(node_id) return flask.json.jsonify(generate_introspection_status(node_info))
def get_node(node_id, ironic=None, locked=False): """Get node from cache. :param node_id: node UUID or name. :param ironic: optional ironic client instance :param locked: if True, get a lock on node before fetching its data :returns: structure NodeInfo. """ if uuidutils.is_uuid_like(node_id): node = None uuid = node_id else: node = ir_utils.get_node(node_id, ironic=ironic) uuid = node.uuid if locked: lock = _get_lock(uuid) lock.acquire() else: lock = None try: row = db.model_query(db.Node).filter_by(uuid=uuid).first() if row is None: raise utils.Error(_('Could not find node %s in cache') % uuid, code=404) return NodeInfo.from_row(row, ironic=ironic, lock=lock, node=node) except Exception: with excutils.save_and_reraise_exception(): if lock is not None: lock.release()
def fsm_event(self, event, strict=False): """Update node_info.state based on a fsm.process_event(event) call. An AutomatonException triggers an error event. If strict, node_info.finished(istate.Events.error, error=str(exc)) is called with the AutomatonException instance and a EventError raised. :param event: an event to process by the fsm :strict: whether to fail the introspection upon an invalid event :raises: NodeStateInvalidEvent """ with self._fsm_ctx() as fsm: LOG.debug('Executing fsm(%(state)s).process_event(%(event)s)', {'state': fsm.current_state, 'event': event}, node_info=self) try: fsm.process_event(event) except automaton_errors.NotFound as exc: msg = _('Invalid event: %s') % exc if strict: LOG.error(msg, node_info=self) # assuming an error event is always possible self.finished(istate.Events.error, error=str(exc)) else: LOG.warning(msg, node_info=self) raise utils.NodeStateInvalidEvent(str(exc), node_info=self)
def get_ipmi_address(node): ipmi_fields = ['ipmi_address'] + CONF.ipmi_address_fields # NOTE(sambetts): IPMI Address is useless to us if bridging is enabled so # just ignore it and return None if node.driver_info.get("ipmi_bridging", "no") != "no": return for name in ipmi_fields: value = node.driver_info.get(name) if not value: continue try: ip = socket.gethostbyname(value) except socket.gaierror: msg = _('Failed to resolve the hostname (%(value)s)' ' for node %(uuid)s') raise utils.Error(msg % {'value': value, 'uuid': node.uuid}, node_info=node) if netaddr.IPAddress(ip).is_loopback(): LOG.warning(_LW('Ignoring loopback BMC address %s'), ip, node_info=node) ip = None return ip
def check_api_version(): requested = flask.request.headers.get(_VERSION_HEADER, _DEFAULT_API_VERSION) try: requested = tuple(int(x) for x in requested.split('.')) except (ValueError, TypeError): return error_response(_('Malformed API version: expected string ' 'in form of X.Y'), code=400) if requested < MINIMUM_API_VERSION or requested > CURRENT_API_VERSION: return error_response(_('Unsupported API version %(requested)s, ' 'supported range is %(min)s to %(max)s') % {'requested': _format_version(requested), 'min': _format_version(MINIMUM_API_VERSION), 'max': _format_version(CURRENT_API_VERSION)}, code=406)
def api_introspection(uuid): utils.check_auth(flask.request) if not uuidutils.is_uuid_like(uuid): raise utils.Error(_('Invalid UUID value'), code=400) if flask.request.method == 'POST': new_ipmi_password = flask.request.args.get('new_ipmi_password', type=str, default=None) if new_ipmi_password: new_ipmi_username = flask.request.args.get('new_ipmi_username', type=str, default=None) new_ipmi_credentials = (new_ipmi_username, new_ipmi_password) else: new_ipmi_credentials = None introspect.introspect(uuid, new_ipmi_credentials=new_ipmi_credentials, token=flask.request.headers.get('X-Auth-Token')) return '', 202 else: node_info = node_cache.get_node(uuid) return flask.json.jsonify(finished=bool(node_info.finished_at), error=node_info.error or None)
def check_provision_state(node): state = node.provision_state.lower() if state not in VALID_STATES: msg = _('Invalid provision state for introspection: ' '"%(state)s", valid states are "%(valid)s"') raise utils.Error(msg % {'state': state, 'valid': list(VALID_STATES)}, node_info=node)
def _finish_set_ipmi_credentials(ironic, node, node_info, introspection_data, new_username, new_password): patch = [{'op': 'add', 'path': '/driver_info/ipmi_username', 'value': new_username}, {'op': 'add', 'path': '/driver_info/ipmi_password', 'value': new_password}] if (not ir_utils.get_ipmi_address(node) and introspection_data.get('ipmi_address')): patch.append({'op': 'add', 'path': '/driver_info/ipmi_address', 'value': introspection_data['ipmi_address']}) node_info.patch(patch) for attempt in range(_CREDENTIALS_WAIT_RETRIES): try: # We use this call because it requires valid credentials. # We don't care about boot device, obviously. ironic.node.get_boot_device(node_info.uuid) except Exception as exc: LOG.info(_LI('Waiting for credentials update, attempt %(attempt)d ' 'current error is %(exc)s') % {'attempt': attempt, 'exc': exc}, node_info=node_info, data=introspection_data) eventlet.greenthread.sleep(_CREDENTIALS_WAIT_PERIOD) else: _finish(ironic, node_info, introspection_data) return msg = (_('Failed to validate updated IPMI credentials for node ' '%s, node might require maintenance') % node_info.uuid) node_info.finished(error=msg) raise utils.Error(msg, node_info=node_info, data=introspection_data)
def _finish(ironic, node_info, introspection_data, power_off=True): if power_off: LOG.debug('Forcing power off of node %s', node_info.uuid) try: ironic.node.set_power_state(node_info.uuid, 'off') except Exception as exc: if node_info.node().provision_state == 'enroll': LOG.info(_LI("Failed to power off the node in" "'enroll' state, ignoring; error was " "%s") % exc, node_info=node_info, data=introspection_data) else: msg = (_('Failed to power off node %(node)s, check ' 'its power management configuration: ' '%(exc)s') % {'node': node_info.uuid, 'exc': exc}) node_info.finished(error=msg) raise utils.Error(msg, node_info=node_info, data=introspection_data) LOG.info(_LI('Node powered-off'), node_info=node_info, data=introspection_data) node_info.finished() LOG.info(_LI('Introspection finished successfully'), node_info=node_info, data=introspection_data)