From 60a8e67a8c2a1b09aeff1c5a6742b809bed17483 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Thu, 25 Aug 2016 17:06:43 -0400 Subject: [PATCH] Use a service user to get Keystone tokens to talk to services The authentication scheme of the REST API is still a bit up in the air so switch this to not rely/expect authentication but instead to use the nova service user to talk to other services. Eventually this should use its own service user. This enables us to get images from glance but also to handle looking up the information we need when Neutron assigns a floating IP address. This means we can create the hostname in IPA DNS in advance so it will be on the public network and not the private one. --- files/join.conf.template | 12 ++++++- novajoin/glance.py | 29 ++++++++--------- novajoin/keystone_client.py | 62 +++++++++++++++++++++++++++++++++++ novajoin/notifications.py | 64 ++++++++++++++++++++++++++++--------- novajoin/wsgi.py | 2 ++ scripts/novajoin-install | 15 ++++++--- 6 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 novajoin/keystone_client.py diff --git a/files/join.conf.template b/files/join.conf.template index bebe1dc..f78d049 100644 --- a/files/join.conf.template +++ b/files/join.conf.template @@ -12,8 +12,18 @@ cacert = /etc/ipa/ca.crt connect_retries = 1 [keystone_authtoken] -auth_uri = $KEYSTONE_AUTH +auth_uri = $KEYSTONE_AUTH_URI admin_password = $NOVA_PASSWORD admin_user = nova admin_tenant_name = services identity_uri = $KEYSTONE_IDENTITY + +[service_credentials] +region_name = RegionOne +auth_type = password +auth_url = $KEYSTONE_AUTH_URL +project_name = services +project_domain_name = Default +username = nova +user_domain_name = Default +password = $NOVA_PASSWORD diff --git a/novajoin/glance.py b/novajoin/glance.py index 8ad5553..48e826e 100644 --- a/novajoin/glance.py +++ b/novajoin/glance.py @@ -27,6 +27,7 @@ from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import timeutils from novajoin import exception +from novajoin import keystone_client import six @@ -37,27 +38,23 @@ LOG = logging.getLogger(__name__) GLANCE_APIVERSION = 2 -def generate_identity_headers(context, status='Confirmed'): - return { - 'X-Auth-Token': getattr(context, 'auth_token', None), - 'X-User-Id': getattr(context, 'user', None), - 'X-Tenant-Id': getattr(context, 'tenant', None), - 'X-Roles': ','.join(getattr(context, 'roles', [])), - 'X-Identity-Status': status, - } - - def get_api_servers(): """Return iterator of glance api_servers to cycle through the list, looping around to the beginning if necessary. """ api_servers = [] - if not CONF.glance_api_servers: - return None + ks = keystone_client.get_client() + catalog = keystone_client.get_service_catalog(ks) + + image_service = catalog.url_for(service_type='image') + if image_service: + api_servers.append(image_service) + + if CONF.glance_api_servers: + for api_server in CONF.glance_api_servers: + api_servers.append(api_server) - for api_server in CONF.glance_api_servers: - api_servers.append(api_server) random.shuffle(api_servers) return itertools.cycle(api_servers) @@ -82,9 +79,9 @@ class GlanceClient(object): params = {} - params['identity_headers'] = generate_identity_headers(context) + session = keystone_client.get_session() return glanceclient.Client(str(self.version), self.api_server, - **params) + session=session, **params) def call(self, context, method, *args, **kwargs): """Call a glance client method.""" diff --git a/novajoin/keystone_client.py b/novajoin/keystone_client.py new file mode 100644 index 0000000..82d99da --- /dev/null +++ b/novajoin/keystone_client.py @@ -0,0 +1,62 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from keystoneauth1 import loading as ks_loading +from keystoneclient.v3 import client as ks_client_v3 +from oslo_config import cfg + +CFG_GROUP = "service_credentials" + +_SESSION = None +_AUTH = None + + +def get_session(): + """Get a service credentials auth session.""" + + global _SESSION + global _AUTH + + if not _AUTH: + _AUTH = ks_loading.load_auth_from_conf_options(cfg.CONF, CFG_GROUP) + if not _SESSION: + _SESSION = ks_loading.load_session_from_conf_options( + cfg.CONF, CFG_GROUP, auth=_AUTH, session=_SESSION + ) + return _SESSION + + +def get_client(trust_id=None): + """Return a client for keystone v3 endpoint, optionally using a trust.""" + session = get_session() + return ks_client_v3.Client(session=session, trust_id=trust_id) + + +def get_service_catalog(client): + return client.session.auth.get_access(client.session).service_catalog + + +def get_auth_token(client): + return client.session.auth.get_access(client.session).auth_token + + +def register_keystoneauth_opts(conf): + ks_loading.register_auth_conf_options(conf, CFG_GROUP) + ks_loading.register_session_conf_options( + conf, CFG_GROUP, + deprecated_opts={'cacert': [ + cfg.DeprecatedOpt('os-cacert', group=CFG_GROUP), + cfg.DeprecatedOpt('os-cacert', group="DEFAULT")] + }) diff --git a/novajoin/notifications.py b/novajoin/notifications.py index 62214f4..0987fd0 100644 --- a/novajoin/notifications.py +++ b/novajoin/notifications.py @@ -21,6 +21,9 @@ import sys import time import json import oslo_messaging +from neutronclient.v2_0 import client as neutron_client +from novaclient import client as nova_client +from novajoin.keystone_client import get_session, register_keystoneauth_opts from oslo_serialization import jsonutils from oslo_log import log as logging @@ -34,18 +37,38 @@ CONF = config.CONF LOG = logging.getLogger(__name__) +def novaclient(): + session = get_session() + return nova_client.Client('2.1', session=session) + + +def neutronclient(): + session = get_session() + return neutron_client.Client(session=session) + + class NotificationEndpoint(object): filter_rule = oslo_messaging.notify.filter.NotificationFilter( publisher_id='^compute.*|^network.*', event_type='^compute.instance.create.end|' '^compute.instance.delete.end|' - '^network.floating_ip.(dis)?associate',) + '^network.floating_ip.(dis)?associate|' + '^floatingip.update.end') def __init__(self): self.uuidcache = cache.Cache() self.ipaclient = IPAClient() + def _generate_hostname(self, hostname): + # FIXME: Don't re-calculate the hostname, fetch it from somewhere + project = 'foo' + if CONF.project_subdomain: + host = '%s.%s.%s' % (hostname, project, CONF.domain) + else: + host = '%s.%s' % (hostname, CONF.domain) + return host + def info(self, ctxt, publisher_id, event_type, payload, metadata): LOG.debug('notification:') LOG.debug(json.dumps(payload, indent=4)) @@ -54,17 +77,13 @@ class NotificationEndpoint(object): event_type, metadata) if event_type == 'compute.instance.create.end': - LOG.info("Add new host") + hostname = self._generate_hostname(payload.get('hostname')) + id = payload.get('instance_id') + LOG.info("Add new host %s (%s)", id, hostname) elif event_type == 'compute.instance.delete.end': - LOG.info("Delete host") - hostname = payload.get('hostname') - # FIXME: Don't re-calculate the hostname, fetch it from somewhere - project = 'foo' - if CONF.project_subdomain: - hostname = '%s.%s.%s' % (hostname, project, CONF.domain) - else: - hostname = '%s.%s' % (hostname, CONF.domain) - + hostname = self._generate_hostname(payload.get('hostname')) + id = payload.get('instance_id') + LOG.info("Delete host %s (%s)", id, hostname) self.ipaclient.delete_host(hostname, {}) elif event_type == 'network.floating_ip.associate': floating_ip = payload.get('floating_ip') @@ -86,26 +105,41 @@ class NotificationEndpoint(object): else: LOG.error("Could not resolve %s into a hostname", payload.get('instance_id')) + elif event_type == 'floatingip.update.end': # Neutron + floatingip = payload.get('floatingip') + floating_ip = floatingip.get('floating_ip_address') + port_id = floatingip.get('port_id') + LOG.info("Neutron floating IP associate: %s" % floating_ip) + nova = novaclient() + neutron = neutronclient() + search_opts = {'id': port_id} + ports = neutron.list_ports(**search_opts).get('ports') + if len(ports) == 1: + device_id = ports[0].get('device_id') + if device_id: + server = nova.servers.get(device_id) + if server: + self.ipaclient.add_ip(server.name, floating_ip) + else: + LOG.error("Expected 1 port, got %d", len(ports)) else: LOG.error("Status update or unknown") def main(): - + register_keystoneauth_opts(CONF) CONF(sys.argv[1:], project='join', version='1.0.0') logging.setup(CONF, 'join') transport = oslo_messaging.get_transport(CONF) targets = [oslo_messaging.Target(topic='notifications')] endpoints = [NotificationEndpoint()] - pool = 'listener-novajoin' server = oslo_messaging.get_notification_listener(transport, targets, endpoints, executor='threading', - allow_requeue=True, - pool=pool) + allow_requeue=True) LOG.info("Starting") server.start() try: diff --git a/novajoin/wsgi.py b/novajoin/wsgi.py index 4d18d5c..2c2e261 100644 --- a/novajoin/wsgi.py +++ b/novajoin/wsgi.py @@ -18,6 +18,7 @@ from oslo_service import service from oslo_service import wsgi from oslo_log import log from novajoin import config +from novajoin import keystone_client from novajoin import exception @@ -103,6 +104,7 @@ def process_launcher(): def main(): + keystone_client.register_keystoneauth_opts(CONF) CONF(sys.argv[1:], project='join', version='1.0.0') log.setup(CONF, 'join') launcher = process_launcher() diff --git a/scripts/novajoin-install b/scripts/novajoin-install index a05efdf..3d1787b 100755 --- a/scripts/novajoin-install +++ b/scripts/novajoin-install @@ -145,7 +145,8 @@ def install(args): confopts = {'FQDN': args['hostname'], 'MASTER': api.env.server, # pylint: disable=no-member 'DOMAIN': api.env.domain, # pylint: disable=no-member - 'KEYSTONE_AUTH': args['keystone_auth'], + 'KEYSTONE_AUTH_URI': args['keystone_auth_uri'], + 'KEYSTONE_AUTH_URL': args['keystone_auth_url'], 'KEYSTONE_IDENTITY': args['keystone_identity'], 'NOVA_PASSWORD': args['nova_password'], } @@ -215,8 +216,10 @@ def parse_args(): parser.add_argument('--password-file', dest='passwordfile', help='path to file containing password for ' 'the principal') - parser.add_argument('--keystone-auth', dest='keystone_auth', + parser.add_argument('--keystone-auth-uri', dest='keystone_auth_uri', help='Keystone auth URI') + parser.add_argument('--keystone-auth-url', dest='keystone_auth_url', + help='Keystone auth URL') parser.add_argument('--keystone-identity', dest='keystone_identity', help='Keystone identity URI') parser.add_argument('--nova-password', dest='nova_password', @@ -251,8 +254,12 @@ def parse_args(): raise ConfigurationError('Hostname: %s is not a FQDN' % args['hostname']) - if not args['keystone_auth']: - args['keystone_auth'] = user_input("Keysone auth URI", "", + if not args['keystone_auth_uri']: + args['keystone_auth_uri'] = user_input("Keysone auth URI", "", + allow_empty=False) + + if not args['keystone_auth_url']: + args['keystone_auth_url'] = user_input("Keysone auth URL", "", allow_empty=False) if not args['keystone_identity']: