Example #1
0
def main():  # pragma: no cover
    old_args = config_shim(sys.argv)
    parser = argparse.ArgumentParser(description='''Hardware introspection
                                                 service for OpenStack Ironic.
                                                 ''')
    parser.add_argument('--config-file', dest='config', required=True)
    # if parse_args is passed None it uses sys.argv instead.
    args = parser.parse_args(old_args)

    conf.read(args.config)
    debug = conf.getboolean('discoverd', 'debug')

    logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
    for third_party in ('urllib3.connectionpool',
                        'keystonemiddleware.auth_token',
                        'requests.packages.urllib3.connectionpool'):
        logging.getLogger(third_party).setLevel(logging.WARNING)
    logging.getLogger('ironicclient.common.http').setLevel(
        logging.INFO if debug else logging.ERROR)

    if old_args:
        LOG.warning('"ironic-discoverd <config-file>" syntax is deprecated use'
                    ' "ironic-discoverd --config-file <config-file>" instead')

    init()
    app.run(debug=debug,
            host=conf.get('discoverd', 'listen_address'),
            port=conf.getint('discoverd', 'listen_port'))
Example #2
0
def main():  # pragma: no cover
    old_args = config_shim(sys.argv)
    parser = argparse.ArgumentParser(description='''Hardware introspection
                                                 service for OpenStack Ironic.
                                                 ''')
    parser.add_argument('--config-file', dest='config', required=True)
    # if parse_args is passed None it uses sys.argv instead.
    args = parser.parse_args(old_args)

    conf.read(args.config)
    debug = conf.getboolean('discoverd', 'debug')

    logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
    logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
    logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(
        logging.WARNING)
    logging.getLogger('ironicclient.common.http').setLevel(
        logging.INFO if debug else logging.ERROR)

    if old_args:
        LOG.warning('"ironic-discoverd <config-file>" syntax is deprecated use'
                    ' "ironic-discoverd --config-file <config-file>" instead')

    init()
    app.run(debug=debug,
            host=conf.get('discoverd', 'listen_address'),
            port=conf.getint('discoverd', 'listen_port'))
Example #3
0
def init():
    if not conf.getboolean('discoverd', 'authenticate'):
        LOG.warning('Starting unauthenticated, please check configuration')

    node_cache.init()
    check_ironic_available()

    if conf.getboolean('discoverd', 'manage_firewall'):
        firewall.init()
        period = conf.getint('discoverd', 'firewall_update_period')
        eventlet.greenthread.spawn_n(periodic_update, period)

    if conf.getint('discoverd', 'timeout') > 0:
        period = conf.getint('discoverd', 'clean_up_period')
        eventlet.greenthread.spawn_n(periodic_clean_up, period)
    else:
        LOG.warning('Timeout is disabled in configuration')
Example #4
0
 def before_update(self, node, ports, node_info):
     """Update node with scheduler properties."""
     overwrite = conf.getboolean('discoverd', 'overwrite_existing')
     patch = [{
         'op': 'add',
         'path': '/properties/%s' % key,
         'value': str(node_info[key])
     } for key in self.KEYS if overwrite or not node.properties.get(key)]
     return patch, {}
Example #5
0
def init():
    if conf.getboolean('discoverd', 'authenticate'):
        utils.add_auth_middleware(app)
    else:
        LOG.warning('Starting unauthenticated, please check configuration')

    node_cache.init()
    check_ironic_available()

    if conf.getboolean('discoverd', 'manage_firewall'):
        firewall.init()
        period = conf.getint('discoverd', 'firewall_update_period')
        eventlet.greenthread.spawn_n(periodic_update, period)

    if conf.getint('discoverd', 'timeout') > 0:
        period = conf.getint('discoverd', 'clean_up_period')
        eventlet.greenthread.spawn_n(periodic_clean_up, period)
    else:
        LOG.warning('Timeout is disabled in configuration')
Example #6
0
def process(node_info):
    """Process data from discovery ramdisk."""
    hooks = plugins_base.processing_hooks_manager()
    for hook_ext in hooks:
        hook_ext.obj.pre_discover(node_info)

    if node_info.get('error'):
        LOG.error('Error happened during discovery: %s',
                  node_info['error'])
        raise utils.DiscoveryFailed(node_info['error'])

    compat = conf.getboolean('discoverd', 'ports_for_inactive_interfaces')
    if 'interfaces' not in node_info and 'macs' in node_info:
        LOG.warning('Using "macs" field is deprecated, please '
                    'update your discovery ramdisk')
        node_info['interfaces'] = {'dummy%d' % i: {'mac': m}
                                   for i, m in enumerate(node_info['macs'])}
        compat = True

    valid_interfaces = {
        n: iface for n, iface in node_info['interfaces'].items()
        if utils.is_valid_mac(iface['mac']) and (compat or iface.get('ip'))
    }
    valid_macs = [iface['mac'] for iface in valid_interfaces.values()]
    if valid_interfaces != node_info['interfaces']:
        LOG.warning(
            'The following interfaces were invalid or not eligible in '
            'discovery data for node with BMC %(ipmi_address)s and were '
            'excluded: %(invalid)s',
            {'invalid': {n: iface
                         for n, iface in node_info['interfaces'].items()
                         if n not in valid_interfaces},
             'ipmi_address': node_info.get('ipmi_address')})
        LOG.info('Eligible interfaces are %s', valid_interfaces)

    uuid = node_cache.pop_node(bmc_address=node_info['ipmi_address'],
                               mac=valid_macs)
    ironic = utils.get_client()
    try:
        node = ironic.node.get(uuid)
    except exceptions.NotFound as exc:
        LOG.error('Node UUID %(uuid)s is in the cache, but not found '
                  'in Ironic: %(exc)s',
                  {'uuid': uuid, 'exc': exc})
        raise utils.DiscoveryFailed('Node UUID %s was found is cache, '
                                    'but is not found in Ironic' % uuid,
                                    code=404)

    if not node.extra.get('on_discovery'):
        LOG.error('Node is not on discovery, cannot proceed')
        raise utils.DiscoveryFailed('Node %s is not on discovery' % uuid,
                                    code=403)

    _process_node(ironic, node, node_info, valid_macs)
Example #7
0
def init():
    """Initialize firewall management.

    Must be called one on start-up.
    """
    if not conf.getboolean('discoverd', 'manage_firewall'):
        return

    global INTERFACE
    INTERFACE = conf.get('discoverd', 'dnsmasq_interface')
    _clean_up(CHAIN)
    # Not really needed, but helps to validate that we have access to iptables
    _iptables('-N', CHAIN)
Example #8
0
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)
Example #9
0
def check_auth(request):
    """Check authentication on request.

    :param request: Flask request
    :raises: utils.Error if access is denied
    """
    if not conf.getboolean('discoverd', 'authenticate'):
        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('Role "admin" not in user role list %s', roles)
        raise Error('Access denied', code=403)
Example #10
0
def _process_node(ironic, node, node_info, valid_macs):
    hooks = plugins_base.processing_hooks_manager()

    ports = {}
    for mac in valid_macs:
        try:
            port = ironic.port.create(node_uuid=node.uuid, address=mac)
            ports[mac] = port
        except exceptions.Conflict:
            LOG.warning('MAC %(mac)s appeared in discovery data for '
                        'node %(node)s, but already exists in '
                        'database - skipping',
                        {'mac': mac, 'node': node.uuid})

    node_patches = []
    port_patches = {}
    for hook_ext in hooks:
        hook_patch = hook_ext.obj.post_discover(node, list(ports.values()),
                                                node_info)
        if not hook_patch:
            continue

        node_patches.extend(hook_patch[0])
        port_patches.update(hook_patch[1])
    node_patches = [p for p in node_patches if p]
    port_patches = {mac: patch for (mac, patch) in port_patches.items()
                    if mac in ports and patch}

    ironic.node.update(node.uuid, node_patches)

    for mac, patches in port_patches.items():
        ironic.port.update(ports[mac].uuid, patches)

    LOG.info('Node %s was updated with data from discovery process', node.uuid)

    firewall.update_filters(ironic)

    if conf.getboolean('discoverd', 'power_off_after_discovery'):
        LOG.info('Forcing power off of node %s', node.uuid)
        try:
            ironic.node.set_power_state(node.uuid, 'off')
        except Exception as exc:
            LOG.error('Failed to power off node %s, check it\'s power '
                      'management configuration:\n%s', node.uuid, exc)
            raise utils.DiscoveryFailed('Failed to power off node %s' %
                                        node.uuid)

    patch = [{'op': 'add', 'path': '/extra/newly_discovered', 'value': 'true'},
             {'op': 'remove', 'path': '/extra/on_discovery'}]
    ironic.node.update(node.uuid, patch)
Example #11
0
def update_filters(ironic=None):
    """Update firewall filter rules for introspection.

    Gives access to PXE boot port for any machine, except for those,
    whose MAC is registered in Ironic and is not on introspection right now.

    This function is called from both introspection initialization code and
    from periodic task. This function is supposed to be resistant to unexpected
    iptables state.

    ``init()`` function must be called once before any call to this function.
    This function is using ``eventlet`` semaphore to serialize access from
    different green threads.

    Does nothing, if firewall management is disabled in configuration.

    :param ironic: Ironic client instance, optional.
    """
    if not conf.getboolean('discoverd', 'manage_firewall'):
        return

    assert INTERFACE is not None
    ironic = utils.get_client() if ironic is None else ironic

    with LOCK:
        macs_active = set(p.address for p in ironic.port.list(limit=0))
        to_blacklist = macs_active - node_cache.active_macs()
        LOG.debug('Blacklisting active MAC\'s %s', to_blacklist)

        # Clean up a bit to account for possible troubles on previous run
        _clean_up(NEW_CHAIN)
        # Operate on temporary chain
        _iptables('-N', NEW_CHAIN)
        # - Blacklist active macs, so that nova can boot them
        for mac in to_blacklist:
            _iptables('-A', NEW_CHAIN, '-m', 'mac',
                      '--mac-source', mac, '-j', 'DROP')
        # - Whitelist everything else
        _iptables('-A', NEW_CHAIN, '-j', 'ACCEPT')

        # Swap chains
        _iptables('-I', 'INPUT', '-i', INTERFACE, '-p', 'udp',
                  '--dport', '67', '-j', NEW_CHAIN)
        _iptables('-D', 'INPUT', '-i', INTERFACE, '-p', 'udp',
                  '--dport', '67', '-j', CHAIN,
                  ignore=True)
        _iptables('-F', CHAIN, ignore=True)
        _iptables('-X', CHAIN, ignore=True)
        _iptables('-E', NEW_CHAIN, CHAIN)
Example #12
0
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)
Example #13
0
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))
    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)
Example #14
0
    def before_processing(self, node_info):
        """Validate information about network interfaces."""
        bmc_address = node_info.get('ipmi_address')

        compat = conf.getboolean('discoverd', 'ports_for_inactive_interfaces')
        if 'interfaces' not in node_info and 'macs' in node_info:
            LOG.warning('Using "macs" field is deprecated, please '
                        'update your discovery ramdisk')
            node_info['interfaces'] = {
                'dummy%d' % i: {
                    'mac': m
                }
                for i, m in enumerate(node_info['macs'])
            }
            compat = True

        valid_interfaces = {
            n: iface
            for n, iface in node_info['interfaces'].items()
            if (utils.is_valid_mac(iface.get('mac')) and (
                compat or iface.get('ip')))
        }
        valid_macs = [iface['mac'] for iface in valid_interfaces.values()]
        if valid_interfaces != node_info['interfaces']:
            LOG.warning(
                'The following interfaces were invalid or not eligible in '
                'introspection data for node with BMC %(ipmi_address)s and '
                'were excluded: %(invalid)s', {
                    'invalid': {
                        n: iface
                        for n, iface in node_info['interfaces'].items()
                        if n not in valid_interfaces
                    },
                    'ipmi_address': bmc_address
                })
            LOG.info('Eligible interfaces are %s', valid_interfaces)

        node_info['interfaces'] = valid_interfaces
        node_info['macs'] = valid_macs