diff options
-rw-r--r-- | README.rst | 103 | ||||
-rw-r--r-- | os_client_config/__init__.py | 23 | ||||
-rw-r--r-- | os_client_config/cloud_config.py | 41 | ||||
-rw-r--r-- | os_client_config/config.py | 214 | ||||
-rw-r--r-- | os_client_config/constructors.py | 28 | ||||
-rw-r--r-- | os_client_config/constructos.json | 10 | ||||
-rw-r--r-- | os_client_config/tests/base.py | 17 | ||||
-rw-r--r-- | os_client_config/tests/test_config.py | 165 | ||||
-rw-r--r-- | requirements.txt | 2 |
9 files changed, 549 insertions, 54 deletions
@@ -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 |