From b6a25d467a18b271a51dc4e283693df82837e080 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Wed, 15 Jul 2020 11:09:40 +1200 Subject: Convert v1 controller to plain, return JSON This change converts the v1 controller from a RestController to a plain controller, and converts the /v1 response to remove the WSME types and return plain JSON. Change-Id: I483c6bb2e6b0da07b9e0c58190dbbc97e04bb6c1 Story: 1651346 Task: 10551 --- ironic/api/controllers/v1/__init__.py | 364 ++++++++++------------ ironic/tests/unit/api/controllers/v1/test_root.py | 125 ++++++++ ironic/tests/unit/api/test_root.py | 9 +- 3 files changed, 294 insertions(+), 204 deletions(-) diff --git a/ironic/api/controllers/v1/__init__.py b/ironic/api/controllers/v1/__init__.py index b0be184f9..a944dec69 100644 --- a/ironic/api/controllers/v1/__init__.py +++ b/ironic/api/controllers/v1/__init__.py @@ -18,8 +18,9 @@ Version 1 of the Ironic API Specification can be found at doc/source/webapi/v1.rst """ +from http import client as http_client + import pecan -from pecan import rest from webob import exc from ironic import api @@ -39,7 +40,7 @@ from ironic.api.controllers.v1 import utils from ironic.api.controllers.v1 import versions from ironic.api.controllers.v1 import volume from ironic.api.controllers import version -from ironic.api import expose +from ironic.api import method from ironic.common.i18n import _ BASE_VERSION = versions.BASE_VERSION @@ -57,205 +58,161 @@ def max_version(): versions.min_version_string(), versions.max_version_string()) -class MediaType(base.Base): - """A media type representation.""" - - base = str - type = str - - def __init__(self, base, type): - self.base = base - self.type = type - - -class V1(base.Base): - """The representation of the version 1 of the API.""" - - id = str - """The ID of the version, also acts as the release number""" - - media_types = [MediaType] - """An array of supported media types for this version""" - - links = None - """Links that point to a specific URL for this version and documentation""" - - chassis = None - """Links to the chassis resource""" - - nodes = None - """Links to the nodes resource""" - - ports = None - """Links to the ports resource""" - - portgroups = None - """Links to the portgroups resource""" - - drivers = None - """Links to the drivers resource""" - - volume = None - """Links to the volume resource""" - - lookup = None - """Links to the lookup resource""" - - heartbeat = None - """Links to the heartbeat resource""" - - conductors = None - """Links to the conductors resource""" - - allocations = None - """Links to the allocations resource""" - - deploy_templates = None - """Links to the deploy_templates resource""" - - version = None - """Version discovery information.""" - - events = None - """Links to the events resource""" - - @staticmethod - def convert(): - v1 = V1() - v1.id = "v1" - v1.links = [link.make_link('self', api.request.public_url, - 'v1', '', bookmark=True), - link.make_link('describedby', - 'https://docs.openstack.org', - '/ironic/latest/contributor/', - 'webapi.html', - bookmark=True, type='text/html') - ] - v1.media_types = [MediaType('application/json', - 'application/vnd.openstack.ironic.v1+json')] - v1.chassis = [link.make_link('self', api.request.public_url, - 'chassis', ''), - link.make_link('bookmark', - api.request.public_url, - 'chassis', '', - bookmark=True) - ] - v1.nodes = [link.make_link('self', api.request.public_url, - 'nodes', ''), - link.make_link('bookmark', - api.request.public_url, - 'nodes', '', - bookmark=True) - ] - v1.ports = [link.make_link('self', api.request.public_url, - 'ports', ''), - link.make_link('bookmark', - api.request.public_url, - 'ports', '', - bookmark=True) - ] - if utils.allow_portgroups(): - v1.portgroups = [ - link.make_link('self', api.request.public_url, - 'portgroups', ''), - link.make_link('bookmark', api.request.public_url, - 'portgroups', '', bookmark=True) - ] - v1.drivers = [link.make_link('self', api.request.public_url, - 'drivers', ''), - link.make_link('bookmark', - api.request.public_url, - 'drivers', '', - bookmark=True) - ] - if utils.allow_volume(): - v1.volume = [ - link.make_link('self', - api.request.public_url, - 'volume', ''), - link.make_link('bookmark', - api.request.public_url, - 'volume', '', - bookmark=True) - ] - if utils.allow_ramdisk_endpoints(): - v1.lookup = [link.make_link('self', api.request.public_url, - 'lookup', ''), - link.make_link('bookmark', - api.request.public_url, - 'lookup', '', - bookmark=True) - ] - v1.heartbeat = [link.make_link('self', - api.request.public_url, - 'heartbeat', ''), - link.make_link('bookmark', - api.request.public_url, - 'heartbeat', '', - bookmark=True) - ] - if utils.allow_expose_conductors(): - v1.conductors = [link.make_link('self', - api.request.public_url, - 'conductors', ''), - link.make_link('bookmark', - api.request.public_url, - 'conductors', '', - bookmark=True) - ] - if utils.allow_allocations(): - v1.allocations = [link.make_link('self', - api.request.public_url, - 'allocations', ''), - link.make_link('bookmark', - api.request.public_url, - 'allocations', '', - bookmark=True) - ] - if utils.allow_expose_events(): - v1.events = [link.make_link('self', api.request.public_url, - 'events', ''), - link.make_link('bookmark', - api.request.public_url, - 'events', '', - bookmark=True) - ] - if utils.allow_deploy_templates(): - v1.deploy_templates = [ - link.make_link('self', - api.request.public_url, - 'deploy_templates', ''), - link.make_link('bookmark', - api.request.public_url, - 'deploy_templates', '', - bookmark=True) - ] - v1.version = version.default_version() - return v1 - - -class Controller(rest.RestController): +def v1(): + v1 = { + 'id': "v1", + 'links': [ + link.make_link('self', api.request.public_url, + 'v1', '', bookmark=True), + link.make_link('describedby', + 'https://docs.openstack.org', + '/ironic/latest/contributor/', + 'webapi.html', + bookmark=True, type='text/html') + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'chassis': [ + link.make_link('self', api.request.public_url, + 'chassis', ''), + link.make_link('bookmark', + api.request.public_url, + 'chassis', '', + bookmark=True) + ], + 'nodes': [ + link.make_link('self', api.request.public_url, + 'nodes', ''), + link.make_link('bookmark', + api.request.public_url, + 'nodes', '', + bookmark=True) + ], + 'ports': [ + link.make_link('self', api.request.public_url, + 'ports', ''), + link.make_link('bookmark', + api.request.public_url, + 'ports', '', + bookmark=True) + ], + 'drivers': [ + link.make_link('self', api.request.public_url, + 'drivers', ''), + link.make_link('bookmark', + api.request.public_url, + 'drivers', '', + bookmark=True) + ], + 'version': version.default_version() + } + if utils.allow_portgroups(): + v1['portgroups'] = [ + link.make_link('self', api.request.public_url, + 'portgroups', ''), + link.make_link('bookmark', api.request.public_url, + 'portgroups', '', bookmark=True) + ] + if utils.allow_volume(): + v1['volume'] = [ + link.make_link('self', + api.request.public_url, + 'volume', ''), + link.make_link('bookmark', + api.request.public_url, + 'volume', '', + bookmark=True) + ] + if utils.allow_ramdisk_endpoints(): + v1['lookup'] = [ + link.make_link('self', api.request.public_url, + 'lookup', ''), + link.make_link('bookmark', + api.request.public_url, + 'lookup', '', + bookmark=True) + ] + v1['heartbeat'] = [ + link.make_link('self', + api.request.public_url, + 'heartbeat', ''), + link.make_link('bookmark', + api.request.public_url, + 'heartbeat', '', + bookmark=True) + ] + if utils.allow_expose_conductors(): + v1['conductors'] = [ + link.make_link('self', + api.request.public_url, + 'conductors', ''), + link.make_link('bookmark', + api.request.public_url, + 'conductors', '', + bookmark=True) + ] + if utils.allow_allocations(): + v1['allocations'] = [ + link.make_link('self', + api.request.public_url, + 'allocations', ''), + link.make_link('bookmark', + api.request.public_url, + 'allocations', '', + bookmark=True) + ] + if utils.allow_expose_events(): + v1['events'] = [ + link.make_link('self', api.request.public_url, + 'events', ''), + link.make_link('bookmark', + api.request.public_url, + 'events', '', + bookmark=True) + ] + if utils.allow_deploy_templates(): + v1['deploy_templates'] = [ + link.make_link('self', + api.request.public_url, + 'deploy_templates', ''), + link.make_link('bookmark', + api.request.public_url, + 'deploy_templates', '', + bookmark=True) + ] + return v1 + + +class Controller(object): """Version 1 API controller root.""" - nodes = node.NodesController() - ports = port.PortsController() - portgroups = portgroup.PortgroupsController() - chassis = chassis.ChassisController() - drivers = driver.DriversController() - volume = volume.VolumeController() - lookup = ramdisk.LookupController() - heartbeat = ramdisk.HeartbeatController() - conductors = conductor.ConductorsController() - allocations = allocation.AllocationsController() - events = event.EventsController() - deploy_templates = deploy_template.DeployTemplatesController() - - @expose.expose(V1) - def get(self): - # NOTE: The reason why convert() it's being called for every + _subcontroller_map = { + 'nodes': node.NodesController(), + 'ports': port.PortsController(), + 'portgroups': portgroup.PortgroupsController(), + 'chassis': chassis.ChassisController(), + 'drivers': driver.DriversController(), + 'volume': volume.VolumeController(), + 'lookup': ramdisk.LookupController(), + 'heartbeat': ramdisk.HeartbeatController(), + 'conductors': conductor.ConductorsController(), + 'allocations': allocation.AllocationsController(), + 'events': event.EventsController(), + 'deploy_templates': deploy_template.DeployTemplatesController() + } + + @method.expose() + def index(self): + # NOTE: The reason why v1() it's being called for every # request is because we need to get the host url from # the request object to make the links. - return V1.convert() + self._add_version_attributes() + if api.request.method != "GET": + pecan.abort(http_client.METHOD_NOT_ALLOWED) + + return v1() def _check_version(self, version, headers=None): if headers is None: @@ -279,8 +236,7 @@ class Controller(rest.RestController): 'max': versions.max_version_string()}, headers=headers) - @pecan.expose() - def _route(self, args, request=None): + def _add_version_attributes(self): v = base.Version(api.request.headers, versions.min_version_string(), versions.max_version_string()) @@ -295,7 +251,15 @@ class Controller(rest.RestController): api.response.headers[base.Version.string] = str(v) api.request.version = v - return super(Controller, self)._route(args, request) + @pecan.expose() + def _lookup(self, primary_key, *remainder): + self._add_version_attributes() + + controller = self._subcontroller_map.get(primary_key) + if not controller: + pecan.abort(http_client.NOT_FOUND) + + return controller, remainder __all__ = ('Controller',) diff --git a/ironic/tests/unit/api/controllers/v1/test_root.py b/ironic/tests/unit/api/controllers/v1/test_root.py index b3e58b817..78d3053e4 100644 --- a/ironic/tests/unit/api/controllers/v1/test_root.py +++ b/ironic/tests/unit/api/controllers/v1/test_root.py @@ -17,6 +17,7 @@ from unittest import mock from webob import exc as webob_exc from ironic.api.controllers import v1 as v1_api +from ironic.api.controllers.v1 import versions from ironic.tests import base as test_base from ironic.tests.unit.api import base as api_base @@ -28,6 +29,130 @@ class TestV1Routing(api_base.BaseApiTest): mock.ANY, mock.ANY) + def test_min_version(self): + response = self.get_json( + '/', + headers={ + 'Accept': 'application/json', + 'X-OpenStack-Ironic-API-Version': + versions.min_version_string() + }) + self.assertEqual({ + 'id': 'v1', + 'links': [ + {'href': 'http://localhost/v1/', 'rel': 'self'}, + {'href': 'https://docs.openstack.org//ironic/latest' + '/contributor//webapi.html', + 'rel': 'describedby', 'type': 'text/html'} + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'version': { + 'id': 'v1', + 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], + 'status': 'CURRENT', + 'min_version': versions.min_version_string(), + 'version': versions.max_version_string() + }, + 'chassis': [ + {'href': 'http://localhost/v1/chassis/', 'rel': 'self'}, + {'href': 'http://localhost/chassis/', 'rel': 'bookmark'} + ], + 'nodes': [ + {'href': 'http://localhost/v1/nodes/', 'rel': 'self'}, + {'href': 'http://localhost/nodes/', 'rel': 'bookmark'} + ], + 'ports': [ + {'href': 'http://localhost/v1/ports/', 'rel': 'self'}, + {'href': 'http://localhost/ports/', 'rel': 'bookmark'} + ], + 'drivers': [ + {'href': 'http://localhost/v1/drivers/', 'rel': 'self'}, + {'href': 'http://localhost/drivers/', 'rel': 'bookmark'} + ], + }, response) + + def test_max_version(self): + response = self.get_json( + '/', + headers={ + 'Accept': 'application/json', + 'X-OpenStack-Ironic-API-Version': + versions.max_version_string() + }) + self.assertEqual({ + 'id': 'v1', + 'links': [ + {'href': 'http://localhost/v1/', 'rel': 'self'}, + {'href': 'https://docs.openstack.org//ironic/latest' + '/contributor//webapi.html', + 'rel': 'describedby', 'type': 'text/html'} + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'version': { + 'id': 'v1', + 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], + 'status': 'CURRENT', + 'min_version': versions.min_version_string(), + 'version': versions.max_version_string() + }, + 'allocations': [ + {'href': 'http://localhost/v1/allocations/', 'rel': 'self'}, + {'href': 'http://localhost/allocations/', 'rel': 'bookmark'} + ], + 'chassis': [ + {'href': 'http://localhost/v1/chassis/', 'rel': 'self'}, + {'href': 'http://localhost/chassis/', 'rel': 'bookmark'} + ], + 'conductors': [ + {'href': 'http://localhost/v1/conductors/', 'rel': 'self'}, + {'href': 'http://localhost/conductors/', 'rel': 'bookmark'} + ], + 'deploy_templates': [ + {'href': 'http://localhost/v1/deploy_templates/', + 'rel': 'self'}, + {'href': 'http://localhost/deploy_templates/', + 'rel': 'bookmark'} + ], + 'drivers': [ + {'href': 'http://localhost/v1/drivers/', 'rel': 'self'}, + {'href': 'http://localhost/drivers/', 'rel': 'bookmark'} + ], + 'events': [ + {'href': 'http://localhost/v1/events/', 'rel': 'self'}, + {'href': 'http://localhost/events/', 'rel': 'bookmark'} + ], + 'heartbeat': [ + {'href': 'http://localhost/v1/heartbeat/', 'rel': 'self'}, + {'href': 'http://localhost/heartbeat/', 'rel': 'bookmark'} + ], + 'lookup': [ + {'href': 'http://localhost/v1/lookup/', 'rel': 'self'}, + {'href': 'http://localhost/lookup/', 'rel': 'bookmark'} + ], + 'nodes': [ + {'href': 'http://localhost/v1/nodes/', 'rel': 'self'}, + {'href': 'http://localhost/nodes/', 'rel': 'bookmark'} + ], + 'portgroups': [ + {'href': 'http://localhost/v1/portgroups/', 'rel': 'self'}, + {'href': 'http://localhost/portgroups/', 'rel': 'bookmark'} + ], + 'ports': [ + {'href': 'http://localhost/v1/ports/', 'rel': 'self'}, + {'href': 'http://localhost/ports/', 'rel': 'bookmark'} + ], + 'volume': [ + {'href': 'http://localhost/v1/volume/', 'rel': 'self'}, + {'href': 'http://localhost/volume/', 'rel': 'bookmark'} + ] + }, response) + class TestCheckVersions(test_base.TestCase): diff --git a/ironic/tests/unit/api/test_root.py b/ironic/tests/unit/api/test_root.py index 9a512d7ad..b784762f3 100644 --- a/ironic/tests/unit/api/test_root.py +++ b/ironic/tests/unit/api/test_root.py @@ -44,9 +44,10 @@ class TestRoot(base.BaseApiTest): self.assertNotIn('