Add utils to work with OpenStack components

Add utils to do basic actions with nova and keystone.
We decide to store in utils/openstack/%service_name.py service
specific clients in classes named like Climate%service_name%Client.
In this classes we define service client with specific variables.
Also, we redefine __getattr__ like this:

   def __getattr__(self, name):
       fn = getattr(self.%service_name, name)
       return fn

Change-Id: I47e0d8689f1d8ca8aba0372eb5e7a4a80f2f9be5
This commit is contained in:
Nikolaj Starodubtsev 2013-10-17 10:49:29 +04:00 committed by Dina Belova
parent df1b79d636
commit 19cc43f79a
13 changed files with 564 additions and 6 deletions

View File

@ -13,22 +13,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from climate import context
from climate import exceptions
from climate import policy
def ctx_from_headers(headers):
try:
service_catalog = json.loads(headers['X-Service-Catalog'])
except KeyError:
raise exceptions.ServiceCatalogNotFound()
except TypeError:
raise exceptions.WrongFormat()
ctx = context.ClimateContext(
user_id=headers['X-User-Id'],
tenant_id=headers['X-Tenant-Id'],
auth_token=headers['X-Auth-Token'],
service_catalog=headers['X-Service-Catalog'],
service_catalog=service_catalog,
user_name=headers['X-User-Name'],
tenant_name=headers['X-Tenant-Name'],
roles=map(unicode.strip, headers['X-Roles'].split(',')),
)
target = {'tenant_id': ctx.tenant_id, 'user_id': ctx.user_id}
if policy.enforce(ctx, "admin", target, do_raise=False):
return ctx.elevated()
else:
return ctx
ctx.is_admin = True
return ctx

View File

@ -68,3 +68,11 @@ class PolicyNotAuthorized(NotAuthorized):
class ConfigNotFound(ClimateException):
msg_fmt = _("Could not find config at %(path)s")
class ServiceCatalogNotFound(NotFound):
msg_fmt = _("Could not find service catalog")
class WrongFormat(ClimateException):
msg_fmt = _("Unenxpectable object format")

View File

@ -71,3 +71,23 @@ class HostHavingServers(exceptions.ClimateException):
class CantAddExtraCapability(exceptions.ClimateException):
code = 409
msg_fmt = _("Can't add extracapabilities %(keys)s to Host %(host)s")
class EndpointsNotFound(exceptions.NotFound):
code = 404
msg_fmt = _("No endpoints for %(service)s")
class ServiceNotFound(exceptions.NotFound):
code = 404
msg_fmt = _("Service %(service)s not found")
class WrongClientVersion(exceptions.ClimateException):
code = 400
msg_fmt = _("Unfortunately you use wrong client version")
class NoManagementUrl(exceptions.NotFound):
code = 404
msg_fmt = _("You haven't management url for service")

View File

@ -13,8 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
from climate.api import context as api_context
from climate import context
from climate import exceptions
from climate import policy
from climate import tests
@ -26,24 +29,42 @@ class ContextTestCase(tests.TestCase):
self.fake_headers = {u'X-User-Id': u'1',
u'X-Tenant-Id': u'1',
u'X-Auth-Token': u'111-111-111',
u'X-Service-Catalog': u'catalog',
u'X-User-Name': u'user_name',
u'X-Tenant-Name': u'tenant_name',
u'X-Roles': u'user_name0, user_name1'}
def test_ctx_from_headers(self):
self.context = self.patch(context, 'ClimateContext')
catalog = json.dumps({'nova': 'catalog'})
self.fake_headers[u'X-Service-Catalog'] = catalog
api_context.ctx_from_headers(self.fake_headers)
self.context.assert_called_once_with(user_id=u'1',
roles=[u'user_name0',
u'user_name1'],
tenant_name=u'tenant_name',
auth_token=u'111-111-111',
service_catalog=u'catalog',
service_catalog={
u'nova': u'catalog'},
tenant_id=u'1',
user_name=u'user_name')
def test_ctx_from_headers_with_admin(self):
catalog = json.dumps({'nova': 'catalog'})
self.fake_headers[u'X-Service-Catalog'] = catalog
self.patch(policy, 'enforce').return_value = True
ctx = api_context.ctx_from_headers(self.fake_headers)
self.assertEqual(True, ctx.is_admin)
def test_ctx_from_headers_no_catalog(self):
self.assertRaises(
exceptions.ServiceCatalogNotFound,
api_context.ctx_from_headers,
self.fake_headers)
def test_ctx_from_headers_wrong_format(self):
catalog = ['etc']
self.fake_headers[u'X-Service-Catalog'] = catalog
self.assertRaises(
exceptions.WrongFormat,
api_context.ctx_from_headers,
self.fake_headers)

View File

View File

@ -0,0 +1,78 @@
# Copyright (c) 2013 Mirantis 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 climate.manager import exceptions
from climate import tests
from climate.utils.openstack import base
class TestBaseStackUtils(tests.TestCase):
def setUp(self):
super(TestBaseStackUtils, self).setUp()
self.base = base
self.service_type = 'fake_service'
self.url = 'http://%s-net.com'
def test_url_for_good_v3(self):
#TODO(n.s.):Can't find v3 endpoint example. Fix it later.
pass
def test_url_for_good_v2_public(self):
service_catalog =\
[{"endpoints": [{"adminURL": self.url % 'admin',
"region": "RegionOne",
"internalURL": self.url % 'internal',
"publicURL": self.url % 'public'}],
"type": "fake_service",
"name": "foo"}]
url = self.base.url_for(service_catalog, self.service_type)
self.assertEqual(url, self.url % 'public')
def test_url_for_good_v2_admin(self):
service_catalog =\
[{"endpoints": [{"adminURL": self.url % 'admin',
"region": "RegionOne",
"internalURL": self.url % 'internal',
"publicURL": self.url % 'public'}],
"type": "fake_service",
"name": "foo"}]
url = self.base.url_for(service_catalog, self.service_type,
endpoint_interface='admin')
self.assertEqual(url, self.url % 'admin')
def test_url_for_no_service(self):
service_catalog =\
[{"endpoints": [{"adminURL": self.url % 'admin',
"region": "RegionOne",
"internalURL": self.url % 'internal',
"publicURL": self.url % 'public'}],
"type": "foo_service",
"name": "foo"}]
self.assertRaises(exceptions.ServiceNotFound, self.base.url_for,
service_catalog, self.service_type)
def test_url_for_no_endpoints(self):
service_catalog =\
[{"type": "fake_service",
"name": "foo"}]
self.assertRaises(exceptions.EndpointsNotFound, self.base.url_for,
service_catalog, self.service_type)

View File

@ -0,0 +1,76 @@
# Copyright (c) 2013 Mirantis 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 keystoneclient import client as keystone_client
from climate import context
from climate import tests
from climate.utils.openstack import base
from climate.utils.openstack import keystone
class TestCKClient(tests.TestCase):
def setUp(self):
super(TestCKClient, self).setUp()
self.keystone = keystone
self.context = context
self.k_client = keystone_client
self.base = base
self.ctx = self.patch(self.context, 'current')
self.client = self.patch(self.k_client, 'Client')
self.patch(self.base, 'url_for').return_value = 'http://fake.com/'
self.version = '1'
self.username = 'fake_user'
self.token = 'fake_token'
self.password = 'fake_pass'
self.tenant_name = 'fake_tenant'
self.auth_url = 'fake_url'
self.trust = 'fake_trust'
self.mgmt_url = 'fake_url'
def test_client_from_kwargs(self):
self.ctx.side_effect = RuntimeError
self.keystone.ClimateKeystoneClient(version=self.version,
username=self.username,
password=self.password,
tenant_name=self.tenant_name,
auth_url=self.auth_url)
self.client.assert_called_once_with(version=self.version,
tenant_name=self.tenant_name,
username=self.username,
password=self.password,
auth_url=self.auth_url)
def test_client_from_ctx(self):
self.keystone.ClimateKeystoneClient()
self.client.assert_called_once_with(version='3',
username=self.ctx().user_name,
token=self.ctx().auth_token,
tenant_name=self.ctx().tenant_name,
auth_url='http://fake.com/',
endpoint='http://fake.com/')
def test_getattr(self):
#TODO(n.s.): Will be done as soon as pypi package will be updated
pass

View File

@ -0,0 +1,77 @@
# Copyright (c) 2013 Mirantis 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 novaclient import client as nova_client
from climate import context
from climate import tests
from climate.utils.openstack import base
from climate.utils.openstack import nova
class TestCNClient(tests.TestCase):
def setUp(self):
super(TestCNClient, self).setUp()
self.nova = nova
self.context = context
self.n_client = nova_client
self.base = base
self.ctx = self.patch(self.context, 'current')
self.client = self.patch(self.n_client, 'Client')
self.patch(self.base, 'url_for').return_value = 'http://fake.com/'
self.version = '2'
self.username = 'fake_user'
self.api_key = self.ctx().auth_token
self.project_id = self.ctx().tenant_id
self.auth_url = 'fake_auth'
self.mgmt_url = 'fake_mgmt'
def test_client_from_kwargs(self):
self.ctx.side_effect = RuntimeError
kwargs = {'version': self.version,
'username': self.username,
'api_key': self.api_key,
'project_id': self.project_id,
'auth_url': self.auth_url,
'mgmt_url': self.mgmt_url}
self.nova.ClimateNovaClient(**kwargs)
self.client.assert_called_once_with(version=self.version,
username=self.username,
api_key=self.api_key,
project_id=self.project_id,
auth_url=self.auth_url)
def test_client_from_ctx(self):
kwargs = {'version': self.version}
self.nova.ClimateNovaClient(**kwargs)
self.client.assert_called_once_with(version=self.version,
username=self.ctx().user_name,
api_key=None,
auth_token=self.ctx().auth_token,
project_id=self.ctx().tenant_id,
auth_url='http://fake.com/')
def test_getattr(self):
#TODO(n.s.): Will be done as soon as pypi package will be updated
pass

View File

View File

@ -0,0 +1,51 @@
# Copyright (c) 2013 Mirantis 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 climate.manager import exceptions
def url_for(service_catalog, service_type, admin=False,
endpoint_interface=None):
"""Gets url of the service to communicate through.
service_catalog - dict contains info about specific OpenStack service
service_type - OpenStack service type specification
"""
if not endpoint_interface:
endpoint_interface = 'public'
if admin:
endpoint_interface = 'admin'
service = None
for srv in service_catalog:
if srv['type'] == service_type:
service = srv
if service:
try:
endpoints = service['endpoints']
except KeyError:
raise exceptions.EndpointsNotFound(
"No endpoints for %s" % service['type'])
try:
# if Keystone API v3 endpoints returned
endpoint = [e for e in endpoints
if e['interface'] == endpoint_interface][0]
return endpoint['url']
except KeyError:
# otherwise
return endpoints[0]['%sURL' % endpoint_interface]
else:
raise exceptions.ServiceNotFound(
'Service "%s" not found' % service_type)

View File

@ -0,0 +1,111 @@
# Copyright (c) 2013 Mirantis 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 keystoneclient import client as keystone_client
from keystoneclient import exceptions as keystone_exception
from oslo.config import cfg
from climate import context
from climate.manager import exceptions as manager_exceptions
from climate.utils.openstack import base
opts = [
cfg.StrOpt('identity_service',
default='identityv3',
help='Identity service to use.')
]
keystone_opts = [
cfg.StrOpt('keystone_client_version',
default='3',
help='Keystoneclient version'),
]
CONF = cfg.CONF
CONF.register_cli_opts(opts)
CONF.register_opts(keystone_opts)
class ClimateKeystoneClient(object):
def __init__(self, **kwargs):
"""Return Keystone client for defined in 'identity_service' conf.
NOTE: We will use tenant_name until we start using keystone V3
client for all our needs.
:param version: service client version which we will use
:type version: str
:param username: username
:type username: str
:param password: password
:type password: str
:param tenant_name: tenant_name
:type tenant_name: str
:param auth_url: auth_url
:type auth_url: str
:param ctx: climate context object
:type ctx: dict
:param auth_url: keystone auth url
:type auth_url: string
:param endpoint: keystone management (endpoint) url
:type endpoint: string
:param token: user token to use for authentication
:type token: string
"""
ctx = kwargs.pop('ctx', None)
if ctx is None:
try:
ctx = context.current()
except RuntimeError:
pass
kwargs.setdefault('version', cfg.CONF.keystone_client_version)
if ctx is not None:
kwargs.setdefault('username', ctx.user_name)
kwargs.setdefault('tenant_name', ctx.tenant_name)
if not kwargs.get('auth_url'):
kwargs['auth_url'] = base.url_for(
ctx.service_catalog, CONF.identity_service)
if not kwargs.get('password'):
kwargs.setdefault('token', ctx.auth_token)
if kwargs['token'] and not kwargs.get('endpoint'):
try:
kwargs['endpoint'] = base.url_for(
ctx.service_catalog, CONF.identity_service,
endpoint_interface='admin')
except AttributeError:
raise manager_exceptions.NoManagementUrl()
try:
#NOTE(n.s.): we shall remove this try: except: clause when
#https://review.openstack.org/#/c/66494/ will be merged
self.keystone = keystone_client.Client(**kwargs)
except AttributeError:
raise manager_exceptions.WrongClientVersion()
self.exceptions = keystone_exception
def __getattr__(self, name):
func = getattr(self.keystone, name)
return func

View File

@ -0,0 +1,107 @@
# Copyright (c) 2013 Mirantis 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 novaclient import client as nova_client
from novaclient import exceptions as nova_exception
from oslo.config import cfg
from climate import context
from climate.manager import exceptions as manager_exceptions
from climate.utils.openstack import base
nova_opts = [
cfg.StrOpt('nova_client_version',
default='2',
help='Novaclient version'),
cfg.StrOpt('compute_service',
default='compute',
help='Nova name in keystone'),
]
CONF = cfg.CONF
CONF.register_opts(nova_opts)
CONF.import_opt('identity_service', 'climate.utils.openstack.keystone')
class ClimateNovaClient(object):
def __init__(self, **kwargs):
"""We suppose that in future we may want to use CNC in some places
where context will be available, so we create 2 different ways of
creating client from context(future) and kwargs(we use it now).
:param version: service client version which we will use
:type version: str
:param username: username
:type username: str
:param api_key: password
:type api_key: str
:param auth_token: keystone auth token
:type auth_token: str
:param project_id: project_id
:type api_key: str
:param auth_url: auth_url
:type auth_url: str
:param mgmt_url: management url
:type mgmt_url: str
"""
ctx = kwargs.pop('ctx', None)
if ctx is None:
try:
ctx = context.current()
except RuntimeError:
pass
kwargs.setdefault('version', cfg.CONF.nova_client_version)
if ctx is not None:
kwargs.setdefault('username', ctx.user_name)
kwargs.setdefault('api_key', None)
kwargs.setdefault('auth_token', ctx.auth_token)
kwargs.setdefault('project_id', ctx.tenant_id)
if not kwargs.get('auth_url'):
kwargs['auth_url'] = base.url_for(
ctx.service_catalog, CONF.identity_service)
try:
mgmt_url = kwargs.pop('mgmt_url', None) or base.url_for(
ctx.service_catalog, CONF.compute_service)
except AttributeError:
raise manager_exceptions.NoManagementUrl()
self.nova = nova_client.Client(**kwargs)
self.nova.client.management_url = mgmt_url
self.exceptions = nova_exception
def _image_create(self, instance_id):
instance = self.nova.servers.get(instance_id)
instance_name = instance.name
self.nova.servers.create_image(instance_id,
"reserved_%s" % instance_name)
def __getattr__(self, name):
if name == 'create_image':
func = self._image_create
else:
func = getattr(self.nova, name)
return func