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:
parent
ded8b5e2f7
commit
60a8e67a8c
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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")]
|
||||
})
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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']:
|
||||
|
|
Loading…
Reference in New Issue