| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import configparser |
| import copy |
| import os |
| import shlex |
| import sys |
|
|
| import botocore.exceptions |
|
|
|
|
| def multi_file_load_config(*filenames): |
| """Load and combine multiple INI configs with profiles. |
| |
| This function will take a list of filesnames and return |
| a single dictionary that represents the merging of the loaded |
| config files. |
| |
| If any of the provided filenames does not exist, then that file |
| is ignored. It is therefore ok to provide a list of filenames, |
| some of which may not exist. |
| |
| Configuration files are **not** deep merged, only the top level |
| keys are merged. The filenames should be passed in order of |
| precedence. The first config file has precedence over the |
| second config file, which has precedence over the third config file, |
| etc. The only exception to this is that the "profiles" key is |
| merged to combine profiles from multiple config files into a |
| single profiles mapping. However, if a profile is defined in |
| multiple config files, then the config file with the highest |
| precedence is used. Profile values themselves are not merged. |
| For example:: |
| |
| FileA FileB FileC |
| [foo] [foo] [bar] |
| a=1 a=2 a=3 |
| b=2 |
| |
| [bar] [baz] [profile a] |
| a=2 a=3 region=e |
| |
| [profile a] [profile b] [profile c] |
| region=c region=d region=f |
| |
| The final result of ``multi_file_load_config(FileA, FileB, FileC)`` |
| would be:: |
| |
| {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3}, |
| "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}}, |
| {"c": {"region": "f"}}} |
| |
| Note that the "foo" key comes from A, even though it's defined in both |
| FileA and FileB. Because "foo" was defined in FileA first, then the values |
| for "foo" from FileA are used and the values for "foo" from FileB are |
| ignored. Also note where the profiles originate from. Profile "a" |
| comes FileA, profile "b" comes from FileB, and profile "c" comes |
| from FileC. |
| |
| """ |
| configs = [] |
| profiles = [] |
| for filename in filenames: |
| try: |
| loaded = load_config(filename) |
| except botocore.exceptions.ConfigNotFound: |
| continue |
| profiles.append(loaded.pop('profiles')) |
| configs.append(loaded) |
| merged_config = _merge_list_of_dicts(configs) |
| merged_profiles = _merge_list_of_dicts(profiles) |
| merged_config['profiles'] = merged_profiles |
| return merged_config |
|
|
|
|
| def _merge_list_of_dicts(list_of_dicts): |
| merged_dicts = {} |
| for single_dict in list_of_dicts: |
| for key, value in single_dict.items(): |
| if key not in merged_dicts: |
| merged_dicts[key] = value |
| return merged_dicts |
|
|
|
|
| def load_config(config_filename): |
| """Parse a INI config with profiles. |
| |
| This will parse an INI config file and map top level profiles |
| into a top level "profile" key. |
| |
| If you want to parse an INI file and map all section names to |
| top level keys, use ``raw_config_parse`` instead. |
| |
| """ |
| parsed = raw_config_parse(config_filename) |
| return build_profile_map(parsed) |
|
|
|
|
| def raw_config_parse(config_filename, parse_subsections=True): |
| """Returns the parsed INI config contents. |
| |
| Each section name is a top level key. |
| |
| :param config_filename: The name of the INI file to parse |
| |
| :param parse_subsections: If True, parse indented blocks as |
| subsections that represent their own configuration dictionary. |
| For example, if the config file had the contents:: |
| |
| s3 = |
| signature_version = s3v4 |
| addressing_style = path |
| |
| The resulting ``raw_config_parse`` would be:: |
| |
| {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}} |
| |
| If False, do not try to parse subsections and return the indented |
| block as its literal value:: |
| |
| {'s3': '\nsignature_version = s3v4\naddressing_style = path'} |
| |
| :returns: A dict with keys for each profile found in the config |
| file and the value of each key being a dict containing name |
| value pairs found in that profile. |
| |
| :raises: ConfigNotFound, ConfigParseError |
| """ |
| config = {} |
| path = config_filename |
| if path is not None: |
| path = os.path.expandvars(path) |
| path = os.path.expanduser(path) |
| if not os.path.isfile(path): |
| raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path)) |
| cp = configparser.RawConfigParser() |
| try: |
| cp.read([path]) |
| except (configparser.Error, UnicodeDecodeError) as e: |
| raise botocore.exceptions.ConfigParseError( |
| path=_unicode_path(path), error=e |
| ) from None |
| else: |
| for section in cp.sections(): |
| config[section] = {} |
| for option in cp.options(section): |
| config_value = cp.get(section, option) |
| if parse_subsections and config_value.startswith('\n'): |
| |
| |
| |
| try: |
| config_value = _parse_nested(config_value) |
| except ValueError as e: |
| raise botocore.exceptions.ConfigParseError( |
| path=_unicode_path(path), error=e |
| ) from None |
| config[section][option] = config_value |
| return config |
|
|
|
|
| def _unicode_path(path): |
| if isinstance(path, str): |
| return path |
| |
| |
| filesystem_encoding = sys.getfilesystemencoding() |
| if filesystem_encoding is None: |
| filesystem_encoding = sys.getdefaultencoding() |
| return path.decode(filesystem_encoding, 'replace') |
|
|
|
|
| def _parse_nested(config_value): |
| |
| |
| |
| |
| |
| |
| parsed = {} |
| for line in config_value.splitlines(): |
| line = line.strip() |
| if not line: |
| continue |
| |
| |
| |
| key, value = line.split('=', 1) |
| parsed[key.strip()] = value.strip() |
| return parsed |
|
|
|
|
| def _parse_section(key, values): |
| result = {} |
| try: |
| parts = shlex.split(key) |
| except ValueError: |
| return result |
| if len(parts) == 2: |
| result[parts[1]] = values |
| return result |
|
|
|
|
| def build_profile_map(parsed_ini_config): |
| """Convert the parsed INI config into a profile map. |
| |
| The config file format requires that every profile except the |
| default to be prepended with "profile", e.g.:: |
| |
| [profile test] |
| aws_... = foo |
| aws_... = bar |
| |
| [profile bar] |
| aws_... = foo |
| aws_... = bar |
| |
| # This is *not* a profile |
| [preview] |
| otherstuff = 1 |
| |
| # Neither is this |
| [foobar] |
| morestuff = 2 |
| |
| The build_profile_map will take a parsed INI config file where each top |
| level key represents a section name, and convert into a format where all |
| the profiles are under a single top level "profiles" key, and each key in |
| the sub dictionary is a profile name. For example, the above config file |
| would be converted from:: |
| |
| {"profile test": {"aws_...": "foo", "aws...": "bar"}, |
| "profile bar": {"aws...": "foo", "aws...": "bar"}, |
| "preview": {"otherstuff": ...}, |
| "foobar": {"morestuff": ...}, |
| } |
| |
| into:: |
| |
| {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"}, |
| "bar": {"aws...": "foo", "aws...": "bar"}, |
| "preview": {"otherstuff": ...}, |
| "foobar": {"morestuff": ...}, |
| } |
| |
| If there are no profiles in the provided parsed INI contents, then |
| an empty dict will be the value associated with the ``profiles`` key. |
| |
| .. note:: |
| |
| This will not mutate the passed in parsed_ini_config. Instead it will |
| make a deepcopy and return that value. |
| |
| """ |
| parsed_config = copy.deepcopy(parsed_ini_config) |
| profiles = {} |
| sso_sessions = {} |
| services = {} |
| final_config = {} |
| for key, values in parsed_config.items(): |
| if key.startswith("profile"): |
| profiles.update(_parse_section(key, values)) |
| elif key.startswith("sso-session"): |
| sso_sessions.update(_parse_section(key, values)) |
| elif key.startswith("services"): |
| services.update(_parse_section(key, values)) |
| elif key == 'default': |
| |
| |
| |
| profiles[key] = values |
| else: |
| final_config[key] = values |
| final_config['profiles'] = profiles |
| final_config['sso_sessions'] = sso_sessions |
| final_config['services'] = services |
| return final_config |
|
|