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.
This commit is contained in:
Rob Crittenden 2016-08-25 17:06:43 -04:00
parent ded8b5e2f7
commit 60a8e67a8c
6 changed files with 148 additions and 36 deletions

View File

@ -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

View File

@ -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."""

View File

@ -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")]
})

View File

@ -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:

View File

@ -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()

View File

@ -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']: