예제 #1
0
    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))
예제 #2
0
    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
예제 #3
0
    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)
예제 #4
0
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
예제 #5
0
    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
예제 #6
0
    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))
예제 #7
0
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]')
예제 #8
0
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()
예제 #9
0
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]')
예제 #10
0
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
예제 #11
0
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)
예제 #12
0
    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
예제 #13
0
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))
예제 #14
0
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)
예제 #15
0
파일: discovery.py 프로젝트: tyws/inspector
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)
예제 #16
0
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
예제 #17
0
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)
예제 #18
0
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()
예제 #19
0
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)
예제 #20
0
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
예제 #21
0
    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)
예제 #22
0
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)
예제 #23
0
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)
예제 #24
0
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)
예제 #25
0
    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
예제 #26
0
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
                })
예제 #27
0
    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)
예제 #28
0
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})
예제 #29
0
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)
예제 #30
0
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
예제 #31
0
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
예제 #32
0
    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)
예제 #33
0
    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
예제 #34
0
    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)
예제 #35
0
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)
예제 #36
0
    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
예제 #37
0
    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
예제 #38
0
    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)
예제 #39
0
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
예제 #40
0
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)})
예제 #41
0
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)
예제 #42
0
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)
예제 #43
0
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)
예제 #44
0
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)
예제 #45
0
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)
예제 #46
0
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)
예제 #47
0
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)
예제 #48
0
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)
예제 #49
0
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)
예제 #50
0
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)
예제 #51
0
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
예제 #52
0
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))
예제 #53
0
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()
예제 #54
0
    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)
예제 #55
0
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
예제 #56
0
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)
예제 #57
0
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)
예제 #58
0
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)
예제 #59
0
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)
예제 #60
0
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)