codekingpro/portable-devtools
114k
1# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/2# Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.3#4# Licensed under the Apache License, Version 2.0 (the "License"). You5# may not use this file except in compliance with the License. A copy of6# the License is located at7#8# http://aws.amazon.com/apache2.0/9#10# or in the "license" file accompanying this file. This file is11# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF12# ANY KIND, either express or implied. See the License for the specific13# language governing permissions and limitations under the License.14import configparser15import copy16import os17import shlex18import sys19 20import botocore.exceptions21 22 23def multi_file_load_config(*filenames):24 """Load and combine multiple INI configs with profiles.25 26 This function will take a list of filesnames and return27 a single dictionary that represents the merging of the loaded28 config files.29 30 If any of the provided filenames does not exist, then that file31 is ignored. It is therefore ok to provide a list of filenames,32 some of which may not exist.33 34 Configuration files are **not** deep merged, only the top level35 keys are merged. The filenames should be passed in order of36 precedence. The first config file has precedence over the37 second config file, which has precedence over the third config file,38 etc. The only exception to this is that the "profiles" key is39 merged to combine profiles from multiple config files into a40 single profiles mapping. However, if a profile is defined in41 multiple config files, then the config file with the highest42 precedence is used. Profile values themselves are not merged.43 For example::44 45 FileA FileB FileC46 [foo] [foo] [bar]47 a=1 a=2 a=348 b=249 50 [bar] [baz] [profile a]51 a=2 a=3 region=e52 53 [profile a] [profile b] [profile c]54 region=c region=d region=f55 56 The final result of ``multi_file_load_config(FileA, FileB, FileC)``57 would be::58 59 {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3},60 "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}},61 {"c": {"region": "f"}}}62 63 Note that the "foo" key comes from A, even though it's defined in both64 FileA and FileB. Because "foo" was defined in FileA first, then the values65 for "foo" from FileA are used and the values for "foo" from FileB are66 ignored. Also note where the profiles originate from. Profile "a"67 comes FileA, profile "b" comes from FileB, and profile "c" comes68 from FileC.69 70 """71 configs = []72 profiles = []73 for filename in filenames:74 try:75 loaded = load_config(filename)76 except botocore.exceptions.ConfigNotFound:77 continue78 profiles.append(loaded.pop('profiles'))79 configs.append(loaded)80 merged_config = _merge_list_of_dicts(configs)81 merged_profiles = _merge_list_of_dicts(profiles)82 merged_config['profiles'] = merged_profiles83 return merged_config84 85 86def _merge_list_of_dicts(list_of_dicts):87 merged_dicts = {}88 for single_dict in list_of_dicts:89 for key, value in single_dict.items():90 if key not in merged_dicts:91 merged_dicts[key] = value92 return merged_dicts93 94 95def load_config(config_filename):96 """Parse a INI config with profiles.97 98 This will parse an INI config file and map top level profiles99 into a top level "profile" key.100 101 If you want to parse an INI file and map all section names to102 top level keys, use ``raw_config_parse`` instead.103 104 """105 parsed = raw_config_parse(config_filename)106 return build_profile_map(parsed)107 108 109def raw_config_parse(config_filename, parse_subsections=True):110 """Returns the parsed INI config contents.111 112 Each section name is a top level key.113 114 :param config_filename: The name of the INI file to parse115 116 :param parse_subsections: If True, parse indented blocks as117 subsections that represent their own configuration dictionary.118 For example, if the config file had the contents::119 120 s3 =121 signature_version = s3v4122 addressing_style = path123 124 The resulting ``raw_config_parse`` would be::125 126 {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}}127 128 If False, do not try to parse subsections and return the indented129 block as its literal value::130 131 {'s3': '\nsignature_version = s3v4\naddressing_style = path'}132 133 :returns: A dict with keys for each profile found in the config134 file and the value of each key being a dict containing name135 value pairs found in that profile.136 137 :raises: ConfigNotFound, ConfigParseError138 """139 config = {}140 path = config_filename141 if path is not None:142 path = os.path.expandvars(path)143 path = os.path.expanduser(path)144 if not os.path.isfile(path):145 raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))146 cp = configparser.RawConfigParser()147 try:148 cp.read([path])149 except (configparser.Error, UnicodeDecodeError) as e:150 raise botocore.exceptions.ConfigParseError(151 path=_unicode_path(path), error=e152 ) from None153 else:154 for section in cp.sections():155 config[section] = {}156 for option in cp.options(section):157 config_value = cp.get(section, option)158 if parse_subsections and config_value.startswith('\n'):159 # Then we need to parse the inner contents as160 # hierarchical. We support a single level161 # of nesting for now.162 try:163 config_value = _parse_nested(config_value)164 except ValueError as e:165 raise botocore.exceptions.ConfigParseError(166 path=_unicode_path(path), error=e167 ) from None168 config[section][option] = config_value169 return config170 171 172def _unicode_path(path):173 if isinstance(path, str):174 return path175 # According to the documentation getfilesystemencoding can return None176 # on unix in which case the default encoding is used instead.177 filesystem_encoding = sys.getfilesystemencoding()178 if filesystem_encoding is None:179 filesystem_encoding = sys.getdefaultencoding()180 return path.decode(filesystem_encoding, 'replace')181 182 183def _parse_nested(config_value):184 # Given a value like this:185 # \n186 # foo = bar187 # bar = baz188 # We need to parse this into189 # {'foo': 'bar', 'bar': 'baz}190 parsed = {}191 for line in config_value.splitlines():192 line = line.strip()193 if not line:194 continue195 # The caller will catch ValueError196 # and raise an appropriate error197 # if this fails.198 key, value = line.split('=', 1)199 parsed[key.strip()] = value.strip()200 return parsed201 202 203def _parse_section(key, values):204 result = {}205 try:206 parts = shlex.split(key)207 except ValueError:208 return result209 if len(parts) == 2:210 result[parts[1]] = values211 return result212 213 214def build_profile_map(parsed_ini_config):215 """Convert the parsed INI config into a profile map.216 217 The config file format requires that every profile except the218 default to be prepended with "profile", e.g.::219 220 [profile test]221 aws_... = foo222 aws_... = bar223 224 [profile bar]225 aws_... = foo226 aws_... = bar227 228 # This is *not* a profile229 [preview]230 otherstuff = 1231 232 # Neither is this233 [foobar]234 morestuff = 2235 236 The build_profile_map will take a parsed INI config file where each top237 level key represents a section name, and convert into a format where all238 the profiles are under a single top level "profiles" key, and each key in239 the sub dictionary is a profile name. For example, the above config file240 would be converted from::241 242 {"profile test": {"aws_...": "foo", "aws...": "bar"},243 "profile bar": {"aws...": "foo", "aws...": "bar"},244 "preview": {"otherstuff": ...},245 "foobar": {"morestuff": ...},246 }247 248 into::249 250 {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"},251 "bar": {"aws...": "foo", "aws...": "bar"},252 "preview": {"otherstuff": ...},253 "foobar": {"morestuff": ...},254 }255 256 If there are no profiles in the provided parsed INI contents, then257 an empty dict will be the value associated with the ``profiles`` key.258 259 .. note::260 261 This will not mutate the passed in parsed_ini_config. Instead it will262 make a deepcopy and return that value.263 264 """265 parsed_config = copy.deepcopy(parsed_ini_config)266 profiles = {}267 sso_sessions = {}268 services = {}269 final_config = {}270 for key, values in parsed_config.items():271 if key.startswith("profile"):272 profiles.update(_parse_section(key, values))273 elif key.startswith("sso-session"):274 sso_sessions.update(_parse_section(key, values))275 elif key.startswith("services"):276 services.update(_parse_section(key, values))277 elif key == 'default':278 # default section is special and is considered a profile279 # name but we don't require you use 'profile "default"'280 # as a section.281 profiles[key] = values282 else:283 final_config[key] = values284 final_config['profiles'] = profiles285 final_config['sso_sessions'] = sso_sessions286 final_config['services'] = services287 return final_config288 