tricircle/tricircle/db/client.py

269 lines
12 KiB
Python

# Copyright 2015 Huawei Technologies Co., Ltd.
# All Rights Reserved
#
# 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 functools
import inspect
import uuid
from keystoneclient.auth.identity import v3 as auth_identity
from keystoneclient.auth import token_endpoint
from keystoneclient import session
from keystoneclient.v3 import client as keystone_client
from oslo_config import cfg
from oslo_log import log as logging
import tricircle.context as tricircle_context
from tricircle.db import exception
from tricircle.db import models
from tricircle.db import resource_handle
client_opts = [
cfg.StrOpt('auth_url',
default='http://127.0.0.1:5000/v3',
help='keystone authorization url'),
cfg.StrOpt('identity_url',
default='http://127.0.0.1:35357/v3',
help='keystone service url'),
cfg.BoolOpt('auto_refresh_endpoint',
default=False,
help='if set to True, endpoint will be automatically'
'refreshed if timeout accessing endpoint'),
cfg.StrOpt('top_site_name',
help='name of top site which client needs to access'),
cfg.StrOpt('admin_username',
help='username of admin account, needed when'
' auto_refresh_endpoint set to True'),
cfg.StrOpt('admin_password',
help='password of admin account, needed when'
' auto_refresh_endpoint set to True'),
cfg.StrOpt('admin_tenant',
help='tenant name of admin account, needed when'
' auto_refresh_endpoint set to True'),
cfg.StrOpt('admin_user_domain_name',
default='Default',
help='user domain name of admin account, needed when'
' auto_refresh_endpoint set to True'),
cfg.StrOpt('admin_tenant_domain_name',
default='Default',
help='tenant domain name of admin account, needed when'
' auto_refresh_endpoint set to True')
]
client_opt_group = cfg.OptGroup('client')
cfg.CONF.register_group(client_opt_group)
cfg.CONF.register_opts(client_opts, group=client_opt_group)
LOG = logging.getLogger(__name__)
class Client(object):
def __init__(self):
self.auth_url = cfg.CONF.client.auth_url
self.resource_service_map = {}
self.service_handle_map = {}
for _, handle_class in inspect.getmembers(resource_handle):
if not inspect.isclass(handle_class):
continue
if not hasattr(handle_class, 'service_type'):
continue
handle_obj = handle_class(self.auth_url)
self.service_handle_map[handle_obj.service_type] = handle_obj
for resource in handle_obj.support_resource:
self.resource_service_map[resource] = handle_obj.service_type
setattr(self, 'list_%ss' % resource,
functools.partial(self.list_resources, resource))
def _get_admin_token(self):
auth = auth_identity.Password(
auth_url=cfg.CONF.client.identity_url,
username=cfg.CONF.client.admin_username,
password=cfg.CONF.client.admin_password,
project_name=cfg.CONF.client.admin_tenant,
user_domain_name=cfg.CONF.client.admin_user_domain_name,
project_domain_name=cfg.CONF.client.admin_tenant_domain_name)
sess = session.Session(auth=auth)
return sess.get_token()
def _get_endpoint_from_keystone(self, cxt):
auth = token_endpoint.Token(cfg.CONF.client.identity_url,
cxt.auth_token)
sess = session.Session(auth=auth)
cli = keystone_client.Client(session=sess)
service_id_name_map = {}
for service in cli.services.list():
service_dict = service.to_dict()
service_id_name_map[service_dict['id']] = service_dict['name']
region_service_endpoint_map = {}
for endpoint in cli.endpoints.list():
endpoint_dict = endpoint.to_dict()
if endpoint_dict['interface'] != 'public':
continue
region_id = endpoint_dict['region']
service_id = endpoint_dict['service_id']
url = endpoint_dict['url']
service_name = service_id_name_map[service_id]
if region_id not in region_service_endpoint_map:
region_service_endpoint_map[region_id] = {}
region_service_endpoint_map[region_id][service_name] = url
return region_service_endpoint_map
def _get_config_with_retry(self, cxt, filters, site, service, retry):
conf_list = models.list_site_service_configuration(cxt, filters)
if len(conf_list) > 1:
raise exception.EndpointNotUnique(site, service)
if len(conf_list) == 0:
if not retry:
raise exception.EndpointNotFound(site, service)
self._update_endpoint_from_keystone(cxt, True)
return self._get_config_with_retry(cxt,
filters, site, service, False)
return conf_list
def _ensure_endpoint_set(self, cxt, service):
handle = self.service_handle_map[service]
if not handle.is_endpoint_url_set():
site_filters = [{'key': 'site_name',
'comparator': 'eq',
'value': cfg.CONF.client.top_site_name}]
site_list = models.list_sites(cxt, site_filters)
if len(site_list) == 0:
raise exception.ResourceNotFound(models.Site,
cfg.CONF.client.top_site_name)
# site_name is unique key, safe to get the first element
site_id = site_list[0]['site_id']
config_filters = [
{'key': 'site_id', 'comparator': 'eq', 'value': site_id},
{'key': 'service_type', 'comparator': 'eq', 'value': service}]
conf_list = self._get_config_with_retry(
cxt, config_filters, site_id, service,
cfg.CONF.client.auto_refresh_endpoint)
url = conf_list[0]['service_url']
handle.update_endpoint_url(url)
def _update_endpoint_from_keystone(self, cxt, is_internal):
"""Update the database by querying service endpoint url from Keystone
:param cxt: context object
:param is_internal: if True, this method utilizes pre-configured admin
username and password to apply an new admin token, this happens only
when auto_refresh_endpoint is set to True. if False, token in cxt is
directly used, users should prepare admin token themselves
:return: None
"""
if is_internal:
admin_context = tricircle_context.Context()
admin_context.auth_token = self._get_admin_token()
endpoint_map = self._get_endpoint_from_keystone(admin_context)
else:
endpoint_map = self._get_endpoint_from_keystone(cxt)
for region in endpoint_map:
# use region name to query site
site_filters = [{'key': 'site_name', 'comparator': 'eq',
'value': region}]
site_list = models.list_sites(cxt, site_filters)
# skip region/site not registered in cascade service
if len(site_list) != 1:
continue
for service in endpoint_map[region]:
site_id = site_list[0]['site_id']
config_filters = [{'key': 'site_id', 'comparator': 'eq',
'value': site_id},
{'key': 'service_type', 'comparator': 'eq',
'value': service}]
config_list = models.list_site_service_configuration(
cxt, config_filters)
if len(config_list) > 1:
raise exception.EndpointNotUnique(site_id, service)
if len(config_list) == 1:
config_id = config_list[0]['service_id']
update_dict = {
'service_url': endpoint_map[region][service]}
models.update_site_service_configuration(
cxt, config_id, update_dict)
else:
config_dict = {
'service_id': str(uuid.uuid4()),
'site_id': site_id,
'service_name': '%s_%s' % (region, service),
'service_type': service,
'service_url': endpoint_map[region][service]
}
models.create_site_service_configuration(
cxt, config_dict)
def get_endpoint(self, cxt, site_id, service):
"""Get endpoint url of given site and service
:param cxt: context object
:param site_id: site id
:param service: service type
:return: endpoint url for given site and service
:raises: EndpointNotUnique, EndpointNotFound
"""
config_filters = [
{'key': 'site_id', 'comparator': 'eq', 'value': site_id},
{'key': 'service_type', 'comparator': 'eq', 'value': service}]
conf_list = self._get_config_with_retry(
cxt, config_filters, site_id, service,
cfg.CONF.client.auto_refresh_endpoint)
return conf_list[0]['service_url']
def update_endpoint_from_keystone(self, cxt):
"""Update the database by querying service endpoint url from Keystone
Only admin should invoke this method since it requires admin token
:param cxt: context object containing admin token
:return: None
"""
self._update_endpoint_from_keystone(cxt, False)
def list_resources(self, resource, cxt, filters=None):
"""Query resource in site of top layer
Directly invoke this method to query resources, or use
list_(resource)s (self, cxt, filters=None), for example,
list_servers (self, cxt, filters=None). These methods are
automatically generated according to the supported resources
of each ResourceHandle class.
:param resource: resource type
:param cxt: context object
:param filters: list of dict with key 'key', 'comparator', 'value'
like {'key': 'name', 'comparator': 'eq', 'value': 'private'}, 'key'
is the field name of resources
:return: list of dict containing resources information
:raises: EndpointNotAvailable
"""
if resource not in self.resource_service_map:
raise exception.ResourceNotSupported(resource, 'list')
service = self.resource_service_map[resource]
self._ensure_endpoint_set(cxt, service)
handle = self.service_handle_map[service]
filters = filters or []
try:
return handle.handle_list(cxt, resource, filters)
except exception.EndpointNotAvailable as e:
if cfg.CONF.client.auto_refresh_endpoint:
LOG.warn(e.message + ', update endpoint and try again')
self._update_endpoint_from_keystone(cxt, True)
self._ensure_endpoint_set(cxt, service)
return handle.handle_list(cxt, resource, filters)
else:
raise e