powervc-driver/common-powervc/powervc/common/client/service.py

474 lines
19 KiB
Python

# Copyright 2013, 2014 IBM Corp.
import logging
import re
import urlparse
import powervc.common.client.delegate as delegate
from oslo.utils import importutils
from powervc.common.constants import SERVICE_TYPES as SERVICE_TYPES
from powervc.common import netutils
from powervc.common.client.extensions.glance import Extended_V2_Client
LOG = logging.getLogger(__name__)
class AbstractService(object):
"""a stub over a service endpoint which permits consumers
to create openstack python clients directly from this object.
"""
def __init__(self, svc_type, version, url, base_args, keystone):
self.svc_type = svc_type
self.version = version
self.url = url
self.base_args = base_args.copy()
self.keystone = keystone
self.base_name = SERVICE_TYPES[svc_type].to_codename()
self.client_version = version = version.replace('.', '_')
self.clazz = self._lookup_client()
self.extension = self._lookup_extension()
self.management_url = url
def _extend(self, client, client_extension=None, *extension_args):
if self.extension is None and client_extension is None:
return client
delegates = []
if client_extension is not None:
delegates.append(client_extension(client, *extension_args))
if self.extension is not None:
delegates.append(self.extension(client, *extension_args))
delegates.append(client)
# extend the base client using a mixin type delegate
return delegate.new_composite_deletgate(delegates)
def _patch(self, client):
try:
# if applicable patch the client
module = (importutils.
import_module("powervc.common.client.patch.%s" %
(self.base_name)))
module.patch_client(self, client)
except ImportError:
pass
return client
def _lookup_client(self):
return importutils.import_class("%sclient.%s.client.Client" %
(self.base_name,
self.get_client_version()))
def _lookup_extension(self):
try:
return (importutils.
import_class("powervc.common.client.extensions.%s.Client" %
(self.base_name)))
except ImportError:
return None
return None
def _chomp_version(self, version):
match = re.search('(v[0-9])[_]*[0-9]*', version, re.IGNORECASE)
if match:
version = match.group(1)
return version
def _init_std_client(self):
region_name = self.base_args.get('region_name', None)
return self._patch(self.clazz(self.base_args['username'],
self.base_args['password'],
self.base_args['tenant_name'],
self.base_args['auth_url'],
self.base_args['insecure'],
region_name=region_name,
cacert=self.base_args['cacert']))
def new_client(self, client_extension=None, *extension_args):
"""build and return a new python client for this service
:param client_extension: the optional subclass of
powervc.common.client.extensions.base to extend the python client with.
:param extension_args: optional arguments to pass to the client
extension when it is created.
"""
return self._extend(self._init_std_client(), client_extension,
*extension_args)
def get_client_version(self):
"""returns the version of the client for this service
"""
return self.client_version
class KeystoneService(AbstractService):
"""wrappers keystone service endpoint
"""
def __init__(self, *kargs):
super(KeystoneService, self).__init__(*kargs)
def new_client(self, client_extension=None, *extension_args):
return self._extend(self.clazz(**self.base_args), client_extension,
*extension_args)
def get_client_version(self):
if self.client_version == 'v3_0':
return 'v3'
return self.client_version
class CinderService(AbstractService):
"""wrappers cinder service endpoint
"""
def __init__(self, *kargs):
super(CinderService, self).__init__(*kargs)
def get_client_version(self):
return self._chomp_version(self.client_version)
class NovaService(AbstractService):
"""wrappers nova service endpoint
"""
def __init__(self, *kargs):
super(NovaService, self).__init__(*kargs)
def get_client_version(self):
if re.search('v2', self.client_version) is not None:
return 'v1_1'
return self.client_version
class GlanceService(AbstractService):
"""wrappers glance service endpoint
"""
def __init__(self, *kargs):
super(GlanceService, self).__init__(*kargs)
def new_client(self, client_extension=None, *extension_args):
url = self.url
if not url.endswith('/'):
url += '/'
if 'v2' in self.version:
client_info = {}
client_info['endpoint'] = self.url
client_info['cacert'] = self.base_args['cacert']
client_info['insecure'] = self.base_args['insecure']
client_info['token'] = self.keystone.auth_token
extened_client = Extended_V2_Client(client_info)
return (self._extend(self._patch(extened_client),
client_extension,
*extension_args))
return (self.
_extend(self.
_patch(self.clazz(url, token=self.keystone.auth_token,
insecure=self.base_args['insecure'],
cacert=self.base_args['cacert'])),
client_extension, *extension_args))
def get_client_version(self):
return self._chomp_version(self.client_version)
class NeutronService(AbstractService):
"""wrappers neutron service endpoint
"""
def __init__(self, *kargs):
super(NeutronService, self).__init__(*kargs)
def new_client(self, client_extension=None, *extension_args):
region_name = self.base_args.get('region_name', None)
return self._extend(self._patch(self.clazz(
username=self.base_args['username'],
tenant_name=self.base_args['tenant_name'],
password=self.base_args['password'],
auth_url=self.base_args['auth_url'],
endpoint_url=self.management_url,
insecure=self.base_args['insecure'],
region_name=region_name,
token=self.keystone.auth_token,
ca_cert=self.base_args['cacert'])),
client_extension, *extension_args)
def get_client_version(self):
if self.client_version.startswith('v1'):
return 'v2_0'
return self.client_version
class ClientServiceCatalog(object):
"""provides a simple catalog of openstack services
for a single host and permits consumers to query
those services based on service types, versions
as well as create new python clients from the service
directly.
"""
def __init__(self, base_client_opts, keystone):
self.base_opts = base_client_opts
self.keystone = keystone
# validate authN
self.token = self.keystone.auth_token
self.host = urlparse.urlsplit(self.base_opts['auth_url']).hostname
self.endpoints = {}
self.blacklist = [str(SERVICE_TYPES.s3), str(SERVICE_TYPES.ec2),
str(SERVICE_TYPES.ttv)]
self._discover_services()
def new_client(self, svc_type, client_extension=None, *extension_args):
"""creates a new python client for the given service type
using the most recent version of the service in the catalog.
:param svc_type: the service type to create a client for
:param client_extension: the optional extension to decorate
the base client with
:param extension_args: optional arguments to pass to the client
extension when it is created.
"""
service_versions = self.get_services(svc_type)
if service_versions:
return service_versions[0].new_client(client_extension,
*extension_args)
return None
def get_services(self, svc_type, version_filter=None):
"""queries this catalogs services based on service type
and version filter.
:param svc_type: the type of service to query.
:param version_filter: a filter string to indicate the
service version the caller wants. if None the highest
version of the service is returned.
"""
if svc_type not in self.endpoints:
return None
def _find_version(versions, version_filter):
for version in versions.keys():
if version.find(version_filter) > -1:
return versions[version]
versions = self.endpoints[svc_type]
# Here we need test version_filter is None or empty, use 'if not'.
if not version_filter:
return versions[max(versions, key=str)]
version = _find_version(versions, version_filter)
if version is not None:
return version
# >> fix bug/1358215 - timing issue between openstack service endpoints
# becoming active and powervc-driver's client initialization of those.
# Check https://review.openstack.org/#/c/115519/ for the details.
# TODO(design): re-consider for #2 in the commit message
else:
# A lock is not necessary here. Only glance sync service use
# specified version apis and might run into this and starup_sync
# won't pass until the specified versions are ready. So there
# shouldn't be concurrent accesses to self.endpoints[svc_type] with
# svc_type='image'.
LOG.info(_("rediscover service for type:%s") % svc_type)
self._rediscover_service(svc_type)
versions = self.endpoints[svc_type]
return _find_version(versions, version_filter)
# << fix bug/1358215
def get_versions(self, svc_type):
"""return a list of the versions for the given service type
:param svc_type: the type of service to query
"""
if svc_type not in self.endpoints:
return None
return self.endpoints[svc_type].keys()
def get_version(self, svc_type, version_filter=None):
"""query a service to determine if a given version exists.
:param svc_type: the service type to query.
:param version_filter: a string to search for in the version.
if None the most recent version of the service type is returned.
"""
if svc_type not in self.endpoints:
return None
for version in self.endpoints[svc_type].keys():
if not version_filter or version.find(version_filter) > -1:
return version
return None
def get_service_types(self):
"""returns a list of all service types in this catalog.
"""
return self.endpoints.keys()
def get_token(self):
"""returns a keystone token for the host this catalog
belongs to.
"""
return self.keystone.auth_token
def get_client(self, svc_type, version_filter=None, client_extension=None,
*extension_args):
"""creates a new python cient for the given service type
and version.
:param svc_type: the service type to create a client for.
:param version_filter: a string to search for in the version
the caller wants. if None the most recent version is used.
:param client_extension: the optional class to extend
the client with
"""
services = self.get_services(svc_type, version_filter)
if not services:
return None
return services[0].new_client(client_extension, *extension_args)
def _parse_link_href(self, links):
hrefs = []
for link_meta in links:
if link_meta['rel'] == 'self':
href = self._filter_host(link_meta['href'])
hrefs.append(href)
return hrefs
def _filter_host(self, loc):
# endpoint urls from base api query will often
# return localhost in the url; resolve those
return loc.replace('localhost',
self.host).replace('127.0.0.1',
self.host).replace('0.0.0.0',
self.host)
def _parse_version_meta(self, ver, ver_map={}):
ver_map[ver['id']] = self._parse_link_href(ver['links'])
return ver_map
def _parse_version(self, response_json, url):
if response_json is not None:
if 'version' in response_json:
return {response_json['version']['id']:
[self._filter_host(url)]}
elif 'versions' in response_json:
services = {}
versions = response_json['versions']
if 'values' in versions:
versions = versions['values']
for version_meta in versions:
if 'status' in version_meta and \
version_meta['status'] == 'CURRENT':
ver = version_meta['id']
if ver not in services:
services[ver] = []
services[ver].append(self._filter_host(url))
return services
return None
def _parse_version_from_url(self, url):
for seg in reversed(url.split('/')):
match = re.search('^(v[0-9][.]?[0-9]?$)', seg, re.IGNORECASE)
if match:
return match.group(0)
return None
def _build_wrappered_services(self, version_map, svc_type):
services = {}
for version in version_map.keys():
wrappers = []
for s_url in version_map[version]:
if svc_type == (str(SERVICE_TYPES.compute) or
svc_type == str(SERVICE_TYPES.computev3)):
wrappers.append(NovaService(svc_type, version,
s_url, self.base_opts,
self.keystone))
elif svc_type == str(SERVICE_TYPES.image):
wrappers.append(GlanceService(svc_type, version,
s_url, self.base_opts,
self.keystone))
elif svc_type == str(SERVICE_TYPES.identity):
# keystone is a special case as the auth url given
# in the base opts may not match the auth url from
# the catalog
keystone_opts = self.base_opts.copy()
keystone_opts['auth_url'] = s_url
wrappers.append(KeystoneService(svc_type, version,
s_url, keystone_opts,
self.keystone))
elif svc_type == str(SERVICE_TYPES.volume):
wrappers.append(CinderService(svc_type, version,
s_url, self.base_opts,
self.keystone))
elif svc_type == str(SERVICE_TYPES.network):
wrappers.append(NeutronService(svc_type, version,
s_url, self.base_opts,
self.keystone))
services[version] = wrappers
return services
def _query_endpoint(self, url):
# query the endpoint to get version info
client = netutils.JSONRESTClient(self.get_token())
urldata = urlparse.urlsplit(url)
host = urldata.scheme + '://' + urldata.netloc
segments = filter(lambda x: x != '', urldata.path.split('/'))
if not segments:
segments = ['']
# chomp uri until we find base of endpoint
for segment in segments[:] or ['']:
endpoint_url = "%s/%s/" % (host, '/'.join(segments))
segments.pop()
response = None
try:
response = client.get(endpoint_url)
except:
continue
versions = self._parse_version(response, url)
if versions is not None:
return versions
return {'v1': [url]}
def _build_endpoint_services(self, url, svc_type):
# try to parse from the url
ver = self._parse_version_from_url(url)
if ver is not None:
return self._build_wrappered_services({ver: [url]}, svc_type)
versions = self._query_endpoint(url)
# From the latest ICM, versions are stripped from image service,
# only ip:port left, in this case only "CURRENT" status client can be
# retrieved, that is to say, for image service only v2.3 url returned
# but v1.1 is lost, this will fail glance sync progress as both of
# v2.3 and v1.1 are necessary, add this workaround to fix the problem.
if svc_type == str(SERVICE_TYPES.image):
versions['v1'] = [url]
return self._build_wrappered_services(versions, svc_type)
def _normalize_catalog_entry(self, entry):
for key in entry.keys():
if re.search('url', key, re.IGNORECASE):
entry[key] = self._filter_host(entry[key])
if self.keystone.version == 'v2.0':
# keystone v2.0 entries differ from v3; normalize
entry['url'] = entry['publicURL']
return entry
def _discover_services(self):
public_eps = (self.keystone.
service_catalog.get_endpoints(endpoint_type='publicURL'))
self.endpoints = {}
for svc_type in public_eps.keys():
if svc_type in self.blacklist:
continue
for entry in public_eps[svc_type]:
entry = self._normalize_catalog_entry(entry)
self.endpoints[svc_type] = \
self._build_endpoint_services(entry['url'], svc_type)
# >> fix bug/1358215, timing issue between openstack service endpoints
# becoming active and powervc-driver's client initialization of those.
def _rediscover_service(self, svc_type):
public_eps = (self.keystone.
service_catalog.get_endpoints(endpoint_type='publicURL'))
for entry in public_eps[svc_type]:
entry = self._normalize_catalog_entry(entry)
self.endpoints[svc_type] = \
self._build_endpoint_services(entry['url'], svc_type)
# << fix bug/1358215