Microversion 2.33 adds pagination support for hypervisors

When there are thousands of compute nodes, it would be slow to get the
whole hypervisor list, and it is bad for user experience to display
thousands of items in a table in horizon. This patch is proposed to
support pagination for hypervisor by adding `limit` and `marker` to
the list api.

Implements blueprint: pagination-for-hypervisor

Change-Id: Ie7f8b5c733b383f3e69fa23188e56257e503b5f7
This commit is contained in:
liyingjun 2016-06-03 13:16:36 +08:00
parent 06fcb47995
commit ec53c6c0ec
24 changed files with 500 additions and 28 deletions

View File

@ -23,6 +23,14 @@ Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403)
Request
-------
.. rest_parameters:: parameters.yaml
- limit: hypervisor_limit
- marker: hypervisor_marker
Response
--------
@ -52,6 +60,14 @@ Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403)
Request
-------
.. rest_parameters:: parameters.yaml
- limit: hypervisor_limit
- marker: hypervisor_marker
Response
--------

View File

@ -402,6 +402,25 @@ host_query:
in: query
required: false
type: string
hypervisor_limit:
description: |
Requests a page size of items. Returns a number of items up to a limit value.
Use the ``limit`` parameter to make an initial limited request and use the ID
of the last-seen item from the response as the ``marker`` parameter value in a
subsequent limited request.
in: query
required: false
type: integer
min_version: 2.33
hypervisor_marker:
description: |
The ID of the last-seen item. Use the ``limit`` parameter to make an initial limited
request and use the ID of the last-seen item from the response as the ``marker``
parameter value in a subsequent limited request.
in: query
required: false
type: string
min_version: 2.33
hypervisor_query:
description: |
Filters the response by a hypervisor type.

View File

@ -0,0 +1,49 @@
{
"hypervisors": [
{
"cpu_info": {
"arch": "x86_64",
"model": "Nehalem",
"vendor": "Intel",
"features": [
"pge",
"clflush"
],
"topology": {
"cores": 1,
"threads": 1,
"sockets": 4
}
},
"current_workload": 0,
"status": "enabled",
"state": "up",
"disk_available_least": 0,
"host_ip": "1.1.1.1",
"free_disk_gb": 1028,
"free_ram_mb": 7680,
"hypervisor_hostname": "fake-mini",
"hypervisor_type": "fake",
"hypervisor_version": 1000,
"id": 2,
"local_gb": 1028,
"local_gb_used": 0,
"memory_mb": 8192,
"memory_mb_used": 512,
"running_vms": 0,
"service": {
"host": "host1",
"id": 7,
"disabled_reason": null
},
"vcpus": 1,
"vcpus_used": 0
}
],
"hypervisors_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors/detail?limit=1&marker=2",
"rel": "next"
}
]
}

View File

@ -0,0 +1,16 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": 2,
"state": "up",
"status": "enabled"
}
],
"hypervisors_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors?limit=1&marker=2",
"rel": "next"
}
]
}

View File

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

View File

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

View File

@ -82,6 +82,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.31 - Fix os-console-auth-tokens to work for all console types.
* 2.32 - Add tag to networks and block_device_mapping_v2 in server boot
request body.
* 2.33 - Add pagination support for hypervisors.
"""
# The minimum and maximum versions of the API supported
@ -90,7 +91,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.32"
_MAX_API_VERSION = "2.33"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -20,6 +20,7 @@ import webob.exc
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack.compute.views import hypervisors as hyper_view
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova import compute
@ -35,6 +36,8 @@ ALIAS = "os-hypervisors"
class HypervisorsController(wsgi.Controller):
"""The Hypervisors API controller for the OpenStack API."""
_view_builder_class = hyper_view.ViewBuilder
def __init__(self):
self.host_api = compute.HostAPI()
self.servicegroup_api = servicegroup.API()
@ -80,28 +83,76 @@ class HypervisorsController(wsgi.Controller):
return hyp_dict
@wsgi.Controller.api_version("2.33") # noqa
@extensions.expected_errors((400))
def index(self, req):
limit, marker = common.get_limit_and_marker(req)
return self._index(req, limit=limit, marker=marker, links=True)
@wsgi.Controller.api_version("2.1", "2.32") # noqa
@extensions.expected_errors(())
def index(self, req):
return self._index(req)
def _index(self, req, limit=None, marker=None, links=False):
context = req.environ['nova.context']
context.can(hv_policies.BASE_POLICY_NAME)
compute_nodes = self.host_api.compute_node_get_all(context)
req.cache_db_compute_nodes(compute_nodes)
return dict(hypervisors=[self._view_hypervisor(
hyp,
self.host_api.service_get_by_compute_host(
context, hyp.host),
False, req)
for hyp in compute_nodes])
try:
compute_nodes = self.host_api.compute_node_get_all(
context, limit=limit, marker=marker)
except exception.MarkerNotFound:
msg = _('marker [%s] not found') % marker
raise webob.exc.HTTPBadRequest(explanation=msg)
req.cache_db_compute_nodes(compute_nodes)
hypervisors_list = [self._view_hypervisor(
hyp,
self.host_api.service_get_by_compute_host(
context, hyp.host),
False, req)
for hyp in compute_nodes]
hypervisors_dict = dict(hypervisors=hypervisors_list)
if links:
hypervisors_links = self._view_builder.get_links(req,
hypervisors_list)
if hypervisors_links:
hypervisors_dict['hypervisors_links'] = hypervisors_links
return hypervisors_dict
@wsgi.Controller.api_version("2.33") # noqa
@extensions.expected_errors((400))
def detail(self, req):
limit, marker = common.get_limit_and_marker(req)
return self._detail(req, limit=limit, marker=marker, links=True)
@wsgi.Controller.api_version("2.1", "2.32") # noqa
@extensions.expected_errors(())
def detail(self, req):
return self._detail(req)
def _detail(self, req, limit=None, marker=None, links=False):
context = req.environ['nova.context']
context.can(hv_policies.BASE_POLICY_NAME)
compute_nodes = self.host_api.compute_node_get_all(context)
try:
compute_nodes = self.host_api.compute_node_get_all(
context, limit=limit, marker=marker)
except exception.MarkerNotFound:
msg = _('marker [%s] not found') % marker
raise webob.exc.HTTPBadRequest(explanation=msg)
req.cache_db_compute_nodes(compute_nodes)
return dict(hypervisors=[self._view_hypervisor(
hypervisors_list = [
self._view_hypervisor(
hyp, self.host_api.service_get_by_compute_host(context, hyp.host),
True, req) for hyp in compute_nodes])
True, req) for hyp in compute_nodes]
hypervisors_dict = dict(hypervisors=hypervisors_list)
if links:
hypervisors_links = self._view_builder.get_links(
req, hypervisors_list, detail=True)
if hypervisors_links:
hypervisors_dict['hypervisors_links'] = hypervisors_links
return hypervisors_dict
@extensions.expected_errors(404)
def show(self, req, id):

View File

@ -0,0 +1,26 @@
# Copyright 2016 Kylin Cloud
# All Rights Reserved.
#
# 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
class ViewBuilder(common.ViewBuilder):
_collection_name = "hypervisors"
def get_links(self, request, hypervisors, detail=False):
coll_name = (self._collection_name + '/detail' if detail else
self._collection_name)
return self._get_collection_links(request, hypervisors, coll_name,
'id')

View File

@ -335,3 +335,11 @@ user documentation.
config drive. For example, a network interface on the virtual PCI bus tagged
with 'nic1' will appear in the metadata along with its bus (PCI), bus address
(ex: 0000:00:02.0), MAC address, and tag ('nic1').
2.33
----
Support pagination for hypervisor by accepting limit and marker from the GET
API request::
GET /v2.1/{tenant_id}/os-hypervisors?marker={hypervisor_id}&limit={limit}

View File

@ -3666,8 +3666,9 @@ class HostAPI(base.Base):
"""Return compute node entry for particular integer ID."""
return objects.ComputeNode.get_by_id(context, int(compute_id))
def compute_node_get_all(self, context):
return objects.ComputeNodeList.get_all(context)
def compute_node_get_all(self, context, limit=None, marker=None):
return objects.ComputeNodeList.get_by_pagination(
context, limit=limit, marker=marker)
def compute_node_search_by_hypervisor(self, context, hypervisor_match):
return objects.ComputeNodeList.get_by_hypervisor(context,

View File

@ -601,7 +601,9 @@ class HostAPI(compute_api.HostAPI):
except exception.CellRoutingInconsistency:
raise exception.ComputeHostNotFound(host=compute_id)
def compute_node_get_all(self, context):
def compute_node_get_all(self, context, limit=None, marker=None):
# NOTE(lyj): No pagination for cells, just make sure the arguments
# for the method are the same with the compute.api for now.
return self.cells_rpcapi.compute_node_get_all(context)
def compute_node_search_by_hypervisor(self, context, hypervisor_match):

View File

@ -244,6 +244,19 @@ def compute_node_get_all(context):
return IMPL.compute_node_get_all(context)
def compute_node_get_all_by_pagination(context, limit=None, marker=None):
"""Get compute nodes by pagination.
:param context: The security context
:param limit: Maximum number of items to return
:param marker: The last item of the previous page, the next results after
this value will be returned
:returns: List of dictionaries each containing compute node properties
"""
return IMPL.compute_node_get_all_by_pagination(context,
limit=limit, marker=marker)
def compute_node_get_all_by_host(context, host):
"""Get compute nodes by host name

View File

@ -564,7 +564,7 @@ def service_update(context, service_id, values):
###################
def _compute_node_select(context, filters=None):
def _compute_node_select(context, filters=None, limit=None, marker=None):
if filters is None:
filters = {}
@ -582,11 +582,22 @@ def _compute_node_select(context, filters=None):
if "hypervisor_hostname" in filters:
hyp_hostname = filters["hypervisor_hostname"]
select = select.where(cn_tbl.c.hypervisor_hostname == hyp_hostname)
if marker is not None:
try:
compute_node_get(context, marker)
except exception.ComputeHostNotFound:
raise exception.MarkerNotFound(marker)
select = select.where(cn_tbl.c.id > marker)
if limit is not None:
select = select.limit(limit)
# Explictly order by id, so we're not dependent on the native sort
# order of the underlying DB.
select = select.order_by(asc("id"))
return select
def _compute_node_fetchall(context, filters=None):
select = _compute_node_select(context, filters)
def _compute_node_fetchall(context, filters=None, limit=None, marker=None):
select = _compute_node_select(context, filters, limit=limit, marker=marker)
engine = get_engine(context)
conn = engine.connect()
@ -648,6 +659,11 @@ def compute_node_get_all(context):
return _compute_node_fetchall(context)
@pick_context_manager_reader
def compute_node_get_all_by_pagination(context, limit=None, marker=None):
return _compute_node_fetchall(context, limit=limit, marker=marker)
@pick_context_manager_reader
def compute_node_search_by_hypervisor(context, hypervisor_match):
field = models.ComputeNode.hypervisor_hostname

View File

@ -452,7 +452,8 @@ class ComputeNodeList(base.ObjectListBase, base.NovaObject):
# Version 1.12 ComputeNode version 1.12
# Version 1.13 ComputeNode version 1.13
# Version 1.14 ComputeNode version 1.14
VERSION = '1.14'
# Version 1.15 ComputeNode version 1.15
VERSION = '1.15'
fields = {
'objects': fields.ListOfObjectsField('ComputeNode'),
}
@ -463,6 +464,13 @@ class ComputeNodeList(base.ObjectListBase, base.NovaObject):
return base.obj_make_list(context, cls(context), objects.ComputeNode,
db_computes)
@base.remotable_classmethod
def get_by_pagination(cls, context, limit=None, marker=None):
db_computes = db.compute_node_get_all_by_pagination(
context, limit=limit, marker=marker)
return base.obj_make_list(context, cls(context), objects.ComputeNode,
db_computes)
@base.remotable_classmethod
def get_by_hypervisor(cls, context, hypervisor_match):
db_computes = db.compute_node_search_by_hypervisor(context,

View File

@ -0,0 +1,49 @@
{
"hypervisors": [
{
"cpu_info": {
"arch": "x86_64",
"model": "Nehalem",
"vendor": "Intel",
"features": [
"pge",
"clflush"
],
"topology": {
"cores": 1,
"threads": 1,
"sockets": 4
}
},
"current_workload": 0,
"state": "up",
"status": "enabled",
"disk_available_least": 0,
"host_ip": "%(ip)s",
"free_disk_gb": 1028,
"free_ram_mb": 7680,
"hypervisor_hostname": "fake-mini",
"hypervisor_type": "fake",
"hypervisor_version": 1000,
"id": %(hypervisor_id)s,
"local_gb": 1028,
"local_gb_used": 0,
"memory_mb": 8192,
"memory_mb_used": 512,
"running_vms": 0,
"service": {
"host": "%(host_name)s",
"id": 7,
"disabled_reason": null
},
"vcpus": 1,
"vcpus_used": 0
}
],
"hypervisors_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors/detail?limit=1&marker=2",
"rel": "next"
}
]
}

View File

@ -0,0 +1,16 @@
{
"hypervisors": [
{
"hypervisor_hostname": "fake-mini",
"id": 2,
"state": "up",
"status": "enabled"
}
],
"hypervisors_links": [
{
"href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/hypervisors?limit=1&marker=2",
"rel": "next"
}
]
}

View File

@ -140,3 +140,30 @@ class HypervisorsSampleJson228Tests(HypervisorsSampleJsonTests):
def setUp(self):
super(HypervisorsSampleJson228Tests, self).setUp()
self.api.microversion = self.microversion
class HypervisorsSampleJson233Tests(api_sample_base.ApiSampleTestBaseV21):
ADMIN_API = True
sample_dir = "os-hypervisors"
microversion = '2.33'
scenarios = [('v2_33', {'api_major_version': 'v2.1'})]
def setUp(self):
super(HypervisorsSampleJson233Tests, self).setUp()
self.api.microversion = self.microversion
# Start a new compute service to fake a record with hypervisor id=2
# for pagination test.
self.start_service('compute', host='host1')
def test_hypervisors_list(self):
response = self._do_get('os-hypervisors?limit=1&marker=1')
self._verify_response('hypervisors-list-resp', {}, response, 200)
def test_hypervisors_detail(self):
subs = {
'hypervisor_id': '2',
'host': 'host1',
'host_name': 'host1'
}
response = self._do_get('os-hypervisors/detail?limit=1&marker=1')
self._verify_response('hypervisors-detail-resp', subs, response, 200)

View File

@ -32,7 +32,7 @@ def fake_compute_node_get(context, compute_id):
raise exception.ComputeHostNotFound(host=compute_id)
def fake_compute_node_get_all(context):
def fake_compute_node_get_all(context, limit=None, marker=None):
return test_hypervisors.TEST_HYPERS_OBJ

View File

@ -108,8 +108,18 @@ TEST_SERVERS = [dict(name="inst1", uuid=uuids.instance_1, host="compute1"),
dict(name="inst4", uuid=uuids.instance_4, host="compute2")]
def fake_compute_node_get_all(context):
return TEST_HYPERS_OBJ
def fake_compute_node_get_all(context, limit=None, marker=None):
if marker in ['99999']:
raise exception.MarkerNotFound(marker)
marker_found = True if marker is None else False
output = []
for hyper in TEST_HYPERS_OBJ:
if not marker_found and marker == str(hyper.id):
marker_found = True
elif marker_found:
if limit is None or len(output) < int(limit):
output.append(hyper)
return output
def fake_compute_node_search_by_hypervisor(context, hypervisor_re):
@ -191,8 +201,9 @@ class HypervisorsTestV21(test.NoDBTestCase):
dict(id=2, hypervisor_hostname="hyper2",
state='up', status='enabled')]
def _get_request(self, use_admin_context):
return fakes.HTTPRequest.blank('', use_admin_context=use_admin_context,
def _get_request(self, use_admin_context, url=''):
return fakes.HTTPRequest.blank(url,
use_admin_context=use_admin_context,
version=self.api_version)
def _set_up_controller(self):
@ -477,7 +488,7 @@ class CellHypervisorsTestV21(HypervisorsTestV21):
for hyp in INDEX_HYPER_DICTS]
@classmethod
def fake_compute_node_get_all(cls, context):
def fake_compute_node_get_all(cls, context, limit=None, marker=None):
return cls.TEST_HYPERS_OBJ
@classmethod
@ -532,3 +543,79 @@ class HypervisorsTestV228(HypervisorsTestV21):
DETAIL_HYPERS_DICTS = copy.deepcopy(HypervisorsTestV21.DETAIL_HYPERS_DICTS)
DETAIL_HYPERS_DICTS[0]['cpu_info'] = jsonutils.loads(CPU_INFO)
DETAIL_HYPERS_DICTS[1]['cpu_info'] = jsonutils.loads(CPU_INFO)
class HypervisorsTestV233(HypervisorsTestV228):
api_version = '2.33'
def test_index_pagination(self):
req = self._get_request(True,
'/v2/1234/os-hypervisors?limit=1&marker=1')
result = self.controller.index(req)
expected = {
'hypervisors': [
{'hypervisor_hostname': 'hyper2',
'id': 2,
'state': 'up',
'status': 'enabled'}
],
'hypervisors_links': [
{'href': 'http://localhost/v2/hypervisors?limit=1&marker=2',
'rel': 'next'}
]
}
self.assertEqual(expected, result)
def test_index_pagination_with_invalid_marker(self):
req = self._get_request(True,
'/v2/1234/os-hypervisors?marker=99999')
self.assertRaises(exc.HTTPBadRequest,
self.controller.index, req)
def test_detail_pagination(self):
req = self._get_request(
True, '/v2/1234/os-hypervisors/detail?limit=1&marker=1')
result = self.controller.detail(req)
link = 'http://localhost/v2/hypervisors/detail?limit=1&marker=2'
expected = {
'hypervisors': [
{'cpu_info': {'arch': 'x86_64',
'features': [],
'model': '',
'topology': {'cores': 1,
'sockets': 1,
'threads': 1},
'vendor': 'fake'},
'current_workload': 2,
'disk_available_least': 100,
'free_disk_gb': 125,
'free_ram_mb': 5120,
'host_ip': netaddr.IPAddress('2.2.2.2'),
'hypervisor_hostname': 'hyper2',
'hypervisor_type': 'xen',
'hypervisor_version': 3,
'id': 2,
'local_gb': 250,
'local_gb_used': 125,
'memory_mb': 10240,
'memory_mb_used': 5120,
'running_vms': 2,
'service': {'disabled_reason': None,
'host': 'compute2',
'id': 2},
'state': 'up',
'status': 'enabled',
'vcpus': 4,
'vcpus_used': 2}
],
'hypervisors_links': [{'href': link, 'rel': 'next'}]
}
self.assertEqual(expected, result)
def test_detail_pagination_with_invalid_marker(self):
req = self._get_request(True,
'/v2/1234/os-hypervisors/detail?marker=99999')
self.assertRaises(exc.HTTPBadRequest,
self.controller.index, req)

View File

@ -7488,6 +7488,58 @@ class ComputeNodeTestCase(test.TestCase, ModelsObjectComparatorMixin):
new_stats = jsonutils.loads(node['stats'])
self.assertEqual(self.stats, new_stats)
def test_compute_node_get_all_by_pagination(self):
service_dict = dict(host='host2', binary='nova-compute',
topic=CONF.compute_topic, report_count=1,
disabled=False)
service = db.service_create(self.ctxt, service_dict)
compute_node_dict = dict(vcpus=2, memory_mb=1024, local_gb=2048,
uuid=uuidsentinel.fake_compute_node,
vcpus_used=0, memory_mb_used=0,
local_gb_used=0, free_ram_mb=1024,
free_disk_gb=2048, hypervisor_type="xen",
hypervisor_version=1, cpu_info="",
running_vms=0, current_workload=0,
service_id=service['id'],
host=service['host'],
disk_available_least=100,
hypervisor_hostname='abcde11',
host_ip='127.0.0.1',
supported_instances='',
pci_stats='',
metrics='',
extra_resources='',
cpu_allocation_ratio=16.0,
ram_allocation_ratio=1.5,
disk_allocation_ratio=1.0,
stats='', numa_topology='')
stats = dict(num_instances=2, num_proj_12345=1,
num_proj_23456=1, num_vm_building=2)
compute_node_dict['stats'] = jsonutils.dumps(stats)
db.compute_node_create(self.ctxt, compute_node_dict)
nodes = db.compute_node_get_all_by_pagination(self.ctxt,
limit=1, marker=1)
self.assertEqual(1, len(nodes))
node = nodes[0]
self._assertEqualObjects(compute_node_dict, node,
ignored_keys=self._ignored_keys +
['stats', 'service'])
new_stats = jsonutils.loads(node['stats'])
self.assertEqual(stats, new_stats)
nodes = db.compute_node_get_all_by_pagination(self.ctxt)
self.assertEqual(2, len(nodes))
node = nodes[0]
self._assertEqualObjects(self.compute_node_dict, node,
ignored_keys=self._ignored_keys +
['stats', 'service'])
new_stats = jsonutils.loads(node['stats'])
self.assertEqual(self.stats, new_stats)
self.assertRaises(exception.MarkerNotFound,
db.compute_node_get_all_by_pagination,
self.ctxt, limit=1, marker=999)
def test_compute_node_get_all_deleted_compute_node(self):
# Create a service and compute node and ensure we can find its stats;
# delete the service and compute node when done and loop again

View File

@ -382,6 +382,16 @@ class _TestComputeNodeObject(object):
subs=self.subs(),
comparators=self.comparators())
@mock.patch('nova.db.compute_node_get_all_by_pagination',
return_value=[fake_compute_node])
def test_get_by_pagination(self, fake_get_by_pagination):
computes = compute_node.ComputeNodeList.get_by_pagination(
self.context, limit=1, marker=1)
self.assertEqual(1, len(computes))
self.compare_obj(computes[0], fake_compute_node,
subs=self.subs(),
comparators=self.comparators())
@mock.patch('nova.db.compute_nodes_get_by_service_id')
def test__get_by_service(self, cn_get_by_svc_id):
cn_get_by_svc_id.return_value = [fake_compute_node]

View File

@ -1108,7 +1108,7 @@ object_data = {
'CellMapping': '1.0-7f1a7e85a22bbb7559fc730ab658b9bd',
'CellMappingList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
'ComputeNode': '1.16-2436e5b836fa0306a3c4e6d9e5ddacec',
'ComputeNodeList': '1.14-3b6f4f5ade621c40e70cb116db237844',
'ComputeNodeList': '1.15-4ec4ea3ed297edbd25c33e2aaf797cca',
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
'Destination': '1.0-4c59dd1288b2e7adbda6051a2de59183',

View File

@ -0,0 +1,5 @@
---
features:
- Added microversion v2.33 which adds paging support for hypervisors, the
admin is able to perform paginate query by using limit and marker to get
a list of hypervisors. The result will be sorted by hypervisor id.