diff --git a/api-ref/source/index.rst b/api-ref/source/index.rst index f3597cb46a0a..b83346f4a721 100644 --- a/api-ref/source/index.rst +++ b/api-ref/source/index.rst @@ -53,6 +53,7 @@ the `API guide `_. .. include:: os-services.inc .. include:: os-simple-tenant-usage.inc .. include:: os-server-external-events.inc +.. include:: server-topology.inc =============== Deprecated APIs diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 82f9ef8801ea..150691f890c8 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -6354,6 +6354,77 @@ server_tags_create: required: false type: array min_version: 2.52 +server_topology_nodes: + description: | + NUMA nodes information of a server. + in: body + required: true + type: array +server_topology_nodes_cpu_pinning: + description: | + The mapping of server cores to host physical CPU. for example:: + + cpu_pinning: { 0: 0, 1: 5} + + This means vcpu 0 is mapped to physical CPU 0, and vcpu 1 is mapped + physical CPU 5. + + By default the ``cpu_pinning`` field is only visible to users with the + administrative role. You can change the default behavior via the policy + rule:: + + compute:server:topology:host:index + in: body + required: false + type: dict +server_topology_nodes_cpu_siblings: + description: | + A mapping of host cpus thread sibling. For example:: + + siblings: [[0,1],[2,3]] + + This means vcpu 0 and vcpu 1 belong to same CPU core, vcpu 2, vcpu 3 + belong to another CPU core. + + By default the ``siblings`` field is only visible to users with the + administrative role. You can change the default behavior via the policy + rule:: + + compute:server:topology:host:index + in: body + required: false + type: list +server_topology_nodes_host_numa_node: + description: | + The host NUMA node the virtual NUMA node is map to. + + By default the ``host_numa_node`` field is only visible to users with the + administrator role. You can change the default behavior via the policy + rule:: + + compute:server:topology:host:index + in: body + required: false + type: integer +server_topology_nodes_memory_mb: + description: | + The amount of memory assigned to this NUMA node in MB. + in: body + required: false + type: integer +server_topology_nodes_vcpu_set: + description: | + A list of IDs of the virtual CPU assigned to this NUMA node. + in: body + required: false + type: list +server_topology_pagesize_kb: + description: | + The page size in KB of a server. This field is ``null`` if the + page size information is not available. + in: body + required: true + type: integer server_trusted_image_certificates_create_req: description: | A list of trusted certificate IDs, which are used during image diff --git a/api-ref/source/server-topology.inc b/api-ref/source/server-topology.inc new file mode 100644 index 000000000000..1f2da300d1b8 --- /dev/null +++ b/api-ref/source/server-topology.inc @@ -0,0 +1,52 @@ +.. -*- rst -*- + +===================================== +Servers Topology (servers, topology) +===================================== + +Shows the NUMA topology information for a server. + +Show Server Topology +==================== + +.. rest_method:: GET /servers/{server_id}/topology +.. versionadded:: 2.78 + +Shows NUMA topology information for a server. + +Policy defaults enable only users with the administrative role or the owners +of the server to perform this operation. Cloud providers can change these +permissions through the ``policy.json`` file. + +Normal response codes: 200 + +Error response codes: unauthorized(401), notfound(404), forbidden(403) + +Request +------- + +.. rest_parameters:: parameters.yaml + + - server_id: server_id_path + +Response +-------- + +All response fields are listed below. If some information is not available or +not allow by policy, the corresponding key value will not exist in response. + +.. rest_parameters:: parameters.yaml + + - nodes: server_topology_nodes + - nodes.cpu_pinning: server_topology_nodes_cpu_pinning + - nodes.vcpu_set: server_topology_nodes_vcpu_set + - nodes.siblings: server_topology_nodes_cpu_siblings + - nodes.memory_mb: server_topology_nodes_memory_mb + - nodes.host_numa_node: server_topology_nodes_host_numa_node + - pagesize_kb: server_topology_pagesize_kb + +**Example Server topology (2.xx)** + +.. literalinclude:: ../../doc/api_samples/os-server-topology/v2.78/servers-topology-resp.json + :language: javascript + diff --git a/doc/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json b/doc/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json new file mode 100644 index 000000000000..0d3677c6c4d2 --- /dev/null +++ b/doc/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json @@ -0,0 +1,31 @@ +{ + "nodes": [ + { + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} diff --git a/doc/api_samples/os-server-topology/v2.78/servers-topology-resp.json b/doc/api_samples/os-server-topology/v2.78/servers-topology-resp.json new file mode 100644 index 000000000000..a918a2ade59d --- /dev/null +++ b/doc/api_samples/os-server-topology/v2.78/servers-topology-resp.json @@ -0,0 +1,41 @@ +{ + "nodes": [ + { + "cpu_pinning": { + "0": 0, + "1": 5 + }, + "host_node": 0, + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "cpu_pinning": { + "2": 1, + "3": 8 + }, + "host_node": 1, + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 7e16157149eb..8ca415c77045 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.77", + "version": "2.78", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 81d4d94fe94c..a475df645c20 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.77", + "version": "2.78", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 77917a4fc2f6..c37c098c729c 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -203,6 +203,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: ``GET /servers/{server_id}/os-instance-actions/{request_id}``. * 2.77 - Add support for specifying ``availability_zone`` to unshelve of a shelved offload server. + * 2.78 - Adds new API ``GET /servers/{server_id}/topology`` which shows + NUMA topology of a given server. """ # The minimum and maximum versions of the API supported @@ -211,7 +213,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.77" +_MAX_API_VERSION = "2.78" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 4bdc30c1f76a..58ebfb833e0b 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -998,3 +998,15 @@ through ``GET /servers/{server_id}/os-instance-actions`` and ---- API microversion 2.77 adds support for specifying availability zone when unshelving a shelved offloaded server. + +2.78 +---- + +Add server sub-resource ``topology`` to show server NUMA information. + +* ``GET /servers/{server_id}/topology`` + +The default behavior is configurable using two new policies: + +* ``compute:server:topology:index`` +* ``compute:server:topology:host:index`` diff --git a/nova/api/openstack/compute/routes.py b/nova/api/openstack/compute/routes.py index ecab9bed5294..763db39d9290 100644 --- a/nova/api/openstack/compute/routes.py +++ b/nova/api/openstack/compute/routes.py @@ -75,6 +75,7 @@ from nova.api.openstack.compute import server_metadata from nova.api.openstack.compute import server_migrations from nova.api.openstack.compute import server_password from nova.api.openstack.compute import server_tags +from nova.api.openstack.compute import server_topology from nova.api.openstack.compute import servers from nova.api.openstack.compute import services from nova.api.openstack.compute import shelve @@ -316,6 +317,8 @@ server_security_groups_controller = functools.partial(_create_controller, server_tags_controller = functools.partial(_create_controller, server_tags.ServerTagsController, []) +server_topology_controller = functools.partial(_create_controller, + server_topology.ServerTopologyController, []) server_volume_attachments_controller = functools.partial(_create_controller, volumes.VolumeAttachmentController, []) @@ -832,6 +835,9 @@ ROUTE_LIST = ( 'PUT': [server_tags_controller, 'update'], 'DELETE': [server_tags_controller, 'delete'] }), + ('/servers/{server_id}/topology', { + 'GET': [server_topology_controller, 'index'] + }), ) diff --git a/nova/api/openstack/compute/server_topology.py b/nova/api/openstack/compute/server_topology.py new file mode 100644 index 000000000000..d6e02750b00b --- /dev/null +++ b/nova/api/openstack/compute/server_topology.py @@ -0,0 +1,73 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import common +from nova.api.openstack import wsgi +from nova.compute import api as compute +from nova.policies import server_topology as st_policies + + +class ServerTopologyController(wsgi.Controller): + + def __init__(self, *args, **kwargs): + super(ServerTopologyController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + @wsgi.Controller.api_version("2.78") + @wsgi.expected_errors((404)) + def index(self, req, server_id): + context = req.environ["nova.context"] + context.can(st_policies.BASE_POLICY_NAME % 'index') + host_policy = (st_policies.BASE_POLICY_NAME % 'host:index') + show_host_info = context.can(host_policy, fatal=False) + + instance = common.get_instance(self.compute_api, context, server_id, + expected_attrs=['numa_topology', + 'vcpu_model']) + + return self._get_numa_topology(context, instance, show_host_info) + + def _get_numa_topology(self, context, instance, show_host_info): + + if instance.numa_topology is None: + return { + 'nodes': [], + 'pagesize_kb': None + } + + topo = {} + cells = [] + pagesize_kb = None + + for cell_ in instance.numa_topology.cells: + cell = {} + cell['vcpu_set'] = cell_.cpuset + cell['siblings'] = cell_.siblings + cell['memory_mb'] = cell_.memory + + if show_host_info: + cell['host_node'] = cell_.id + if cell_.cpu_pinning is None: + cell['cpu_pinning'] = {} + else: + cell['cpu_pinning'] = cell_.cpu_pinning + + if cell_.pagesize: + pagesize_kb = cell_.pagesize + + cells.append(cell) + + topo['nodes'] = cells + topo['pagesize_kb'] = pagesize_kb + + return topo diff --git a/nova/policies/__init__.py b/nova/policies/__init__.py index b31b350fead3..d82ad6d1ab5f 100644 --- a/nova/policies/__init__.py +++ b/nova/policies/__init__.py @@ -61,6 +61,7 @@ from nova.policies import server_groups from nova.policies import server_metadata from nova.policies import server_password from nova.policies import server_tags +from nova.policies import server_topology from nova.policies import servers from nova.policies import servers_migrations from nova.policies import services @@ -123,6 +124,7 @@ def list_rules(): server_metadata.list_rules(), server_password.list_rules(), server_tags.list_rules(), + server_topology.list_rules(), servers.list_rules(), servers_migrations.list_rules(), services.list_rules(), diff --git a/nova/policies/server_topology.py b/nova/policies/server_topology.py new file mode 100644 index 000000000000..8e7152d2ea5c --- /dev/null +++ b/nova/policies/server_topology.py @@ -0,0 +1,48 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_policy import policy + +from nova.policies import base + + +BASE_POLICY_NAME = 'compute:server:topology:%s' + +server_topology_policies = [ + policy.DocumentedRuleDefault( + BASE_POLICY_NAME % 'index', + base.RULE_ADMIN_OR_OWNER, + "Show the NUMA topology data for a server", + [ + { + 'method': 'GET', + 'path': '/servers/{server_id}/topology' + } + ]), + policy.DocumentedRuleDefault( + # Control host NUMA node and cpu pinning information + BASE_POLICY_NAME % 'host:index', + base.RULE_ADMIN_API, + "Show the NUMA topology data for a server with host NUMA ID and CPU " + "pinning information", + [ + { + 'method': 'GET', + 'path': '/servers/{server_id}/topology' + } + ]), +] + + +def list_rules(): + return server_topology_policies diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json.tpl new file mode 100644 index 000000000000..05e278088e90 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp-user.json.tpl @@ -0,0 +1,31 @@ +{ + "nodes": [ + { + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp.json.tpl new file mode 100644 index 000000000000..a918a2ade59d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-topology/v2.78/servers-topology-resp.json.tpl @@ -0,0 +1,41 @@ +{ + "nodes": [ + { + "cpu_pinning": { + "0": 0, + "1": 5 + }, + "host_node": 0, + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "cpu_pinning": { + "2": 1, + "3": 8 + }, + "host_node": 1, + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} diff --git a/nova/tests/functional/api_sample_tests/test_server_topology.py b/nova/tests/functional/api_sample_tests/test_server_topology.py new file mode 100644 index 000000000000..dca79441e363 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/test_server_topology.py @@ -0,0 +1,67 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.objects import instance_numa as numa +from nova.objects import virt_cpu_topology as cpu_topo +from nova.tests.functional.api_sample_tests import test_servers + + +def fake_get_numa(): + cpu_info = {'sockets': 2, 'cores': 1, 'threads': 2} + cpu_topology = cpu_topo.VirtCPUTopology.from_dict(cpu_info) + cell_0 = numa.InstanceNUMACell(node=0, memory=1024, pagesize=4, id=0, + cpu_topology=cpu_topology, + cpu_pinning={0: 0, 1: 5}, + cpuset=set([0, 1])) + + cell_1 = numa.InstanceNUMACell(node=1, memory=2048, pagesize=4, id=1, + cpu_topology=cpu_topology, + cpu_pinning={2: 1, 3: 8}, + cpuset=set([2, 3])) + + return numa.InstanceNUMATopology(cells=[cell_0, cell_1]) + + +class ServerTopologySamplesJson(test_servers.ServersSampleBase): + microversion = '2.78' + scenarios = [('v2_78', {'api_major_version': 'v2.1'})] + sample_dir = "os-server-topology" + + def setUp(self): + super(ServerTopologySamplesJson, self).setUp() + + def _load_numa(self, *args, **argv): + self.numa_topology = fake_get_numa() + + self.stub_out('nova.objects.instance.Instance._load_numa_topology', + _load_numa) + + +class ServerTopologySamplesJsonTestV278_Admin(ServerTopologySamplesJson): + ADMIN_API = True + + def test_get_servers_topology_admin(self): + uuid = self._post_server() + response = self._do_get('servers/%s/topology' % uuid) + self._verify_response( + 'servers-topology-resp', {}, response, 200) + + +class ServerTopologySamplesJsonTestV278(ServerTopologySamplesJson): + ADMIN_API = False + + def test_get_servers_topology_user(self): + uuid = self._post_server() + response = self._do_get('servers/%s/topology' % uuid) + self._verify_response( + 'servers-topology-resp-user', {}, response, 200) diff --git a/nova/tests/unit/api/openstack/compute/test_server_topology.py b/nova/tests/unit/api/openstack/compute/test_server_topology.py new file mode 100644 index 000000000000..603f961b66ae --- /dev/null +++ b/nova/tests/unit/api/openstack/compute/test_server_topology.py @@ -0,0 +1,116 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_utils.fixture import uuidsentinel as uuids +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack.compute import server_topology +from nova import exception +from nova import objects +from nova.objects import instance_numa as numa +from nova import test +from nova.tests.unit.api.openstack import fakes + + +class ServerTopologyTestV278(test.NoDBTestCase): + mock_method = 'get_by_instance_uuid' + api_version = '2.78' + + def setUp(self): + super(ServerTopologyTestV278, self).setUp() + self.uuid = uuids.instance + self.req = fakes.HTTPRequest.blank( + '/v2/fake/servers/%s/topology' % self.uuid, + version=self.api_version, + use_admin_context=True) + self.controller = server_topology.ServerTopologyController() + + def _fake_numa(self, cpu_pinning=None): + ce0 = numa.InstanceNUMACell(node=0, memory=1024, pagesize=4, id=0, + cpu_topology=None, + cpu_pinning=cpu_pinning, + cpuset=set([0, 1])) + + return numa.InstanceNUMATopology(cells=[ce0]) + + @mock.patch.object(common, 'get_instance', + side_effect=exc.HTTPNotFound('Fake')) + def test_get_topology_with_invalid_instance(self, mock_get): + excep = self.assertRaises(exc.HTTPNotFound, + self.controller.index, + self.req, + self.uuid) + self.assertEqual("Fake", str(excep)) + + @mock.patch.object(common, 'get_instance') + def test_get_topology_with_no_topology(self, fake_get): + expect = {'nodes': [], 'pagesize_kb': None} + inst = objects.instance.Instance(uuid=self.uuid, host='123') + inst.numa_topology = None + fake_get.return_value = inst + + output = self.controller.index(self.req, self.uuid) + self.assertEqual(expect, output) + + @mock.patch.object(common, 'get_instance') + def test_get_topology_cpu_pinning_with_none(self, fake_get): + expect = {'nodes': [ + {'memory_mb': 1024, + 'siblings': [], + 'vcpu_set': set([0, 1]), + 'host_node': 0, + 'cpu_pinning':{}}], + 'pagesize_kb': 4} + + inst = objects.instance.Instance(uuid=self.uuid, host='123') + inst.numa_topology = self._fake_numa(cpu_pinning=None) + fake_get.return_value = inst + + output = self.controller.index(self.req, self.uuid) + self.assertEqual(expect, output) + + inst.numa_topology = self._fake_numa(cpu_pinning={}) + fake_get.return_value = inst + output = self.controller.index(self.req, self.uuid) + self.assertEqual(expect, output) + + def test_hit_topology_before278(self): + req = fakes.HTTPRequest.blank( + '/v2/fake/servers/%s/topology' % self.uuid, + version='2.77') + excep = self.assertRaises(exception.VersionNotFoundForAPIMethod, + self.controller.index, + req, + self.uuid) + self.assertEqual(400, excep.code) + + +class ServerTopologyEnforcementV278(test.NoDBTestCase): + api_version = '2.78' + + def setUp(self): + super(ServerTopologyEnforcementV278, self).setUp() + self.controller = server_topology.ServerTopologyController() + self.req = fakes.HTTPRequest.blank('', version=self.api_version) + + def test_get_topology_policy_failed(self): + rule_name = "compute:server:topology:index" + self.policy.set_rules({rule_name: "project:non_fake"}) + exc = self.assertRaises( + exception.PolicyNotAuthorized, + self.controller.index, self.req, fakes.FAKE_UUID) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index 952c47ebdb3a..d80328d4d814 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -288,6 +288,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase): self.fake_policy = jsonutils.loads(fake_policy.policy_data) self.admin_only_rules = ( +"compute:server:topology:host:index", "network:attach_external_network", "os_compute_api:servers:create:forced_host", "compute:servers:create:requested_destination", @@ -351,6 +352,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase): ) self.admin_or_owner_rules = ( +"compute:server:topology:index", "os_compute_api:servers:start", "os_compute_api:servers:stop", "os_compute_api:servers:trigger_crash_dump", diff --git a/releasenotes/notes/add-server-subresource-topology-c52e21f36497e62c.yaml b/releasenotes/notes/add-server-subresource-topology-c52e21f36497e62c.yaml new file mode 100644 index 000000000000..fa01b87f4d4b --- /dev/null +++ b/releasenotes/notes/add-server-subresource-topology-c52e21f36497e62c.yaml @@ -0,0 +1,21 @@ +--- +features: + - | + Microversion 2.78 adds a new ``topology`` sub-resource to the + servers API: + + - ``GET /servers/{server_id}/topology`` + + This API provides information about the NUMA topology of a server, + including instance to host CPU pin mappings, if CPU pinning is used, and + pagesize information. + + The information exposed by this API is admin or owner only by default, + controlled by rule: + + - ``compute:server:topology:index`` + + And following fine control policy use to keep host only information to + admin: + + - ``compute:server:topology:host:index``