Add server sub-resource topology API

Add a new server topology API to show server NUMA information:
  - GET /servers/{server_id}/topology

Add new policy to control the default behavior:
  - compute:server:topology:index
  - compute:server:topology:host:index

Change-Id: Ie647ef96597195b0ef00f77cece16c2bef8a78d4
Implements: blueprint show-server-numa-topology
Signed-off-by: Yongli He <yongli.he@intel.com>
This commit is contained in:
Yongli He 2019-02-26 06:57:52 +08:00
parent eb6fcb2191
commit 3dcb404b1f
19 changed files with 620 additions and 3 deletions

View File

@ -53,6 +53,7 @@ the `API guide <https://docs.openstack.org/api-guide/compute/index.html>`_.
.. include:: os-services.inc
.. include:: os-simple-tenant-usage.inc
.. include:: os-server-external-events.inc
.. include:: server-topology.inc
===============
Deprecated APIs

View File

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

View File

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

View File

@ -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
}

View File

@ -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
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.77",
"version": "2.78",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.77",
"version": "2.78",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

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

View File

@ -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``

View File

@ -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']
}),
)

View File

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

View File

@ -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(),

View File

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

View File

@ -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
}

View File

@ -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
}

View File

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

View File

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

View File

@ -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",

View File

@ -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``