Support Proxy-specific region_name

Proxy (a subclass of keystoneauth1 Adapter) is set up to know about
region_name. This is done so that consumers of cloud_region.from_conf
will have their [$project]region_name setting honored rather than
ignored.

Previously, CloudRegion was set up to deal with a single region, masking
(or rather overriding) each Proxy's notion of what its region_name
should be.

With this change, (service-specific) region_nameZ are honored by the
CloudRegion constructor, as follows:

- A service-specific region_name in the ``config`` dict (e.g.
  'compute_region_name': 'SnowflakeComputeRegion') takes first
  precedence for that service.
- If no service-specific region_name is present in the ``config`` dict,
  the value of the region_name kwarg to the CloudRegion constructor is
  used. (This kwarg should really go away, but is preserved for backward
  compatibility.)
- If neither of the above is given, the value of the (unprefixed)
  'region_name' key in the ``config`` dict is used. (This should really
  take precedence over the kwarg, but it's done this way for backward
  compatibility.)
- If none of the above exist, None is used (the previous behavior,
  whatever that may mean).

This change should hopefully be transparent to any existing usages of
the CloudRegion constructor, as ${service}_region_name would have been
ignored if present. However, if any existing usages had such a config
setting present, it will no longer be ignored, resulting in potential
RBB [1].

[1] That's "Revoked Bug Benefit"

Change-Id: Ie253823a753d09d52e45df9d515fd22870c2d4c5
This commit is contained in:
Eric Fried
2019-06-03 16:31:36 -05:00
committed by Monty Taylor
parent 86ad9debd1
commit 5f0401a206
10 changed files with 132 additions and 45 deletions

View File

@@ -39,6 +39,11 @@ class ComputeCloudMixin(_normalize.Normalizer):
self._servers_time = 0 self._servers_time = 0
self._servers_lock = threading.Lock() self._servers_lock = threading.Lock()
@property
def _compute_region(self):
# This is only used in exception messages. Can we get rid of it?
return self.config.get_region_name('compute')
def get_flavor_name(self, flavor_id): def get_flavor_name(self, flavor_id):
flavor = self.get_flavor(flavor_id, get_extra=False) flavor = self.get_flavor(flavor_id, get_extra=False)
if flavor: if flavor:
@@ -896,7 +901,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
'Network {network} is not a valid network in' 'Network {network} is not a valid network in'
' {cloud}:{region}'.format( ' {cloud}:{region}'.format(
network=network, network=network,
cloud=self.name, region=self.config.region_name)) cloud=self.name, region=self._compute_region))
nics.append({'net-id': network_obj['id']}) nics.append({'net-id': network_obj['id']})
kwargs['nics'] = nics kwargs['nics'] = nics
@@ -1025,7 +1030,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
'Volume {boot_volume} is not a valid volume' 'Volume {boot_volume} is not a valid volume'
' in {cloud}:{region}'.format( ' in {cloud}:{region}'.format(
boot_volume=boot_volume, boot_volume=boot_volume,
cloud=self.name, region=self.config.region_name)) cloud=self.name, region=self._compute_region))
block_mapping = { block_mapping = {
'boot_index': '0', 'boot_index': '0',
'delete_on_termination': terminate_volume, 'delete_on_termination': terminate_volume,
@@ -1046,7 +1051,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
'Image {image} is not a valid image in' 'Image {image} is not a valid image in'
' {cloud}:{region}'.format( ' {cloud}:{region}'.format(
image=image, image=image,
cloud=self.name, region=self.config.region_name)) cloud=self.name, region=self._compute_region))
block_mapping = { block_mapping = {
'boot_index': '0', 'boot_index': '0',
@@ -1076,7 +1081,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
'Volume {volume} is not a valid volume' 'Volume {volume} is not a valid volume'
' in {cloud}:{region}'.format( ' in {cloud}:{region}'.format(
volume=volume, volume=volume,
cloud=self.name, region=self.config.region_name)) cloud=self.name, region=self._compute_region))
block_mapping = { block_mapping = {
'boot_index': '-1', 'boot_index': '-1',
'delete_on_termination': False, 'delete_on_termination': False,

View File

@@ -563,7 +563,10 @@ class Normalizer(object):
ret['config_drive'] = config_drive ret['config_drive'] = config_drive
ret['project_id'] = project_id ret['project_id'] = project_id
ret['tenant_id'] = project_id ret['tenant_id'] = project_id
ret['region'] = self.config.region_name # TODO(efried): This is hardcoded to 'compute' because this method
# should only ever be used by the compute proxy. (That said, it
# doesn't appear to be used at all, so can we get rid of it?)
ret['region'] = self.config.get_region_name('compute')
ret['cloud'] = self.config.name ret['cloud'] = self.config.name
ret['az'] = az ret['az'] = az
for key, val in ret['properties'].items(): for key, val in ret['properties'].items():

View File

@@ -340,7 +340,9 @@ def _get_interface_ip(cloud, server):
def get_groups_from_server(cloud, server, server_vars): def get_groups_from_server(cloud, server, server_vars):
groups = [] groups = []
region = cloud.config.region_name # NOTE(efried): This is hardcoded to 'compute' because this method is only
# used from ComputeCloudMixin.
region = cloud.config.get_region_name('compute')
cloud_name = cloud.name cloud_name = cloud.name
# Create a group for the cloud # Create a group for the cloud

View File

@@ -358,7 +358,7 @@ class _OpenStackCloudMixin(object):
service_name=self.config.get_service_name(service_type), service_name=self.config.get_service_name(service_type),
interface=self.config.get_interface(service_type), interface=self.config.get_interface(service_type),
endpoint_override=self.config.get_endpoint(service_type), endpoint_override=self.config.get_endpoint(service_type),
region_name=self.config.region_name, region_name=self.config.get_region_name(service_type),
statsd_prefix=self.config.get_statsd_prefix(), statsd_prefix=self.config.get_statsd_prefix(),
statsd_client=self.config.get_statsd_client(), statsd_client=self.config.get_statsd_client(),
prometheus_counter=self.config.get_prometheus_counter(), prometheus_counter=self.config.get_prometheus_counter(),
@@ -374,7 +374,7 @@ class _OpenStackCloudMixin(object):
service_name=self.config.get_service_name(service_type), service_name=self.config.get_service_name(service_type),
interface=self.config.get_interface(service_type), interface=self.config.get_interface(service_type),
endpoint_override=self.config.get_endpoint(service_type), endpoint_override=self.config.get_endpoint(service_type),
region_name=self.config.region_name, region_name=self.config.get_region_name(service_type),
min_version=min_version, min_version=min_version,
max_version=max_version) max_version=max_version)
@@ -413,7 +413,7 @@ class _OpenStackCloudMixin(object):
interface=self.config.get_interface(service_type), interface=self.config.get_interface(service_type),
endpoint_override=self.config.get_endpoint( endpoint_override=self.config.get_endpoint(
service_type) or endpoint_override, service_type) or endpoint_override,
region_name=self.config.region_name) region_name=self.config.get_region_name(service_type))
def _is_client_version(self, client, version): def _is_client_version(self, client, version):
client_name = '_{client}_client'.format(client=client) client_name = '_{client}_client'.format(client=client)
@@ -529,7 +529,9 @@ class _OpenStackCloudMixin(object):
def _get_current_location(self, project_id=None, zone=None): def _get_current_location(self, project_id=None, zone=None):
return munch.Munch( return munch.Munch(
cloud=self.name, cloud=self.name,
region_name=self.config.region_name, # TODO(efried): This is wrong, but it only seems to be used in a
# repr; can we get rid of it?
region_name=self.config.get_region_name(),
zone=zone, zone=zone,
project=self._get_project_info(project_id), project=self._get_project_info(project_id),
) )
@@ -649,7 +651,8 @@ class _OpenStackCloudMixin(object):
return self.name return self.name
def get_region(self): def get_region(self):
return self.config.region_name # TODO(efried): This seems to be unused. Can we get rid of it?
return self.config.get_region_name()
def get_session_endpoint(self, service_key): def get_session_endpoint(self, service_key):
try: try:
@@ -666,7 +669,7 @@ class _OpenStackCloudMixin(object):
" {error}".format( " {error}".format(
service=service_key, service=service_key,
cloud=self.name, cloud=self.name,
region=self.config.region_name, region=self.config.get_region_name(service_key),
error=str(e))) error=str(e)))
return endpoint return endpoint

View File

@@ -127,8 +127,6 @@ def from_conf(conf, session=None, **kwargs):
raise exceptions.ConfigException("A Session must be supplied.") raise exceptions.ConfigException("A Session must be supplied.")
config_dict = kwargs.pop('config', config_defaults.get_defaults()) config_dict = kwargs.pop('config', config_defaults.get_defaults())
stm = os_service_types.ServiceTypes() stm = os_service_types.ServiceTypes()
# TODO(mordred) Think about region_name here
region_name = kwargs.pop('region_name', None)
for st in stm.all_types_by_service_type: for st in stm.all_types_by_service_type:
project_name = stm.get_project_name(st) project_name = stm.get_project_name(st)
if project_name not in conf: if project_name not in conf:
@@ -143,14 +141,10 @@ def from_conf(conf, session=None, **kwargs):
continue continue
# Load them into config_dict under keys prefixed by ${service_type}_ # Load them into config_dict under keys prefixed by ${service_type}_
for raw_name, opt_val in opt_dict.items(): for raw_name, opt_val in opt_dict.items():
if raw_name == 'region_name':
region_name = opt_val
continue
config_name = '_'.join([st, raw_name]) config_name = '_'.join([st, raw_name])
config_dict[config_name] = opt_val config_dict[config_name] = opt_val
return CloudRegion( return CloudRegion(
session=session, region_name=region_name, config=config_dict, session=session, config=config_dict, **kwargs)
**kwargs)
class CloudRegion(object): class CloudRegion(object):
@@ -158,6 +152,28 @@ class CloudRegion(object):
A CloudRegion encapsulates the config information needed for connections A CloudRegion encapsulates the config information needed for connections
to all of the services in a Region of a Cloud. to all of the services in a Region of a Cloud.
TODO(efried): Doc the rest of the kwargs
:param str region_name:
The default region name for all services in this CloudRegion. If
both ``region_name`` and ``config['region_name'] are specified, the
kwarg takes precedence. May be overridden for a given ${service}
via a ${service}_region_name key in the ``config`` dict.
:param dict config:
A dict of configuration values for the CloudRegion and its
services. The key for a ${config_option} for a specific ${service}
should be ${service}_${config_option}. For example, to configure
the endpoint_override for the block_storage service, the ``config``
dict should contain::
'block_storage_endpoint_override': 'http://...'
To provide a default to be used if no service-specific override is
present, just use the unprefixed ${config_option} as the service
key, e.g.::
'interface': 'public'
""" """
def __init__(self, name=None, region_name=None, config=None, def __init__(self, name=None, region_name=None, config=None,
force_ipv4=False, auth_plugin=None, force_ipv4=False, auth_plugin=None,
@@ -170,8 +186,12 @@ class CloudRegion(object):
statsd_host=None, statsd_port=None, statsd_prefix=None, statsd_host=None, statsd_port=None, statsd_prefix=None,
collector_registry=None): collector_registry=None):
self._name = name self._name = name
self.region_name = region_name
self.config = _util.normalize_keys(config) self.config = _util.normalize_keys(config)
# NOTE(efried): For backward compatibility: a) continue to accept the
# region_name kwarg; b) make it take precedence over (non-service_type-
# specific) region_name set in the config dict.
if region_name is not None:
self.config['region_name'] = region_name
self._extra_config = extra_config or {} self._extra_config = extra_config or {}
self.log = _log.setup_logging('openstack.config') self.log = _log.setup_logging('openstack.config')
self._force_ipv4 = force_ipv4 self._force_ipv4 = force_ipv4
@@ -213,7 +233,8 @@ class CloudRegion(object):
def __eq__(self, other): def __eq__(self, other):
return ( return (
self.name == other.name self.name == other.name
and self.region_name == other.region_name # Ew
and self.get_region_name() == other.get_region_name()
and self.config == other.config) and self.config == other.config)
def __ne__(self, other): def __ne__(self, other):
@@ -236,12 +257,13 @@ class CloudRegion(object):
Always returns a valid string. It will have name and region_name Always returns a valid string. It will have name and region_name
or just one of the two if only one is set, or else 'unknown'. or just one of the two if only one is set, or else 'unknown'.
""" """
if self.name and self.region_name: region_name = self.get_region_name()
return ":".join([self.name, self.region_name]) if self.name and region_name:
elif self.name and not self.region_name: return ":".join([self.name, region_name])
elif self.name and not region_name:
return self.name return self.name
elif not self.name and self.region_name: elif not self.name and region_name:
return self.region_name return region_name
else: else:
return 'unknown' return 'unknown'
@@ -335,6 +357,12 @@ class CloudRegion(object):
if st in config_dict: if st in config_dict:
return config_dict[st] return config_dict[st]
def get_region_name(self, service_type=None):
# If a region_name for the specific service_type is configured, use it;
# else use the one configured for the CloudRegion as a whole.
return self._get_config(
'region_name', service_type, fallback_to_unprefixed=True)
def get_interface(self, service_type=None): def get_interface(self, service_type=None):
return self._get_config( return self._get_config(
'interface', service_type, fallback_to_unprefixed=True) 'interface', service_type, fallback_to_unprefixed=True)
@@ -523,12 +551,13 @@ class CloudRegion(object):
# Seriously. Don't think about the existential crisis # Seriously. Don't think about the existential crisis
# that is the next line. You'll wind up in cthulhu's lair. # that is the next line. You'll wind up in cthulhu's lair.
service_type = self.get_service_type(service_type) service_type = self.get_service_type(service_type)
region_name = self.get_region_name(service_type)
versions = self.get_session().get_all_version_data( versions = self.get_session().get_all_version_data(
service_type=service_type, service_type=service_type,
interface=self.get_interface(service_type), interface=self.get_interface(service_type),
region_name=self.region_name, region_name=region_name,
) )
region_versions = versions.get(self.region_name, {}) region_versions = versions.get(region_name, {})
interface_versions = region_versions.get( interface_versions = region_versions.get(
self.get_interface(service_type), {}) self.get_interface(service_type), {})
return interface_versions.get(service_type, []) return interface_versions.get(service_type, [])
@@ -539,7 +568,7 @@ class CloudRegion(object):
service_type=self.get_service_type(service_type), service_type=self.get_service_type(service_type),
service_name=self.get_service_name(service_type), service_name=self.get_service_name(service_type),
interface=self.get_interface(service_type), interface=self.get_interface(service_type),
region_name=self.region_name, region_name=self.get_region_name(service_type),
) )
endpoint = adapter.get_endpoint() endpoint = adapter.get_endpoint()
if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0': if not endpoint.rstrip().rsplit('/')[-1] == 'v2.0':
@@ -567,6 +596,7 @@ class CloudRegion(object):
""" """
version_request = self._get_version_request(service_type, version) version_request = self._get_version_request(service_type, version)
kwargs.setdefault('region_name', self.get_region_name(service_type))
kwargs.setdefault('connect_retries', kwargs.setdefault('connect_retries',
self.get_connect_retries(service_type)) self.get_connect_retries(service_type))
kwargs.setdefault('status_code_retries', kwargs.setdefault('status_code_retries',
@@ -599,7 +629,6 @@ class CloudRegion(object):
service_type=self.get_service_type(service_type), service_type=self.get_service_type(service_type),
service_name=self.get_service_name(service_type), service_name=self.get_service_name(service_type),
interface=self.get_interface(service_type), interface=self.get_interface(service_type),
region_name=self.region_name,
version=version, version=version,
min_version=min_api_version, min_version=min_api_version,
max_version=max_api_version, max_version=max_api_version,
@@ -666,6 +695,7 @@ class CloudRegion(object):
if override_endpoint: if override_endpoint:
return override_endpoint return override_endpoint
region_name = self.get_region_name(service_type)
service_name = self.get_service_name(service_type) service_name = self.get_service_name(service_type)
interface = self.get_interface(service_type) interface = self.get_interface(service_type)
session = self.get_session() session = self.get_session()
@@ -680,7 +710,7 @@ class CloudRegion(object):
# the request # the request
endpoint = session.get_endpoint( endpoint = session.get_endpoint(
service_type=service_type, service_type=service_type,
region_name=self.region_name, region_name=region_name,
interface=interface, interface=interface,
service_name=service_name, service_name=service_name,
**version_kwargs **version_kwargs
@@ -695,7 +725,7 @@ class CloudRegion(object):
service_type, service_type,
service_name, service_name,
interface, interface,
self.region_name, region_name,
) )
return endpoint return endpoint

View File

@@ -178,20 +178,21 @@ class ServiceDescription(object):
) )
found_version = temp_adapter.get_api_major_version() found_version = temp_adapter.get_api_major_version()
if found_version is None: if found_version is None:
region_name = instance.config.get_region_name(self.service_type)
if version_kwargs: if version_kwargs:
raise exceptions.NotSupported( raise exceptions.NotSupported(
"The {service_type} service for {cloud}:{region_name}" "The {service_type} service for {cloud}:{region_name}"
" exists but does not have any supported versions.".format( " exists but does not have any supported versions.".format(
service_type=self.service_type, service_type=self.service_type,
cloud=instance.name, cloud=instance.name,
region_name=instance.config.region_name)) region_name=region_name))
else: else:
raise exceptions.NotSupported( raise exceptions.NotSupported(
"The {service_type} service for {cloud}:{region_name}" "The {service_type} service for {cloud}:{region_name}"
" exists but no version was discoverable.".format( " exists but no version was discoverable.".format(
service_type=self.service_type, service_type=self.service_type,
cloud=instance.name, cloud=instance.name,
region_name=instance.config.region_name)) region_name=region_name))
proxy_class = self.supported_versions.get(str(found_version[0])) proxy_class = self.supported_versions.get(str(found_version[0]))
if not proxy_class: if not proxy_class:
# Maybe openstacksdk is being used for the passthrough # Maybe openstacksdk is being used for the passthrough

View File

@@ -25,7 +25,9 @@ PUBLIC_V6 = '2001:0db8:face:0da0:face::0b00:1c' # rfc3849
class FakeConfig(object): class FakeConfig(object):
region_name = 'test-region' def get_region_name(self, service_type=None):
# TODO(efried): Validate service_type?
return 'test-region'
class FakeCloud(object): class FakeCloud(object):

View File

@@ -74,7 +74,7 @@ class TestOperatorCloud(base.TestCase):
session_mock.get_endpoint.side_effect = side_effect session_mock.get_endpoint.side_effect = side_effect
get_session_mock.return_value = session_mock get_session_mock.return_value = session_mock
self.cloud.name = 'testcloud' self.cloud.name = 'testcloud'
self.cloud.config.region_name = 'testregion' self.cloud.config.config['region_name'] = 'testregion'
with testtools.ExpectedException( with testtools.ExpectedException(
exc.OpenStackCloudException, exc.OpenStackCloudException,
"Error getting image endpoint on testcloud:testregion:" "Error getting image endpoint on testcloud:testregion:"

View File

@@ -181,6 +181,55 @@ class TestCloudRegion(base.TestCase):
self.assertEqual(1, cc.get_connect_retries('compute')) self.assertEqual(1, cc.get_connect_retries('compute'))
self.assertEqual(3, cc.get_connect_retries('baremetal')) self.assertEqual(3, cc.get_connect_retries('baremetal'))
def test_get_region_name(self):
# TODO(efried): ddt this
# No region_name kwarg, no regions specified in services dict
# (including the default).
cc = cloud_region.CloudRegion(config=fake_services_dict)
self.assertIsNone(cc.region_name)
self.assertIsNone(cc.get_region_name())
self.assertIsNone(cc.get_region_name(service_type=None))
self.assertIsNone(cc.get_region_name(service_type='compute'))
self.assertIsNone(cc.get_region_name(service_type='placement'))
# Only region_name kwarg; it's returned for everything
cc = cloud_region.CloudRegion(
region_name='foo', config=fake_services_dict)
self.assertEqual('foo', cc.region_name)
self.assertEqual('foo', cc.get_region_name())
self.assertEqual('foo', cc.get_region_name(service_type=None))
self.assertEqual('foo', cc.get_region_name(service_type='compute'))
self.assertEqual('foo', cc.get_region_name(service_type='placement'))
# No region_name kwarg; values (including default) show through from
# config dict
services_dict = dict(
fake_services_dict,
region_name='the-default', compute_region_name='compute-region')
cc = cloud_region.CloudRegion(config=services_dict)
self.assertEqual('the-default', cc.region_name)
self.assertEqual('the-default', cc.get_region_name())
self.assertEqual('the-default', cc.get_region_name(service_type=None))
self.assertEqual(
'compute-region', cc.get_region_name(service_type='compute'))
self.assertEqual(
'the-default', cc.get_region_name(service_type='placement'))
# region_name kwarg overrides config dict default (for backward
# compatibility), but service-specific region_name takes precedence.
services_dict = dict(
fake_services_dict,
region_name='dict', compute_region_name='compute-region')
cc = cloud_region.CloudRegion(
region_name='kwarg', config=services_dict)
self.assertEqual('kwarg', cc.region_name)
self.assertEqual('kwarg', cc.get_region_name())
self.assertEqual('kwarg', cc.get_region_name(service_type=None))
self.assertEqual(
'compute-region', cc.get_region_name(service_type='compute'))
self.assertEqual('kwarg', cc.get_region_name(service_type='placement'))
def test_aliases(self): def test_aliases(self):
services_dict = fake_services_dict.copy() services_dict = fake_services_dict.copy()
services_dict['volume_api_version'] = 12 services_dict['volume_api_version'] = 12

View File

@@ -56,13 +56,6 @@ class TestFromConf(base.TestCase):
oslocfg, session=self.cloud.session, name='from_conf.example.com') oslocfg, session=self.cloud.session, name='from_conf.example.com')
self.assertEqual('from_conf.example.com', config.name) self.assertEqual('from_conf.example.com', config.name)
# TODO(efried): Currently region_name gets set to the last value seen
# in the config, which is nondeterministic and surely incorrect.
# Sometimes that's SpecialRegion, but some tests use the base fixtures
# which have no compute endpoint in SpecialRegion. Force override for
# now to make those tests work.
config.region_name = None
return connection.Connection(config=config) return connection.Connection(config=config)
def test_adapter_opts_set(self): def test_adapter_opts_set(self):
@@ -94,8 +87,7 @@ class TestFromConf(base.TestCase):
]) ])
adap = conn.orchestration adap = conn.orchestration
# TODO(efried): Fix this when region_name behaves correctly. self.assertEqual('SpecialRegion', adap.region_name)
# self.assertEqual('SpecialRegion', adap.region_name)
self.assertEqual('orchestration', adap.service_type) self.assertEqual('orchestration', adap.service_type)
self.assertEqual('internal', adap.interface) self.assertEqual('internal', adap.interface)
self.assertEqual('https://example.org:8888/heat/v2', self.assertEqual('https://example.org:8888/heat/v2',