# Copyright 2015 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from oslo_concurrency import processutils from oslo_config import cfg from oslo_utils import excutils from oslo_utils import units import requests import stevedore from ironic_python_agent import encoding from ironic_python_agent import errors from ironic_python_agent import hardware from ironic_python_agent import utils LOG = logging.getLogger(__name__) CONF = cfg.CONF DEFAULT_COLLECTOR = 'default' _COLLECTOR_NS = 'ironic_python_agent.inspector.collectors' def extension_manager(names): try: return stevedore.NamedExtensionManager(_COLLECTOR_NS, names=names, name_order=True) except KeyError as exc: raise errors.InspectionError('Failed to load collector %s' % exc) def inspect(): """Optionally run inspection on the current node. If ``inspection_callback_url`` is set in the configuration, get the hardware inventory from the node and post it back to the inspector. :return: node UUID if inspection was successful, None if associated node was not found in inspector cache. None is also returned if inspector support is not enabled. """ if not CONF.inspection_callback_url: LOG.info('Inspection is disabled, skipping') return collector_names = [x.strip() for x in CONF.inspection_collectors.split(',') if x.strip()] LOG.info('inspection is enabled with collectors %s', collector_names) # NOTE(dtantsur): inspection process tries to delay raising any exceptions # until after we posted some data back to inspector. This is because # inspection is run automatically on (mostly) unknown nodes, so if it # fails, we don't have much information for debugging. failures = utils.AccumulatedFailures(exc_class=errors.InspectionError) data = {} try: ext_mgr = extension_manager(collector_names) collectors = [(ext.name, ext.plugin) for ext in ext_mgr] except Exception as exc: with excutils.save_and_reraise_exception(): failures.add(exc) call_inspector(data, failures) for name, collector in collectors: try: collector(data, failures) except Exception as exc: # No reraise here, try to keep going failures.add('collector %s failed: %s', name, exc) resp = call_inspector(data, failures) # Now raise everything we were delaying failures.raise_if_needed() if resp is None: LOG.info('stopping inspection, as inspector returned an error') return # Optionally update IPMI credentials setup_ipmi_credentials(resp) LOG.info('inspection finished successfully') return resp.get('uuid') def call_inspector(data, failures): """Post data to inspector.""" data['error'] = failures.get_error() LOG.info('posting collected data to %s', CONF.inspection_callback_url) LOG.debug('collected data: %s', data) encoder = encoding.RESTJSONEncoder() data = encoder.encode(data) resp = requests.post(CONF.inspection_callback_url, data=data) if resp.status_code >= 400: LOG.error('inspector error %d: %s, proceeding with lookup', resp.status_code, resp.content.decode('utf-8')) return return resp.json() def setup_ipmi_credentials(resp): """Setup IPMI credentials, if requested. :param resp: JSON response from inspector. """ if not resp.get('ipmi_setup_credentials'): LOG.info('setting IPMI credentials was not requested') return user, password = resp['ipmi_username'], resp['ipmi_password'] LOG.debug('setting IPMI credentials: user %s', user) commands = [ ('user', 'set', 'name', '2', user), ('user', 'set', 'password', '2', password), ('user', 'enable', '2'), ('channel', 'setaccess', '1', '2', 'link=on', 'ipmi=on', 'callin=on', 'privilege=4'), ] for cmd in commands: try: utils.execute('ipmitool', *cmd) except processutils.ProcessExecutionError: LOG.exception('failed to update IPMI credentials') raise errors.InspectionError('failed to update IPMI credentials') LOG.info('successfully set IPMI credentials: user %s', user) def discover_network_properties(inventory, data, failures): """Discover network and BMC related properties. Populates 'boot_interface', 'ipmi_address' and 'interfaces' keys. """ # Both boot interface and IPMI address might not be present, # we don't count it as failure data['boot_interface'] = utils.get_agent_params().get('BOOTIF') LOG.info('boot devices was %s', data['boot_interface']) data['ipmi_address'] = inventory.get('bmc_address') LOG.info('BMC IP address: %s', data['ipmi_address']) data.setdefault('interfaces', {}) for iface in inventory['interfaces']: if iface.name == 'lo' or iface.ipv4_address == '127.0.0.1': LOG.debug('ignoring local network interface %s', iface.name) continue LOG.debug('found network interface %s', iface.name) if not iface.mac_address: LOG.debug('no link information for interface %s', iface.name) continue if not iface.ipv4_address: LOG.debug('no IP address for interface %s', iface.name) data['interfaces'][iface.name] = {'mac': iface.mac_address, 'ip': iface.ipv4_address} if data['interfaces']: LOG.info('network interfaces: %s', data['interfaces']) else: failures.add('no network interfaces found') def discover_scheduling_properties(inventory, data): data['cpus'] = inventory['cpu'].count data['cpu_arch'] = inventory['cpu'].architecture data['memory_mb'] = inventory['memory'].physical_mb # Replicate the same logic as in deploy. This logic will be moved to # inspector itself, but we need it for backward compatibility. try: disk = utils.guess_root_disk(inventory['disks']) except errors.DeviceNotFound: LOG.warn('no suitable root device detected') else: # -1 is required to give Ironic some spacing for partitioning data['local_gb'] = disk.size / units.Gi - 1 for key in ('cpus', 'local_gb', 'memory_mb'): try: data[key] = int(data[key]) except (KeyError, ValueError, TypeError): LOG.warn('value for %s is missing or malformed: %s', key, data.get(key)) else: LOG.info('value for %s is %s', key, data[key]) def collect_default(data, failures): inventory = hardware.dispatch_to_managers('list_hardware_info') # These 2 calls are required for backward compatibility and should be # dropped after inspector is ready (probably in Mitaka cycle). discover_network_properties(inventory, data, failures) discover_scheduling_properties(inventory, data) # In the future we will only need the current version of inventory, # everything else will be done by inspector itself and its plugins data['inventory'] = inventory