Merge "rt: ensure resource provider records exist from RT"
This commit is contained in:
commit
dd44096a04
@ -67,6 +67,7 @@ from nova.conf import novnc
|
||||
from nova.conf import osapi_v21
|
||||
from nova.conf import paths
|
||||
from nova.conf import pci
|
||||
from nova.conf import placement
|
||||
from nova.conf import quota
|
||||
from nova.conf import rdp
|
||||
from nova.conf import remote_debug
|
||||
@ -141,6 +142,7 @@ novnc.register_opts(CONF)
|
||||
osapi_v21.register_opts(CONF)
|
||||
paths.register_opts(CONF)
|
||||
pci.register_opts(CONF)
|
||||
placement.register_opts(CONF)
|
||||
quota.register_opts(CONF)
|
||||
rdp.register_opts(CONF)
|
||||
rpc.register_opts(CONF)
|
||||
|
44
nova/conf/placement.py
Normal file
44
nova/conf/placement.py
Normal file
@ -0,0 +1,44 @@
|
||||
# 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 keystoneauth1 import loading as ks_loading
|
||||
from oslo_config import cfg
|
||||
|
||||
placement_group = cfg.OptGroup(
|
||||
'placement',
|
||||
title='Placement Service Options',
|
||||
help="Configuration options for connecting to the placement API service")
|
||||
|
||||
placement_opts = [
|
||||
cfg.StrOpt('os_region_name',
|
||||
help="""
|
||||
Region name of this node. This is used when picking the URL in the service
|
||||
catalog.
|
||||
|
||||
Possible values:
|
||||
|
||||
* Any string representing region name
|
||||
"""),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(placement_group)
|
||||
conf.register_opts(placement_opts, group=placement_group)
|
||||
ks_loading.register_auth_conf_options(conf,
|
||||
placement_group.name)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {
|
||||
placement_group.name: placement_opts
|
||||
}
|
@ -102,7 +102,8 @@ class RequestContext(context.RequestContext):
|
||||
if service_catalog:
|
||||
# Only include required parts of service_catalog
|
||||
self.service_catalog = [s for s in service_catalog
|
||||
if s.get('type') in ('volume', 'volumev2', 'key-manager')]
|
||||
if s.get('type') in ('volume', 'volumev2', 'key-manager',
|
||||
'placement')]
|
||||
else:
|
||||
# if list is empty or none
|
||||
self.service_catalog = []
|
||||
|
@ -13,13 +13,189 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
|
||||
from keystoneauth1 import exceptions as ks_exc
|
||||
from keystoneauth1 import loading as keystone
|
||||
from keystoneauth1 import session
|
||||
from oslo_log import log as logging
|
||||
|
||||
import nova.conf
|
||||
from nova.i18n import _LE, _LI, _LW
|
||||
from nova import objects
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def safe_connect(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(self, *a, **k):
|
||||
try:
|
||||
# We've failed in a non recoverable way, fully give up.
|
||||
if self._disabled:
|
||||
return
|
||||
return f(self, *a, **k)
|
||||
except ks_exc.EndpointNotFound:
|
||||
msg = _LW("The placement API endpoint not found. Optional use of "
|
||||
"placement API for reporting is now disabled.")
|
||||
LOG.warning(msg)
|
||||
self._disabled = True
|
||||
except ks_exc.MissingAuthPlugin:
|
||||
msg = _LW("No authentication information found for placement API. "
|
||||
"Optional use of placement API for reporting is now "
|
||||
"disabled.")
|
||||
LOG.warning(msg)
|
||||
self._disabled = True
|
||||
except ks_exc.ConnectFailure:
|
||||
msg = _LW('Placement API service is not responding.')
|
||||
LOG.warning(msg)
|
||||
return wrapper
|
||||
|
||||
|
||||
class SchedulerReportClient(object):
|
||||
"""Client class for updating the scheduler."""
|
||||
|
||||
ks_filter = {'service_type': 'placement',
|
||||
'region_name': CONF.placement.os_region_name}
|
||||
|
||||
def __init__(self):
|
||||
# A dict, keyed by the resource provider UUID, of ResourceProvider
|
||||
# objects that will have their inventories and allocations tracked by
|
||||
# the placement API for the compute host
|
||||
self._resource_providers = {}
|
||||
auth_plugin = keystone.load_auth_from_conf_options(
|
||||
CONF, 'placement')
|
||||
self._client = session.Session(auth=auth_plugin)
|
||||
# TODO(sdague): use this to disable fully when we don't find
|
||||
# the endpoint.
|
||||
self._disabled = False
|
||||
|
||||
def get(self, url):
|
||||
return self._client.get(
|
||||
url,
|
||||
endpoint_filter=self.ks_filter, raise_exc=False)
|
||||
|
||||
def post(self, url, data):
|
||||
# NOTE(sdague): using json= instead of data= sets the
|
||||
# media type to application/json for us. Placement API is
|
||||
# more sensitive to this than other APIs in the OpenStack
|
||||
# ecosystem.
|
||||
return self._client.post(
|
||||
url, json=data,
|
||||
endpoint_filter=self.ks_filter, raise_exc=False)
|
||||
|
||||
@safe_connect
|
||||
def _get_resource_provider(self, uuid):
|
||||
"""Queries the placement API for a resource provider record with the
|
||||
supplied UUID.
|
||||
|
||||
Returns an `objects.ResourceProvider` object if found or None if no
|
||||
such resource provider could be found.
|
||||
|
||||
:param uuid: UUID identifier for the resource provider to look up
|
||||
"""
|
||||
resp = self.get("/resource_providers/%s" % uuid)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
return objects.ResourceProvider(
|
||||
uuid=uuid,
|
||||
name=data['name'],
|
||||
generation=data['generation'],
|
||||
)
|
||||
elif resp.status_code == 404:
|
||||
return None
|
||||
else:
|
||||
msg = _LE("Failed to retrieve resource provider record from "
|
||||
"placement API for UUID %(uuid)s. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
args = {
|
||||
'uuid': uuid,
|
||||
'status_code': resp.status_code,
|
||||
'err_text': resp.text,
|
||||
}
|
||||
LOG.error(msg, args)
|
||||
|
||||
@safe_connect
|
||||
def _create_resource_provider(self, uuid, name):
|
||||
"""Calls the placement API to create a new resource provider record.
|
||||
|
||||
Returns an `objects.ResourceProvider` object representing the
|
||||
newly-created resource provider object.
|
||||
|
||||
:param uuid: UUID of the new resource provider
|
||||
:param name: Name of the resource provider
|
||||
"""
|
||||
url = "/resource_providers"
|
||||
payload = {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
}
|
||||
resp = self.post(url, payload)
|
||||
if resp.status_code == 201:
|
||||
msg = _LI("Created resource provider record via placement API "
|
||||
"for resource provider with UUID {0} and name {1}.")
|
||||
msg = msg.format(uuid, name)
|
||||
LOG.info(msg)
|
||||
return objects.ResourceProvider(
|
||||
uuid=uuid,
|
||||
name=name,
|
||||
generation=1,
|
||||
)
|
||||
elif resp.status_code == 409:
|
||||
# Another thread concurrently created a resource provider with the
|
||||
# same UUID. Log a warning and then just return the resource
|
||||
# provider object from _get_resource_provider()
|
||||
msg = _LI("Another thread already created a resource provider "
|
||||
"with the UUID {0}. Grabbing that record from "
|
||||
"the placement API.")
|
||||
msg = msg.format(uuid)
|
||||
LOG.info(msg)
|
||||
return self._get_resource_provider(uuid)
|
||||
else:
|
||||
msg = _LE("Failed to create resource provider record in "
|
||||
"placement API for UUID %(uuid)s. "
|
||||
"Got %(status_code)d: %(err_text)s.")
|
||||
args = {
|
||||
'uuid': uuid,
|
||||
'status_code': resp.status_code,
|
||||
'err_text': resp.text,
|
||||
}
|
||||
LOG.error(msg, args)
|
||||
|
||||
def _ensure_resource_provider(self, uuid, name=None):
|
||||
"""Ensures that the placement API has a record of a resource provider
|
||||
with the supplied UUID. If not, creates the resource provider record in
|
||||
the placement API for the supplied UUID, optionally passing in a name
|
||||
for the resource provider.
|
||||
|
||||
The found or created resource provider object is returned from this
|
||||
method. If the resource provider object for the supplied uuid was not
|
||||
found and the resource provider record could not be created in the
|
||||
placement API, we return None.
|
||||
|
||||
:param uuid: UUID identifier for the resource provider to ensure exists
|
||||
:param name: Optional name for the resource provider if the record
|
||||
does not exist. If empty, the name is set to the UUID
|
||||
value
|
||||
"""
|
||||
if uuid in self._resource_providers:
|
||||
return self._resource_providers[uuid]
|
||||
|
||||
rp = self._get_resource_provider(uuid)
|
||||
if rp is None:
|
||||
name = name or uuid
|
||||
rp = self._create_resource_provider(uuid, name)
|
||||
if rp is None:
|
||||
return
|
||||
self._resource_providers[uuid] = rp
|
||||
return rp
|
||||
|
||||
def update_resource_stats(self, compute_node):
|
||||
"""Creates or updates stats for the supplied compute node.
|
||||
|
||||
:param compute_node: updated nova.objects.ComputeNode to report
|
||||
"""
|
||||
compute_node.save()
|
||||
self._ensure_resource_provider(compute_node.uuid,
|
||||
compute_node.hypervisor_hostname)
|
||||
|
@ -12,11 +12,15 @@
|
||||
|
||||
import mock
|
||||
|
||||
import nova.conf
|
||||
from nova import context
|
||||
from nova import objects
|
||||
from nova.objects import pci_device_pool
|
||||
from nova.objects import base as obj_base
|
||||
from nova.scheduler.client import report
|
||||
from nova import test
|
||||
from nova.tests import uuidsentinel as uuids
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
|
||||
|
||||
class SchedulerReportClientTestCase(test.NoDBTestCase):
|
||||
@ -24,20 +28,265 @@ class SchedulerReportClientTestCase(test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
super(SchedulerReportClientTestCase, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.ks_sess_mock = mock.Mock()
|
||||
|
||||
self.flags(use_local=True, group='conductor')
|
||||
with test.nested(
|
||||
mock.patch('keystoneauth1.session.Session',
|
||||
return_value=self.ks_sess_mock),
|
||||
mock.patch('keystoneauth1.loading.load_auth_from_conf_options')
|
||||
) as (_auth_mock, _sess_mock):
|
||||
self.client = report.SchedulerReportClient()
|
||||
|
||||
self.client = report.SchedulerReportClient()
|
||||
@mock.patch('keystoneauth1.session.Session')
|
||||
@mock.patch('keystoneauth1.loading.load_auth_from_conf_options')
|
||||
def test_constructor(self, load_auth_mock, ks_sess_mock):
|
||||
report.SchedulerReportClient()
|
||||
|
||||
load_auth_mock.assert_called_once_with(CONF, 'placement')
|
||||
ks_sess_mock.assert_called_once_with(auth=load_auth_mock.return_value)
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_create_resource_provider')
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_get_resource_provider')
|
||||
def test_ensure_resource_provider_exists_in_cache(self, get_rp_mock,
|
||||
create_rp_mock):
|
||||
# Override the client object's cache to contain a resource provider
|
||||
# object for the compute host and check that
|
||||
# _ensure_resource_provider() doesn't call _get_resource_provider() or
|
||||
# _create_resource_provider()
|
||||
self.client._resource_providers = {
|
||||
uuids.compute_node: mock.sentinel.rp
|
||||
}
|
||||
|
||||
self.client._ensure_resource_provider(uuids.compute_node)
|
||||
self.assertFalse(get_rp_mock.called)
|
||||
self.assertFalse(create_rp_mock.called)
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_create_resource_provider')
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_get_resource_provider')
|
||||
def test_ensure_resource_provider_get(self, get_rp_mock, create_rp_mock):
|
||||
# No resource provider exists in the client's cache, so validate that
|
||||
# if we get the resource provider from the placement API that we don't
|
||||
# try to create the resource provider.
|
||||
get_rp_mock.return_value = mock.sentinel.rp
|
||||
|
||||
self.client._ensure_resource_provider(uuids.compute_node)
|
||||
|
||||
get_rp_mock.assert_called_once_with(uuids.compute_node)
|
||||
self.assertEqual({uuids.compute_node: mock.sentinel.rp},
|
||||
self.client._resource_providers)
|
||||
self.assertFalse(create_rp_mock.called)
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_create_resource_provider')
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_get_resource_provider')
|
||||
def test_ensure_resource_provider_create_none(self, get_rp_mock,
|
||||
create_rp_mock):
|
||||
# No resource provider exists in the client's cache, and
|
||||
# _create_provider returns None, indicating there was an error with the
|
||||
# create call. Ensure we don't populate the resource provider cache
|
||||
# with a None value.
|
||||
get_rp_mock.return_value = None
|
||||
create_rp_mock.return_value = None
|
||||
|
||||
self.client._ensure_resource_provider(uuids.compute_node)
|
||||
|
||||
get_rp_mock.assert_called_once_with(uuids.compute_node)
|
||||
create_rp_mock.assert_called_once_with(uuids.compute_node,
|
||||
uuids.compute_node)
|
||||
self.assertEqual({}, self.client._resource_providers)
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_create_resource_provider')
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_get_resource_provider')
|
||||
def test_ensure_resource_provider_create(self, get_rp_mock,
|
||||
create_rp_mock):
|
||||
# No resource provider exists in the client's cache and no resource
|
||||
# provider was returned from the placement API, so verify that in this
|
||||
# case we try to create the resource provider via the placement API.
|
||||
get_rp_mock.return_value = None
|
||||
create_rp_mock.return_value = mock.sentinel.rp
|
||||
|
||||
self.client._ensure_resource_provider(uuids.compute_node)
|
||||
|
||||
get_rp_mock.assert_called_once_with(uuids.compute_node)
|
||||
create_rp_mock.assert_called_once_with(
|
||||
uuids.compute_node,
|
||||
uuids.compute_node, # name param defaults to UUID if None
|
||||
)
|
||||
self.assertEqual({uuids.compute_node: mock.sentinel.rp},
|
||||
self.client._resource_providers)
|
||||
|
||||
create_rp_mock.reset_mock()
|
||||
self.client._resource_providers = {}
|
||||
|
||||
self.client._ensure_resource_provider(uuids.compute_node,
|
||||
mock.sentinel.name)
|
||||
|
||||
create_rp_mock.assert_called_once_with(
|
||||
uuids.compute_node,
|
||||
mock.sentinel.name,
|
||||
)
|
||||
|
||||
def test_get_resource_provider_found(self):
|
||||
# Ensure _get_resource_provider() returns a ResourceProvider object if
|
||||
# it finds a resource provider record from the placement API
|
||||
uuid = uuids.compute_node
|
||||
resp_mock = mock.Mock(status_code=200)
|
||||
json_data = {
|
||||
'uuid': uuid,
|
||||
'name': uuid,
|
||||
'generation': 42,
|
||||
}
|
||||
resp_mock.json.return_value = json_data
|
||||
self.ks_sess_mock.get.return_value = resp_mock
|
||||
|
||||
result = self.client._get_resource_provider(uuid)
|
||||
|
||||
expected_provider = objects.ResourceProvider(
|
||||
uuid=uuid,
|
||||
name=uuid,
|
||||
generation=42,
|
||||
)
|
||||
expected_url = '/resource_providers/' + uuid
|
||||
self.ks_sess_mock.get.assert_called_once_with(expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
raise_exc=False)
|
||||
self.assertTrue(obj_base.obj_equal_prims(expected_provider,
|
||||
result))
|
||||
|
||||
def test_get_resource_provider_not_found(self):
|
||||
# Ensure _get_resource_provider() just returns None when the placement
|
||||
# API doesn't find a resource provider matching a UUID
|
||||
resp_mock = mock.Mock(status_code=404)
|
||||
self.ks_sess_mock.get.return_value = resp_mock
|
||||
|
||||
uuid = uuids.compute_node
|
||||
result = self.client._get_resource_provider(uuid)
|
||||
|
||||
expected_url = '/resource_providers/' + uuid
|
||||
self.ks_sess_mock.get.assert_called_once_with(expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
raise_exc=False)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch.object(report.LOG, 'error')
|
||||
def test_get_resource_provider_error(self, logging_mock):
|
||||
# Ensure _get_resource_provider() sets the error flag when trying to
|
||||
# communicate with the placement API and not getting an error we can
|
||||
# deal with
|
||||
resp_mock = mock.Mock(status_code=503)
|
||||
self.ks_sess_mock.get.return_value = resp_mock
|
||||
|
||||
uuid = uuids.compute_node
|
||||
result = self.client._get_resource_provider(uuid)
|
||||
|
||||
expected_url = '/resource_providers/' + uuid
|
||||
self.ks_sess_mock.get.assert_called_once_with(expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
raise_exc=False)
|
||||
# A 503 Service Unavailable should trigger an error logged and
|
||||
# return None from _get_resource_provider()
|
||||
self.assertTrue(logging_mock.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_create_resource_provider(self):
|
||||
# Ensure _create_resource_provider() returns a ResourceProvider object
|
||||
# constructed after creating a resource provider record in the
|
||||
# placement API
|
||||
uuid = uuids.compute_node
|
||||
name = 'computehost'
|
||||
resp_mock = mock.Mock(status_code=201)
|
||||
self.ks_sess_mock.post.return_value = resp_mock
|
||||
|
||||
result = self.client._create_resource_provider(uuid, name)
|
||||
|
||||
expected_payload = {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
}
|
||||
expected_provider = objects.ResourceProvider(
|
||||
uuid=uuid,
|
||||
name=name,
|
||||
generation=1,
|
||||
)
|
||||
expected_url = '/resource_providers'
|
||||
self.ks_sess_mock.post.assert_called_once_with(
|
||||
expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
json=expected_payload,
|
||||
raise_exc=False)
|
||||
self.assertTrue(obj_base.obj_equal_prims(expected_provider,
|
||||
result))
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_get_resource_provider')
|
||||
def test_create_resource_provider_concurrent_create(self, get_rp_mock):
|
||||
# Ensure _create_resource_provider() returns a ResourceProvider object
|
||||
# gotten from _get_resource_provider() if the call to create the
|
||||
# resource provider in the placement API returned a 409 Conflict,
|
||||
# indicating another thread concurrently created the resource provider
|
||||
# record.
|
||||
uuid = uuids.compute_node
|
||||
name = 'computehost'
|
||||
resp_mock = mock.Mock(status_code=409)
|
||||
self.ks_sess_mock.post.return_value = resp_mock
|
||||
|
||||
get_rp_mock.return_value = mock.sentinel.get_rp
|
||||
|
||||
result = self.client._create_resource_provider(uuid, name)
|
||||
|
||||
expected_payload = {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
}
|
||||
expected_url = '/resource_providers'
|
||||
self.ks_sess_mock.post.assert_called_once_with(
|
||||
expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
json=expected_payload,
|
||||
raise_exc=False)
|
||||
self.assertEqual(mock.sentinel.get_rp, result)
|
||||
|
||||
@mock.patch.object(report.LOG, 'error')
|
||||
def test_create_resource_provider_error(self, logging_mock):
|
||||
# Ensure _create_resource_provider() sets the error flag when trying to
|
||||
# communicate with the placement API and not getting an error we can
|
||||
# deal with
|
||||
uuid = uuids.compute_node
|
||||
name = 'computehost'
|
||||
resp_mock = mock.Mock(status_code=503)
|
||||
self.ks_sess_mock.post.return_value = resp_mock
|
||||
|
||||
result = self.client._create_resource_provider(uuid, name)
|
||||
|
||||
expected_payload = {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
}
|
||||
expected_url = '/resource_providers'
|
||||
self.ks_sess_mock.post.assert_called_once_with(
|
||||
expected_url,
|
||||
endpoint_filter=mock.ANY,
|
||||
json=expected_payload,
|
||||
raise_exc=False)
|
||||
# A 503 Service Unavailable should log an error and
|
||||
# _create_resource_provider() should return None
|
||||
self.assertTrue(logging_mock.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch('nova.scheduler.client.report.SchedulerReportClient.'
|
||||
'_ensure_resource_provider')
|
||||
@mock.patch.object(objects.ComputeNode, 'save')
|
||||
def test_update_resource_stats_saves(self, mock_save):
|
||||
cn = objects.ComputeNode(context=self.context)
|
||||
cn.host = 'fakehost'
|
||||
cn.hypervisor_hostname = 'fakenode'
|
||||
cn.pci_device_pools = pci_device_pool.from_pci_stats(
|
||||
[{"vendor_id": "foo",
|
||||
"product_id": "foo",
|
||||
"count": 1,
|
||||
"a": "b"}])
|
||||
def test_update_resource_stats_saves(self, mock_save, mock_ensure):
|
||||
cn = objects.ComputeNode(context=self.context,
|
||||
uuid=uuids.compute_node,
|
||||
hypervisor_hostname='host1')
|
||||
self.client.update_resource_stats(cn)
|
||||
mock_save.assert_called_once_with()
|
||||
mock_ensure.assert_called_once_with(uuids.compute_node, 'host1')
|
||||
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- The nova-compute worker now communicates with the new placement API
|
||||
service. Nova determines the placement API service by querying the
|
||||
OpenStack service catalog for the service with a service type of
|
||||
'placement'.
|
||||
- A new [placement] section is added to the nova.conf configuration file for
|
||||
configuration options affecting how Nova interacts with the new placement
|
||||
API service. The only configuration option currently available is
|
||||
`os_region_name` which provides support for Nova to query the appropriate
|
||||
OpenStack region's service catalog for the placement service.
|
Loading…
x
Reference in New Issue
Block a user