From 2eab4b317bf4db8e8107a003cc0df194f65b50da Mon Sep 17 00:00:00 2001 From: Yogeshwar Srikrishnan Date: Sun, 27 Nov 2011 14:04:35 -0600 Subject: Bug #890801 Changes to support /extensions call. - Introduced a new extension reader to read static extension content. - Added additional rst files explaining extensions. - Removed functionality from additional middleware that used to support /extensions call.ie RAX-KEY-extension - Removed service extension test as it was no more relavent. - Added unit test that checks toggling of extensions. - Additional notes on the conf file. Change-Id: Ia7b47b123e94704ca5d88dcce0db4ee1ac5eb3ba --- doc/source/extensions.rst | 125 +++++++++++++++++++++ doc/source/index.rst | 2 + doc/source/keystone.conf.rst | 4 + etc/keystone.conf | 3 +- keystone/contrib/extensions/__init__.py | 8 +- keystone/contrib/extensions/admin/__init__.py | 16 ++- keystone/contrib/extensions/extensions.json | 1 + keystone/contrib/extensions/extensions.xml | 5 + keystone/contrib/extensions/service/__init__.py | 1 + keystone/contrib/extensions/service/raxgrp/api.py | 5 +- .../contrib/extensions/service/raxkey/frontend.py | 58 ++-------- keystone/controllers/extensions.py | 30 +++-- keystone/logic/extension_reader.py | 109 ++++++++++++++++++ keystone/logic/types/extension.py | 12 ++ keystone/routers/admin.py | 1 - keystone/routers/service.py | 3 +- keystone/test/functional/test_ext_raxkskey.py | 30 ----- keystone/test/functional/test_extensions.py | 31 ++++- keystone/test/unit/test_extensions.py | 65 +++++++++++ 19 files changed, 392 insertions(+), 117 deletions(-) create mode 100644 doc/source/extensions.rst create mode 100644 keystone/contrib/extensions/extensions.json create mode 100644 keystone/contrib/extensions/extensions.xml create mode 100644 keystone/logic/extension_reader.py create mode 100644 keystone/logic/types/extension.py delete mode 100644 keystone/test/functional/test_ext_raxkskey.py create mode 100644 keystone/test/unit/test_extensions.py diff --git a/doc/source/extensions.rst b/doc/source/extensions.rst new file mode 100644 index 00000000..d377ba24 --- /dev/null +++ b/doc/source/extensions.rst @@ -0,0 +1,125 @@ +================ +Extensions +================ + +Extensions support adding features and functions to OpenStack APIs at any time, without prior +approval or waiting for a new API and release cycles. + +The extension mechanism is in development and documented in extensions_ and extensionspresentation_. + +This document describes the extensions included with Keystone, how to enable and disable them, +and briefly touches on how to write your own extensions. + +.. _extensions: http://docs.openstack.org/trunk/openstack-compute/developer/openstack-api-extensions/content/ch02s01.html +.. _extensionspresentation: http://www.slideshare.net/RackerWilliams/openstack-extensions + + +Built-in Extensions +------------------- + +Keystone ships with a number of extensions found under the +keystone/contib/extensions folder. + +The following built-in extensions are included: + +OS-KSADM + + This is an extensions that supports managing users, tenants, and roles + through the API. Without this extensions, the ony way to manage those + objects is through keystone-manage or directly in the underlying database. + + This is an Admin API extension only. + +OS-KSCATALOG + + This extensions supports managing Endpoints and prrovides the Endpoint + Template mechanism for managing bulk endpoints. + + This is an Admin API extension only. + +OS-EC2 + + This extension adds support for EC2 credentials. + + This is an Admin and Service API extension. + +RAX-GRP + + This extension adds functionality the enables groups. + + This is an Admin and Service API extension. + +RAX-KEY + + This extensions adds support for authentication with an API Key (the core + Keystone API only supports username/password credentials) + + This is an Admin and Service API extension. + + +.. note:: + + The included extensions are in the process of being rewritten. Currently + only osksadm and oskscatalog work with this new extensions design. + + +Enabling/Disabling extensions. +------------------------------ + The Keystone conf file has a property called extensions. This property holds + the list of supported extensions that you want enabled. If you want to + add/remove an extension from being supported, add/remove the extension key + from this property. The key is the name of the folder of the extension + under the keystone/contrib/extensions folder. + **If you want to load different extensions in the service API than the Admin API + you need to use different config files. + +Adding additional extensions. +------------------------------ + +To add a new extension, these are the steps involved. + +1. Register your identifier (this process is not ready. For now, find a short +identifier that you know won't conflict with other extension writers). + + Example: OS for OpenStack, RAX for Rackspace + +2. Decide a short hand name for extension. + + Example: OS-KSADM (for OpenStack's Keystone Admin extensions) + +3. Decide whether the extension enhances Admin API, Service API or both. + +4. Add a folder with the name we have already decided @ +/contrib/extensions/{admin or service} based on which API you are enhancing. + +5. Add static extension files for json (name it as extension.json) and xml +(name it as extension.xml) to the new extension folder. Refer to `Service Guide `_ +`Sample extension XML `_ +`Sample extension JSON `_ for the the content and structure. + +6. If your extension is adding additional methods override the base class +'BaseExtensionHandler', call it 'ExtensionHandler', and add your methods. + +7. If your extension modifies existing calls yu need to modify existing code to support the extension. + +8. Modify this documentation to refelect the availability of your extension. + +9. Add your extensions documentation, WADL, and XSD files in the keystone/content +folder + +10. Add your extension name to the list of supported extensions in the keystone conf file. + + +Finding out which extensions are running +---------------------------------------- + +A quick and simple test is:: + + curl http://localhost:35357/v2.0/extensions + + or + + curl http://localhost:5000/v2.0/extensions + +The response will list the extensions available. + \ No newline at end of file diff --git a/doc/source/index.rst b/doc/source/index.rst index c338e57a..3611d8a2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -43,6 +43,8 @@ Getting Started setup testing + migration + extensions configuration controllingservers configuringservices diff --git a/doc/source/keystone.conf.rst b/doc/source/keystone.conf.rst index 39482ee3..64798902 100644 --- a/doc/source/keystone.conf.rst +++ b/doc/source/keystone.conf.rst @@ -45,6 +45,10 @@ keystone.conf example 'swift' : 'X-Storage-Url', 'cdn' : 'X-CDN-Management-Url'} + #List of extensions currently loaded. + #Refer docs for list of supported extensions. + extensions= osksadm,oskscatalog + # Address to bind the API server # TODO Properties defined within app not available via pipeline. service_host = 0.0.0.0 diff --git a/etc/keystone.conf b/etc/keystone.conf index 459c09e5..075f2ffd 100644 --- a/etc/keystone.conf +++ b/etc/keystone.conf @@ -26,7 +26,8 @@ service-header-mappings = { 'swift' : 'X-Storage-Url', 'cdn' : 'X-CDN-Management-Url'} -#List of extensions currently supported +#List of extensions currently loaded. +#Refer docs for list of supported extensions. extensions= osksadm,oskscatalog # Address to bind the API server diff --git a/keystone/contrib/extensions/__init__.py b/keystone/contrib/extensions/__init__.py index 35bba8c3..865efcb2 100644 --- a/keystone/contrib/extensions/__init__.py +++ b/keystone/contrib/extensions/__init__.py @@ -20,13 +20,15 @@ import ast import logging from keystone import utils EXTENSION_PREFIX = 'keystone.contrib.extensions.' +DEFAULT_EXTENSIONS = 'osksadm,oskscatalog' +CONFIG_EXTENSION_PROPERTY = 'extensions' class BaseExtensionConfigurer(object): - def configure_extensions(self, extension_property, extension_type, - extension_defaults, mapper, options): + def configure_extensions(self, extension_type, + mapper, options): supported_extensions = options.get( - extension_property, extension_defaults) + CONFIG_EXTENSION_PROPERTY, DEFAULT_EXTENSIONS) for supported_extension in supported_extensions.split(','): self.extension_handlers = [] supported_extension = EXTENSION_PREFIX\ diff --git a/keystone/contrib/extensions/admin/__init__.py b/keystone/contrib/extensions/admin/__init__.py index 9530970c..16be5a64 100644 --- a/keystone/contrib/extensions/admin/__init__.py +++ b/keystone/contrib/extensions/admin/__init__.py @@ -20,21 +20,19 @@ import ast import logging from keystone import utils from keystone.contrib.extensions import BaseExtensionConfigurer -DEFAULT_EXTENSIONS = 'osksadm,oskscatalog' -CONFIGURER = None -CONFIG_EXTENSION_PROPERTY = 'extensions' +ADMIN_CONFIGURER = None EXTENSION_ADMIN_PREFIX = 'admin' class AdminExtensionConfigurer(BaseExtensionConfigurer): def configure(self, mapper, options): - self.configure_extensions(CONFIG_EXTENSION_PROPERTY, + self.configure_extensions( EXTENSION_ADMIN_PREFIX, - DEFAULT_EXTENSIONS, mapper, options) + mapper, options) def get_extension_configurer(): - global CONFIGURER - if not CONFIGURER: - CONFIGURER = AdminExtensionConfigurer() - return CONFIGURER + global ADMIN_CONFIGURER + if not ADMIN_CONFIGURER: + ADMIN_CONFIGURER = AdminExtensionConfigurer() + return ADMIN_CONFIGURER diff --git a/keystone/contrib/extensions/extensions.json b/keystone/contrib/extensions/extensions.json new file mode 100644 index 00000000..9e1c96d0 --- /dev/null +++ b/keystone/contrib/extensions/extensions.json @@ -0,0 +1 @@ +{ "extensions" : { "values" : []}} \ No newline at end of file diff --git a/keystone/contrib/extensions/extensions.xml b/keystone/contrib/extensions/extensions.xml new file mode 100644 index 00000000..ed5ee9c6 --- /dev/null +++ b/keystone/contrib/extensions/extensions.xml @@ -0,0 +1,5 @@ + + + + diff --git a/keystone/contrib/extensions/service/__init__.py b/keystone/contrib/extensions/service/__init__.py index e69de29b..e0eeb867 100644 --- a/keystone/contrib/extensions/service/__init__.py +++ b/keystone/contrib/extensions/service/__init__.py @@ -0,0 +1 @@ +EXTENSION_SERVICE_PREFIX = 'service' diff --git a/keystone/contrib/extensions/service/raxgrp/api.py b/keystone/contrib/extensions/service/raxgrp/api.py index ef9e7fd5..1882a13f 100755 --- a/keystone/contrib/extensions/service/raxgrp/api.py +++ b/keystone/contrib/extensions/service/raxgrp/api.py @@ -61,10 +61,11 @@ class RAXEXTBaseUserAPI(BaseUserAPI): def users_get_page_markers(self, marker, limit): raise NotImplementedError - def users_get_by_tenant_get_page(self, tenant_id, marker, limit): + def users_get_by_tenant_get_page(self, tenant_id, role_id, marker, limit): raise NotImplementedError - def users_get_by_tenant_get_page_markers(self, tenant_id, marker, limit): + def users_get_by_tenant_get_page_markers(self, tenant_id, + role_id, marker, limit): raise NotImplementedError def check_password(self, user, password): diff --git a/keystone/contrib/extensions/service/raxkey/frontend.py b/keystone/contrib/extensions/service/raxkey/frontend.py index c461466a..bd203b78 100644 --- a/keystone/contrib/extensions/service/raxkey/frontend.py +++ b/keystone/contrib/extensions/service/raxkey/frontend.py @@ -19,18 +19,14 @@ """ RACKSPACE API KEY EXTENSION -This WSGI component -- detects calls with extensions in them. -- processes the necessary components +Soon to be deprecated middleware. """ - -import os -import json -from lxml import etree -from webob.exc import Request, Response +import logging EXTENSION_ALIAS = "RAX-KEY" +LOG = logging.getLogger('keystone.contrib.extensions') + class FrontEndFilter(object): """API Key Middleware that handles authentication with API Key""" @@ -42,49 +38,9 @@ class FrontEndFilter(object): self.app = app def __call__(self, env, start_response): - """ Handle incoming request. Transform. And send downstream. """ - request = Request(env) - if request.path == "/extensions": - if env['KEYSTONE_API_VERSION'] == '2.0': - request = Request(env) - response = request.get_response(self.app) - if response.status_int == 200: - if response.content_type == 'application/json': - #load json for this extension from file - thisextension = open(os.path.join( - os.path.dirname(__file__), - "extension.json")).read() - thisextensionjson = json.loads(thisextension) - - #load json in response - body = json.loads(response.body) - extensionsarray = body["extensions"]["values"] - - #add this extension and return the response - extensionsarray.append(thisextensionjson) - newresp = Response( - content_type='application/json', - body=json.dumps(body)) - return newresp(env, start_response) - elif response.content_type == 'application/xml': - #load xml for this extension from file - thisextensionxml = etree.parse(os.path.join( - os.path.dirname(__file__), - "extension.xml")).getroot() - #load xml being returned in response - body = etree.fromstring(response.body) - - #add this extension and return the response - body.append(thisextensionxml) - newresp = Response( - content_type='application/xml', - body=etree.tostring(body)) - return newresp(env, start_response) - - # return the response - return response(env, start_response) - - #default action, bypass + LOG.warn('This middleware is soon to be deprecated." +\ + "Please remove related entries from conf files.') + #Kept for backward compatibility.Does nothing as of now. return self.app(env, start_response) diff --git a/keystone/controllers/extensions.py b/keystone/controllers/extensions.py index 159c2ebe..96032839 100644 --- a/keystone/controllers/extensions.py +++ b/keystone/controllers/extensions.py @@ -1,26 +1,24 @@ -from webob import Response - from keystone import utils -from keystone.common import template, wsgi +from keystone.common import wsgi +from keystone.logic.extension_reader import ExtensionsReader +from keystone.contrib.extensions.admin import EXTENSION_ADMIN_PREFIX +from keystone.contrib.extensions.service import EXTENSION_SERVICE_PREFIX class ExtensionsController(wsgi.Controller): """Controller for extensions related methods""" - def __init__(self, options): + def __init__(self, options, is_service_operation=None): super(ExtensionsController, self).__init__() self.options = options - - @utils.wrap_error - def get_extensions_info(self, req, path): - resp = Response() - - if utils.is_xml_response(req): - resp_file = "%s.xml" % path - mime_type = "application/xml" + if is_service_operation: + self.extension_prefix = EXTENSION_SERVICE_PREFIX else: - resp_file = "%s.json" % path - mime_type = "application/json" + self.extension_prefix = EXTENSION_ADMIN_PREFIX + self.extension_reader = ExtensionsReader(options, + self.extension_prefix) - return template.static_file(resp, req, resp_file, - root=utils.get_app_root(), mimetype=mime_type) + @utils.wrap_error + def get_extensions_info(self, req): + return utils.send_result(200, req, + self.extension_reader.get_extensions()) diff --git a/keystone/logic/extension_reader.py b/keystone/logic/extension_reader.py new file mode 100644 index 00000000..1c7d4914 --- /dev/null +++ b/keystone/logic/extension_reader.py @@ -0,0 +1,109 @@ +import os +import json +from lxml import etree + +from keystone import utils +from keystone.contrib.extensions import CONFIG_EXTENSION_PROPERTY +from keystone.contrib.extensions import DEFAULT_EXTENSIONS +from keystone.logic.types.extension import Extensions + +EXTENSIONS_PATH = 'contrib/extensions' + + +class ExtensionsReader(object): + """Reader to read static extensions content""" + + def __init__(self, options, extension_prefix): + self.extensions = None + self.options = options + self.extension_prefix = extension_prefix + self.root = None + self.supported_extensions = None + self.__init_extensions() + + def __init_extensions(self): + self.extensions = Extensions(self.__get_json_extensions(), + self.__get_xml_extensions()) + + def __get_json_extensions(self): + """ Initializes and returns all json static extension content.""" + body = self.__get_all_json_extensions() + extensionsarray = body["extensions"]["values"] + for supported_extension in self.__get_supported_extensions(): + thisextensionjson = self.__get_extension_json( + supported_extension) + if thisextensionjson is not None: + extensionsarray.append(thisextensionjson) + return json.dumps(body) + + def __get_xml_extensions(self): + """ Initializes and returns all xml static extension content.""" + body = self.__get_all_xml_extensions() + for supported_extension in self.__get_supported_extensions(): + thisextensionxml = self.__get_extension_xml(supported_extension) + if thisextensionxml is not None: + body.append(thisextensionxml) + return etree.tostring(body) + + def __get_root(self): + """ Returns application root.Has a local reference for reuse.""" + if self.root is None: + self.root = utils.get_app_root() + self.root = os.path.abspath(self.root) + os.sep + return self.root + + def __get_file(self, resp_file): + """ Helper get file method.""" + root = self.__get_root() + filename = os.path.abspath(os.path.join(root, resp_file.strip('/\\'))) + return open(filename).read() + + def __get_all_json_extensions(self): + """ Gets empty json extensions content to which specific + extensions are added.""" + resp_file = "%s/%s.json" % (EXTENSIONS_PATH, 'extensions') + allextensions = self.__get_file(resp_file) + return json.loads(allextensions) + + def __get_all_xml_extensions(self): + """ Gets empty xml extensions content + to which specific extensions are added.""" + resp_file = "%s/%s.xml" % (EXTENSIONS_PATH, 'extensions') + allextensions = self.__get_file(resp_file) + return etree.fromstring(allextensions) + + def __get_supported_extensions(self): + """ Returns list of supported extensions.""" + if self.supported_extensions is None: + self.supported_extensions = self.options.get( + CONFIG_EXTENSION_PROPERTY, DEFAULT_EXTENSIONS).split(',') + return self.supported_extensions + + def __get_extension_json(self, extension_name): + """Returns specific extension's json content.""" + thisextension = self.__get_extension_file(extension_name, 'json') + return thisextension if not thisextension\ + else json.loads(thisextension.read()) + + def __get_extension_xml(self, extension_name): + """Returns specific extension's xml content.""" + thisextension = self.__get_extension_file(extension_name, 'xml') + return thisextension if not thisextension\ + else etree.parse(thisextension).getroot() + + def __get_extension_file(self, extension_name, request_type): + """Returns specific static extension file.""" + try: + extension_dir = "%s/%s/%s" % (EXTENSIONS_PATH, + self.extension_prefix, extension_name) + extension_dir = os.path.abspath(os.path.join(self.__get_root(), + extension_dir.strip('/\\'))) + extension_file = open(os.path.join(extension_dir, + "extension." + request_type)) + return extension_file + except IOError: + return None + + def get_extensions(self): + """Return Extensions result.""" + return self.extensions diff --git a/keystone/logic/types/extension.py b/keystone/logic/types/extension.py new file mode 100644 index 00000000..bc8bab59 --- /dev/null +++ b/keystone/logic/types/extension.py @@ -0,0 +1,12 @@ +class Extensions(object): + """An extensions type to hold static extensions content.""" + + def __init__(self, json_content, xml_content): + self.xml_content = xml_content + self.json_content = json_content + + def to_json(self): + return self.json_content + + def to_xml(self): + return self.xml_content diff --git a/keystone/routers/admin.py b/keystone/routers/admin.py index 8e1d5748..26d0d5cb 100755 --- a/keystone/routers/admin.py +++ b/keystone/routers/admin.py @@ -87,7 +87,6 @@ class AdminApi(wsgi.Router): mapper.connect("/extensions", controller=extensions_controller, action="get_extensions_info", - path="content/admin/extensions", conditions=dict(method=["GET"])) # Static Files Controller diff --git a/keystone/routers/service.py b/keystone/routers/service.py index f8d9dc5d..96063130 100644 --- a/keystone/routers/service.py +++ b/keystone/routers/service.py @@ -56,11 +56,10 @@ class ServiceApi(wsgi.Router): action="get_version_info", file="service/version", conditions=dict(method=["GET"])) - extensions_controller = ExtensionsController(options) + extensions_controller = ExtensionsController(options, True) mapper.connect("/extensions", controller=extensions_controller, action="get_extensions_info", - path="content/service/extensions", conditions=dict(method=["GET"])) # Static Files Controller diff --git a/keystone/test/functional/test_ext_raxkskey.py b/keystone/test/functional/test_ext_raxkskey.py deleted file mode 100644 index 81b3b341..00000000 --- a/keystone/test/functional/test_ext_raxkskey.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest2 as unittest -from keystone.test.functional import common - - -class TestExtensions(common.ApiTestCase): - def test_extensions_json(self): - r = self.service_request(path='/extensions.json', - assert_status=200) - self.assertTrue('json' in r.getheader('Content-Type')) - content = r.json - self.assertIsNotNone(content['extensions']) - self.assertIsNotNone(content['extensions']['values']) - found = False - for value in content['extensions']['values']: - if value['extension']['alias'] == 'RAX-KSKEY': - found = True - break - self.assertTrue(found) - - def test_extensions_xml(self): - r = self.service_request(path='/extensions.xml') - self.assertTrue('xml' in r.getheader('Content-Type')) - content = r.xml - extension = content.find( - "{http://docs.openstack.org/common/api/v1.0}extension") - self.assertEqual(extension.get("alias"), "RAX-KSKEY") - - -if __name__ == '__main__': - unittest.main() diff --git a/keystone/test/functional/test_extensions.py b/keystone/test/functional/test_extensions.py index 8dbbf97d..de0fe841 100644 --- a/keystone/test/functional/test_extensions.py +++ b/keystone/test/functional/test_extensions.py @@ -6,6 +6,9 @@ class TestExtensions(common.FunctionalTestCase): def test_extensions_json(self): r = self.service_request(path='/extensions.json') self.assertTrue('json' in r.getheader('Content-Type')) + content = r.json + self.assertIsNotNone(content['extensions']) + self.assertIsNotNone(content['extensions']['values']) def test_extensions_xml(self): r = self.service_request(path='/extensions.xml') @@ -14,12 +17,36 @@ class TestExtensions(common.FunctionalTestCase): class TestAdminExtensions(common.ApiTestCase): def test_extensions_json(self): - r = self.service_request(path='/extensions.json') + r = self.admin_request(path='/extensions.json') self.assertTrue('json' in r.getheader('Content-Type')) + content = r.json + self.assertIsNotNone(content['extensions']) + self.assertIsNotNone(content['extensions']['values']) + found_osksadm = False + found_oskscatalog = False + for value in content['extensions']['values']: + if value['extension']['alias'] == 'OS-KSADM': + found_osksadm = True + if value['extension']['alias'] == 'OS-KSCATALOG': + found_oskscatalog = True + self.assertTrue(found_osksadm, "Missing OS-KSADM extension.") + self.assertTrue(found_oskscatalog, "Missing OS-KSCATALOG extension.") def test_extensions_xml(self): - r = self.service_request(path='/extensions.xml') + r = self.admin_request(path='/extensions.xml') self.assertTrue('xml' in r.getheader('Content-Type')) + content = r.xml + extensions = content.findall( + "{http://docs.openstack.org/common/api/v1.0}extension") + found_osksadm = False + found_oskscatalog = False + for extension in extensions: + if extension.get("alias") == 'OS-KSADM': + found_osksadm = True + if extension.get("alias") == 'OS-KSCATALOG': + found_oskscatalog = True + self.assertTrue(found_osksadm, "Missing OS-KSADM extension.") + self.assertTrue(found_oskscatalog, "Missing OS-KSCATALOG extension.") if __name__ == '__main__': diff --git a/keystone/test/unit/test_extensions.py b/keystone/test/unit/test_extensions.py new file mode 100644 index 00000000..5e161ca5 --- /dev/null +++ b/keystone/test/unit/test_extensions.py @@ -0,0 +1,65 @@ +import unittest2 as unittest +from keystone.logic.extension_reader import ExtensionsReader +from keystone.contrib.extensions import CONFIG_EXTENSION_PROPERTY +from keystone.contrib.extensions.admin import EXTENSION_ADMIN_PREFIX +from xml.etree import ElementTree +import json + + +class MockOptions(object): + """ Mock object that mimics options.""" + def __init__(self, loaded_extensions): + self.loaded_extensions = loaded_extensions + + def get(self, prop_name, default): + if prop_name == CONFIG_EXTENSION_PROPERTY: + return self.loaded_extensions + + +class TestExtensionReader(unittest.TestCase): + """Unit tests for ExtensionsReader.These + tests check whether the returned extensions vary + when they are configured differently.""" + + def setUp(self): + self.options = MockOptions("osksadm") + self.extensions_reader = ExtensionsReader(self.options, + EXTENSION_ADMIN_PREFIX) + + def test_extensions_with_only_osksadm_json(self): + r = self.extensions_reader.get_extensions().to_json() + content = json.loads(r) + self.assertIsNotNone(content['extensions']) + self.assertIsNotNone(content['extensions']['values']) + found_osksadm = False + found_oskscatalog = False + for value in content['extensions']['values']: + if value['extension']['alias'] == 'OS-KSADM': + found_osksadm = True + if value['extension']['alias'] == 'OS-KSCATALOG': + found_oskscatalog = True + self.assertTrue(found_osksadm, + "Missing OS-KSADM extension.") + self.assertFalse(found_oskscatalog, + "Non configured OS-KSCATALOG extension returned.") + + def test_extensions_with_only_osksadm_xml(self): + r = self.extensions_reader.get_extensions().to_xml() + content = ElementTree.XML(r) + extensions = content.findall( + "{http://docs.openstack.org/common/api/v1.0}extension") + found_osksadm = False + found_oskscatalog = False + for extension in extensions: + if extension.get("alias") == 'OS-KSADM': + found_osksadm = True + if extension.get("alias") == 'OS-KSCATALOG': + found_oskscatalog = True + self.assertTrue(found_osksadm, + "Missing OS-KSADM extension.") + self.assertFalse(found_oskscatalog, + "Non configured OS-KSCATALOG extension returned.") + + +if __name__ == '__main__': + unittest.main() -- cgit