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"). You
5
# may not use this file except in compliance with the License. A copy of
6
# the License is located at
7
#
8
# http://aws.amazon.com/apache2.0/
9
#
10
# or in the "license" file accompanying this file. This file is
11
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12
# ANY KIND, either express or implied. See the License for the specific
13
# language governing permissions and limitations under the License.
14 11
import os
15 11
import shlex
16 11
import copy
17 11
import sys
18

19 11
from botocore.compat import six
20

21 11
import botocore.exceptions
22

23

24 11
def multi_file_load_config(*filenames):
25
    """Load and combine multiple INI configs with profiles.
26

27
    This function will take a list of filesnames and return
28
    a single dictionary that represents the merging of the loaded
29
    config files.
30

31
    If any of the provided filenames does not exist, then that file
32
    is ignored.  It is therefore ok to provide a list of filenames,
33
    some of which may not exist.
34

35
    Configuration files are **not** deep merged, only the top level
36
    keys are merged.  The filenames should be passed in order of
37
    precedence.  The first config file has precedence over the
38
    second config file, which has precedence over the third config file,
39
    etc.  The only exception to this is that the "profiles" key is
40
    merged to combine profiles from multiple config files into a
41
    single profiles mapping.  However, if a profile is defined in
42
    multiple config files, then the config file with the highest
43
    precedence is used.  Profile values themselves are not merged.
44
    For example::
45

46
        FileA              FileB                FileC
47
        [foo]             [foo]                 [bar]
48
        a=1               a=2                   a=3
49
                          b=2
50

51
        [bar]             [baz]                [profile a]
52
        a=2               a=3                  region=e
53

54
        [profile a]       [profile b]          [profile c]
55
        region=c          region=d             region=f
56

57
    The final result of ``multi_file_load_config(FileA, FileB, FileC)``
58
    would be::
59

60
        {"foo": {"a": 1}, "bar": {"a": 2}, "baz": {"a": 3},
61
        "profiles": {"a": {"region": "c"}}, {"b": {"region": d"}},
62
                    {"c": {"region": "f"}}}
63

64
    Note that the "foo" key comes from A, even though it's defined in both
65
    FileA and FileB.  Because "foo" was defined in FileA first, then the values
66
    for "foo" from FileA are used and the values for "foo" from FileB are
67
    ignored.  Also note where the profiles originate from.  Profile "a"
68
    comes FileA, profile "b" comes from FileB, and profile "c" comes
69
    from FileC.
70

71
    """
72 11
    configs = []
73 11
    profiles = []
74 11
    for filename in filenames:
75 11
        try:
76 11
            loaded = load_config(filename)
77 11
        except botocore.exceptions.ConfigNotFound:
78 11
            continue
79 11
        profiles.append(loaded.pop('profiles'))
80 11
        configs.append(loaded)
81 11
    merged_config = _merge_list_of_dicts(configs)
82 11
    merged_profiles = _merge_list_of_dicts(profiles)
83 11
    merged_config['profiles'] = merged_profiles
84 11
    return merged_config
85

86

87 11
def _merge_list_of_dicts(list_of_dicts):
88 11
    merged_dicts = {}
89 11
    for single_dict in list_of_dicts:
90 11
        for key, value in single_dict.items():
91 11
            if key not in merged_dicts:
92 11
                merged_dicts[key] = value
93 11
    return merged_dicts
94

95

96 11
def load_config(config_filename):
97
    """Parse a INI config with profiles.
98

99
    This will parse an INI config file and map top level profiles
100
    into a top level "profile" key.
101

102
    If you want to parse an INI file and map all section names to
103
    top level keys, use ``raw_config_parse`` instead.
104

105
    """
106 11
    parsed = raw_config_parse(config_filename)
107 11
    return build_profile_map(parsed)
108

109

110 11
def raw_config_parse(config_filename, parse_subsections=True):
111
    """Returns the parsed INI config contents.
112

113
    Each section name is a top level key.
114

115
    :param config_filename: The name of the INI file to parse
116

117
    :param parse_subsections: If True, parse indented blocks as
118
       subsections that represent their own configuration dictionary.
119
       For example, if the config file had the contents::
120

121
           s3 =
122
              signature_version = s3v4
123
              addressing_style = path
124

125
        The resulting ``raw_config_parse`` would be::
126

127
            {'s3': {'signature_version': 's3v4', 'addressing_style': 'path'}}
128

129
       If False, do not try to parse subsections and return the indented
130
       block as its literal value::
131

132
            {'s3': '\nsignature_version = s3v4\naddressing_style = path'}
133

134
    :returns: A dict with keys for each profile found in the config
135
        file and the value of each key being a dict containing name
136
        value pairs found in that profile.
137

138
    :raises: ConfigNotFound, ConfigParseError
139
    """
140 11
    config = {}
141 11
    path = config_filename
142 11
    if path is not None:
143 11
        path = os.path.expandvars(path)
144 11
        path = os.path.expanduser(path)
145 11
        if not os.path.isfile(path):
146 11
            raise botocore.exceptions.ConfigNotFound(path=_unicode_path(path))
147 11
        cp = six.moves.configparser.RawConfigParser()
148 11
        try:
149 11
            cp.read([path])
150 11
        except (six.moves.configparser.Error, UnicodeDecodeError):
151 11
            raise botocore.exceptions.ConfigParseError(
152
                path=_unicode_path(path))
153
        else:
154 11
            for section in cp.sections():
155 11
                config[section] = {}
156 11
                for option in cp.options(section):
157 11
                    config_value = cp.get(section, option)
158 11
                    if parse_subsections and config_value.startswith('\n'):
159
                        # Then we need to parse the inner contents as
160
                        # hierarchical.  We support a single level
161
                        # of nesting for now.
162 11
                        try:
163 11
                            config_value = _parse_nested(config_value)
164 11
                        except ValueError:
165 11
                            raise botocore.exceptions.ConfigParseError(
166
                                path=_unicode_path(path))
167 11
                    config[section][option] = config_value
168 11
    return config
169

170

171 11
def _unicode_path(path):
172 11
    if isinstance(path, six.text_type):
173 9
        return path
174
    # According to the documentation getfilesystemencoding can return None
175
    # on unix in which case the default encoding is used instead.
176 11
    filesystem_encoding = sys.getfilesystemencoding()
177 11
    if filesystem_encoding is None:
178 11
        filesystem_encoding = sys.getdefaultencoding()
179 11
    return path.decode(filesystem_encoding, 'replace')
180

181

182 11
def _parse_nested(config_value):
183
    # Given a value like this:
184
    # \n
185
    # foo = bar
186
    # bar = baz
187
    # We need to parse this into
188
    # {'foo': 'bar', 'bar': 'baz}
189 11
    parsed = {}
190 11
    for line in config_value.splitlines():
191 11
        line = line.strip()
192 11
        if not line:
193 11
            continue
194
        # The caller will catch ValueError
195
        # and raise an appropriate error
196
        # if this fails.
197 11
        key, value = line.split('=', 1)
198 11
        parsed[key.strip()] = value.strip()
199 11
    return parsed
200

201

202 11
def build_profile_map(parsed_ini_config):
203
    """Convert the parsed INI config into a profile map.
204

205
    The config file format requires that every profile except the
206
    default to be prepended with "profile", e.g.::
207

208
        [profile test]
209
        aws_... = foo
210
        aws_... = bar
211

212
        [profile bar]
213
        aws_... = foo
214
        aws_... = bar
215

216
        # This is *not* a profile
217
        [preview]
218
        otherstuff = 1
219

220
        # Neither is this
221
        [foobar]
222
        morestuff = 2
223

224
    The build_profile_map will take a parsed INI config file where each top
225
    level key represents a section name, and convert into a format where all
226
    the profiles are under a single top level "profiles" key, and each key in
227
    the sub dictionary is a profile name.  For example, the above config file
228
    would be converted from::
229

230
        {"profile test": {"aws_...": "foo", "aws...": "bar"},
231
         "profile bar": {"aws...": "foo", "aws...": "bar"},
232
         "preview": {"otherstuff": ...},
233
         "foobar": {"morestuff": ...},
234
         }
235

236
    into::
237

238
        {"profiles": {"test": {"aws_...": "foo", "aws...": "bar"},
239
                      "bar": {"aws...": "foo", "aws...": "bar"},
240
         "preview": {"otherstuff": ...},
241
         "foobar": {"morestuff": ...},
242
        }
243

244
    If there are no profiles in the provided parsed INI contents, then
245
    an empty dict will be the value associated with the ``profiles`` key.
246

247
    .. note::
248

249
        This will not mutate the passed in parsed_ini_config.  Instead it will
250
        make a deepcopy and return that value.
251

252
    """
253 11
    parsed_config = copy.deepcopy(parsed_ini_config)
254 11
    profiles = {}
255 11
    final_config = {}
256 11
    for key, values in parsed_config.items():
257 11
        if key.startswith("profile"):
258 11
            try:
259 11
                parts = shlex.split(key)
260 11
            except ValueError:
261 11
                continue
262 11
            if len(parts) == 2:
263 11
                profiles[parts[1]] = values
264 11
        elif key == 'default':
265
            # default section is special and is considered a profile
266
            # name but we don't require you use 'profile "default"'
267
            # as a section.
268 11
            profiles[key] = values
269
        else:
270 11
            final_config[key] = values
271 11
    final_config['profiles'] = profiles
272 11
    return final_config

Read our documentation on viewing source code .

Loading