def process(node_info): """Process data from the discovery ramdisk. This function heavily relies on the hooks to do the actual data processing. """ hooks = plugins_base.processing_hooks_manager() for hook_ext in hooks: hook_ext.obj.before_processing(node_info) cached_node = node_cache.find_node( bmc_address=node_info.get('ipmi_address'), mac=node_info.get('macs')) ironic = utils.get_client() try: node = ironic.node.get(cached_node.uuid) except exceptions.NotFound: msg = ('Node UUID %s was found in cache, but is not found in Ironic' % cached_node.uuid) cached_node.finished(error=msg) raise utils.Error(msg, code=404) try: return _process_node(ironic, node, node_info, cached_node) except utils.Error as exc: cached_node.finished(error=str(exc)) raise except Exception as exc: msg = 'Unexpected exception during processing' LOG.exception(msg) cached_node.finished(error=msg) raise utils.Error(msg)
def check_auth(): """Check whether request is properly authenticated.""" if not conf.getboolean('discoverd', 'authenticate'): return if not flask.request.headers.get('X-Auth-Token'): LOG.error("No X-Auth-Token header, rejecting request") raise utils.Error('Authentication required', code=401) try: utils.check_is_admin(token=flask.request.headers['X-Auth-Token']) except exceptions.Unauthorized as exc: LOG.error("Keystone denied access: %s, rejecting request", exc) raise utils.Error('Access denied', code=403)
def find_node(**attributes): """Find node in cache. :param attributes: attributes known about this node (like macs, BMC etc) :returns: structure NodeInfo with attributes ``uuid`` and ``created_at`` :raises: Error if node is not found """ # NOTE(dtantsur): sorting is not required, but gives us predictability found = set() db = _db() 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)) rows = db.execute( 'select distinct uuid from attributes where ' + ' OR '.join('name=? AND value=?' for _ in value), sum(([name, v] for v in value), [])).fetchall() if rows: found.update(item[0] for item in rows) if not found: raise utils.Error('Could not find a node for attributes %s' % attributes, code=404) elif len(found) > 1: raise utils.Error( 'Multiple matching nodes found for attributes %s: %s' % (attributes, list(found)), code=404) uuid = found.pop() row = db.execute('select started_at, finished_at from nodes where uuid=?', (uuid, )).fetchone() 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 %s already finished on %s' % (uuid, row['finished_at'])) return NodeInfo(uuid=uuid, started_at=row['started_at'])
def _prepare_for_pxe(ironic, cached_node, macs, setup_ipmi_credentials): if macs: LOG.info('Whitelisting MAC\'s %s for node %s on the firewall', macs, cached_node.uuid) firewall.update_filters(ironic) if not setup_ipmi_credentials: try: utils.retry_on_conflict(ironic.node.set_boot_device, cached_node.uuid, 'pxe', persistent=False) except Exception as exc: LOG.warning('Failed to set boot device to PXE for node %s: %s', cached_node.uuid, exc) try: utils.retry_on_conflict(ironic.node.set_power_state, cached_node.uuid, 'reboot') except Exception as exc: raise utils.Error('Failed to power on node %s, check it\'s power ' 'management configuration:\n%s' % (cached_node.uuid, exc)) else: LOG.info('Introspection environment is ready for node %s, ' 'manual power on is required within %d seconds', cached_node.uuid, conf.getint('discoverd', 'timeout'))
def test_not_found_in_cache(self, cli, pop_mock, process_mock): pop_mock.side_effect = utils.Error('not found') self.assertRaisesRegexp(utils.Error, 'not found', process.process, self.data) self.assertFalse(cli.node.get.called) self.assertFalse(process_mock.called)
def test_intospect_failed(self, introspect_mock): introspect_mock.side_effect = utils.Error("boom") res = self.app.post('/v1/introspection/uuid1') self.assertEqual(400, res.status_code) self.assertEqual(b"boom", res.data) introspect_mock.assert_called_once_with("uuid1", setup_ipmi_credentials=False)
def test_expected_exception(self, finished_mock, client_mock, pop_mock, process_mock): pop_mock.return_value = node_cache.NodeInfo(uuid=self.node.uuid, started_at=self.started_at) process_mock.side_effect = utils.Error('boom') self.assertRaisesRegexp(utils.Error, 'boom', process.process, self.data) finished_mock.assert_called_once_with(mock.ANY, error='boom')
def get_node(uuid): """Get node from cache by it's UUID. :param uuid: node UUID. :returns: structure NodeInfo. """ row = _db().execute('select * from nodes where uuid=?', (uuid, )).fetchone() if row is None: raise utils.Error('Could not find node %s in cache' % uuid, code=404) return NodeInfo.from_row(row)
def before_processing(self, node_info): """Validate that required properties are provided by the ramdisk.""" missing = [key for key in self.KEYS if not node_info.get(key)] if missing: raise utils.Error( 'The following required parameters are missing: %s' % missing) LOG.info( 'Discovered data: CPUs: %(cpus)s %(cpu_arch)s, ' 'memory %(memory_mb)s MiB, disk %(local_gb)s GiB', {key: node_info.get(key) for key in self.KEYS})
def _force_power_off(ironic, cached_node): LOG.debug('Forcing power off of node %s', cached_node.uuid) try: utils.retry_on_conflict(ironic.node.set_power_state, cached_node.uuid, 'off') except Exception as exc: msg = ('Failed to power off node %s, check it\'s power ' 'management configuration: %s' % (cached_node.uuid, exc)) cached_node.finished(error=msg) raise utils.Error(msg) deadline = cached_node.started_at + conf.getint('discoverd', 'timeout') while time.time() < deadline: node = ironic.node.get(cached_node.uuid) if (node.power_state or '').lower() == 'power off': return LOG.info('Waiting for node %s to power off, current state is %s', cached_node.uuid, node.power_state) eventlet.greenthread.sleep(_POWER_OFF_CHECK_PERIOD) msg = ('Timeout waiting for node %s to power off after introspection' % cached_node.uuid) cached_node.finished(error=msg) raise utils.Error(msg)
def add_node(uuid, **attributes): """Store information about a node under introspection. All existing information about this node is dropped. Empty values are skipped. :param uuid: Ironic node UUID :param attributes: attributes known about this node (like macs, BMC etc) :returns: NodeInfo """ started_at = time.time() with _db() as db: db.execute("delete from nodes where uuid=?", (uuid, )) db.execute("delete from attributes where uuid=?", (uuid, )) db.execute("delete from options where uuid=?", (uuid, )) db.execute("insert into nodes(uuid, started_at) " "values(?, ?)", (uuid, started_at)) for (name, value) in attributes.items(): if not value: continue if not isinstance(value, list): value = [value] try: db.executemany( "insert into attributes(name, value, uuid) " "values(?, ?, ?)", [(name, v, uuid) for v in value]) except sqlite3.IntegrityError as exc: LOG.error( 'Database integrity error %s during ' 'adding attributes', exc) raise utils.Error( 'Some or all of %(name)s\'s %(value)s are already ' 'on introspection' % { 'name': name, 'value': value }) return NodeInfo(uuid=uuid, started_at=started_at)
def introspect(uuid, setup_ipmi_credentials=False): """Initiate hardware properties introspection for a given node. :param uuid: node uuid :raises: Error """ ironic = utils.get_client() 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 %s: %s" % (uuid, exc)) if (setup_ipmi_credentials and not conf.getboolean('discoverd', 'enable_setting_ipmi_credentials')): raise utils.Error( 'IPMI credentials setup is disabled in configuration') if not node.maintenance: provision_state = node.provision_state if provision_state and provision_state.lower() not in VALID_STATES: msg = ('Refusing to introspect node %s with provision state "%s" ' 'and maintenance mode off') raise utils.Error(msg % (node.uuid, provision_state)) power_state = node.power_state if power_state and power_state.lower() not in VALID_POWER_STATES: msg = ('Refusing to introspect node %s with power state "%s" ' 'and maintenance mode off') raise utils.Error(msg % (node.uuid, power_state)) else: LOG.info('Node %s is in maintenance mode, skipping power and provision' ' states check') if not setup_ipmi_credentials: validation = utils.retry_on_conflict(ironic.node.validate, node.uuid) if not validation.power['result']: msg = ('Failed validation of power interface for node %s, ' 'reason: %s') raise utils.Error(msg % (node.uuid, validation.power['reason'])) eventlet.greenthread.spawn_n(_background_start_discover, ironic, node, setup_ipmi_credentials=setup_ipmi_credentials)
def test_continue_failed(self, process_mock): process_mock.side_effect = utils.Error("boom") res = self.app.post('/v1/continue', data='"JSON"') self.assertEqual(400, res.status_code) process_mock.assert_called_once_with("JSON") self.assertEqual(b'boom', res.data)
def before_processing(self, node_info): if not node_info.get('error'): return raise utils.Error('Ramdisk reported error: %s' % node_info['error'])