yatin
5fa5f9c51d
With keystone v3 to get a public endpoint for a service, we need to check for interface="public". Change-Id: I36de9adf04352073c9727bd3cc88379fbc353aa8
273 lines
10 KiB
Python
273 lines
10 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2013 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.
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
import requests
|
|
import urllib3
|
|
import urlparse
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
MULTIPLE_SLASH = re.compile(r'/+')
|
|
|
|
|
|
class ServiceError(Exception):
|
|
pass
|
|
|
|
|
|
class Service(object):
|
|
def __init__(self, name, service_url, token, disable_ssl_validation):
|
|
self.name = name
|
|
self.service_url = service_url
|
|
self.headers = {'Accept': 'application/json', 'X-Auth-Token': token}
|
|
self.disable_ssl_validation = disable_ssl_validation
|
|
|
|
def do_get(self, url, top_level=False, top_level_path=""):
|
|
parts = list(urlparse.urlparse(url))
|
|
# 2 is the path offset
|
|
if top_level:
|
|
parts[2] = '/' + top_level_path
|
|
|
|
parts[2] = MULTIPLE_SLASH.sub('/', parts[2])
|
|
url = urlparse.urlunparse(parts)
|
|
|
|
try:
|
|
if self.disable_ssl_validation:
|
|
urllib3.disable_warnings()
|
|
http = urllib3.PoolManager(cert_reqs='CERT_NONE')
|
|
else:
|
|
http = urllib3.PoolManager()
|
|
r = http.request('GET', url, headers=self.headers)
|
|
except Exception as e:
|
|
LOG.error("Request on service '%s' with url '%s' failed",
|
|
(self.name, url))
|
|
raise e
|
|
if r.status >= 400:
|
|
raise ServiceError("Request on service '%s' with url '%s' failed"
|
|
" with code %d" % (self.name, url, r.status))
|
|
return r.data
|
|
|
|
def get_extensions(self):
|
|
return []
|
|
|
|
def get_versions(self):
|
|
return []
|
|
|
|
|
|
class VersionedService(Service):
|
|
def get_versions(self, top_level=True):
|
|
body = self.do_get(self.service_url, top_level=top_level)
|
|
body = json.loads(body)
|
|
return self.deserialize_versions(body)
|
|
|
|
def deserialize_versions(self, body):
|
|
return map(lambda x: x['id'], body['versions'])
|
|
|
|
def no_port_cut_url(self):
|
|
# if there is no port defined, cut the url from version to the end
|
|
u = urllib3.util.parse_url(self.service_url)
|
|
url = self.service_url
|
|
if u.port is None:
|
|
found = re.findall(r'v\d', url)
|
|
if len(found) > 0:
|
|
index = url.index(found[0])
|
|
url = self.service_url[:index]
|
|
return (url, u.port is not None)
|
|
|
|
|
|
class ComputeService(VersionedService):
|
|
def get_extensions(self):
|
|
body = self.do_get(self.service_url + '/extensions')
|
|
body = json.loads(body)
|
|
return map(lambda x: x['alias'], body['extensions'])
|
|
|
|
def get_versions(self):
|
|
url, top_level = self.no_port_cut_url()
|
|
body = self.do_get(url, top_level=top_level)
|
|
body = json.loads(body)
|
|
return self.deserialize_versions(body)
|
|
|
|
|
|
class ImageService(VersionedService):
|
|
def get_versions(self):
|
|
return super(ImageService, self).get_versions(top_level=False)
|
|
|
|
|
|
class NetworkService(VersionedService):
|
|
def get_extensions(self):
|
|
body = self.do_get(self.service_url + '/v2.0/extensions.json')
|
|
body = json.loads(body)
|
|
return map(lambda x: x['alias'], body['extensions'])
|
|
|
|
|
|
class VolumeService(VersionedService):
|
|
def get_extensions(self):
|
|
body = self.do_get(self.service_url + '/extensions')
|
|
body = json.loads(body)
|
|
return map(lambda x: x['alias'], body['extensions'])
|
|
|
|
def get_versions(self):
|
|
url, top_level = self.no_port_cut_url()
|
|
body = self.do_get(url, top_level=top_level)
|
|
body = json.loads(body)
|
|
return self.deserialize_versions(body)
|
|
|
|
|
|
class IdentityService(VersionedService):
|
|
def __init__(self, name, service_url, token, disable_ssl_validation):
|
|
super(VersionedService, self).__init__(
|
|
name, service_url, token, disable_ssl_validation)
|
|
version = ''
|
|
if 'v2' in self.service_url:
|
|
version = '/v2.0'
|
|
url_parse = urlparse.urlparse(self.service_url)
|
|
self.service_url = '{}://{}{}'.format(url_parse.scheme,
|
|
url_parse.netloc, version)
|
|
|
|
def get_extensions(self):
|
|
if 'v2' in self.service_url:
|
|
body = self.do_get(self.service_url + '/extensions')
|
|
body = json.loads(body)
|
|
return map(lambda x: x['alias'], body['extensions']['values'])
|
|
# Keystone api changed in v3, the concept of extensions change. Right
|
|
# now, all the existin extensions are part of keystone core api, so,
|
|
# there's no longer the /extensions endpoint. The extensions that are
|
|
# stable, are enabled by default, the ones marked as experimental are
|
|
# disabled by default. Checking the tempest source, there's no test
|
|
# pointing to extensions endpoint, so I am very confident that this
|
|
# will not be an issue. If so, we need to list all the /OS-XYZ
|
|
# extensions to identify what is enabled or not. This would be a manual
|
|
# check every time keystone change, add or delete an extension, so I
|
|
# rather prefer to return empty here for now.
|
|
return []
|
|
|
|
def deserialize_versions(self, body):
|
|
if 'v2' in self.service_url:
|
|
return map(lambda x: x['id'], body['versions']['values'])
|
|
else:
|
|
return []
|
|
|
|
def get_versions(self):
|
|
return super(IdentityService, self).get_versions(top_level=False)
|
|
|
|
|
|
class ObjectStorageService(Service):
|
|
def get_extensions(self):
|
|
body = self.do_get(self.service_url, top_level=True,
|
|
top_level_path="info")
|
|
body = json.loads(body)
|
|
# Remove Swift general information from extensions list
|
|
body.pop('swift')
|
|
return body.keys()
|
|
|
|
|
|
service_dict = {'compute': ComputeService,
|
|
'image': ImageService,
|
|
'network': NetworkService,
|
|
'object-store': ObjectStorageService,
|
|
'volumev3': VolumeService,
|
|
'identity': IdentityService}
|
|
|
|
|
|
def get_service_class(service_name):
|
|
return service_dict.get(service_name, Service)
|
|
|
|
|
|
def get_identity_v3_extensions(keystone_v3_url):
|
|
"""Returns discovered identity v3 extensions
|
|
|
|
As keystone V3 uses a JSON Home to store the extensions,
|
|
this method is kept here just for the sake of functionality, but it
|
|
implements a different discovery method.
|
|
|
|
:param keystone_v3_url: Keystone V3 auth url
|
|
:return: A list with the discovered extensions
|
|
"""
|
|
try:
|
|
r = requests.get(keystone_v3_url,
|
|
verify=False,
|
|
headers={'Accept': 'application/json-home'})
|
|
except requests.exceptions.RequestException as re:
|
|
LOG.error("Request on service '%s' with url '%s' failed",
|
|
'identity', keystone_v3_url)
|
|
raise re
|
|
ext_h = 'http://docs.openstack.org/api/openstack-identity/3/ext/'
|
|
res = [x for x in json.loads(r.content)['resources'].keys()]
|
|
ext = [ex for ex in res if 'ext' in ex]
|
|
return list(set([str(e).replace(ext_h, '').split('/')[0] for e in ext]))
|
|
|
|
|
|
def discover(auth_provider, region, object_store_discovery=True,
|
|
api_version=2, disable_ssl_certificate_validation=True):
|
|
"""Returns a dict with discovered apis.
|
|
|
|
:param auth_provider: An AuthProvider to obtain service urls.
|
|
:param region: A specific region to use. If the catalog has only one region
|
|
then that region will be used.
|
|
:return: A dict with an entry for the type of each discovered service.
|
|
Each entry has keys for 'extensions' and 'versions'.
|
|
"""
|
|
token, auth_data = auth_provider.get_auth()
|
|
services = {}
|
|
service_catalog = 'serviceCatalog'
|
|
public_url = 'publicURL'
|
|
identity_port = urlparse.urlparse(auth_provider.auth_url).port
|
|
if identity_port is None:
|
|
identity_port = ""
|
|
else:
|
|
identity_port = ":" + str(identity_port)
|
|
identity_version = urlparse.urlparse(auth_provider.auth_url).path
|
|
if api_version == 3:
|
|
service_catalog = 'catalog'
|
|
public_url = 'url'
|
|
|
|
# FIXME(chandankumar): It is a workaround to filter services whose
|
|
# endpoints does not exist. Once it is merged. Let's rewrite the whole
|
|
# stuff.
|
|
auth_data[service_catalog] = [data for data in auth_data[service_catalog]
|
|
if data['endpoints']]
|
|
|
|
for entry in auth_data[service_catalog]:
|
|
name = entry['type']
|
|
services[name] = dict()
|
|
for _ep in entry['endpoints']:
|
|
if api_version == 3:
|
|
if _ep['region'] == region and _ep['interface'] == 'public':
|
|
ep = _ep
|
|
break
|
|
else:
|
|
if _ep['region'] == region:
|
|
ep = _ep
|
|
break
|
|
else:
|
|
ep = entry['endpoints'][0]
|
|
if 'identity' in ep[public_url]:
|
|
services[name]['url'] = ep[public_url].replace(
|
|
"/identity", "{0}{1}".format(
|
|
identity_port, identity_version))
|
|
else:
|
|
services[name]['url'] = ep[public_url]
|
|
service_class = get_service_class(name)
|
|
service = service_class(name, services[name]['url'], token,
|
|
disable_ssl_certificate_validation)
|
|
if name == 'object-store' and not object_store_discovery:
|
|
services[name]['extensions'] = []
|
|
elif 'v3' not in ep[public_url]: # is not v3 url
|
|
services[name]['extensions'] = service.get_extensions()
|
|
services[name]['versions'] = service.get_versions()
|
|
return services
|