# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import eventlet eventlet.monkey_patch(thread=False) import argparse import functools import json import logging import sys import flask from keystoneclient import exceptions from ironic_discoverd import conf from ironic_discoverd import firewall from ironic_discoverd import introspect from ironic_discoverd import node_cache from ironic_discoverd import process from ironic_discoverd import utils app = flask.Flask(__name__) LOG = logging.getLogger('ironic_discoverd.main') def check_auth(): """Check whether request is properly authenticated.""" if not conf.getboolean('discoverd', 'authenticate'): return if not flask.request.headers.get('X-Auth-Token'): LOG.error("No X-Auth-Token header, rejecting request") raise utils.Error('Authentication required', code=401) try: utils.check_is_admin(token=flask.request.headers['X-Auth-Token']) except exceptions.Unauthorized as exc: LOG.error("Keystone denied access: %s, rejecting request", exc) raise utils.Error('Access denied', code=403) def convert_exceptions(func): @functools.wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except utils.Error as exc: return str(exc), exc.http_code return wrapper @app.route('/v1/continue', methods=['POST']) @convert_exceptions def api_continue(): data = flask.request.get_json(force=True) LOG.debug("/v1/continue got JSON %s", data) res = process.process(data) return json.dumps(res), 200, {'Content-Type': 'applications/json'} @app.route('/v1/introspection/', methods=['GET', 'POST']) @convert_exceptions def api_introspection(uuid): check_auth() if flask.request.method == 'POST': setup_ipmi_credentials = flask.request.args.get( 'setup_ipmi_credentials', type=bool, default=False) introspect.introspect(uuid, setup_ipmi_credentials=setup_ipmi_credentials) 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) @app.route('/v1/discover', methods=['POST']) @convert_exceptions def api_discover(): check_auth() data = flask.request.get_json(force=True) LOG.debug("/v1/discover got JSON %s", data) for uuid in data: introspect.introspect(uuid) return "", 202 def periodic_update(period): # pragma: no cover while True: LOG.debug('Running periodic update of filters') try: firewall.update_filters() except Exception: LOG.exception('Periodic update failed') eventlet.greenthread.sleep(period) def periodic_clean_up(period): # pragma: no cover while True: LOG.debug('Running periodic clean up of node cache') try: if node_cache.clean_up(): firewall.update_filters() except Exception: LOG.exception('Periodic clean up of node cache failed') eventlet.greenthread.sleep(period) def check_ironic_available(): """Try to make sure we can reach Ironic. Ensure that: 1. Keystone access is configured properly 2. Keystone has already started 3. Ironic has already started """ attempts = conf.getint('discoverd', 'ironic_retry_attempts') assert attempts >= 0 retry_period = conf.getint('discoverd', 'ironic_retry_period') LOG.debug('Trying to connect to Ironic') for i in range(attempts + 1): # one attempt always required try: utils.get_client().driver.list() except Exception as exc: if i == attempts: raise LOG.warning('Unable to connect to Ironic or Keystone, retrying %d ' 'times more: %s', attempts - i, exc) else: break eventlet.greenthread.sleep(retry_period) def config_shim(args): """Make new argument parsing method backwards compatible.""" if len(args) == 2 and args[1][0] != '-': return ['--config-file', args[1]] 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') 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 " syntax is deprecated use' ' "ironic-discoverd --config-file " instead') init() app.run(debug=debug, host=conf.get('discoverd', 'listen_address'), port=conf.getint('discoverd', 'listen_port'))