Return uuid attribute for aggregates
Adds a Compute API microversion that triggers returning an aggregate's UUID field. This field is necessary for scripts that must populate the placement API with resource provider to aggregate relationships, which rely on UUIDs for global identification. APIImpact blueprint: return-uuid-from-os-aggregates-api Change-Id: I4112ccd508eb85403933fec8b52efd468e866772 Closes-bug: #1652642
This commit is contained in:
parent
aba18ab045
commit
8b4fd32e7b
@ -34,6 +34,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example List Aggregates: JSON response**
|
||||
|
||||
@ -79,6 +80,7 @@ Response
|
||||
- id: aggregate_id_body
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Create Aggregate: JSON response**
|
||||
|
||||
@ -118,6 +120,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Show Aggregate Details: JSON response**
|
||||
|
||||
@ -168,6 +171,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Update Aggregate: JSON response**
|
||||
|
||||
@ -240,6 +244,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Add Host: JSON response**
|
||||
|
||||
@ -289,6 +294,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Remove Host: JSON response**
|
||||
|
||||
@ -338,6 +344,7 @@ Response
|
||||
- metadata: aggregate_metadata
|
||||
- name: aggregate_name
|
||||
- updated_at: updated_consider_null
|
||||
- uuid: aggregate_uuid
|
||||
|
||||
**Example Create Or Update Aggregate Metadata: JSON response**
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"add_host": {
|
||||
"host": "compute"
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"set_metadata":
|
||||
{
|
||||
"metadata":
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"aggregate":
|
||||
{
|
||||
"name": "name",
|
||||
"availability_zone": "nova"
|
||||
}
|
||||
}
|
12
doc/api_samples/os-aggregates/v2.41/aggregate-post-resp.json
Normal file
12
doc/api_samples/os-aggregates/v2.41/aggregate-post-resp.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T22:51:32.877711",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"id": 1,
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "86a0da0e-9f0c-4f51-a1e0-3c25edab3783"
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"remove_host": {
|
||||
"host": "compute"
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"aggregate":
|
||||
{
|
||||
"name": "newname",
|
||||
"availability_zone": "nova2"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova2",
|
||||
"created_at": "2016-12-27T23:47:32.897139",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova2"
|
||||
},
|
||||
"name": "newname",
|
||||
"updated_at": "2016-12-27T23:47:33.067180",
|
||||
"uuid": "6f74e3f3-df28-48f3-98e1-ac941b1c5e43"
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T23:47:30.594805",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [
|
||||
"compute"
|
||||
],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "d1842372-89c5-4fbd-ad5a-5d2e16c85456"
|
||||
}
|
||||
}
|
16
doc/api_samples/os-aggregates/v2.41/aggregates-get-resp.json
Normal file
16
doc/api_samples/os-aggregates/v2.41/aggregates-get-resp.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T23:47:30.563527",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "fd0a5b12-7e8d-469d-bfd5-64a6823e7407"
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"aggregates": [
|
||||
{
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T23:47:32.911515",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [
|
||||
"compute"
|
||||
],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "6ba28ba7-f29b-45cc-a30b-6e3a40c2fb14"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T23:59:18.623100",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova",
|
||||
"key": "value"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": "2016-12-27T23:59:18.723348",
|
||||
"uuid": "26002bdb-62cc-41bd-813a-0ad22db32625"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "2016-12-27T23:47:30.594805",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "d1842372-89c5-4fbd-ad5a-5d2e16c85456"
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import datetime
|
||||
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import api_version_request
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack.compute.schemas import aggregates
|
||||
from nova.api.openstack import extensions
|
||||
@ -47,7 +48,7 @@ class AggregateController(wsgi.Controller):
|
||||
context = _get_context(req)
|
||||
context.can(aggr_policies.POLICY_ROOT % 'index')
|
||||
aggregates = self.api.get_aggregate_list(context)
|
||||
return {'aggregates': [self._marshall_aggregate(a)['aggregate']
|
||||
return {'aggregates': [self._marshall_aggregate(req, a)['aggregate']
|
||||
for a in aggregates]}
|
||||
|
||||
# NOTE(gmann): Returns 200 for backwards compatibility but should be 201
|
||||
@ -77,7 +78,7 @@ class AggregateController(wsgi.Controller):
|
||||
except exception.InvalidAggregateAction as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
|
||||
agg = self._marshall_aggregate(aggregate)
|
||||
agg = self._marshall_aggregate(req, aggregate)
|
||||
|
||||
# To maintain the same API result as before the changes for returning
|
||||
# nova objects were made.
|
||||
@ -95,7 +96,7 @@ class AggregateController(wsgi.Controller):
|
||||
aggregate = self.api.get_aggregate(context, id)
|
||||
except exception.AggregateNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.format_message())
|
||||
return self._marshall_aggregate(aggregate)
|
||||
return self._marshall_aggregate(req, aggregate)
|
||||
|
||||
@extensions.expected_errors((400, 404, 409))
|
||||
@validation.schema(aggregates.update_v20, '2.0', '2.0')
|
||||
@ -117,7 +118,7 @@ class AggregateController(wsgi.Controller):
|
||||
except exception.InvalidAggregateAction as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
|
||||
return self._marshall_aggregate(aggregate)
|
||||
return self._marshall_aggregate(req, aggregate)
|
||||
|
||||
# NOTE(gmann): Returns 200 for backwards compatibility but should be 204
|
||||
# as this operation complete the deletion of aggregate resource and return
|
||||
@ -154,7 +155,7 @@ class AggregateController(wsgi.Controller):
|
||||
except (exception.AggregateHostExists,
|
||||
exception.InvalidAggregateAction) as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
return self._marshall_aggregate(aggregate)
|
||||
return self._marshall_aggregate(req, aggregate)
|
||||
|
||||
# NOTE(gmann): Returns 200 for backwards compatibility but should be 202
|
||||
# for representing async API as this API just accepts the request and
|
||||
@ -179,7 +180,7 @@ class AggregateController(wsgi.Controller):
|
||||
msg = _('Cannot remove host %(host)s in aggregate %(id)s') % {
|
||||
'host': host, 'id': id}
|
||||
raise exc.HTTPConflict(explanation=msg)
|
||||
return self._marshall_aggregate(aggregate)
|
||||
return self._marshall_aggregate(req, aggregate)
|
||||
|
||||
@extensions.expected_errors((400, 404))
|
||||
@wsgi.action('set_metadata')
|
||||
@ -198,18 +199,19 @@ class AggregateController(wsgi.Controller):
|
||||
except exception.InvalidAggregateAction as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
|
||||
return self._marshall_aggregate(aggregate)
|
||||
return self._marshall_aggregate(req, aggregate)
|
||||
|
||||
def _marshall_aggregate(self, aggregate):
|
||||
def _marshall_aggregate(self, req, aggregate):
|
||||
_aggregate = {}
|
||||
for key, value in self._build_aggregate_items(aggregate):
|
||||
for key, value in self._build_aggregate_items(req, aggregate):
|
||||
# NOTE(danms): The original API specified non-TZ-aware timestamps
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = value.replace(tzinfo=None)
|
||||
_aggregate[key] = value
|
||||
return {"aggregate": _aggregate}
|
||||
|
||||
def _build_aggregate_items(self, aggregate):
|
||||
def _build_aggregate_items(self, req, aggregate):
|
||||
show_uuid = api_version_request.is_supported(req, min_version="2.41")
|
||||
keys = aggregate.obj_fields
|
||||
# NOTE(rlrossit): Within the compute API, metadata will always be
|
||||
# set on the aggregate object (at a minimum to {}). Because of this,
|
||||
@ -217,11 +219,9 @@ class AggregateController(wsgi.Controller):
|
||||
# case it is only ['availability_zone']) without worrying about
|
||||
# lazy-loading an unset variable
|
||||
for key in keys:
|
||||
# NOTE(danms): Skip the uuid field because we have no microversion
|
||||
# to expose it
|
||||
if ((aggregate.obj_attr_is_set(key)
|
||||
or key in aggregate.obj_extra_fields) and
|
||||
key != 'uuid'):
|
||||
(show_uuid or key != 'uuid')):
|
||||
yield key, getattr(aggregate, key)
|
||||
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"add_host": {
|
||||
"host": "%(host_name)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"set_metadata":
|
||||
{
|
||||
"metadata":
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"aggregate":
|
||||
{
|
||||
"name": "name",
|
||||
"availability_zone": "nova"
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"id": %(aggregate_id)s,
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"remove_host": {
|
||||
"host": "%(host_name)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"aggregate":
|
||||
{
|
||||
"name": "newname",
|
||||
"availability_zone": "nova2"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova2",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova2"
|
||||
},
|
||||
"name": "newname",
|
||||
"updated_at": "%(strtime)s",
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [
|
||||
"%(compute_host)s"
|
||||
],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"aggregates": [
|
||||
{
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [
|
||||
"%(compute_host)s"
|
||||
],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova",
|
||||
"key": "value"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": %(strtime)s,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"aggregate": {
|
||||
"availability_zone": "nova",
|
||||
"created_at": "%(strtime)s",
|
||||
"deleted": false,
|
||||
"deleted_at": null,
|
||||
"hosts": [],
|
||||
"id": 1,
|
||||
"metadata": {
|
||||
"availability_zone": "nova"
|
||||
},
|
||||
"name": "name",
|
||||
"updated_at": null,
|
||||
"uuid": "%(uuid)s"
|
||||
}
|
||||
}
|
@ -13,12 +13,18 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from nova.tests.functional.api_sample_tests import api_sample_base
|
||||
|
||||
|
||||
class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
ADMIN_API = True
|
||||
sample_dir = "os-aggregates"
|
||||
# extra_subs is a noop in the base v2.1 test class; it's used to sub in
|
||||
# additional details for response verification of actions performed on an
|
||||
# existing aggregate.
|
||||
extra_subs = {}
|
||||
|
||||
def _test_aggregate_create(self):
|
||||
subs = {
|
||||
@ -37,6 +43,7 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
}
|
||||
response = self._do_post('os-aggregates/%s/action' % aggregate_id,
|
||||
'aggregate-add-host-post-req', subs)
|
||||
subs.update(self.extra_subs)
|
||||
self._verify_response('aggregates-add-host-post-resp', subs,
|
||||
response, 200)
|
||||
|
||||
@ -49,14 +56,15 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
def test_aggregate_get(self):
|
||||
agg_id = self._test_aggregate_create()
|
||||
response = self._do_get('os-aggregates/%s' % agg_id)
|
||||
self._verify_response('aggregates-get-resp', {}, response, 200)
|
||||
self._verify_response('aggregates-get-resp', self.extra_subs,
|
||||
response, 200)
|
||||
|
||||
def test_add_metadata(self):
|
||||
agg_id = self._test_aggregate_create()
|
||||
response = self._do_post('os-aggregates/%s/action' % agg_id,
|
||||
'aggregate-metadata-post-req',
|
||||
{'action': 'set_metadata'})
|
||||
self._verify_response('aggregates-metadata-post-resp', {},
|
||||
self._verify_response('aggregates-metadata-post-resp', self.extra_subs,
|
||||
response, 200)
|
||||
|
||||
def test_add_host(self):
|
||||
@ -70,6 +78,7 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
}
|
||||
response = self._do_post('os-aggregates/1/action',
|
||||
'aggregate-remove-host-post-req', subs)
|
||||
subs.update(self.extra_subs)
|
||||
self._verify_response('aggregates-remove-host-post-resp',
|
||||
subs, response, 200)
|
||||
|
||||
@ -78,4 +87,33 @@ class AggregatesSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
response = self._do_put('os-aggregates/%s' % aggregate_id,
|
||||
'aggregate-update-post-req', {})
|
||||
self._verify_response('aggregate-update-post-resp',
|
||||
{}, response, 200)
|
||||
self.extra_subs, response, 200)
|
||||
|
||||
|
||||
class AggregatesV2_41_SampleJsonTest(AggregatesSampleJsonTest):
|
||||
microversion = '2.41'
|
||||
scenarios = [
|
||||
(
|
||||
"v2_41", {
|
||||
'api_major_version': 'v2.1',
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
def _test_aggregate_create(self):
|
||||
subs = {
|
||||
"aggregate_id": '(?P<id>\d+)',
|
||||
}
|
||||
response = self._do_post('os-aggregates', 'aggregate-post-req', subs)
|
||||
# This feels like cheating since we're getting the uuid from the
|
||||
# response before we even validate that it exists in the response based
|
||||
# on the sample, but we'll fail with a KeyError if it doesn't which is
|
||||
# maybe good enough. Alternatively we have to mock out the DB API
|
||||
# to return a fake aggregate with a hard-coded uuid that matches the
|
||||
# API sample which isn't fun either.
|
||||
subs['uuid'] = jsonutils.loads(response.content)['aggregate']['uuid']
|
||||
# save off the uuid for subs validation on other actions performed
|
||||
# on this aggregate
|
||||
self.extra_subs['uuid'] = subs['uuid']
|
||||
return self._verify_response('aggregate-post-resp',
|
||||
subs, response, 200)
|
||||
|
@ -18,6 +18,7 @@
|
||||
import mock
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import api_version_request
|
||||
from nova.api.openstack.compute import aggregates as aggregates_v21
|
||||
from nova.compute import api as compute_api
|
||||
from nova import context
|
||||
@ -743,11 +744,23 @@ class AggregateTestCaseV21(test.NoDBTestCase):
|
||||
'metadata': {'foo': 'bar', 'availability_zone': 'nova'},
|
||||
'hosts': ['host1', 'host2']}
|
||||
agg_obj = _make_agg_obj(agg)
|
||||
marshalled_agg = self.controller._marshall_aggregate(agg_obj)
|
||||
|
||||
# _marshall_aggregate() puts all fields and obj_extra_fields in the
|
||||
# top-level dict, so we need to put availability_zone at the top also
|
||||
agg['availability_zone'] = 'nova'
|
||||
|
||||
avr_v240 = api_version_request.APIVersionRequest("2.40")
|
||||
avr_v241 = api_version_request.APIVersionRequest("2.41")
|
||||
|
||||
req = mock.MagicMock(api_version_request=avr_v241)
|
||||
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
|
||||
|
||||
self.assertEqual(agg, marshalled_agg['aggregate'])
|
||||
|
||||
req = mock.MagicMock(api_version_request=avr_v240)
|
||||
marshalled_agg = self.controller._marshall_aggregate(req, agg_obj)
|
||||
|
||||
# UUID isn't in microversion 2.40 and before
|
||||
del agg['uuid']
|
||||
self.assertEqual(agg, marshalled_agg['aggregate'])
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- A new 2.41 microversion was added to the Compute API. Users specifying this
|
||||
microversion will now see the 'uuid' attribute of aggregates when calling
|
||||
the `os-aggregates` REST API endpoint.
|
Loading…
Reference in New Issue
Block a user