
Implement site create RPC. Now tricircle api can utilize RPC to notify cascade service to start rpc server for new site. Partially implements: blueprint implement-api Ref: https://blueprints.launchpad.net/tricircle/+spec/implement-api Change-Id: I73879a84d31b5ac9004cfe3f18cb9a984d53099c
380 lines
16 KiB
Python
380 lines
16 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 collections
|
|
import functools
|
|
import inspect
|
|
import six
|
|
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__)
|
|
|
|
|
|
def _safe_operation(operation_name):
|
|
def handle_func(func):
|
|
@six.wraps(func)
|
|
def handle_args(*args, **kwargs):
|
|
instance, resource, context = args[:3]
|
|
if resource not in instance.operation_resources_map[
|
|
operation_name]:
|
|
raise exception.ResourceNotSupported(resource, operation_name)
|
|
retries = 1
|
|
for _ in xrange(retries + 1):
|
|
try:
|
|
service = instance.resource_service_map[resource]
|
|
instance._ensure_endpoint_set(context, service)
|
|
return func(*args, **kwargs)
|
|
except exception.EndpointNotAvailable as e:
|
|
if cfg.CONF.client.auto_refresh_endpoint:
|
|
LOG.warn(e.message + ', update endpoint and try again')
|
|
instance._update_endpoint_from_keystone(context, True)
|
|
else:
|
|
raise
|
|
return handle_args
|
|
return handle_func
|
|
|
|
|
|
class Client(object):
|
|
def __init__(self):
|
|
self.auth_url = cfg.CONF.client.auth_url
|
|
self.resource_service_map = {}
|
|
self.operation_resources_map = collections.defaultdict(set)
|
|
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
|
|
for operation, index in six.iteritems(
|
|
resource_handle.operation_index_map):
|
|
# add parentheses to emphasize we mean to do bitwise and
|
|
if (handle_obj.support_resource[resource] & index) == 0:
|
|
continue
|
|
self.operation_resources_map[operation].add(resource)
|
|
setattr(self, '%s_%ss' % (operation, resource),
|
|
functools.partial(
|
|
getattr(self, '%s_resources' % operation),
|
|
resource))
|
|
|
|
def _get_keystone_session(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)
|
|
return session.Session(auth=auth)
|
|
|
|
def _get_admin_token(self):
|
|
return self._get_keystone_session().get_token()
|
|
|
|
def _get_admin_project_id(self):
|
|
return self._get_keystone_session().get_project_id()
|
|
|
|
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_configurations(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_configurations(
|
|
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_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)
|
|
|
|
@_safe_operation('list')
|
|
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 cxt.is_admin and not cxt.auth_token:
|
|
cxt.auth_token = self._get_admin_token()
|
|
cxt.tenant = self._get_admin_project_id()
|
|
|
|
service = self.resource_service_map[resource]
|
|
handle = self.service_handle_map[service]
|
|
filters = filters or []
|
|
return handle.handle_list(cxt, resource, filters)
|
|
|
|
@_safe_operation('create')
|
|
def create_resources(self, resource, cxt, *args, **kwargs):
|
|
"""Create resource in site of top layer
|
|
|
|
Directly invoke this method to create resources, or use
|
|
create_(resource)s (self, cxt, *args, **kwargs). These methods are
|
|
automatically generated according to the supported resources of each
|
|
ResourceHandle class.
|
|
|
|
:param resource: resource type
|
|
:param cxt: context object
|
|
:param args, kwargs: passed according to resource type
|
|
--------------------------
|
|
resource -> args -> kwargs
|
|
--------------------------
|
|
aggregate -> name, availability_zone_name -> none
|
|
--------------------------
|
|
:return: a dict containing resource information
|
|
:raises: EndpointNotAvailable
|
|
"""
|
|
if cxt.is_admin and not cxt.auth_token:
|
|
cxt.auth_token = self._get_admin_token()
|
|
cxt.tenant = self._get_admin_project_id()
|
|
|
|
service = self.resource_service_map[resource]
|
|
handle = self.service_handle_map[service]
|
|
return handle.handle_create(cxt, resource, *args, **kwargs)
|
|
|
|
@_safe_operation('delete')
|
|
def delete_resources(self, resource, cxt, resource_id):
|
|
"""Delete resource in site of top layer
|
|
|
|
Directly invoke this method to delete resources, or use
|
|
delete_(resource)s (self, cxt, obj_id). These methods are
|
|
automatically generated according to the supported resources
|
|
of each ResourceHandle class.
|
|
:param resource: resource type
|
|
:param cxt: context object
|
|
:param resource_id: id of resource
|
|
:return: None
|
|
:raises: EndpointNotAvailable
|
|
"""
|
|
if cxt.is_admin and not cxt.auth_token:
|
|
cxt.auth_token = self._get_admin_token()
|
|
cxt.tenant = self._get_admin_project_id()
|
|
|
|
service = self.resource_service_map[resource]
|
|
handle = self.service_handle_map[service]
|
|
handle.handle_delete(cxt, resource, resource_id)
|
|
|
|
@_safe_operation('action')
|
|
def action_resources(self, resource, cxt, action, *args, **kwargs):
|
|
"""Apply action on resource in site of top layer
|
|
|
|
Directly invoke this method to apply action, or use
|
|
action_(resource)s (self, cxt, action, *args, **kwargs). These methods
|
|
are automatically generated according to the supported resources of
|
|
each ResourceHandle class.
|
|
|
|
:param resource: resource type
|
|
:param cxt: context object
|
|
:param action: action applied on resource
|
|
:param args, kwargs: passed according to resource type
|
|
--------------------------
|
|
resource -> action -> args -> kwargs
|
|
--------------------------
|
|
aggregate -> add_host -> aggregate, host -> none
|
|
--------------------------
|
|
:return: None
|
|
:raises: EndpointNotAvailable
|
|
"""
|
|
if cxt.is_admin and not cxt.auth_token:
|
|
cxt.auth_token = self._get_admin_token()
|
|
cxt.tenant = self._get_admin_project_id()
|
|
|
|
service = self.resource_service_map[resource]
|
|
handle = self.service_handle_map[service]
|
|
return handle.handle_action(cxt, resource, action, *args, **kwargs)
|