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
This commit is contained in:
Steve Baker 2020-07-15 11:09:40 +12:00
parent bf15520119
commit b6a25d467a
3 changed files with 291 additions and 201 deletions

View File

@ -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
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 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):
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()
_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()
}
@expose.expose(V1)
def get(self):
# NOTE: The reason why convert() it's being called for every
@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',)

View File

@ -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):

View File

@ -44,9 +44,10 @@ class TestRoot(base.BaseApiTest):
self.assertNotIn('<html', response.json['error_message'])
def test_no_html_errors2(self):
response = self.delete('/v1', expect_errors=True)
response = self.delete('/', expect_errors=True)
self.assertEqual(http_client.METHOD_NOT_ALLOWED, response.status_int)
self.assertIn('Not Allowed', response.json['error_message'])
self.assertIn('malformed or otherwise incorrect',
response.json['error_message'])
self.assertNotIn('<html', response.json['error_message'])
@ -68,8 +69,8 @@ class TestV1Root(base.BaseApiTest):
expected_resources = (['chassis', 'drivers', 'nodes', 'ports']
+ additional_expected_resources)
self.assertEqual(sorted(expected_resources), sorted(actual_resources))
self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json',
'base': 'application/json'}, data['media_types'])
self.assertEqual({'type': 'application/vnd.openstack.ironic.v1+json',
'base': 'application/json'}, data['media_types'])
version1 = data['version']
self.assertEqual('v1', version1['id'])