From f5d9e5cb2f606f6ad65c67a7c636666ffb1ff08e Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Tue, 5 Nov 2024 07:43:57 +0000 Subject: [PATCH] api: Add response body schemas for hypervisors APIs (2/3) We split this one up due to its size, which itself is mainly due to the amount of aliasing that went on in early versions as well as the amount of changes that have been made over the years. This focuses on the show view as well as the closely related detailed view. Change-Id: I06b1a8f0a30e9260f118921a13e85c4c534d9c8f --- nova/api/openstack/compute/hypervisors.py | 11 ++ .../openstack/compute/schemas/hypervisors.py | 168 ++++++++++++++++++ nova/tests/unit/policies/test_hypervisors.py | 49 ++++- 3 files changed, 222 insertions(+), 6 deletions(-) diff --git a/nova/api/openstack/compute/hypervisors.py b/nova/api/openstack/compute/hypervisors.py index 4c32061b41..3b2201f088 100644 --- a/nova/api/openstack/compute/hypervisors.py +++ b/nova/api/openstack/compute/hypervisors.py @@ -255,6 +255,11 @@ class HypervisorsController(wsgi.Controller): @validation.query_schema(schema.index_query, '2.1', '2.32') @validation.query_schema(schema.index_query_v233, '2.33', '2.52') @validation.query_schema(schema.index_query_v253, '2.53') + @validation.response_body_schema(schema.detail_response, '2.1', '2.26') + @validation.response_body_schema(schema.detail_response_v227, '2.27', '2.32') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v233, '2.33', '2.52') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v253, '2.53', '2.87') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v288, '2.88') def detail(self, req): """List hypervisors with extra details. @@ -307,6 +312,10 @@ class HypervisorsController(wsgi.Controller): @wsgi.expected_errors((400, 404), '2.53') @validation.query_schema(schema.show_query, '2.1', '2.52') @validation.query_schema(schema.show_query_v253, '2.53') + @validation.response_body_schema(schema.show_response, '2.1', '2.26') + @validation.response_body_schema(schema.show_response_v227, '2.27', '2.52') + @validation.response_body_schema(schema.show_response_v253, '2.53', '2.87') + @validation.response_body_schema(schema.show_response_v288, '2.88') def show(self, req, id): """Show a hypervisor. @@ -365,6 +374,8 @@ class HypervisorsController(wsgi.Controller): @wsgi.api_version('2.1', '2.87') @wsgi.expected_errors((400, 404, 501)) @validation.query_schema(schema.uptime_query) + @validation.response_body_schema(schema.uptime_response, '2.1', '2.52') + @validation.response_body_schema(schema.uptime_response_v253, '2.53', '2.87') # noqa: E501 def uptime(self, req, id): """Prior to microversion 2.88, you could retrieve a special version of the hypervisor detail view that included uptime. Starting in 2.88, this diff --git a/nova/api/openstack/compute/schemas/hypervisors.py b/nova/api/openstack/compute/schemas/hypervisors.py index ec8ee37e50..daa30fdce8 100644 --- a/nova/api/openstack/compute/schemas/hypervisors.py +++ b/nova/api/openstack/compute/schemas/hypervisors.py @@ -104,6 +104,95 @@ _hypervisor_response_v253['properties'].update({ 'servers': copy.deepcopy(_servers_response), }) +_hypervisor_detail_response = copy.deepcopy(_hypervisor_response) +_hypervisor_detail_response['properties'].update({ + 'cpu_info': {'type': 'string'}, + 'current_workload': {'type': 'integer'}, + 'disk_available_least': {'type': 'integer'}, + 'free_disk_gb': {'type': 'integer'}, + 'free_ram_mb': {'type': 'integer'}, + 'host_ip': {'type': 'string'}, + 'hypervisor_type': {'type': 'string'}, + 'hypervisor_version': {'type': ['string', 'integer']}, + 'local_gb': {'type': 'integer'}, + 'local_gb_used': {'type': 'integer'}, + 'memory_mb': {'type': 'integer'}, + 'memory_mb_used': {'type': 'integer'}, + 'running_vms': {'type': 'integer'}, + 'service': { + 'type': 'object', + 'properties': { + 'disabled_reason': {'type': ['null', 'string']}, + 'host': {'type': 'string'}, + 'id': {'type': 'integer'}, + }, + 'required': ['disabled_reason', 'host', 'id'], + }, + 'vcpus': {'type': 'integer'}, + 'vcpus_used': {'type': 'integer'}, +}) +_hypervisor_detail_response['required'].extend([ + 'cpu_info', + 'current_workload', + 'free_disk_gb', + 'free_ram_mb', + 'local_gb', + 'local_gb_used', + 'memory_mb', + 'memory_mb_used', + 'running_vms', + 'service', + 'vcpus', + 'vcpus_used', +]) + +_hypervisor_detail_response_v227 = copy.deepcopy(_hypervisor_detail_response) +_hypervisor_detail_response_v227['properties'].update({ + 'cpu_info': { + # NOTE(stephenfin): This is virt-driver specific hence no schema + 'type': 'object', + 'properties': {}, + 'required': [], + 'additionalProperties': True, + }, +}) + +_hypervisor_detail_response_v253 = copy.deepcopy( + _hypervisor_detail_response_v227 +) +_hypervisor_detail_response_v253['properties'].update( + _hypervisor_response_v253['properties'] +) +_hypervisor_detail_response_v253['properties']['service'][ + 'properties' +].update({ + 'id': {'type': 'string', 'format': 'uuid'}, +}) + +_hypervisor_detail_response_v288 = copy.deepcopy( + _hypervisor_detail_response_v253 +) +for field in { + 'cpu_info', + 'current_workload', + 'free_disk_gb', + 'free_ram_mb', + 'local_gb', + 'local_gb_used', + 'memory_mb', + 'memory_mb_used', + 'running_vms', + 'vcpus', + 'vcpus_used', +}: + del _hypervisor_detail_response_v288['properties'][field] + _hypervisor_detail_response_v288['required'].remove(field) + +_hypervisor_detail_response_v288['properties'].update({ + 'uptime': {'type': ['string', 'null']} +}) +_hypervisor_detail_response_v288['required'].append('uptime') + index_response = { 'type': 'object', 'properties': { @@ -137,3 +226,82 @@ servers_response = copy.deepcopy(index_response) servers_response['properties']['hypervisors']['items']['properties'].update({ 'servers': copy.deepcopy(_servers_response), }) + +detail_response = copy.deepcopy(index_response) +detail_response['properties']['hypervisors'][ + 'items' +] = _hypervisor_detail_response + +# v2.27 changes the 'cpu_info' field from a stringified object to a real object +detail_response_v227 = copy.deepcopy(detail_response) +detail_response_v227['properties']['hypervisors'][ + 'items' +] = _hypervisor_detail_response_v227 + +# v2.33 adds the hypervisors_links field +detail_response_v233 = copy.deepcopy(detail_response_v227) +detail_response_v233['properties'].update({ + 'hypervisors_links': response_types.collection_links, +}) + +# v2.53 adds the 'servers' field but only if a user requests it via the +# 'with_servers' query arg. It also changes the 'id' field to a UUID. Note that +# v2.75 makes the 'servers' property always present even if empty, but that's +# not something we can capture with jsonschema so we don't try +detail_response_v253 = copy.deepcopy(detail_response_v233) +detail_response_v253['properties']['hypervisors'][ + 'items' +] = _hypervisor_detail_response_v253 + +# v2.88 drops a whole lot of fields that were now duplicated in placement. It +# also adds the uptime field into the response rather than a separate API +detail_response_v288 = copy.deepcopy(detail_response_v253) +detail_response_v288['properties']['hypervisors'][ + 'items' +] = _hypervisor_detail_response_v288 + +show_response = { + 'type': 'object', + 'properties': { + 'hypervisor': copy.deepcopy(_hypervisor_detail_response), + }, + 'required': ['hypervisor'], + 'additionalProperties': False, +} + +show_response_v227 = copy.deepcopy(show_response) +show_response_v227['properties']['hypervisor'] = copy.deepcopy( + _hypervisor_detail_response_v227 +) + +show_response_v253 = copy.deepcopy(show_response_v227) +show_response_v253['properties']['hypervisor'] = copy.deepcopy( + _hypervisor_detail_response_v253 +) + +show_response_v288 = copy.deepcopy(show_response_v253) +show_response_v288['properties']['hypervisor'] = copy.deepcopy( + _hypervisor_detail_response_v288 +) + +uptime_response = { + 'type': 'object', + 'properties': { + 'hypervisor': copy.deepcopy(_hypervisor_response), + }, + 'required': ['hypervisor'], + 'additionalProperties': False, +} +uptime_response['properties']['hypervisor']['properties'].update({ + 'uptime': {'type': ['string', 'null']} +}) +uptime_response['properties']['hypervisor']['required'].append('uptime') + +uptime_response_v253 = copy.deepcopy(uptime_response) +uptime_response_v253['properties']['hypervisor'] = copy.deepcopy( + _hypervisor_response_v253 +) +uptime_response_v253['properties']['hypervisor']['properties'].update({ + 'uptime': {'type': ['string', 'null']} +}) +uptime_response_v253['properties']['hypervisor']['required'].append('uptime') diff --git a/nova/tests/unit/policies/test_hypervisors.py b/nova/tests/unit/policies/test_hypervisors.py index dd17ebe2fe..e4d4764855 100644 --- a/nova/tests/unit/policies/test_hypervisors.py +++ b/nova/tests/unit/policies/test_hypervisors.py @@ -10,9 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from unittest import mock from nova.api.openstack.compute import hypervisors +from nova import objects from nova.policies import base as base_policy from nova.policies import hypervisors as hv_policies from nova.tests.unit.api.openstack import fakes @@ -33,8 +35,43 @@ class HypervisorsPolicyTest(base.BasePolicyTest): self.req = fakes.HTTPRequest.blank('') self.controller._get_compute_nodes_by_name_pattern = mock.MagicMock() self.controller.host_api.compute_node_get_all = mock.MagicMock() - self.controller.host_api.service_get_by_compute_host = mock.MagicMock() - self.controller.host_api.compute_node_get = mock.MagicMock() + self.controller.host_api.service_get_by_compute_host = mock.MagicMock( + return_value=objects.Service( + id=123, + binary='nova-compute', + created_at=datetime.datetime(2012, 9, 18, 2, 46, 27), + disabled=False, + disabled_reason=None, + forced_down=False, + host='foo', + last_seen_up=None, + ) + ) + self.controller.host_api.compute_node_get = mock.MagicMock( + return_value=objects.ComputeNode( + id=123, + cpu_info='{}', + current_workload=1, + disk_available_least=10, + free_disk_gb=50, + free_ram_mb=512, + host='foo', + host_ip='1.2.3.4', + hypervisor_hostname='foo', + hypervisor_type='libvirt', + hypervisor_version='123', + local_gb=1000, + local_gb_used=10, + memory_mb=8192, + memory_mb_used=1024, + running_vms=1, + vcpus=16, + vcpus_used=8, + ), + ) + self.controller.host_api.get_host_uptime = mock.MagicMock( + return_value=None + ) # With legacy rule and scope check disabled by default, system admin, # legacy admin, and project admin will be able to perform hypervisors @@ -59,26 +96,26 @@ class HypervisorsPolicyTest(base.BasePolicyTest): rule_name = hv_policies.BASE_POLICY_NAME % 'show' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.show, - self.req, 11111) + self.req, '123') @mock.patch('nova.compute.api.HostAPI.get_host_uptime') def test_uptime_hypervisors_policy(self, mock_uptime): rule_name = hv_policies.BASE_POLICY_NAME % 'uptime' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.uptime, - self.req, 11111) + self.req, '123') def test_search_hypervisors_policy(self): rule_name = hv_policies.BASE_POLICY_NAME % 'search' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.search, - self.req, 11111) + self.req, '123') def test_servers_hypervisors_policy(self): rule_name = hv_policies.BASE_POLICY_NAME % 'servers' self.common_policy_auth(self.project_admin_authorized_contexts, rule_name, self.controller.servers, - self.req, 11111) + self.req, '123') @mock.patch('nova.compute.api.HostAPI.compute_node_statistics') def test_statistics_hypervisors_policy(self, mock_statistics):