From 618e637f8a7811db258a17656cb6a8c528661d75 Mon Sep 17 00:00:00 2001 From: JinLi Date: Tue, 25 Apr 2017 16:36:21 -0700 Subject: [PATCH] Proton Version Management This patch impletements the Proton Version Management specifications located here: https://review.openstack.org/#/c/456772/ Change-Id: I7581b55dbaac170439ac9274454a5e2c553fa7ca --- gluon/api/proton_controller.py | 40 ++++-- gluon/particleGenerator/ApiGenerator.py | 117 ++++++++++++++++-- gluon/particleGenerator/cli.py | 48 ++++--- gluon/particleGenerator/generator.py | 11 +- gluon/tests/particleGenerator/test_cli.py | 18 ++- .../tests/particleGenerator/test_generator.py | 13 +- 6 files changed, 201 insertions(+), 46 deletions(-) diff --git a/gluon/api/proton_controller.py b/gluon/api/proton_controller.py index c7052d7..beaae2c 100644 --- a/gluon/api/proton_controller.py +++ b/gluon/api/proton_controller.py @@ -19,24 +19,50 @@ import wsmeext.pecan as wsme_pecan from gluon.api.baseObject import APIBase from gluon.api import link +from gluon.api import types from gluon.particleGenerator import generator as particle_generator from oslo_config import cfg -class ProtonRoot(APIBase): - """The representation of the version 1 of the API.""" +class Proton(APIBase): - id = wtypes.text - """The ID of the version, also acts as the release number""" + proton_service = wtypes.text + """The name of Proton service""" + + status = types.create_enum_type('CURRENT', 'STABLE', 'DEPRECATED') + """Status of the API, which can be CURRENT, STABLE or DEPRECATED""" links = [link.Link] + """A Link that point to the root of proton""" + + @staticmethod + def convert(service, status='CURRENT'): + proton = Proton() + proton.status = status + proton.proton_service = service + proton.links = [link.Link.make_link('self', + pecan.request.host_url, + 'proton', + service, + bookmark=True)] + return proton + + +class ProtonRoot(APIBase): + + protons = [Proton] + """List of protons in their current version""" @staticmethod def convert(): + service_list = particle_generator.get_service_list() + protons = list() + for service in service_list: + proton = Proton.convert(service) + protons.append(proton) + root = ProtonRoot() - root.id = "proton" - root.links = [link.Link.make_link('self', pecan.request.host_url, - 'proton', '', bookmark=True), ] + root.protons = protons return root diff --git a/gluon/particleGenerator/ApiGenerator.py b/gluon/particleGenerator/ApiGenerator.py index 7f48806..2991d17 100644 --- a/gluon/particleGenerator/ApiGenerator.py +++ b/gluon/particleGenerator/ApiGenerator.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import pecan from pecan import rest import six from wsme import types as wtypes @@ -21,9 +22,11 @@ from gluon.api.baseObject import APIBase from gluon.api.baseObject import APIBaseObject from gluon.api.baseObject import RootObjectController from gluon.api.baseObject import SubObjectController +from gluon.api import link from gluon.api import types from gluon.particleGenerator.DataBaseModelGenerator \ import DataBaseModelProcessor +from gluon.particleGenerator.generator import load_model_for_service class MyData(object): @@ -34,26 +37,117 @@ ApiGenData = MyData() ApiGenData.svc_controllers = {} -class ServiceRoot(APIBase): - """The root service URL""" - id = wtypes.text +class ProtonVersion(APIBase): + """The root of proton service URL""" + + proton_service = wtypes.text + """Name of proton service""" + + version_id = wtypes.text + """Version id of proton service, e.g. v1.0, v2.0, ...""" + + status = types.create_enum_type('CURRENT', 'STABLE', 'DEPRECATED') + """Status of the API, which can be CURRENT, STABLE or DEPRECATED""" + + links = [link.Link] + """A Link that point to a specific version of the API""" @staticmethod - def convert(api_name): + def convert(service, version_id, status='CURRENT'): + version = ProtonVersion() + version.proton_service = service + version.version_id = version_id + version.status = status + resource_args = service + '/' + version_id + version.links = [link.Link.make_link('self', + pecan.request.host_url, + 'proton', + resource_args, + bookmark=True)] + return version + + +class Resource(ProtonVersion): + """Resource under a specific version of proton service""" + + resource_name = wtypes.text + """The resource name""" + + @staticmethod + def convert(service, version_id, resource_name, status='CURRENT'): + resource = Resource() + resource.proton_service = service + resource.version_id = version_id + resource.status = status + resource.resource_name = resource_name + resource_args = service + '/' + version_id + '/' + resource_name + resource.links = [link.Link.make_link('self', + pecan.request.host_url, + 'proton', + resource_args, + bookmark=True)] + return resource + + +class ServiceRoot(APIBase): + """The root service URL of a proton""" + + default_version = ProtonVersion + """Default version of a service""" + + versions = [ProtonVersion] + """Supported versions of a service""" + + # TODO(JinLi) need to handle multiple versions of a proton service. + # for now we only have one version, so we put it in + # both default_version and versions. + @staticmethod + def convert(service, version_id): + version = ProtonVersion.convert(service, version_id) root = ServiceRoot() - root.id = api_name + root.default_version = version + root.versions = [version] + return root + + +class ServiceVersionRoot(APIBase): + """The root service URL of a specific version of a proton""" + + resources = [Resource] + """Resources available for a specific version of a proton service""" + + @staticmethod + def convert(service_name, version_id): + model = load_model_for_service(service_name) + root = ServiceVersionRoot() + root.resources = list() + for table_name, table_data in model['api_objects'].items(): + resource_name = table_data['api']['plural_name'] + resource = Resource.convert(service_name, + version_id, + resource_name) + root.resources.append(resource) return root class ServiceController(rest.RestController): """Version 1 API controller root.""" - def __init__(self, api_name): + def __init__(self, api_name, version_id): self.api_name = api_name + self.version_id = version_id @wsme_pecan.wsexpose(ServiceRoot) def get(self): - return ServiceRoot.convert(self.api_name) + return ServiceRoot.convert(self.api_name, self.version_id) + + +class ServiceVersionController(ServiceController): + """Version 1 API controller root.""" + + @wsme_pecan.wsexpose(ServiceVersionRoot) + def get(self): + return ServiceVersionRoot.convert(self.api_name, self.version_id) class APIGenerator(object): @@ -65,11 +159,16 @@ class APIGenerator(object): def add_model(self, model): self.data = model - def create_controller(self, service_name, root): - controller = ServiceController(service_name) + def create_controller(self, service_name, version_id, root): + controller = ServiceController(service_name, version_id) setattr(root, service_name, controller) return controller + def create_version_controller(self, service_name, version_id, root): + controller = ServiceVersionController(service_name, version_id) + setattr(root, version_id, controller) + return controller + def create_api(self, root, service_name, db_models): self.db_models = db_models self.service_name = service_name diff --git a/gluon/particleGenerator/cli.py b/gluon/particleGenerator/cli.py index 9a30d9a..c1b5ef9 100644 --- a/gluon/particleGenerator/cli.py +++ b/gluon/particleGenerator/cli.py @@ -160,9 +160,10 @@ def make_url(host, port, *args): return url -def make_list_func(api_model, tablename): +def make_list_func(api_model, version, tablename): def list_func(**kwargs): - url = make_url(kwargs["host"], kwargs["port"], api_model, tablename) + url = make_url(kwargs["host"], kwargs["port"], api_model, + version, tablename) result = json_get(url) print(json.dumps(result, indent=4)) return result @@ -170,10 +171,10 @@ def make_list_func(api_model, tablename): return list_func -def make_show_func(api_model, tablename, primary_key): +def make_show_func(api_model, version, tablename, primary_key): def show_func(**kwargs): - url = make_url(kwargs["host"], kwargs["port"], api_model, tablename, - kwargs[primary_key]) + url = make_url(kwargs["host"], kwargs["port"], api_model, + version, tablename, kwargs[primary_key]) result = json_get(url) print(json.dumps(result, indent=4)) return result @@ -181,9 +182,10 @@ def make_show_func(api_model, tablename, primary_key): return show_func -def make_create_func(api_model, tablename): +def make_create_func(api_model, version, tablename): def create_func(**kwargs): - url = make_url(kwargs["host"], kwargs["port"], api_model, tablename) + url = make_url(kwargs["host"], kwargs["port"], api_model, + version, tablename) del kwargs["host"] del kwargs["port"] data = {} @@ -196,10 +198,10 @@ def make_create_func(api_model, tablename): return create_func -def make_update_func(api_model, tablename, primary_key): +def make_update_func(api_model, version, tablename, primary_key): def update_func(**kwargs): - url = make_url(kwargs["host"], kwargs["port"], api_model, tablename, - kwargs[primary_key]) + url = make_url(kwargs["host"], kwargs["port"], api_model, + version, tablename, kwargs[primary_key]) del kwargs["host"] del kwargs["port"] del kwargs[primary_key] @@ -213,15 +215,21 @@ def make_update_func(api_model, tablename, primary_key): return update_func -def make_delete_func(api_model, tablename, primary_key): +def make_delete_func(api_model, version, tablename, primary_key): def delete_func(**kwargs): - url = make_url(kwargs["host"], kwargs["port"], api_model, tablename, - kwargs[primary_key]) + url = make_url(kwargs["host"], kwargs["port"], api_model, + version, tablename, kwargs[primary_key]) do_delete(url) return delete_func +def get_version(model): + version = str(model['info']['version']) + version = 'v' + version + return version + + def get_primary_key(table_data): primary = [] for k, v in table_data['attributes'].items(): @@ -261,6 +269,7 @@ def proc_model(cli, package_name="unknown", portdefault=0): # print("loading model") model = load_model(package_name, model_dir, api_model) + version = get_version(model) for table_name, table_data in model['api_objects'].items(): get_primary_key(table_data) for table_name, table_data in model['api_objects'].items(): @@ -300,7 +309,7 @@ def proc_model(cli, package_name="unknown", # hosthelp = "Host of endpoint (%s) " % hostenv porthelp = "Port of endpoint (%s) " % portenv - list = make_list_func(api_model, attrs['__tablename__']) + list = make_list_func(api_model, version, attrs['__tablename__']) list.func_name = "%s-list" % (attrs['__objname__']) list = click.option("--host", envvar=hostenv, default=hostdefault, help=hosthelp)(list) @@ -308,7 +317,7 @@ def proc_model(cli, package_name="unknown", default=portdefault, help=porthelp)(list) cli.command()(list) - show = make_show_func(api_model, attrs['__tablename__'], + show = make_show_func(api_model, version, attrs['__tablename__'], attrs['_primary_key']) show.func_name = "%s-show" % (attrs['__objname__']) show = click.option("--host", envvar=hostenv, @@ -318,7 +327,8 @@ def proc_model(cli, package_name="unknown", show = click.argument(attrs['_primary_key'])(show) cli.command()(show) - create = make_create_func(api_model, attrs['__tablename__']) + create = make_create_func(api_model, version, + attrs['__tablename__']) create.func_name = "%s-create" % (attrs['__objname__']) create = click.option("--host", envvar=hostenv, default=hostdefault, help=hosthelp)(create) @@ -336,7 +346,8 @@ def proc_model(cli, package_name="unknown", create = click.option(option_name, **kwargs)(create) cli.command()(create) - update = make_update_func(api_model, attrs['__tablename__'], + update = make_update_func(api_model, version, + attrs['__tablename__'], attrs['_primary_key']) update.func_name = "%s-update" % (attrs['__objname__']) update = click.option("--host", envvar=hostenv, @@ -355,7 +366,8 @@ def proc_model(cli, package_name="unknown", update = click.argument(attrs['_primary_key'])(update) cli.command()(update) - del_func = make_delete_func(api_model, attrs['__tablename__'], + del_func = make_delete_func(api_model, version, + attrs['__tablename__'], attrs['_primary_key']) del_func.func_name = "%s-delete" % (attrs['__objname__']) del_func = click.option("--host", envvar=hostenv, diff --git a/gluon/particleGenerator/generator.py b/gluon/particleGenerator/generator.py index c8db1da..73afd35 100644 --- a/gluon/particleGenerator/generator.py +++ b/gluon/particleGenerator/generator.py @@ -367,11 +367,16 @@ def build_sql_models(service_list): def build_api(root, service_list): from gluon.particleGenerator.ApiGenerator import APIGenerator for service in service_list: - load_model_for_service(service) + model = load_model_for_service(service) + version_id = str(model['info']['version']) + version_id = 'v' + version_id api_gen = APIGenerator() - service_root = api_gen.create_controller(service, root) + service_root = api_gen.create_controller(service, version_id, root) + service_version_root = \ + api_gen.create_version_controller(service, version_id, + service_root) api_gen.add_model(load_model_for_service(service)) - api_gen.create_api(service_root, service, + api_gen.create_api(service_version_root, service, GenData.DBGeneratorInstance.get_db_models(service)) diff --git a/gluon/tests/particleGenerator/test_cli.py b/gluon/tests/particleGenerator/test_cli.py index ba63988..95dcb25 100644 --- a/gluon/tests/particleGenerator/test_cli.py +++ b/gluon/tests/particleGenerator/test_cli.py @@ -212,9 +212,10 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase): def test_make_list_func(self, mock_json_get): kwargs = {"host": "gluonURL.net", "port": 8080} api_model = "apimodel" + version = "v1" tablename = "tablename" mock_json_get.return_value = {"result": "successful"} - list_func = cli.make_list_func(api_model, tablename) + list_func = cli.make_list_func(api_model, version, tablename) observed = list_func(**kwargs) # self.assertIsNone(observed) expected = {"result": "successful"} @@ -227,10 +228,12 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase): def test_make_show_func(self, mock_json_get): kwargs = {"host": "gluonURL.net", "port": 8080, "id": 1} api_model = "apimodel" + version = "v1" tablename = "tablename" primary_key = "id" mock_json_get.return_value = {"result": "successful"} - show_func = cli.make_show_func(api_model, tablename, primary_key) + show_func = cli.make_show_func(api_model, version, + tablename, primary_key) observed = show_func(**kwargs) # self.assertIsNone(observed) expected = {"result": "successful"} @@ -245,9 +248,10 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase): kwargs = {"host": "gluonURL.net", "port": 8080, "id": 1, "firstname": "Jane", "lastName": "Doe"} api_model = "apiModel" + version = "v1" tablename = "user" mock_do_post.return_value = '{"result": "successful"}' - create_func = cli.make_create_func(api_model, tablename) + create_func = cli.make_create_func(api_model, version, tablename) observed = create_func(**kwargs) self.assertIsNone(observed) @@ -261,9 +265,11 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase): "firstname": "Jane", "lastName": "Doe"} api_model = "apiModel" tablename = "user" + version = "v1" primary_key = "id" mock_do_put.return_value = '{"result": "successful"}' - update_func = cli.make_update_func(api_model, tablename, primary_key) + update_func = cli.make_update_func(api_model, version, + tablename, primary_key) observed = update_func(**kwargs) self.assertIsNone(observed) @@ -275,10 +281,12 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase): def test_make_delete_func(self, mock_do_delete): kwargs = {"host": "gluonURL.net", "port": 8080, "id": 1} api_model = "apiModel" + version = "v1" tablename = "user" primary_key = "id" mock_do_delete.return_value = '{"result": "successful"}' - delete_func = cli.make_delete_func(api_model, tablename, primary_key) + delete_func = cli.make_delete_func(api_model, version, + tablename, primary_key) observed = delete_func(**kwargs) self.assertIsNone(observed) diff --git a/gluon/tests/particleGenerator/test_generator.py b/gluon/tests/particleGenerator/test_generator.py index 47d2618..903311f 100644 --- a/gluon/tests/particleGenerator/test_generator.py +++ b/gluon/tests/particleGenerator/test_generator.py @@ -74,6 +74,7 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase): """ @mock.patch('gluon.particleGenerator.generator.load_model') @mock.patch.object(APIGenerator, 'create_controller') + @mock.patch.object(APIGenerator, 'create_version_controller') @mock.patch.object(APIGenerator, 'add_model') @mock.patch.object(APIGenerator, 'create_api') @mock.patch('gluon.particleGenerator.generator.GenData.DBGeneratorInstance' @@ -82,24 +83,28 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase): mock_DBGeneratorInstance, mock_create_api, mock_add_model, + mock_create_version_controller, mock_create_controller, mock_load_model): root = object() service_list = ['test'] - mock_service = {'foo': 'bar'} + mock_service = {'foo': 'bar', 'info': {'version': '1.0'}} mock_load_model.return_value = mock_service db_models = mock.Mock() mock_DBGeneratorInstance.get_db_models.return_value = db_models service_root = mock.Mock() + version_root = mock.Mock() mock_create_controller.return_value = service_root - + mock_create_version_controller.return_value = version_root generator.build_api(root, service_list) mock_load_model.assert_called_with('gluon', 'models', 'test') - mock_create_controller.assert_called_with('test', root) + mock_create_controller.assert_called_with('test', 'v1.0', root) + mock_create_version_controller.assert_called_with('test', 'v1.0', + service_root) mock_add_model.assert_called_with(mock_service) mock_DBGeneratorInstance.get_db_models.assert_called_with('test') - mock_create_api.assert_called_with(service_root, + mock_create_api.assert_called_with(version_root, 'test', db_models) """