summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst103
-rw-r--r--os_client_config/__init__.py23
-rw-r--r--os_client_config/cloud_config.py41
-rw-r--r--os_client_config/config.py214
-rw-r--r--os_client_config/constructors.py28
-rw-r--r--os_client_config/constructos.json10
-rw-r--r--os_client_config/tests/base.py17
-rw-r--r--os_client_config/tests/test_config.py165
-rw-r--r--requirements.txt2
9 files changed, 549 insertions, 54 deletions
diff --git a/README.rst b/README.rst
index 0bdc182..ced3b18 100644
--- a/README.rst
+++ b/README.rst
@@ -29,7 +29,7 @@ Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
for trove set
-::
+.. code-block:: bash
export OS_DATABASE_SERVICE_TYPE=rax:database
@@ -56,7 +56,7 @@ Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
for trove (because you're using Rackspace) set:
-::
+.. code-block:: yaml
database_service_type: 'rax:database'
@@ -85,7 +85,7 @@ look in an OS specific config dir
An example config file is probably helpful:
-::
+.. code-block:: yaml
clouds:
mordred:
@@ -155,7 +155,7 @@ the same location rules as `clouds.yaml`. It can contain anything you put
in `clouds.yaml` and will take precedence over anything in the `clouds.yaml`
file.
-::
+.. code-block:: yaml
# clouds.yaml
clouds:
@@ -209,7 +209,7 @@ that the resource should never expire.
and presents the cache information so that your various applications that
are connecting to OpenStack can share a cache should you desire.
-::
+.. code-block:: yaml
cache:
class: dogpile.cache.pylibmc
@@ -242,7 +242,7 @@ caused it to not actually function. In that case, there is a config option
you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean
environment variable.
-::
+.. code-block:: yaml
client:
force_ipv4: true
@@ -265,12 +265,44 @@ environment variable.
The above snippet will tell client programs to prefer returning an IPv4
address.
+Per-region settings
+-------------------
+
+Sometimes you have a cloud provider that has config that is common to the
+cloud, but also with some things you might want to express on a per-region
+basis. For instance, Internap provides a public and private network specific
+to the user in each region, and putting the values of those networks into
+config can make consuming programs more efficient.
+
+To support this, the region list can actually be a list of dicts, and any
+setting that can be set at the cloud level can be overridden for that
+region.
+
+::
+
+ clouds:
+ internap:
+ profile: internap
+ auth:
+ password: XXXXXXXXXXXXXXXXX
+ username: api-55f9a00fb2619
+ project_name: inap-17037
+ regions:
+ - name: ams01
+ values:
+ external_network: inap-17037-WAN1654
+ internal_network: inap-17037-LAN4820
+ - name: nyj01
+ values:
+ external_network: inap-17037-WAN7752
+ internal_network: inap-17037-LAN6745
+
Usage
-----
The simplest and least useful thing you can do is:
-::
+.. code-block:: python
python -m os_client_config.config
@@ -279,7 +311,7 @@ it from python, which is much more likely what you want to do, things like:
Get a named cloud.
-::
+.. code-block:: python
import os_client_config
@@ -289,10 +321,63 @@ Get a named cloud.
Or, get all of the clouds.
-::
+.. code-block:: python
import os_client_config
cloud_config = os_client_config.OpenStackConfig().get_all_clouds()
for cloud in cloud_config:
print(cloud.name, cloud.region, cloud.config)
+
+argparse
+--------
+
+If you're using os-client-config from a program that wants to process
+command line options, there is a registration function to register the
+arguments that both os-client-config and keystoneauth know how to deal
+with - as well as a consumption argument.
+
+.. code-block:: python
+
+ import argparse
+ import sys
+
+ import os_client_config
+
+ cloud_config = os_client_config.OpenStackConfig()
+ parser = argparse.ArgumentParser()
+ cloud_config.register_argparse_arguments(parser, sys.argv)
+
+ options = parser.parse_args()
+
+ cloud = cloud_config.get_one_cloud(argparse=options)
+
+Constructing Legacy Client objects
+----------------------------------
+
+If all you want to do is get a Client object from a python-*client library,
+and you want it to do all the normal things related to clouds.yaml, `OS_`
+environment variables, a helper function is provided. The following
+will get you a fully configured `novaclient` instance.
+
+.. code-block:: python
+
+ import argparse
+
+ import os_client_config
+
+ nova = os_client_config.make_client('compute')
+
+If you want to do the same thing but also support command line parsing.
+
+.. code-block:: python
+
+ import argparse
+
+ import os_client_config
+
+ nova = os_client_config.make_client(
+ 'compute', options=argparse.ArgumentParser())
+
+If you want to get fancier than that in your python, then the rest of the
+API is avaiable to you. But often times, you just want to do the one thing.
diff --git a/os_client_config/__init__.py b/os_client_config/__init__.py
index 00e6ff5..ac585f2 100644
--- a/os_client_config/__init__.py
+++ b/os_client_config/__init__.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import sys
+
from os_client_config.config import OpenStackConfig # noqa
@@ -30,3 +32,24 @@ def simple_client(service_key, cloud=None, region_name=None):
"""
return OpenStackConfig().get_one_cloud(
cloud=cloud, region_name=region_name).get_session_client('compute')
+
+
+def make_client(service_key, constructor, options=None, **kwargs):
+ """Simple wrapper for getting a client instance from a client lib.
+
+ OpenStack Client Libraries all have a fairly consistent constructor
+ interface which os-client-config supports. In the simple case, there
+ is one and only one right way to construct a client object. If as a user
+ you don't want to do fancy things, just use this. It honors OS_ environment
+ variables and clouds.yaml - and takes as **kwargs anything you'd expect
+ to pass in.
+ """
+ config = OpenStackConfig()
+ if options:
+ config.register_argparse_options(options, sys.argv, service_key)
+ parsed_options = options.parse_args(sys.argv)
+ else:
+ parsed_options = None
+
+ cloud_config = config.get_one_cloud(options=parsed_options, **kwargs)
+ return cloud_config.get_legacy_client(service_key, constructor)
diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py
index 18ea4c1..6b3b5d9 100644
--- a/os_client_config/cloud_config.py
+++ b/os_client_config/cloud_config.py
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import importlib
import warnings
from keystoneauth1 import adapter
@@ -20,9 +21,44 @@ from keystoneauth1 import session
import requestsexceptions
from os_client_config import _log
+from os_client_config import constructors
from os_client_config import exceptions
+def _get_client(service_key):
+ class_mapping = constructors.get_constructor_mapping()
+ if service_key not in class_mapping:
+ raise exceptions.OpenStackConfigException(
+ "Service {service_key} is unkown. Please pass in a client"
+ " constructor or submit a patch to os-client-config".format(
+ service_key=service_key))
+ mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1)
+ lib_name = mod_name.split('.')[0]
+ try:
+ mod = importlib.import_module(mod_name)
+ except ImportError:
+ raise exceptions.OpenStackConfigException(
+ "Client for '{service_key}' was requested, but"
+ " {mod_name} was unable to be imported. Either import"
+ " the module yourself and pass the constructor in as an argument,"
+ " or perhaps you do not have python-{lib_name} installed.".format(
+ service_key=service_key,
+ mod_name=mod_name,
+ lib_name=lib_name))
+ try:
+ ctr = getattr(mod, ctr_name)
+ except AttributeError:
+ raise exceptions.OpenStackConfigException(
+ "Client for '{service_key}' was requested, but although"
+ " {mod_name} imported fine, the constructor at {fullname}"
+ " as not found. Please check your installation, we have no"
+ " clue what is wrong with your computer.".format(
+ service_key=service_key,
+ mod_name=mod_name,
+ fullname=class_mapping[service_key]))
+ return ctr
+
+
def _make_key(key, service_type):
if not service_type:
return key
@@ -217,7 +253,7 @@ class CloudConfig(object):
return endpoint
def get_legacy_client(
- self, service_key, client_class, interface_key=None,
+ self, service_key, client_class=None, interface_key=None,
pass_version_arg=True, **kwargs):
"""Return a legacy OpenStack client object for the given config.
@@ -254,6 +290,9 @@ class CloudConfig(object):
Client constructor, so this is in case anything
additional needs to be passed in.
"""
+ if not client_class:
+ client_class = _get_client(service_key)
+
# Because of course swift is different
if service_key == 'object-store':
return self._get_swift_client(client_class=client_class, **kwargs)
diff --git a/os_client_config/config.py b/os_client_config/config.py
index dff637a..70989bf 100644
--- a/os_client_config/config.py
+++ b/os_client_config/config.py
@@ -13,11 +13,15 @@
# under the License.
+# alias because we already had an option named argparse
+import argparse as argparse_mod
+import copy
import json
import os
import warnings
import appdirs
+from keystoneauth1 import adapter
from keystoneauth1 import loading
import yaml
@@ -118,8 +122,9 @@ def _merge_clouds(old_dict, new_dict):
return ret
-def _auth_update(old_dict, new_dict):
+def _auth_update(old_dict, new_dict_source):
"""Like dict.update, except handling the nested dict called auth."""
+ new_dict = copy.deepcopy(new_dict_source)
for (k, v) in new_dict.items():
if k == 'auth':
if k in old_dict:
@@ -245,6 +250,9 @@ class OpenStackConfig(object):
self._cache_expiration = cache_settings.get(
'expiration', self._cache_expiration)
+ # Flag location to hold the peeked value of an argparse timeout value
+ self._argv_timeout = False
+
def _load_config_file(self):
return self._load_yaml_json_file(self._config_files)
@@ -296,17 +304,29 @@ class OpenStackConfig(object):
return self._cache_class
def get_cache_arguments(self):
- return self._cache_arguments.copy()
+ return copy.deepcopy(self._cache_arguments)
def get_cache_expiration(self):
- return self._cache_expiration.copy()
+ return copy.deepcopy(self._cache_expiration)
+
+ def _expand_region_name(self, region_name):
+ return {'name': region_name, 'values': {}}
+
+ def _expand_regions(self, regions):
+ ret = []
+ for region in regions:
+ if isinstance(region, dict):
+ ret.append(copy.deepcopy(region))
+ else:
+ ret.append(self._expand_region_name(region))
+ return ret
def _get_regions(self, cloud):
if cloud not in self.cloud_config['clouds']:
- return ['']
+ return [self._expand_region_name('')]
config = self._normalize_keys(self.cloud_config['clouds'][cloud])
if 'regions' in config:
- return config['regions']
+ return self._expand_regions(config['regions'])
elif 'region_name' in config:
regions = config['region_name'].split(',')
if len(regions) > 1:
@@ -314,22 +334,39 @@ class OpenStackConfig(object):
"Comma separated lists in region_name are deprecated."
" Please use a yaml list in the regions"
" parameter in {0} instead.".format(self.config_filename))
- return regions
+ return self._expand_regions(regions)
else:
# crappit. we don't have a region defined.
new_cloud = dict()
our_cloud = self.cloud_config['clouds'].get(cloud, dict())
self._expand_vendor_profile(cloud, new_cloud, our_cloud)
if 'regions' in new_cloud and new_cloud['regions']:
- return new_cloud['regions']
+ return self._expand_regions(new_cloud['regions'])
elif 'region_name' in new_cloud and new_cloud['region_name']:
- return [new_cloud['region_name']]
+ return [self._expand_region_name(new_cloud['region_name'])]
else:
# Wow. We really tried
- return ['']
+ return [self._expand_region_name('')]
+
+ def _get_region(self, cloud=None, region_name=''):
+ if not cloud:
+ return self._expand_region_name(region_name)
+
+ regions = self._get_regions(cloud)
+ if not region_name:
+ return regions[0]
+
+ for region in regions:
+ if region['name'] == region_name:
+ return region
- def _get_region(self, cloud=None):
- return self._get_regions(cloud)[0]
+ raise exceptions.OpenStackConfigException(
+ 'Region {region_name} is not a valid region name for cloud'
+ ' {cloud}. Valid choices are {region_list}. Please note that'
+ ' region names are case sensitive.'.format(
+ region_name=region_name,
+ region_list=','.join([r['name'] for r in regions]),
+ cloud=cloud))
def get_cloud_names(self):
return self.cloud_config['clouds'].keys()
@@ -451,6 +488,94 @@ class OpenStackConfig(object):
cloud['auth_type'] = 'password'
return cloud
+ def register_argparse_arguments(self, parser, argv, service_keys=[]):
+ """Register all of the common argparse options needed.
+
+ Given an argparse parser, register the keystoneauth Session arguments,
+ the keystoneauth Auth Plugin Options and os-cloud. Also, peek in the
+ argv to see if all of the auth plugin options should be registered
+ or merely the ones already configured.
+ :param argparse.ArgumentParser: parser to attach argparse options to
+ :param list argv: the arguments provided to the application
+ :param string service_keys: Service or list of services this argparse
+ should be specialized for, if known.
+ The first item in the list will be used
+ as the default value for service_type
+ (optional)
+
+ :raises exceptions.OpenStackConfigException if an invalid auth-type
+ is requested
+ """
+
+ local_parser = argparse_mod.ArgumentParser(add_help=False)
+
+ for p in (parser, local_parser):
+ p.add_argument(
+ '--os-cloud',
+ metavar='<name>',
+ default=os.environ.get('OS_CLOUD', None),
+ help='Named cloud to connect to')
+
+ # we need to peek to see if timeout was actually passed, since
+ # the keystoneauth declaration of it has a default, which means
+ # we have no clue if the value we get is from the ksa default
+ # for from the user passing it explicitly. We'll stash it for later
+ local_parser.add_argument('--timeout', metavar='<timeout>')
+
+ # Peek into the future and see if we have an auth-type set in
+ # config AND a cloud set, so that we know which command line
+ # arguments to register and show to the user (the user may want
+ # to say something like:
+ # openstack --os-cloud=foo --os-oidctoken=bar
+ # although I think that user is the cause of my personal pain
+ options, _args = local_parser.parse_known_args(argv)
+ if options.timeout:
+ self._argv_timeout = True
+
+ # validate = False because we're not _actually_ loading here
+ # we're only peeking, so it's the wrong time to assert that
+ # the rest of the arguments given are invalid for the plugin
+ # chosen (for instance, --help may be requested, so that the
+ # user can see what options he may want to give
+ cloud = self.get_one_cloud(argparse=options, validate=False)
+ default_auth_type = cloud.config['auth_type']
+
+ try:
+ loading.register_auth_argparse_arguments(
+ parser, argv, default=default_auth_type)
+ except Exception:
+ # Hidiing the keystoneauth exception because we're not actually
+ # loading the auth plugin at this point, so the error message
+ # from it doesn't actually make sense to os-client-config users
+ options, _args = parser.parse_known_args(argv)
+ plugin_names = loading.get_available_plugin_names()
+ raise exceptions.OpenStackConfigException(
+ "An invalid auth-type was specified: {auth_type}."
+ " Valid choices are: {plugin_names}.".format(
+ auth_type=options.os_auth_type,
+ plugin_names=",".join(plugin_names)))
+
+ if service_keys:
+ primary_service = service_keys[0]
+ else:
+ primary_service = None
+ loading.register_session_argparse_arguments(parser)
+ adapter.register_adapter_argparse_arguments(
+ parser, service_type=primary_service)
+ for service_key in service_keys:
+ # legacy clients have un-prefixed api-version options
+ parser.add_argument(
+ '--{service_key}-api-version'.format(
+ service_key=service_key.replace('_', '-'),
+ help=argparse_mod.SUPPRESS))
+ adapter.register_service_adapter_argparse_arguments(
+ parser, service_type=service_key)
+
+ # Backwards compat options for legacy clients
+ parser.add_argument('--http-timeout', help=argparse_mod.SUPPRESS)
+ parser.add_argument('--os-endpoint-type', help=argparse_mod.SUPPRESS)
+ parser.add_argument('--endpoint-type', help=argparse_mod.SUPPRESS)
+
def _fix_backwards_interface(self, cloud):
new_cloud = {}
for key in cloud.keys():
@@ -461,13 +586,39 @@ class OpenStackConfig(object):
new_cloud[target_key] = cloud[key]
return new_cloud
+ def _fix_backwards_api_timeout(self, cloud):
+ new_cloud = {}
+ # requests can only have one timeout, which means that in a single
+ # cloud there is no point in different timeout values. However,
+ # for some reason many of the legacy clients decided to shove their
+ # service name in to the arg name for reasons surpassin sanity. If
+ # we find any values that are not api_timeout, overwrite api_timeout
+ # with the value
+ service_timeout = None
+ for key in cloud.keys():
+ if key.endswith('timeout') and not (
+ key == 'timeout' or key == 'api_timeout'):
+ service_timeout = cloud[key]
+ else:
+ new_cloud[key] = cloud[key]
+ if service_timeout is not None:
+ new_cloud['api_timeout'] = service_timeout
+ # The common argparse arg from keystoneauth is called timeout, but
+ # os-client-config expects it to be called api_timeout
+ if self._argv_timeout:
+ if 'timeout' in new_cloud and new_cloud['timeout']:
+ new_cloud['api_timeout'] = new_cloud.pop('timeout')
+ return new_cloud
+
def get_all_clouds(self):
clouds = []
for cloud in self.get_cloud_names():
for region in self._get_regions(cloud):
- clouds.append(self.get_one_cloud(cloud, region_name=region))
+ if region:
+ clouds.append(self.get_one_cloud(
+ cloud, region_name=region['name']))
return clouds
def _fix_args(self, args, argparse=None):
@@ -646,30 +797,33 @@ class OpenStackConfig(object):
else:
cloud = self.default_cloud
- if 'region_name' not in args or args['region_name'] is None:
- args['region_name'] = self._get_region(cloud)
-
config = self._get_base_cloud_config(cloud)
+ # Get region specific settings
+ if 'region_name' not in args:
+ args['region_name'] = ''
+ region = self._get_region(cloud=cloud, region_name=args['region_name'])
+ args['region_name'] = region['name']
+ region_args = copy.deepcopy(region['values'])
+
# Regions is a list that we can use to create a list of cloud/region
# objects. It does not belong in the single-cloud dict
- regions = config.pop('regions', None)
- if regions and args['region_name'] not in regions:
- raise exceptions.OpenStackConfigException(
- 'Region {region_name} is not a valid region name for cloud'
- ' {cloud}. Valid choices are {region_list}. Please note that'
- ' region names are case sensitive.'.format(
- region_name=args['region_name'],
- region_list=','.join(regions),
- cloud=cloud))
+ config.pop('regions', None)
# Can't just do update, because None values take over
- for (key, val) in iter(args.items()):
- if val is not None:
- if key == 'auth' and config[key] is not None:
- config[key] = _auth_update(config[key], val)
- else:
- config[key] = val
+ for arg_list in region_args, args:
+ for (key, val) in iter(arg_list.items()):
+ if val is not None:
+ if key == 'auth' and config[key] is not None:
+ config[key] = _auth_update(config[key], val)
+ else:
+ config[key] = val
+
+ # These backwards compat values are only set via argparse. If it's
+ # there, it's because it was passed in explicitly, and should win
+ config = self._fix_backwards_api_timeout(config)
+ if 'endpoint_type' in config:
+ config['interface'] = config.pop('endpoint_type')
for key in BOOL_KEYS:
if key in config:
diff --git a/os_client_config/constructors.py b/os_client_config/constructors.py
new file mode 100644
index 0000000..e88ac92
--- /dev/null
+++ b/os_client_config/constructors.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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 json
+import os
+
+_json_path = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'constructors.json')
+_class_mapping = None
+
+
+def get_constructor_mapping():
+ global _class_mapping
+ if not _class_mapping:
+ with open(_json_path, 'r') as json_file:
+ _class_mapping = json.load(json_file)
+ return _class_mapping
diff --git a/os_client_config/constructos.json b/os_client_config/constructos.json
new file mode 100644
index 0000000..d9ebf2c
--- /dev/null
+++ b/os_client_config/constructos.json
@@ -0,0 +1,10 @@
+{
+ "compute": "novaclient.client.Client",
+ "database": "troveclient.client.Client",
+ "identity": "keystoneclient.client.Client",
+ "image": "glanceclient.Client",
+ "network": "neutronclient.neutron.client.Client",
+ "object-store": "swiftclient.client.Connection",
+ "orchestration": "heatclient.client.Client",
+ "volume": "cinderclient.client.Client"
+}
diff --git a/os_client_config/tests/base.py b/os_client_config/tests/base.py
index 33a868d..6d9e093 100644
--- a/os_client_config/tests/base.py
+++ b/os_client_config/tests/base.py
@@ -16,6 +16,7 @@
# under the License.
+import copy
import os
import tempfile
@@ -96,8 +97,18 @@ USER_CONF = {
'auth_url': 'http://example.com/v2',
},
'regions': [
- 'region1',
- 'region2',
+ {
+ 'name': 'region1',
+ 'values': {
+ 'external_network': 'region1-network',
+ }
+ },
+ {
+ 'name': 'region2',
+ 'values': {
+ 'external_network': 'my-network',
+ }
+ }
],
},
'_test_cloud_hyphenated': {
@@ -139,7 +150,7 @@ class TestCase(base.BaseTestCase):
super(TestCase, self).setUp()
self.useFixture(fixtures.NestedTempfile())
- conf = dict(USER_CONF)
+ conf = copy.deepcopy(USER_CONF)
tdir = self.useFixture(fixtures.TempDir())
conf['cache']['path'] = tdir.path
self.cloud_yaml = _write_yaml(conf)
diff --git a/os_client_config/tests/test_config.py b/os_client_config/tests/test_config.py
index aff8c6d..a6a35ad 100644
--- a/os_client_config/tests/test_config.py
+++ b/os_client_config/tests/test_config.py
@@ -17,6 +17,7 @@ import copy
import os
import fixtures
+import testtools
import yaml
from os_client_config import cloud_config
@@ -225,6 +226,8 @@ class TestConfig(base.TestCase):
new_config)
with open(self.cloud_yaml) as fh:
written_config = yaml.safe_load(fh)
+ # We write a cache config for testing
+ written_config['cache'].pop('path', None)
self.assertEqual(written_config, resulting_config)
@@ -238,18 +241,26 @@ class TestConfigArgparse(base.TestCase):
username='user',
password='password',
project_name='project',
- region_name='other-test-region',
+ region_name='region2',
snack_type='cookie',
)
self.options = argparse.Namespace(**self.args)
+ def test_get_one_cloud_bad_region_argparse(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+
+ self.assertRaises(
+ exceptions.OpenStackConfigException, c.get_one_cloud,
+ cloud='_test-cloud_', argparse=self.options)
+
def test_get_one_cloud_argparse(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
- cc = c.get_one_cloud(cloud='_test-cloud_', argparse=self.options)
- self._assert_cloud_details(cc)
- self.assertEqual(cc.region_name, 'other-test-region')
+ cc = c.get_one_cloud(
+ cloud='_test_cloud_regions', argparse=self.options)
+ self.assertEqual(cc.region_name, 'region2')
self.assertEqual(cc.snack_type, 'cookie')
def test_get_one_cloud_just_argparse(self):
@@ -258,7 +269,7 @@ class TestConfigArgparse(base.TestCase):
cc = c.get_one_cloud(argparse=self.options)
self.assertIsNone(cc.cloud)
- self.assertEqual(cc.region_name, 'other-test-region')
+ self.assertEqual(cc.region_name, 'region2')
self.assertEqual(cc.snack_type, 'cookie')
def test_get_one_cloud_just_kwargs(self):
@@ -267,7 +278,7 @@ class TestConfigArgparse(base.TestCase):
cc = c.get_one_cloud(**self.args)
self.assertIsNone(cc.cloud)
- self.assertEqual(cc.region_name, 'other-test-region')
+ self.assertEqual(cc.region_name, 'region2')
self.assertEqual(cc.snack_type, 'cookie')
def test_get_one_cloud_dash_kwargs(self):
@@ -317,10 +328,10 @@ class TestConfigArgparse(base.TestCase):
def test_get_one_cloud_bad_region_no_regions(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
-
- cc = c.get_one_cloud(cloud='_test-cloud_', region_name='bad_region')
- self._assert_cloud_details(cc)
- self.assertEqual(cc.region_name, 'bad_region')
+ self.assertRaises(
+ exceptions.OpenStackConfigException,
+ c.get_one_cloud,
+ cloud='_test-cloud_', region_name='bad_region')
def test_get_one_cloud_no_argparse_region2(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
@@ -332,6 +343,26 @@ class TestConfigArgparse(base.TestCase):
self.assertEqual(cc.region_name, 'region2')
self.assertIsNone(cc.snack_type)
+ def test_get_one_cloud_network(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+
+ cc = c.get_one_cloud(
+ cloud='_test_cloud_regions', region_name='region1', argparse=None)
+ self._assert_cloud_details(cc)
+ self.assertEqual(cc.region_name, 'region1')
+ self.assertEqual('region1-network', cc.config['external_network'])
+
+ def test_get_one_cloud_per_region_network(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+
+ cc = c.get_one_cloud(
+ cloud='_test_cloud_regions', region_name='region2', argparse=None)
+ self._assert_cloud_details(cc)
+ self.assertEqual(cc.region_name, 'region2')
+ self.assertEqual('my-network', cc.config['external_network'])
+
def test_fix_env_args(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
@@ -341,6 +372,120 @@ class TestConfigArgparse(base.TestCase):
self.assertDictEqual({'compute_api_version': 1}, fixed_args)
+ def test_register_argparse_cloud(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ c.register_argparse_arguments(parser, [])
+ opts, _remain = parser.parse_known_args(['--os-cloud', 'foo'])
+ self.assertEqual(opts.os_cloud, 'foo')
+
+ def test_register_argparse_bad_plugin(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ self.assertRaises(
+ exceptions.OpenStackConfigException,
+ c.register_argparse_arguments,
+ parser, ['--os-auth-type', 'foo'])
+
+ def test_register_argparse_not_password(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-auth-type', 'v3token',
+ '--os-token', 'some-secret',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_token, 'some-secret')
+
+ def test_register_argparse_password(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-password', 'some-secret',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_password, 'some-secret')
+ with testtools.ExpectedException(AttributeError):
+ opts.os_token
+
+ def test_register_argparse_service_type(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-service-type', 'network',
+ '--os-endpoint-type', 'admin',
+ '--http-timeout', '20',
+ ]
+ c.register_argparse_arguments(parser, args)
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_service_type, 'network')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.http_timeout, '20')
+ with testtools.ExpectedException(AttributeError):
+ opts.os_network_service_type
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'network')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['api_timeout'], '20')
+ self.assertNotIn('http_timeout', cloud.config)
+
+ def test_register_argparse_network_service_type(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-endpoint-type', 'admin',
+ '--network-api-version', '4',
+ ]
+ c.register_argparse_arguments(parser, args, ['network'])
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_service_type, 'network')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.os_network_service_type, None)
+ self.assertEqual(opts.os_network_api_version, None)
+ self.assertEqual(opts.network_api_version, '4')
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'network')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['network_api_version'], '4')
+ self.assertNotIn('http_timeout', cloud.config)
+
+ def test_register_argparse_network_service_types(self):
+ c = config.OpenStackConfig(config_files=[self.cloud_yaml],
+ vendor_files=[self.vendor_yaml])
+ parser = argparse.ArgumentParser()
+ args = [
+ '--os-compute-service-name', 'cloudServers',
+ '--os-network-service-type', 'badtype',
+ '--os-endpoint-type', 'admin',
+ '--network-api-version', '4',
+ ]
+ c.register_argparse_arguments(
+ parser, args, ['compute', 'network', 'volume'])
+ opts, _remain = parser.parse_known_args(args)
+ self.assertEqual(opts.os_network_service_type, 'badtype')
+ self.assertEqual(opts.os_compute_service_type, None)
+ self.assertEqual(opts.os_volume_service_type, None)
+ self.assertEqual(opts.os_service_type, 'compute')
+ self.assertEqual(opts.os_compute_service_name, 'cloudServers')
+ self.assertEqual(opts.os_endpoint_type, 'admin')
+ self.assertEqual(opts.os_network_api_version, None)
+ self.assertEqual(opts.network_api_version, '4')
+ cloud = c.get_one_cloud(argparse=opts, verify=False)
+ self.assertEqual(cloud.config['service_type'], 'compute')
+ self.assertEqual(cloud.config['network_service_type'], 'badtype')
+ self.assertEqual(cloud.config['interface'], 'admin')
+ self.assertEqual(cloud.config['network_api_version'], '4')
+ self.assertNotIn('volume_service_type', cloud.config)
+ self.assertNotIn('http_timeout', cloud.config)
+
class TestConfigDefault(base.TestCase):
diff --git a/requirements.txt b/requirements.txt
index 3c32ced..1531be8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,5 @@
# process, which may cause wedges in the gate later.
PyYAML>=3.1.0
appdirs>=1.3.0
-keystoneauth1>=1.0.0
+keystoneauth1>=2.1.0
requestsexceptions>=1.1.1 # Apache-2.0