1 # Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
16 # alias because we already had an option named argparse
17 import argparse as argparse_mod
26 from keystoneauth1 import adapter
27 from keystoneauth1 import loading
30 from os_client_config import _log
31 from os_client_config import cloud_config
32 from os_client_config import defaults
33 from os_client_config import exceptions
34 from os_client_config import vendors
36 APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc')
37 CONFIG_HOME = APPDIRS.user_config_dir
38 CACHE_PATH = APPDIRS.user_cache_dir
40 UNIX_CONFIG_HOME = os.path.join(
41 os.path.expanduser(os.path.join('~', '.config')), 'openstack')
42 UNIX_SITE_CONFIG_HOME = '/etc/openstack'
44 SITE_CONFIG_HOME = APPDIRS.site_config_dir
46 CONFIG_SEARCH_PATH = [
48 CONFIG_HOME, UNIX_CONFIG_HOME,
49 SITE_CONFIG_HOME, UNIX_SITE_CONFIG_HOME
51 YAML_SUFFIXES = ('.yaml', '.yml')
52 JSON_SUFFIXES = ('.json',)
54 os.path.join(d, 'clouds' + s)
55 for d in CONFIG_SEARCH_PATH
56 for s in YAML_SUFFIXES + JSON_SUFFIXES
59 os.path.join(d, 'secure' + s)
60 for d in CONFIG_SEARCH_PATH
61 for s in YAML_SUFFIXES + JSON_SUFFIXES
64 os.path.join(d, 'clouds-public' + s)
65 for d in CONFIG_SEARCH_PATH
66 for s in YAML_SUFFIXES + JSON_SUFFIXES
69 BOOL_KEYS = ('insecure', 'cache')
72 # NOTE(dtroyer): This turns out to be not the best idea so let's move
73 # overriding defaults to a kwarg to OpenStackConfig.__init__()
74 # Remove this sometime in June 2015 once OSC is comfortably
75 # changed-over and global-defaults is updated.
76 def set_default(key, value):
78 "Use of set_default() is deprecated. Defaults should be set with the "
79 "`override_defaults` parameter of OpenStackConfig."
81 defaults.get_defaults() # make sure the dict is initialized
82 defaults._defaults[key] = value
85 def get_boolean(value):
88 if type(value) is bool:
90 if value.lower() == 'true':
95 def _get_os_environ(envvar_prefix=None):
96 ret = defaults.get_defaults()
98 # This makes the or below be OS_ or OS_ which is a no-op
100 environkeys = [k for k in os.environ.keys()
101 if (k.startswith('OS_') or k.startswith(envvar_prefix))
102 and not k.startswith('OS_TEST') # infra CI var
103 and not k.startswith('OS_STD') # infra CI var
105 for k in environkeys:
106 newkey = k.split('_', 1)[-1].lower()
107 ret[newkey] = os.environ[k]
108 # If the only environ keys are cloud and region_name, don't return anything
109 # because they are cloud selectors
110 if set(environkeys) - set(['OS_CLOUD', 'OS_REGION_NAME']):
115 def _merge_clouds(old_dict, new_dict):
116 """Like dict.update, except handling nested dicts."""
117 ret = old_dict.copy()
118 for (k, v) in new_dict.items():
119 if isinstance(v, dict):
121 ret[k] = _merge_clouds(ret[k], v)
129 def _auth_update(old_dict, new_dict_source):
130 """Like dict.update, except handling the nested dict called auth."""
131 new_dict = copy.deepcopy(new_dict_source)
132 for (k, v) in new_dict.items():
135 old_dict[k].update(v)
137 old_dict[k] = v.copy()
144 # Transform any _ characters in arg names to - so that we don't
145 # have to throw billions of compat argparse arguments around all
147 processed = collections.defaultdict(list)
148 for index in range(0, len(argv)):
149 if argv[index].startswith('--'):
150 split_args = argv[index].split('=')
152 new = orig.replace('_', '-')
155 argv[index] = "=".join(split_args)
156 # Save both for later so we can throw an error about dupes
157 processed[new].append(orig)
159 for new, old in processed.items():
163 raise exceptions.OpenStackConfigException(
164 "The following options were given: '{options}' which contain"
165 " duplicates except that one has _ and one has -. There is"
166 " no sane way for us to know what you're doing. Remove the"
167 " duplicate option and try again".format(
168 options=','.join(overlap)))
171 class OpenStackConfig(object):
173 def __init__(self, config_files=None, vendor_files=None,
174 override_defaults=None, force_ipv4=None,
175 envvar_prefix=None, secure_files=None,
176 pw_func=None, session_constructor=None):
177 self.log = _log.setup_logging(__name__)
178 self._session_constructor = session_constructor
180 self._config_files = config_files or CONFIG_FILES
181 self._secure_files = secure_files or SECURE_FILES
182 self._vendor_files = vendor_files or VENDOR_FILES
184 config_file_override = os.environ.pop('OS_CLIENT_CONFIG_FILE', None)
185 if config_file_override:
186 self._config_files.insert(0, config_file_override)
188 secure_file_override = os.environ.pop('OS_CLIENT_SECURE_FILE', None)
189 if secure_file_override:
190 self._secure_files.insert(0, secure_file_override)
192 self.defaults = defaults.get_defaults()
193 if override_defaults:
194 self.defaults.update(override_defaults)
196 # First, use a config file if it exists where expected
197 self.config_filename, self.cloud_config = self._load_config_file()
198 _, secure_config = self._load_secure_file()
200 self.cloud_config = _merge_clouds(
201 self.cloud_config, secure_config)
203 if not self.cloud_config:
204 self.cloud_config = {'clouds': {}}
205 if 'clouds' not in self.cloud_config:
206 self.cloud_config['clouds'] = {}
208 # Grab ipv6 preference settings from env
209 client_config = self.cloud_config.get('client', {})
211 if force_ipv4 is not None:
212 # If it's passed in to the constructor, honor it.
213 self.force_ipv4 = force_ipv4
215 # Get the backwards compat value
216 prefer_ipv6 = get_boolean(
218 'OS_PREFER_IPV6', client_config.get(
219 'prefer_ipv6', client_config.get(
220 'prefer-ipv6', True))))
221 force_ipv4 = get_boolean(
223 'OS_FORCE_IPV4', client_config.get(
224 'force_ipv4', client_config.get(
225 'broken-ipv6', False))))
227 self.force_ipv4 = force_ipv4
229 # this will only be false if someone set it explicitly
231 self.force_ipv4 = True
233 # Next, process environment variables and add them to the mix
234 self.envvar_key = os.environ.pop('OS_CLOUD_NAME', 'envvars')
235 if self.envvar_key in self.cloud_config['clouds']:
236 raise exceptions.OpenStackConfigException(
237 '"{0}" defines a cloud named "{1}", but'
238 ' OS_CLOUD_NAME is also set to "{1}". Please rename'
239 ' either your environment based cloud, or one of your'
240 ' file-based clouds.'.format(self.config_filename,
242 # Pull out OS_CLOUD so that if it's the only thing set, do not
243 # make an envvars cloud
244 self.default_cloud = os.environ.pop('OS_CLOUD', None)
246 envvars = _get_os_environ(envvar_prefix=envvar_prefix)
248 self.cloud_config['clouds'][self.envvar_key] = envvars
249 if not self.default_cloud:
250 self.default_cloud = self.envvar_key
252 # Finally, fall through and make a cloud that starts with defaults
253 # because we need somewhere to put arguments, and there are neither
254 # config files or env vars
255 if not self.cloud_config['clouds']:
256 self.cloud_config = dict(
257 clouds=dict(defaults=dict(self.defaults)))
258 self.default_cloud = 'defaults'
260 self._cache_expiration_time = 0
261 self._cache_path = CACHE_PATH
262 self._cache_class = 'dogpile.cache.null'
263 self._cache_arguments = {}
264 self._cache_expiration = {}
265 if 'cache' in self.cloud_config:
266 cache_settings = self._normalize_keys(self.cloud_config['cache'])
268 # expiration_time used to be 'max_age' but the dogpile setting
269 # is expiration_time. Support max_age for backwards compat.
270 self._cache_expiration_time = cache_settings.get(
271 'expiration_time', cache_settings.get(
272 'max_age', self._cache_expiration_time))
274 # If cache class is given, use that. If not, but if cache time
275 # is given, default to memory. Otherwise, default to nothing.
277 if self._cache_expiration_time:
278 self._cache_class = 'dogpile.cache.memory'
279 self._cache_class = self.cloud_config['cache'].get(
280 'class', self._cache_class)
282 self._cache_path = os.path.expanduser(
283 cache_settings.get('path', self._cache_path))
284 self._cache_arguments = cache_settings.get(
285 'arguments', self._cache_arguments)
286 self._cache_expiration = cache_settings.get(
287 'expiration', self._cache_expiration)
289 # Flag location to hold the peeked value of an argparse timeout value
290 self._argv_timeout = False
292 # Save the password callback
293 # password = self._pw_callback(prompt="Password: ")
294 self._pw_callback = pw_func
296 def get_extra_config(self, key, defaults=None):
297 """Fetch an arbitrary extra chunk of config, laying in defaults.
299 :param string key: name of the config section to fetch
300 :param dict defaults: (optional) default values to merge under the
305 return _merge_clouds(
306 self._normalize_keys(defaults),
307 self._normalize_keys(self.cloud_config.get(key, {})))
309 def _load_config_file(self):
310 return self._load_yaml_json_file(self._config_files)
312 def _load_secure_file(self):
313 return self._load_yaml_json_file(self._secure_files)
315 def _load_vendor_file(self):
316 return self._load_yaml_json_file(self._vendor_files)
318 def _load_yaml_json_file(self, filelist):
319 for path in filelist:
320 if os.path.exists(path):
321 with open(path, 'r') as f:
322 if path.endswith('json'):
323 return path, json.load(f)
325 return path, yaml.safe_load(f)
328 def _normalize_keys(self, config):
330 for key, value in config.items():
331 key = key.replace('-', '_')
332 if isinstance(value, dict):
333 new_config[key] = self._normalize_keys(value)
334 elif isinstance(value, bool):
335 new_config[key] = value
336 elif isinstance(value, int) and key != 'verbose_level':
337 new_config[key] = str(value)
338 elif isinstance(value, float):
339 new_config[key] = str(value)
341 new_config[key] = value
344 def get_cache_expiration_time(self):
345 return int(self._cache_expiration_time)
347 def get_cache_interval(self):
348 return self.get_cache_expiration_time()
350 def get_cache_max_age(self):
351 return self.get_cache_expiration_time()
353 def get_cache_path(self):
354 return self._cache_path
356 def get_cache_class(self):
357 return self._cache_class
359 def get_cache_arguments(self):
360 return copy.deepcopy(self._cache_arguments)
362 def get_cache_expiration(self):
363 return copy.deepcopy(self._cache_expiration)
365 def _expand_region_name(self, region_name):
366 return {'name': region_name, 'values': {}}
368 def _expand_regions(self, regions):
370 for region in regions:
371 if isinstance(region, dict):
372 ret.append(copy.deepcopy(region))
374 ret.append(self._expand_region_name(region))
377 def _get_regions(self, cloud):
378 if cloud not in self.cloud_config['clouds']:
379 return [self._expand_region_name('')]
380 regions = self._get_known_regions(cloud)
382 # We don't know of any regions use a workable default.
383 regions = [self._expand_region_name('')]
386 def _get_known_regions(self, cloud):
387 config = self._normalize_keys(self.cloud_config['clouds'][cloud])
388 if 'regions' in config:
389 return self._expand_regions(config['regions'])
390 elif 'region_name' in config:
391 if isinstance(config['region_name'], list):
392 regions = config['region_name']
394 regions = config['region_name'].split(',')
397 "Comma separated lists in region_name are deprecated."
398 " Please use a yaml list in the regions"
399 " parameter in {0} instead.".format(self.config_filename))
400 return self._expand_regions(regions)
402 # crappit. we don't have a region defined.
404 our_cloud = self.cloud_config['clouds'].get(cloud, dict())
405 self._expand_vendor_profile(cloud, new_cloud, our_cloud)
406 if 'regions' in new_cloud and new_cloud['regions']:
407 return self._expand_regions(new_cloud['regions'])
408 elif 'region_name' in new_cloud and new_cloud['region_name']:
409 return [self._expand_region_name(new_cloud['region_name'])]
411 def _get_region(self, cloud=None, region_name=''):
412 if region_name is None:
415 return self._expand_region_name(region_name)
417 regions = self._get_known_regions(cloud)
419 return self._expand_region_name(region_name)
424 for region in regions:
425 if region['name'] == region_name:
428 raise exceptions.OpenStackConfigException(
429 'Region {region_name} is not a valid region name for cloud'
430 ' {cloud}. Valid choices are {region_list}. Please note that'
431 ' region names are case sensitive.'.format(
432 region_name=region_name,
433 region_list=','.join([r['name'] for r in regions]),
436 def get_cloud_names(self):
437 return self.cloud_config['clouds'].keys()
439 def _get_base_cloud_config(self, name):
442 # Only validate cloud name if one was given
443 if name and name not in self.cloud_config['clouds']:
444 raise exceptions.OpenStackConfigException(
445 "Cloud {name} was not found.".format(
448 our_cloud = self.cloud_config['clouds'].get(name, dict())
451 cloud.update(self.defaults)
452 self._expand_vendor_profile(name, cloud, our_cloud)
454 if 'auth' not in cloud:
455 cloud['auth'] = dict()
457 _auth_update(cloud, our_cloud)
463 def _expand_vendor_profile(self, name, cloud, our_cloud):
464 # Expand a profile if it exists. 'cloud' is an old confusing name
466 profile_name = our_cloud.get('profile', our_cloud.get('cloud', None))
467 if profile_name and profile_name != self.envvar_key:
468 if 'cloud' in our_cloud:
470 "{0} use the keyword 'cloud' to reference a known "
471 "vendor profile. This has been deprecated in favor of the "
472 "'profile' keyword.".format(self.config_filename))
473 vendor_filename, vendor_file = self._load_vendor_file()
474 if vendor_file and profile_name in vendor_file['public-clouds']:
475 _auth_update(cloud, vendor_file['public-clouds'][profile_name])
477 profile_data = vendors.get_profile(profile_name)
479 status = profile_data.pop('status', 'active')
480 message = profile_data.pop('message', '')
481 if status == 'deprecated':
483 "{profile_name} is deprecated: {message}".format(
484 profile_name=profile_name, message=message))
485 elif status == 'shutdown':
486 raise exceptions.OpenStackConfigException(
487 "{profile_name} references a cloud that no longer"
488 " exists: {message}".format(
489 profile_name=profile_name, message=message))
490 _auth_update(cloud, profile_data)
492 # Can't find the requested vendor config, go about business
493 warnings.warn("Couldn't find the vendor profile '{0}', for"
494 " the cloud '{1}'".format(profile_name,
497 def _project_scoped(self, cloud):
498 return ('project_id' in cloud or 'project_name' in cloud
499 or 'project_id' in cloud['auth']
500 or 'project_name' in cloud['auth'])
502 def _validate_networks(self, networks, key):
505 if value and net[key]:
506 raise exceptions.OpenStackConfigException(
507 "Duplicate network entries for {key}: {net1} and {net2}."
508 " Only one network can be flagged with {key}".format(
512 if not value and net[key]:
515 def _fix_backwards_networks(self, cloud):
516 # Leave the external_network and internal_network keys in the
517 # dict because consuming code might be expecting them.
519 # Normalize existing network entries
520 for net in cloud.get('networks', []):
521 name = net.get('name')
523 raise exceptions.OpenStackConfigException(
524 'Entry in network list is missing required field "name".')
527 routes_externally=get_boolean(net.get('routes_externally')),
528 nat_destination=get_boolean(net.get('nat_destination')),
529 default_interface=get_boolean(net.get('default_interface')),
531 # routes_ipv4_externally defaults to the value of routes_externally
532 network['routes_ipv4_externally'] = get_boolean(
534 'routes_ipv4_externally', network['routes_externally']))
535 # routes_ipv6_externally defaults to the value of routes_externally
536 network['routes_ipv6_externally'] = get_boolean(
538 'routes_ipv6_externally', network['routes_externally']))
539 networks.append(network)
541 for key in ('external_network', 'internal_network'):
542 external = key.startswith('external')
543 if key in cloud and 'networks' in cloud:
544 raise exceptions.OpenStackConfigException(
545 "Both {key} and networks were specified in the config."
546 " Please remove {key} from the config and use the network"
547 " list to configure network behavior.".format(key=key))
550 "{key} is deprecated. Please replace with an entry in"
551 " a dict inside of the networks list with name: {name}"
552 " and routes_externally: {external}".format(
553 key=key, name=cloud[key], external=external))
554 networks.append(dict(
556 routes_externally=external,
557 nat_destination=not external,
558 default_interface=external))
560 # Validate that we don't have duplicates
561 self._validate_networks(networks, 'nat_destination')
562 self._validate_networks(networks, 'default_interface')
564 cloud['networks'] = networks
567 def _handle_domain_id(self, cloud):
568 # Allow people to just specify domain once if it's the same
570 'domain_id': ('user_domain_id', 'project_domain_id'),
571 'domain_name': ('user_domain_name', 'project_domain_name'),
573 for target_key, possible_values in mappings.items():
574 if not self._project_scoped(cloud):
575 if target_key in cloud and target_key not in cloud['auth']:
576 cloud['auth'][target_key] = cloud.pop(target_key)
578 for key in possible_values:
579 if target_key in cloud['auth'] and key not in cloud['auth']:
580 cloud['auth'][key] = cloud['auth'][target_key]
581 cloud.pop(target_key, None)
582 cloud['auth'].pop(target_key, None)
585 def _fix_backwards_project(self, cloud):
586 # Do the lists backwards so that project_name is the ultimate winner
587 # Also handle moving domain names into auth so that domain mapping
590 'domain_id': ('domain_id', 'domain-id'),
591 'domain_name': ('domain_name', 'domain-name'),
592 'user_domain_id': ('user_domain_id', 'user-domain-id'),
593 'user_domain_name': ('user_domain_name', 'user-domain-name'),
594 'project_domain_id': ('project_domain_id', 'project-domain-id'),
595 'project_domain_name': (
596 'project_domain_name', 'project-domain-name'),
597 'token': ('auth-token', 'auth_token', 'token'),
599 if cloud.get('auth_type', None) == 'v2password':
600 # If v2password is explcitly requested, this is to deal with old
601 # clouds. That's fine - we need to map settings in the opposite
603 mappings['tenant_id'] = (
604 'project_id', 'project-id', 'tenant_id', 'tenant-id')
605 mappings['tenant_name'] = (
606 'project_name', 'project-name', 'tenant_name', 'tenant-name')
608 mappings['project_id'] = (
609 'tenant_id', 'tenant-id', 'project_id', 'project-id')
610 mappings['project_name'] = (
611 'tenant_name', 'tenant-name', 'project_name', 'project-name')
612 for target_key, possible_values in mappings.items():
614 for key in possible_values:
616 target = str(cloud[key])
618 if key in cloud['auth']:
619 target = str(cloud['auth'][key])
620 del cloud['auth'][key]
622 cloud['auth'][target_key] = target
625 def _fix_backwards_auth_plugin(self, cloud):
626 # Do the lists backwards so that auth_type is the ultimate winner
628 'auth_type': ('auth_plugin', 'auth_type'),
630 for target_key, possible_values in mappings.items():
632 for key in possible_values:
636 cloud[target_key] = target
637 # Because we force alignment to v3 nouns, we want to force
638 # use of the auth plugin that can do auto-selection and dealing
639 # with that based on auth parameters. v2password is basically
643 def register_argparse_arguments(self, parser, argv, service_keys=None):
644 """Register all of the common argparse options needed.
646 Given an argparse parser, register the keystoneauth Session arguments,
647 the keystoneauth Auth Plugin Options and os-cloud. Also, peek in the
648 argv to see if all of the auth plugin options should be registered
649 or merely the ones already configured.
650 :param argparse.ArgumentParser: parser to attach argparse options to
651 :param list argv: the arguments provided to the application
652 :param string service_keys: Service or list of services this argparse
653 should be specialized for, if known.
654 The first item in the list will be used
655 as the default value for service_type
658 :raises exceptions.OpenStackConfigException if an invalid auth-type
662 if service_keys is None:
665 # Fix argv in place - mapping any keys with embedded _ in them to -
668 local_parser = argparse_mod.ArgumentParser(add_help=False)
670 for p in (parser, local_parser):
674 default=os.environ.get('OS_CLOUD', None),
675 help='Named cloud to connect to')
677 # we need to peek to see if timeout was actually passed, since
678 # the keystoneauth declaration of it has a default, which means
679 # we have no clue if the value we get is from the ksa default
680 # for from the user passing it explicitly. We'll stash it for later
681 local_parser.add_argument('--timeout', metavar='<timeout>')
683 # We need for get_one_cloud to be able to peek at whether a token
684 # was passed so that we can swap the default from password to
685 # token if it was. And we need to also peek for --os-auth-token
686 # for novaclient backwards compat
687 local_parser.add_argument('--os-token')
688 local_parser.add_argument('--os-auth-token')
690 # Peek into the future and see if we have an auth-type set in
691 # config AND a cloud set, so that we know which command line
692 # arguments to register and show to the user (the user may want
693 # to say something like:
694 # openstack --os-cloud=foo --os-oidctoken=bar
695 # although I think that user is the cause of my personal pain
696 options, _args = local_parser.parse_known_args(argv)
698 self._argv_timeout = True
700 # validate = False because we're not _actually_ loading here
701 # we're only peeking, so it's the wrong time to assert that
702 # the rest of the arguments given are invalid for the plugin
703 # chosen (for instance, --help may be requested, so that the
704 # user can see what options he may want to give
705 cloud = self.get_one_cloud(argparse=options, validate=False)
706 default_auth_type = cloud.config['auth_type']
709 loading.register_auth_argparse_arguments(
710 parser, argv, default=default_auth_type)
712 # Hidiing the keystoneauth exception because we're not actually
713 # loading the auth plugin at this point, so the error message
714 # from it doesn't actually make sense to os-client-config users
715 options, _args = parser.parse_known_args(argv)
716 plugin_names = loading.get_available_plugin_names()
717 raise exceptions.OpenStackConfigException(
718 "An invalid auth-type was specified: {auth_type}."
719 " Valid choices are: {plugin_names}.".format(
720 auth_type=options.os_auth_type,
721 plugin_names=",".join(plugin_names)))
724 primary_service = service_keys[0]
726 primary_service = None
727 loading.register_session_argparse_arguments(parser)
728 adapter.register_adapter_argparse_arguments(
729 parser, service_type=primary_service)
730 for service_key in service_keys:
731 # legacy clients have un-prefixed api-version options
733 '--{service_key}-api-version'.format(
734 service_key=service_key.replace('_', '-'),
735 help=argparse_mod.SUPPRESS))
736 adapter.register_service_adapter_argparse_arguments(
737 parser, service_type=service_key)
739 # Backwards compat options for legacy clients
740 parser.add_argument('--http-timeout', help=argparse_mod.SUPPRESS)
741 parser.add_argument('--os-endpoint-type', help=argparse_mod.SUPPRESS)
742 parser.add_argument('--endpoint-type', help=argparse_mod.SUPPRESS)
744 def _fix_backwards_interface(self, cloud):
746 for key in cloud.keys():
747 if key.endswith('endpoint_type'):
748 target_key = key.replace('endpoint_type', 'interface')
751 new_cloud[target_key] = cloud[key]
754 def _fix_backwards_api_timeout(self, cloud):
756 # requests can only have one timeout, which means that in a single
757 # cloud there is no point in different timeout values. However,
758 # for some reason many of the legacy clients decided to shove their
759 # service name in to the arg name for reasons surpassin sanity. If
760 # we find any values that are not api_timeout, overwrite api_timeout
762 service_timeout = None
763 for key in cloud.keys():
764 if key.endswith('timeout') and not (
765 key == 'timeout' or key == 'api_timeout'):
766 service_timeout = cloud[key]
768 new_cloud[key] = cloud[key]
769 if service_timeout is not None:
770 new_cloud['api_timeout'] = service_timeout
771 # The common argparse arg from keystoneauth is called timeout, but
772 # os-client-config expects it to be called api_timeout
773 if self._argv_timeout:
774 if 'timeout' in new_cloud and new_cloud['timeout']:
775 new_cloud['api_timeout'] = new_cloud.pop('timeout')
778 def get_all_clouds(self):
782 for cloud in self.get_cloud_names():
783 for region in self._get_regions(cloud):
785 clouds.append(self.get_one_cloud(
786 cloud, region_name=region['name']))
789 def _fix_args(self, args=None, argparse=None):
790 """Massage the passed-in options
792 Replace - with _ and strip os_ prefixes.
794 Convert an argparse Namespace object to a dict, removing values
795 that are either None or ''.
801 # Convert the passed-in Namespace
802 o_dict = vars(argparse)
805 if o_dict[k] is not None and o_dict[k] != '':
806 parsed_args[k] = o_dict[k]
807 args.update(parsed_args)
811 for (key, val) in iter(args.items()):
812 if type(args[key]) == dict:
813 # dive into the auth dict
814 new_args[key] = self._fix_args(args[key])
817 key = key.replace('-', '_')
818 if key.startswith('os_'):
819 os_args[key[3:]] = val
822 new_args.update(os_args)
825 def _find_winning_auth_value(self, opt, config):
826 opt_name = opt.name.replace('-', '_')
827 if opt_name in config:
828 return config[opt_name]
830 deprecated = getattr(opt, 'deprecated', getattr(
831 opt, 'deprecated_opts', []))
832 for d_opt in deprecated:
833 d_opt_name = d_opt.name.replace('-', '_')
834 if d_opt_name in config:
835 return config[d_opt_name]
837 def auth_config_hook(self, config):
838 """Allow examination of config values before loading auth plugin
840 OpenStackClient will override this to perform additional checks
845 def _get_auth_loader(self, config):
846 # Re-use the admin_token plugin for the "None" plugin
847 # since it does not look up endpoints or tokens but rather
848 # does a passthrough. This is useful for things like Ironic
849 # that have a keystoneless operational mode, but means we're
850 # still dealing with a keystoneauth Session object, so all the
851 # _other_ things (SSL arg handling, timeout) all work consistently
852 if config['auth_type'] in (None, "None", ''):
853 config['auth_type'] = 'admin_token'
854 # Set to notused rather than None because validate_auth will
855 # strip the value if it's actually python None
856 config['auth']['token'] = 'notused'
857 elif config['auth_type'] == 'token_endpoint':
858 # Humans have been trained to use a thing called token_endpoint
859 # That it does not exist in keystoneauth is irrelvant- it not
860 # doing what they want causes them sorrow.
861 config['auth_type'] = 'admin_token'
862 return loading.get_plugin_loader(config['auth_type'])
864 def _validate_auth_ksc(self, config, cloud):
866 import keystoneclient.auth as ksc_auth
870 # May throw a keystoneclient.exceptions.NoMatchingPlugin
871 plugin_options = ksc_auth.get_plugin_class(
872 config['auth_type']).get_options()
874 for p_opt in plugin_options:
875 # if it's in config.auth, win, kill it from config dict
876 # if it's in config and not in config.auth, move it
877 # deprecated loses to current
878 # provided beats default, deprecated or not
879 winning_value = self._find_winning_auth_value(
883 if not winning_value:
884 winning_value = self._find_winning_auth_value(
889 # if the plugin tells us that this value is required
890 # then error if it's doesn't exist now
891 if not winning_value and p_opt.required:
892 raise exceptions.OpenStackConfigException(
893 'Unable to find auth information for cloud'
894 ' {cloud} in config files {files}'
895 ' or environment variables. Missing value {auth_key}'
896 ' required for auth plugin {plugin}'.format(
897 cloud=cloud, files=','.join(self._config_files),
898 auth_key=p_opt.name, plugin=config.get('auth_type')))
900 # Clean up after ourselves
901 for opt in [p_opt.name] + [o.name for o in p_opt.deprecated_opts]:
902 opt = opt.replace('-', '_')
903 config.pop(opt, None)
904 config['auth'].pop(opt, None)
907 # Prefer the plugin configuration dest value if the value's key
908 # is marked as depreciated.
909 if p_opt.dest is None:
910 config['auth'][p_opt.name.replace('-', '_')] = (
913 config['auth'][p_opt.dest] = winning_value
914 if p_opt.dest == 'endpoint':
915 config['auth']['url'] = winning_value
919 def _validate_auth(self, config, loader):
920 # May throw a keystoneauth1.exceptions.NoMatchingPlugin
922 plugin_options = loader.get_options()
924 for p_opt in plugin_options:
925 # if it's in config.auth, win, kill it from config dict
926 # if it's in config and not in config.auth, move it
927 # deprecated loses to current
928 # provided beats default, deprecated or not
929 winning_value = self._find_winning_auth_value(
933 if not winning_value:
934 winning_value = self._find_winning_auth_value(
939 config = self._clean_up_after_ourselves(
946 # Prefer the plugin configuration dest value if the value's key
947 # is marked as deprecated.
948 if p_opt.dest is None:
949 good_name = p_opt.name.replace('-', '_')
950 config['auth'][good_name] = winning_value
952 config['auth'][p_opt.dest] = winning_value
953 if p_opt.dest == 'endpoint':
954 config['auth']['url'] = winning_value
957 # See if this needs a prompting
958 config = self.option_prompt(config, p_opt)
962 def _validate_auth_correctly(self, config, loader):
963 # May throw a keystoneauth1.exceptions.NoMatchingPlugin
965 plugin_options = loader.get_options()
967 for p_opt in plugin_options:
968 # if it's in config, win, move it and kill it from config dict
969 # if it's in config.auth but not in config it's good
970 # deprecated loses to current
971 # provided beats default, deprecated or not
972 winning_value = self._find_winning_auth_value(
976 if not winning_value:
977 winning_value = self._find_winning_auth_value(
982 config = self._clean_up_after_ourselves(
988 # See if this needs a prompting
989 config = self.option_prompt(config, p_opt)
993 def option_prompt(self, config, p_opt):
994 """Prompt user for option that requires a value"""
996 p_opt.prompt is not None and
997 p_opt.dest not in config['auth'] and
998 self._pw_callback is not None
1000 config['auth'][p_opt.dest] = self._pw_callback(p_opt.prompt)
1003 def _clean_up_after_ourselves(self, config, p_opt, winning_value):
1005 # Clean up after ourselves
1006 for opt in [p_opt.name] + [o.name for o in p_opt.deprecated]:
1007 opt = opt.replace('-', '_')
1008 config.pop(opt, None)
1009 config['auth'].pop(opt, None)
1012 # Prefer the plugin configuration dest value if the value's key
1013 # is marked as depreciated.
1014 if p_opt.dest is None:
1015 config['auth'][p_opt.name.replace('-', '_')] = (
1018 config['auth'][p_opt.dest] = winning_value
1021 def magic_fixes(self, config):
1022 """Perform the set of magic argument fixups"""
1024 # Infer token plugin if a token was given
1025 if (('auth' in config and 'token' in config['auth']) or
1026 ('auth_token' in config and config['auth_token']) or
1027 ('token' in config and config['token'])):
1028 config.setdefault('token', config.pop('auth_token', None))
1030 # These backwards compat values are only set via argparse. If it's
1031 # there, it's because it was passed in explicitly, and should win
1032 config = self._fix_backwards_api_timeout(config)
1033 if 'endpoint_type' in config:
1034 config['interface'] = config.pop('endpoint_type')
1036 config = self._fix_backwards_auth_plugin(config)
1037 config = self._fix_backwards_project(config)
1038 config = self._fix_backwards_interface(config)
1039 config = self._fix_backwards_networks(config)
1040 config = self._handle_domain_id(config)
1042 for key in BOOL_KEYS:
1044 if type(config[key]) is not bool:
1045 config[key] = get_boolean(config[key])
1047 # TODO(mordred): Special casing auth_url here. We should
1048 # come back to this betterer later so that it's
1050 if 'auth' in config and 'auth_url' in config['auth']:
1051 config['auth']['auth_url'] = config['auth']['auth_url'].format(
1056 def get_one_cloud(self, cloud=None, validate=True,
1057 argparse=None, **kwargs):
1058 """Retrieve a single cloud configuration and merge additional options
1060 :param string cloud:
1061 The name of the configuration to load from clouds.yaml
1062 :param boolean validate:
1063 Validate the config. Setting this to False causes no auth plugin
1064 to be created. It's really only useful for testing.
1065 :param Namespace argparse:
1066 An argparse Namespace object; allows direct passing in of
1067 argparse options to be added to the cloud config. Values
1068 of None and '' will be removed.
1069 :param region_name: Name of the region of the cloud.
1070 :param kwargs: Additional configuration options
1072 :raises: keystoneauth1.exceptions.MissingRequiredOptions
1073 on missing required auth parameters
1076 args = self._fix_args(kwargs, argparse=argparse)
1080 cloud = args['cloud']
1082 cloud = self.default_cloud
1084 config = self._get_base_cloud_config(cloud)
1086 # Get region specific settings
1087 if 'region_name' not in args:
1088 args['region_name'] = ''
1089 region = self._get_region(cloud=cloud, region_name=args['region_name'])
1090 args['region_name'] = region['name']
1091 region_args = copy.deepcopy(region['values'])
1093 # Regions is a list that we can use to create a list of cloud/region
1094 # objects. It does not belong in the single-cloud dict
1095 config.pop('regions', None)
1097 # Can't just do update, because None values take over
1098 for arg_list in region_args, args:
1099 for (key, val) in iter(arg_list.items()):
1101 if key == 'auth' and config[key] is not None:
1102 config[key] = _auth_update(config[key], val)
1106 config = self.magic_fixes(config)
1107 config = self._normalize_keys(config)
1109 # NOTE(dtroyer): OSC needs a hook into the auth args before the
1110 # plugin is loaded in order to maintain backward-
1111 # compatible behaviour
1112 config = self.auth_config_hook(config)
1116 loader = self._get_auth_loader(config)
1117 config = self._validate_auth(config, loader)
1118 auth_plugin = loader.load_from_options(**config['auth'])
1119 except Exception as e:
1120 # We WANT the ksa exception normally
1121 # but OSC can't handle it right now, so we try deferring
1122 # to ksc. If that ALSO fails, it means there is likely
1123 # a deeper issue, so we assume the ksa error was correct
1124 self.log.debug("Deferring keystone exception: {e}".format(e=e))
1127 config = self._validate_auth_ksc(config, cloud)
1133 # If any of the defaults reference other values, we need to expand
1134 for (key, value) in config.items():
1135 if hasattr(value, 'format'):
1136 config[key] = value.format(**config)
1138 force_ipv4 = config.pop('force_ipv4', self.force_ipv4)
1139 prefer_ipv6 = config.pop('prefer_ipv6', True)
1146 cloud_name = str(cloud)
1147 return cloud_config.CloudConfig(
1149 region=config['region_name'],
1151 force_ipv4=force_ipv4,
1152 auth_plugin=auth_plugin,
1153 openstack_config=self,
1154 session_constructor=self._session_constructor,
1157 def get_one_cloud_osc(
1164 """Retrieve a single cloud configuration and merge additional options
1166 :param string cloud:
1167 The name of the configuration to load from clouds.yaml
1168 :param boolean validate:
1169 Validate the config. Setting this to False causes no auth plugin
1170 to be created. It's really only useful for testing.
1171 :param Namespace argparse:
1172 An argparse Namespace object; allows direct passing in of
1173 argparse options to be added to the cloud config. Values
1174 of None and '' will be removed.
1175 :param region_name: Name of the region of the cloud.
1176 :param kwargs: Additional configuration options
1178 :raises: keystoneauth1.exceptions.MissingRequiredOptions
1179 on missing required auth parameters
1182 args = self._fix_args(kwargs, argparse=argparse)
1186 cloud = args['cloud']
1188 cloud = self.default_cloud
1190 config = self._get_base_cloud_config(cloud)
1192 # Get region specific settings
1193 if 'region_name' not in args:
1194 args['region_name'] = ''
1195 region = self._get_region(cloud=cloud, region_name=args['region_name'])
1196 args['region_name'] = region['name']
1197 region_args = copy.deepcopy(region['values'])
1199 # Regions is a list that we can use to create a list of cloud/region
1200 # objects. It does not belong in the single-cloud dict
1201 config.pop('regions', None)
1203 # Can't just do update, because None values take over
1204 for arg_list in region_args, args:
1205 for (key, val) in iter(arg_list.items()):
1207 if key == 'auth' and config[key] is not None:
1208 config[key] = _auth_update(config[key], val)
1212 config = self.magic_fixes(config)
1214 # NOTE(dtroyer): OSC needs a hook into the auth args before the
1215 # plugin is loaded in order to maintain backward-
1216 # compatible behaviour
1217 config = self.auth_config_hook(config)
1220 loader = self._get_auth_loader(config)
1221 config = self._validate_auth_correctly(config, loader)
1222 auth_plugin = loader.load_from_options(**config['auth'])
1226 # If any of the defaults reference other values, we need to expand
1227 for (key, value) in config.items():
1228 if hasattr(value, 'format'):
1229 config[key] = value.format(**config)
1231 force_ipv4 = config.pop('force_ipv4', self.force_ipv4)
1232 prefer_ipv6 = config.pop('prefer_ipv6', True)
1239 cloud_name = str(cloud)
1240 return cloud_config.CloudConfig(
1242 region=config['region_name'],
1243 config=self._normalize_keys(config),
1244 force_ipv4=force_ipv4,
1245 auth_plugin=auth_plugin,
1246 openstack_config=self,
1250 def set_one_cloud(config_file, cloud, set_config=None):
1251 """Set a single cloud configuration.
1253 :param string config_file:
1254 The path to the config file to edit. If this file does not exist
1256 :param string cloud:
1257 The name of the configuration to save to clouds.yaml
1258 :param dict set_config: Configuration options to be set
1261 set_config = set_config or {}
1264 with open(config_file) as fh:
1265 cur_config = yaml.safe_load(fh)
1266 except IOError as e:
1272 clouds_config = cur_config.get('clouds', {})
1273 cloud_config = _auth_update(clouds_config.get(cloud, {}), set_config)
1274 clouds_config[cloud] = cloud_config
1275 cur_config['clouds'] = clouds_config
1277 with open(config_file, 'w') as fh:
1278 yaml.safe_dump(cur_config, fh, default_flow_style=False)
1280 if __name__ == '__main__':
1281 config = OpenStackConfig().get_all_clouds()
1282 for cloud in config:
1284 if len(sys.argv) == 1:
1286 elif len(sys.argv) == 3 and (
1287 sys.argv[1] == cloud.name and sys.argv[2] == cloud.region):
1289 elif len(sys.argv) == 2 and (
1290 sys.argv[1] == cloud.name):
1294 print(cloud.name, cloud.region, cloud.config)