swift 2.33.0__py2.py3-none-any.whl → 2.34.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. swift/account/auditor.py +11 -0
  2. swift/account/reaper.py +11 -1
  3. swift/account/replicator.py +22 -0
  4. swift/account/server.py +12 -1
  5. swift-2.33.0.data/scripts/swift-account-audit → swift/cli/account_audit.py +6 -2
  6. swift-2.33.0.data/scripts/swift-config → swift/cli/config.py +1 -1
  7. swift-2.33.0.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +6 -2
  8. swift-2.33.0.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +12 -3
  9. swift-2.33.0.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +6 -2
  10. swift/cli/info.py +103 -2
  11. swift-2.33.0.data/scripts/swift-oldies → swift/cli/oldies.py +6 -3
  12. swift-2.33.0.data/scripts/swift-orphans → swift/cli/orphans.py +7 -2
  13. swift/cli/recon_cron.py +5 -5
  14. swift-2.33.0.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  15. swift/cli/relinker.py +1 -1
  16. swift/cli/ringbuilder.py +24 -0
  17. swift/common/db.py +2 -1
  18. swift/common/db_auditor.py +2 -2
  19. swift/common/db_replicator.py +6 -0
  20. swift/common/exceptions.py +12 -0
  21. swift/common/manager.py +102 -0
  22. swift/common/memcached.py +6 -13
  23. swift/common/middleware/account_quotas.py +144 -43
  24. swift/common/middleware/backend_ratelimit.py +166 -24
  25. swift/common/middleware/catch_errors.py +1 -3
  26. swift/common/middleware/cname_lookup.py +3 -5
  27. swift/common/middleware/container_sync.py +6 -10
  28. swift/common/middleware/crypto/crypto_utils.py +4 -5
  29. swift/common/middleware/crypto/decrypter.py +4 -5
  30. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  31. swift/common/middleware/listing_formats.py +26 -38
  32. swift/common/middleware/proxy_logging.py +22 -16
  33. swift/common/middleware/ratelimit.py +6 -7
  34. swift/common/middleware/recon.py +6 -7
  35. swift/common/middleware/s3api/acl_handlers.py +9 -0
  36. swift/common/middleware/s3api/controllers/multi_upload.py +1 -9
  37. swift/common/middleware/s3api/controllers/obj.py +20 -1
  38. swift/common/middleware/s3api/s3api.py +2 -0
  39. swift/common/middleware/s3api/s3request.py +171 -62
  40. swift/common/middleware/s3api/s3response.py +35 -6
  41. swift/common/middleware/s3api/s3token.py +2 -2
  42. swift/common/middleware/s3api/utils.py +1 -0
  43. swift/common/middleware/slo.py +153 -52
  44. swift/common/middleware/tempauth.py +6 -4
  45. swift/common/middleware/tempurl.py +2 -2
  46. swift/common/middleware/x_profile/exceptions.py +1 -4
  47. swift/common/middleware/x_profile/html_viewer.py +10 -11
  48. swift/common/middleware/x_profile/profile_model.py +1 -2
  49. swift/common/middleware/xprofile.py +6 -2
  50. swift/common/request_helpers.py +69 -0
  51. swift/common/statsd_client.py +207 -0
  52. swift/common/utils/__init__.py +97 -1635
  53. swift/common/utils/base.py +138 -0
  54. swift/common/utils/config.py +443 -0
  55. swift/common/utils/logs.py +999 -0
  56. swift/common/wsgi.py +11 -3
  57. swift/container/auditor.py +11 -0
  58. swift/container/backend.py +10 -10
  59. swift/container/reconciler.py +11 -2
  60. swift/container/replicator.py +22 -1
  61. swift/container/server.py +12 -1
  62. swift/container/sharder.py +36 -12
  63. swift/container/sync.py +11 -1
  64. swift/container/updater.py +11 -2
  65. swift/obj/auditor.py +18 -2
  66. swift/obj/diskfile.py +8 -6
  67. swift/obj/expirer.py +155 -36
  68. swift/obj/reconstructor.py +28 -4
  69. swift/obj/replicator.py +61 -22
  70. swift/obj/server.py +64 -36
  71. swift/obj/updater.py +11 -2
  72. swift/proxy/controllers/base.py +38 -22
  73. swift/proxy/controllers/obj.py +23 -26
  74. swift/proxy/server.py +15 -1
  75. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/AUTHORS +11 -3
  76. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/METADATA +34 -35
  77. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/RECORD +82 -108
  78. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/WHEEL +1 -1
  79. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/entry_points.txt +38 -1
  80. swift-2.34.1.dist-info/pbr.json +1 -0
  81. swift-2.33.0.data/scripts/swift-account-auditor +0 -23
  82. swift-2.33.0.data/scripts/swift-account-info +0 -52
  83. swift-2.33.0.data/scripts/swift-account-reaper +0 -23
  84. swift-2.33.0.data/scripts/swift-account-replicator +0 -34
  85. swift-2.33.0.data/scripts/swift-account-server +0 -23
  86. swift-2.33.0.data/scripts/swift-container-auditor +0 -23
  87. swift-2.33.0.data/scripts/swift-container-info +0 -59
  88. swift-2.33.0.data/scripts/swift-container-reconciler +0 -21
  89. swift-2.33.0.data/scripts/swift-container-replicator +0 -34
  90. swift-2.33.0.data/scripts/swift-container-server +0 -23
  91. swift-2.33.0.data/scripts/swift-container-sharder +0 -37
  92. swift-2.33.0.data/scripts/swift-container-sync +0 -23
  93. swift-2.33.0.data/scripts/swift-container-updater +0 -23
  94. swift-2.33.0.data/scripts/swift-dispersion-report +0 -24
  95. swift-2.33.0.data/scripts/swift-form-signature +0 -20
  96. swift-2.33.0.data/scripts/swift-init +0 -119
  97. swift-2.33.0.data/scripts/swift-object-auditor +0 -29
  98. swift-2.33.0.data/scripts/swift-object-expirer +0 -33
  99. swift-2.33.0.data/scripts/swift-object-info +0 -60
  100. swift-2.33.0.data/scripts/swift-object-reconstructor +0 -33
  101. swift-2.33.0.data/scripts/swift-object-relinker +0 -23
  102. swift-2.33.0.data/scripts/swift-object-replicator +0 -37
  103. swift-2.33.0.data/scripts/swift-object-server +0 -27
  104. swift-2.33.0.data/scripts/swift-object-updater +0 -23
  105. swift-2.33.0.data/scripts/swift-proxy-server +0 -23
  106. swift-2.33.0.data/scripts/swift-recon +0 -24
  107. swift-2.33.0.data/scripts/swift-recon-cron +0 -24
  108. swift-2.33.0.data/scripts/swift-ring-builder +0 -37
  109. swift-2.33.0.data/scripts/swift-ring-builder-analyzer +0 -22
  110. swift-2.33.0.data/scripts/swift-ring-composer +0 -22
  111. swift-2.33.0.dist-info/pbr.json +0 -1
  112. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/LICENSE +0 -0
  113. {swift-2.33.0.dist-info → swift-2.34.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,138 @@
1
+ # Copyright (c) 2010-2024 OpenStack Foundation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+ # implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """
17
+ Miscellaneous utility functions that may be used in other utils modules.
18
+
19
+ This module is imported by other utils modules.
20
+ This module should not import from other utils modules.
21
+ """
22
+
23
+ import codecs
24
+ import hashlib
25
+
26
+ import six
27
+ from six.moves.urllib.parse import quote as _quote
28
+
29
+
30
+ try:
31
+ _test_md5 = hashlib.md5(usedforsecurity=False) # nosec
32
+
33
+ def md5(string=b'', usedforsecurity=True):
34
+ """Return an md5 hashlib object using usedforsecurity parameter
35
+
36
+ For python distributions that support the usedforsecurity keyword
37
+ parameter, this passes the parameter through as expected.
38
+ See https://bugs.python.org/issue9216
39
+ """
40
+ return hashlib.md5(string, usedforsecurity=usedforsecurity) # nosec
41
+ except TypeError:
42
+ def md5(string=b'', usedforsecurity=True):
43
+ """Return an md5 hashlib object without usedforsecurity parameter
44
+
45
+ For python distributions that do not yet support this keyword
46
+ parameter, we drop the parameter
47
+ """
48
+ return hashlib.md5(string) # nosec
49
+
50
+
51
+ utf8_decoder = codecs.getdecoder('utf-8')
52
+ utf8_encoder = codecs.getencoder('utf-8')
53
+ if not six.PY2:
54
+ # Apparently under py3 we need to go to utf-16 to collapse surrogates?
55
+ utf16_decoder = codecs.getdecoder('utf-16')
56
+ utf16_encoder = codecs.getencoder('utf-16')
57
+
58
+
59
+ def get_valid_utf8_str(str_or_unicode):
60
+ """
61
+ Get valid parts of utf-8 str from str, unicode and even invalid utf-8 str
62
+
63
+ :param str_or_unicode: a string or an unicode which can be invalid utf-8
64
+ """
65
+ if six.PY2:
66
+ if isinstance(str_or_unicode, six.text_type):
67
+ (str_or_unicode, _len) = utf8_encoder(str_or_unicode, 'replace')
68
+ (valid_unicode_str, _len) = utf8_decoder(str_or_unicode, 'replace')
69
+ else:
70
+ if isinstance(str_or_unicode, six.binary_type):
71
+ try:
72
+ (str_or_unicode, _len) = utf8_decoder(str_or_unicode,
73
+ 'surrogatepass')
74
+ except UnicodeDecodeError:
75
+ (str_or_unicode, _len) = utf8_decoder(str_or_unicode,
76
+ 'replace')
77
+ (str_or_unicode, _len) = utf16_encoder(str_or_unicode, 'surrogatepass')
78
+ (valid_unicode_str, _len) = utf16_decoder(str_or_unicode, 'replace')
79
+ return valid_unicode_str.encode('utf-8')
80
+
81
+
82
+ def quote(value, safe='/'):
83
+ """
84
+ Patched version of urllib.quote that encodes utf-8 strings before quoting
85
+ """
86
+ quoted = _quote(get_valid_utf8_str(value), safe)
87
+ if isinstance(value, six.binary_type):
88
+ quoted = quoted.encode('utf-8')
89
+ return quoted
90
+
91
+
92
+ def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False):
93
+ """
94
+ Validate and split the given HTTP request path.
95
+
96
+ **Examples**::
97
+
98
+ ['a'] = split_path('/a')
99
+ ['a', None] = split_path('/a', 1, 2)
100
+ ['a', 'c'] = split_path('/a/c', 1, 2)
101
+ ['a', 'c', 'o/r'] = split_path('/a/c/o/r', 1, 3, True)
102
+
103
+ :param path: HTTP Request path to be split
104
+ :param minsegs: Minimum number of segments to be extracted
105
+ :param maxsegs: Maximum number of segments to be extracted
106
+ :param rest_with_last: If True, trailing data will be returned as part
107
+ of last segment. If False, and there is
108
+ trailing data, raises ValueError.
109
+ :returns: list of segments with a length of maxsegs (non-existent
110
+ segments will return as None)
111
+ :raises ValueError: if given an invalid path
112
+ """
113
+ if not maxsegs:
114
+ maxsegs = minsegs
115
+ if minsegs > maxsegs:
116
+ raise ValueError('minsegs > maxsegs: %d > %d' % (minsegs, maxsegs))
117
+ if rest_with_last:
118
+ segs = path.split('/', maxsegs)
119
+ minsegs += 1
120
+ maxsegs += 1
121
+ count = len(segs)
122
+ if (segs[0] or count < minsegs or count > maxsegs or
123
+ '' in segs[1:minsegs]):
124
+ raise ValueError('Invalid path: %s' % quote(path))
125
+ else:
126
+ minsegs += 1
127
+ maxsegs += 1
128
+ segs = path.split('/', maxsegs)
129
+ count = len(segs)
130
+ if (segs[0] or count < minsegs or count > maxsegs + 1 or
131
+ '' in segs[1:minsegs] or
132
+ (count == maxsegs + 1 and segs[maxsegs])):
133
+ raise ValueError('Invalid path: %s' % quote(path))
134
+ segs = segs[1:maxsegs]
135
+ if not all(segs[:-1]):
136
+ raise ValueError('Invalid path: %s' % quote(path))
137
+ segs.extend([None] * (maxsegs - 1 - len(segs)))
138
+ return segs
@@ -0,0 +1,443 @@
1
+ # Copyright (c) 2010-2012 OpenStack Foundation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12
+ # implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import six
17
+ import os
18
+ import operator
19
+ import re
20
+ from six.moves import configparser
21
+ from six.moves.configparser import (ConfigParser, RawConfigParser)
22
+
23
+ # Used when reading config values
24
+ TRUE_VALUES = {'true', '1', 'yes', 'on', 't', 'y'}
25
+
26
+
27
+ def config_true_value(value):
28
+ """
29
+ Returns True if the value is either True or a string in TRUE_VALUES.
30
+ Returns False otherwise.
31
+ """
32
+ return value is True or \
33
+ (isinstance(value, six.string_types) and value.lower() in TRUE_VALUES)
34
+
35
+
36
+ def _non_negative_number(value, expected_type_f=float,
37
+ expected_type_description='float number'):
38
+ try:
39
+ value = expected_type_f(value)
40
+ if value < 0:
41
+ raise ValueError
42
+ except (TypeError, ValueError):
43
+ raise ValueError('Value must be a non-negative %s, not "%s".'
44
+ % (expected_type_description, value))
45
+ return value
46
+
47
+
48
+ def non_negative_float(value):
49
+ """
50
+ Check that the value casts to a float and is non-negative.
51
+
52
+ :param value: value to check
53
+ :raises ValueError: if the value cannot be cast to a float or is negative.
54
+ :return: a float
55
+ """
56
+ return _non_negative_number(value)
57
+
58
+
59
+ def non_negative_int(value):
60
+ """
61
+ Check that the value casts to an int and is a whole number.
62
+
63
+ :param value: value to check
64
+ :raises ValueError: if the value cannot be cast to an int or does not
65
+ represent a whole number.
66
+ :return: an int
67
+ """
68
+ return _non_negative_number(value, expected_type_f=int,
69
+ expected_type_description='integer')
70
+
71
+
72
+ def config_positive_int_value(value):
73
+ """
74
+ Returns positive int value if it can be cast by int() and it's an
75
+ integer > 0. (not including zero) Raises ValueError otherwise.
76
+ """
77
+ try:
78
+ result = int(value)
79
+ if result < 1:
80
+ raise ValueError()
81
+ except (TypeError, ValueError):
82
+ raise ValueError(
83
+ 'Config option must be an positive int number, not "%s".' % value)
84
+ return result
85
+
86
+
87
+ def config_float_value(value, minimum=None, maximum=None):
88
+ try:
89
+ val = float(value)
90
+ if minimum is not None and val < minimum:
91
+ raise ValueError()
92
+ if maximum is not None and val > maximum:
93
+ raise ValueError()
94
+ return val
95
+ except (TypeError, ValueError):
96
+ min_ = ', greater than %s' % minimum if minimum is not None else ''
97
+ max_ = ', less than %s' % maximum if maximum is not None else ''
98
+ raise ValueError('Config option must be a number%s%s, not "%s".' %
99
+ (min_, max_, value))
100
+
101
+
102
+ def config_auto_int_value(value, default):
103
+ """
104
+ Returns default if value is None or 'auto'.
105
+ Returns value as an int or raises ValueError otherwise.
106
+ """
107
+ if value is None or \
108
+ (isinstance(value, six.string_types) and value.lower() == 'auto'):
109
+ return default
110
+ try:
111
+ value = int(value)
112
+ except (TypeError, ValueError):
113
+ raise ValueError('Config option must be an integer or the '
114
+ 'string "auto", not "%s".' % value)
115
+ return value
116
+
117
+
118
+ def config_percent_value(value):
119
+ try:
120
+ return config_float_value(value, 0, 100) / 100.0
121
+ except ValueError as err:
122
+ raise ValueError("%s: %s" % (str(err), value))
123
+
124
+
125
+ def config_request_node_count_value(value):
126
+ try:
127
+ value_parts = value.lower().split()
128
+ rnc_value = int(value_parts[0])
129
+ except (ValueError, AttributeError):
130
+ pass
131
+ else:
132
+ if len(value_parts) == 1:
133
+ return lambda replicas: rnc_value
134
+ elif (len(value_parts) == 3 and
135
+ value_parts[1] == '*' and
136
+ value_parts[2] == 'replicas'):
137
+ return lambda replicas: rnc_value * replicas
138
+ raise ValueError(
139
+ 'Invalid request_node_count value: %r' % value)
140
+
141
+
142
+ def config_fallocate_value(reserve_value):
143
+ """
144
+ Returns fallocate reserve_value as an int or float.
145
+ Returns is_percent as a boolean.
146
+ Returns a ValueError on invalid fallocate value.
147
+ """
148
+ try:
149
+ if str(reserve_value[-1:]) == '%':
150
+ reserve_value = float(reserve_value[:-1])
151
+ is_percent = True
152
+ else:
153
+ reserve_value = int(reserve_value)
154
+ is_percent = False
155
+ except ValueError:
156
+ raise ValueError('Error: %s is an invalid value for fallocate'
157
+ '_reserve.' % reserve_value)
158
+ return reserve_value, is_percent
159
+
160
+
161
+ def config_read_prefixed_options(conf, prefix_name, defaults):
162
+ """
163
+ Read prefixed options from configuration
164
+
165
+ :param conf: the configuration
166
+ :param prefix_name: the prefix (including, if needed, an underscore)
167
+ :param defaults: a dict of default values. The dict supplies the
168
+ option name and type (string or comma separated string)
169
+ :return: a dict containing the options
170
+ """
171
+ params = {}
172
+ for option_name in defaults.keys():
173
+ value = conf.get('%s%s' % (prefix_name, option_name))
174
+ if value:
175
+ if isinstance(defaults.get(option_name), list):
176
+ params[option_name] = []
177
+ for role in value.lower().split(','):
178
+ params[option_name].append(role.strip())
179
+ else:
180
+ params[option_name] = value.strip()
181
+ return params
182
+
183
+
184
+ def append_underscore(prefix):
185
+ if prefix and not prefix.endswith('_'):
186
+ prefix += '_'
187
+ return prefix
188
+
189
+
190
+ def config_read_reseller_options(conf, defaults):
191
+ """
192
+ Read reseller_prefix option and associated options from configuration
193
+
194
+ Reads the reseller_prefix option, then reads options that may be
195
+ associated with a specific reseller prefix. Reads options such that an
196
+ option without a prefix applies to all reseller prefixes unless an option
197
+ has an explicit prefix.
198
+
199
+ :param conf: the configuration
200
+ :param defaults: a dict of default values. The key is the option
201
+ name. The value is either an array of strings or a string
202
+ :return: tuple of an array of reseller prefixes and a dict of option values
203
+ """
204
+ reseller_prefix_opt = conf.get('reseller_prefix', 'AUTH').split(',')
205
+ reseller_prefixes = []
206
+ for prefix in [pre.strip() for pre in reseller_prefix_opt if pre.strip()]:
207
+ if prefix == "''":
208
+ prefix = ''
209
+ prefix = append_underscore(prefix)
210
+ if prefix not in reseller_prefixes:
211
+ reseller_prefixes.append(prefix)
212
+ if len(reseller_prefixes) == 0:
213
+ reseller_prefixes.append('')
214
+
215
+ # Get prefix-using config options
216
+ associated_options = {}
217
+ for prefix in reseller_prefixes:
218
+ associated_options[prefix] = dict(defaults)
219
+ associated_options[prefix].update(
220
+ config_read_prefixed_options(conf, '', defaults))
221
+ prefix_name = prefix if prefix != '' else "''"
222
+ associated_options[prefix].update(
223
+ config_read_prefixed_options(conf, prefix_name, defaults))
224
+ return reseller_prefixes, associated_options
225
+
226
+
227
+ def affinity_key_function(affinity_str):
228
+ """Turns an affinity config value into a function suitable for passing to
229
+ sort(). After doing so, the array will be sorted with respect to the given
230
+ ordering.
231
+
232
+ For example, if affinity_str is "r1=1, r2z7=2, r2z8=2", then the array
233
+ will be sorted with all nodes from region 1 (r1=1) first, then all the
234
+ nodes from region 2 zones 7 and 8 (r2z7=2 and r2z8=2), then everything
235
+ else.
236
+
237
+ Note that the order of the pieces of affinity_str is irrelevant; the
238
+ priority values are what comes after the equals sign.
239
+
240
+ If affinity_str is empty or all whitespace, then the resulting function
241
+ will not alter the ordering of the nodes.
242
+
243
+ :param affinity_str: affinity config value, e.g. "r1z2=3"
244
+ or "r1=1, r2z1=2, r2z2=2"
245
+ :returns: single-argument function
246
+ :raises ValueError: if argument invalid
247
+ """
248
+ affinity_str = affinity_str.strip()
249
+
250
+ if not affinity_str:
251
+ return lambda x: 0
252
+
253
+ priority_matchers = []
254
+ pieces = [s.strip() for s in affinity_str.split(',')]
255
+ for piece in pieces:
256
+ # matches r<number>=<number> or r<number>z<number>=<number>
257
+ match = re.match(r"r(\d+)(?:z(\d+))?=(\d+)$", piece)
258
+ if match:
259
+ region, zone, priority = match.groups()
260
+ region = int(region)
261
+ priority = int(priority)
262
+ zone = int(zone) if zone else None
263
+
264
+ matcher = {'region': region, 'priority': priority}
265
+ if zone is not None:
266
+ matcher['zone'] = zone
267
+ priority_matchers.append(matcher)
268
+ else:
269
+ raise ValueError("Invalid affinity value: %r" % affinity_str)
270
+
271
+ priority_matchers.sort(key=operator.itemgetter('priority'))
272
+
273
+ def keyfn(ring_node):
274
+ for matcher in priority_matchers:
275
+ if (matcher['region'] == ring_node['region']
276
+ and ('zone' not in matcher
277
+ or matcher['zone'] == ring_node['zone'])):
278
+ return matcher['priority']
279
+ return 4294967296 # 2^32, i.e. "a big number"
280
+ return keyfn
281
+
282
+
283
+ def affinity_locality_predicate(write_affinity_str):
284
+ """
285
+ Turns a write-affinity config value into a predicate function for nodes.
286
+ The returned value will be a 1-arg function that takes a node dictionary
287
+ and returns a true value if it is "local" and a false value otherwise. The
288
+ definition of "local" comes from the affinity_str argument passed in here.
289
+
290
+ For example, if affinity_str is "r1, r2z2", then only nodes where region=1
291
+ or where (region=2 and zone=2) are considered local.
292
+
293
+ If affinity_str is empty or all whitespace, then the resulting function
294
+ will consider everything local
295
+
296
+ :param write_affinity_str: affinity config value, e.g. "r1z2"
297
+ or "r1, r2z1, r2z2"
298
+ :returns: single-argument function, or None if affinity_str is empty
299
+ :raises ValueError: if argument invalid
300
+ """
301
+ affinity_str = write_affinity_str.strip()
302
+
303
+ if not affinity_str:
304
+ return None
305
+
306
+ matchers = []
307
+ pieces = [s.strip() for s in affinity_str.split(',')]
308
+ for piece in pieces:
309
+ # matches r<number> or r<number>z<number>
310
+ match = re.match(r"r(\d+)(?:z(\d+))?$", piece)
311
+ if match:
312
+ region, zone = match.groups()
313
+ region = int(region)
314
+ zone = int(zone) if zone else None
315
+
316
+ matcher = {'region': region}
317
+ if zone is not None:
318
+ matcher['zone'] = zone
319
+ matchers.append(matcher)
320
+ else:
321
+ raise ValueError("Invalid write-affinity value: %r" % affinity_str)
322
+
323
+ def is_local(ring_node):
324
+ for matcher in matchers:
325
+ if (matcher['region'] == ring_node['region']
326
+ and ('zone' not in matcher
327
+ or matcher['zone'] == ring_node['zone'])):
328
+ return True
329
+ return False
330
+ return is_local
331
+
332
+
333
+ def read_conf_dir(parser, conf_dir):
334
+ conf_files = []
335
+ for f in os.listdir(conf_dir):
336
+ if f.endswith('.conf') and not f.startswith('.'):
337
+ conf_files.append(os.path.join(conf_dir, f))
338
+ return parser.read(sorted(conf_files))
339
+
340
+
341
+ if six.PY2:
342
+ NicerInterpolation = None # just don't cause ImportErrors over in wsgi.py
343
+ else:
344
+ class NicerInterpolation(configparser.BasicInterpolation):
345
+ def before_get(self, parser, section, option, value, defaults):
346
+ if '%(' not in value:
347
+ return value
348
+ return super(NicerInterpolation, self).before_get(
349
+ parser, section, option, value, defaults)
350
+
351
+
352
+ def readconf(conf_path, section_name=None, log_name=None, defaults=None,
353
+ raw=False):
354
+ """
355
+ Read config file(s) and return config items as a dict
356
+
357
+ :param conf_path: path to config file/directory, or a file-like object
358
+ (hasattr readline)
359
+ :param section_name: config section to read (will return all sections if
360
+ not defined)
361
+ :param log_name: name to be used with logging (will use section_name if
362
+ not defined)
363
+ :param defaults: dict of default values to pre-populate the config with
364
+ :returns: dict of config items
365
+ :raises ValueError: if section_name does not exist
366
+ :raises IOError: if reading the file failed
367
+ """
368
+ if defaults is None:
369
+ defaults = {}
370
+ if raw:
371
+ c = RawConfigParser(defaults)
372
+ else:
373
+ if six.PY2:
374
+ c = ConfigParser(defaults)
375
+ else:
376
+ # In general, we haven't really thought much about interpolation
377
+ # in configs. Python's default ConfigParser has always supported
378
+ # it, though, so *we* got it "for free". Unfortunatley, since we
379
+ # "supported" interpolation, we have to assume there are
380
+ # deployments in the wild that use it, and try not to break them.
381
+ # So, do what we can to mimic the py2 behavior of passing through
382
+ # values like "1%" (which we want to support for
383
+ # fallocate_reserve).
384
+ c = ConfigParser(defaults, interpolation=NicerInterpolation())
385
+ c.optionxform = str # Don't lower-case keys
386
+
387
+ if hasattr(conf_path, 'readline'):
388
+ if hasattr(conf_path, 'seek'):
389
+ conf_path.seek(0)
390
+ if six.PY2:
391
+ c.readfp(conf_path)
392
+ else:
393
+ c.read_file(conf_path)
394
+ else:
395
+ if os.path.isdir(conf_path):
396
+ # read all configs in directory
397
+ success = read_conf_dir(c, conf_path)
398
+ else:
399
+ success = c.read(conf_path)
400
+ if not success:
401
+ raise IOError("Unable to read config from %s" %
402
+ conf_path)
403
+ if section_name:
404
+ if c.has_section(section_name):
405
+ conf = dict(c.items(section_name))
406
+ else:
407
+ raise ValueError(
408
+ "Unable to find %(section)s config section in %(conf)s" %
409
+ {'section': section_name, 'conf': conf_path})
410
+ if "log_name" not in conf:
411
+ if log_name is not None:
412
+ conf['log_name'] = log_name
413
+ else:
414
+ conf['log_name'] = section_name
415
+ else:
416
+ conf = {}
417
+ for s in c.sections():
418
+ conf.update({s: dict(c.items(s))})
419
+ if 'log_name' not in conf:
420
+ conf['log_name'] = log_name
421
+ conf['__file__'] = conf_path
422
+ return conf
423
+
424
+
425
+ def parse_prefixed_conf(conf_file, prefix):
426
+ """
427
+ Search the config file for any common-prefix sections and load those
428
+ sections to a dict mapping the after-prefix reference to options.
429
+
430
+ :param conf_file: the file name of the config to parse
431
+ :param prefix: the common prefix of the sections
432
+ :return: a dict mapping policy reference -> dict of policy options
433
+ :raises ValueError: if a policy config section has an invalid name
434
+ """
435
+
436
+ ret_config = {}
437
+ all_conf = readconf(conf_file)
438
+ for section, options in all_conf.items():
439
+ if not section.startswith(prefix):
440
+ continue
441
+ target_ref = section[len(prefix):]
442
+ ret_config[target_ref] = options
443
+ return ret_config