swift 2.23.2__py3-none-any.whl → 2.35.0__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 (208) hide show
  1. swift/__init__.py +29 -50
  2. swift/account/auditor.py +21 -118
  3. swift/account/backend.py +33 -28
  4. swift/account/reaper.py +37 -28
  5. swift/account/replicator.py +22 -0
  6. swift/account/server.py +60 -26
  7. swift/account/utils.py +28 -11
  8. swift-2.23.2.data/scripts/swift-account-audit → swift/cli/account_audit.py +23 -13
  9. swift-2.23.2.data/scripts/swift-config → swift/cli/config.py +2 -2
  10. swift/cli/container_deleter.py +5 -11
  11. swift-2.23.2.data/scripts/swift-dispersion-populate → swift/cli/dispersion_populate.py +8 -7
  12. swift/cli/dispersion_report.py +10 -9
  13. swift-2.23.2.data/scripts/swift-drive-audit → swift/cli/drive_audit.py +63 -21
  14. swift/cli/form_signature.py +3 -7
  15. swift-2.23.2.data/scripts/swift-get-nodes → swift/cli/get_nodes.py +8 -2
  16. swift/cli/info.py +183 -29
  17. swift/cli/manage_shard_ranges.py +708 -37
  18. swift-2.23.2.data/scripts/swift-oldies → swift/cli/oldies.py +25 -14
  19. swift-2.23.2.data/scripts/swift-orphans → swift/cli/orphans.py +7 -3
  20. swift/cli/recon.py +196 -67
  21. swift-2.23.2.data/scripts/swift-recon-cron → swift/cli/recon_cron.py +17 -20
  22. swift-2.23.2.data/scripts/swift-reconciler-enqueue → swift/cli/reconciler_enqueue.py +2 -3
  23. swift/cli/relinker.py +807 -126
  24. swift/cli/reload.py +135 -0
  25. swift/cli/ringbuilder.py +217 -20
  26. swift/cli/ringcomposer.py +0 -1
  27. swift/cli/shard-info.py +4 -3
  28. swift/common/base_storage_server.py +9 -20
  29. swift/common/bufferedhttp.py +48 -74
  30. swift/common/constraints.py +20 -15
  31. swift/common/container_sync_realms.py +9 -11
  32. swift/common/daemon.py +25 -8
  33. swift/common/db.py +198 -127
  34. swift/common/db_auditor.py +168 -0
  35. swift/common/db_replicator.py +95 -55
  36. swift/common/digest.py +141 -0
  37. swift/common/direct_client.py +144 -33
  38. swift/common/error_limiter.py +93 -0
  39. swift/common/exceptions.py +25 -1
  40. swift/common/header_key_dict.py +2 -9
  41. swift/common/http_protocol.py +373 -0
  42. swift/common/internal_client.py +129 -59
  43. swift/common/linkat.py +3 -4
  44. swift/common/manager.py +284 -67
  45. swift/common/memcached.py +396 -147
  46. swift/common/middleware/__init__.py +4 -0
  47. swift/common/middleware/account_quotas.py +211 -46
  48. swift/common/middleware/acl.py +3 -8
  49. swift/common/middleware/backend_ratelimit.py +230 -0
  50. swift/common/middleware/bulk.py +22 -34
  51. swift/common/middleware/catch_errors.py +1 -3
  52. swift/common/middleware/cname_lookup.py +6 -11
  53. swift/common/middleware/container_quotas.py +1 -1
  54. swift/common/middleware/container_sync.py +39 -17
  55. swift/common/middleware/copy.py +12 -0
  56. swift/common/middleware/crossdomain.py +22 -9
  57. swift/common/middleware/crypto/__init__.py +2 -1
  58. swift/common/middleware/crypto/crypto_utils.py +11 -15
  59. swift/common/middleware/crypto/decrypter.py +28 -11
  60. swift/common/middleware/crypto/encrypter.py +12 -17
  61. swift/common/middleware/crypto/keymaster.py +8 -15
  62. swift/common/middleware/crypto/kms_keymaster.py +2 -1
  63. swift/common/middleware/dlo.py +15 -11
  64. swift/common/middleware/domain_remap.py +5 -4
  65. swift/common/middleware/etag_quoter.py +128 -0
  66. swift/common/middleware/formpost.py +73 -70
  67. swift/common/middleware/gatekeeper.py +8 -1
  68. swift/common/middleware/keystoneauth.py +33 -3
  69. swift/common/middleware/list_endpoints.py +4 -4
  70. swift/common/middleware/listing_formats.py +85 -49
  71. swift/common/middleware/memcache.py +4 -81
  72. swift/common/middleware/name_check.py +3 -2
  73. swift/common/middleware/proxy_logging.py +160 -92
  74. swift/common/middleware/ratelimit.py +17 -10
  75. swift/common/middleware/read_only.py +6 -4
  76. swift/common/middleware/recon.py +59 -22
  77. swift/common/middleware/s3api/acl_handlers.py +25 -3
  78. swift/common/middleware/s3api/acl_utils.py +6 -1
  79. swift/common/middleware/s3api/controllers/__init__.py +6 -0
  80. swift/common/middleware/s3api/controllers/acl.py +3 -2
  81. swift/common/middleware/s3api/controllers/bucket.py +242 -137
  82. swift/common/middleware/s3api/controllers/logging.py +2 -2
  83. swift/common/middleware/s3api/controllers/multi_delete.py +43 -20
  84. swift/common/middleware/s3api/controllers/multi_upload.py +219 -133
  85. swift/common/middleware/s3api/controllers/obj.py +112 -8
  86. swift/common/middleware/s3api/controllers/object_lock.py +44 -0
  87. swift/common/middleware/s3api/controllers/s3_acl.py +2 -2
  88. swift/common/middleware/s3api/controllers/tagging.py +57 -0
  89. swift/common/middleware/s3api/controllers/versioning.py +36 -7
  90. swift/common/middleware/s3api/etree.py +22 -9
  91. swift/common/middleware/s3api/exception.py +0 -4
  92. swift/common/middleware/s3api/s3api.py +113 -41
  93. swift/common/middleware/s3api/s3request.py +384 -218
  94. swift/common/middleware/s3api/s3response.py +126 -23
  95. swift/common/middleware/s3api/s3token.py +16 -17
  96. swift/common/middleware/s3api/schema/delete.rng +1 -1
  97. swift/common/middleware/s3api/subresource.py +7 -10
  98. swift/common/middleware/s3api/utils.py +27 -10
  99. swift/common/middleware/slo.py +665 -358
  100. swift/common/middleware/staticweb.py +64 -37
  101. swift/common/middleware/symlink.py +52 -19
  102. swift/common/middleware/tempauth.py +76 -58
  103. swift/common/middleware/tempurl.py +192 -174
  104. swift/common/middleware/versioned_writes/__init__.py +51 -0
  105. swift/common/middleware/{versioned_writes.py → versioned_writes/legacy.py} +27 -26
  106. swift/common/middleware/versioned_writes/object_versioning.py +1482 -0
  107. swift/common/middleware/x_profile/exceptions.py +1 -4
  108. swift/common/middleware/x_profile/html_viewer.py +18 -19
  109. swift/common/middleware/x_profile/profile_model.py +1 -2
  110. swift/common/middleware/xprofile.py +10 -10
  111. swift-2.23.2.data/scripts/swift-container-server → swift/common/recon.py +13 -8
  112. swift/common/registry.py +147 -0
  113. swift/common/request_helpers.py +324 -57
  114. swift/common/ring/builder.py +67 -25
  115. swift/common/ring/composite_builder.py +1 -1
  116. swift/common/ring/ring.py +177 -51
  117. swift/common/ring/utils.py +1 -1
  118. swift/common/splice.py +10 -6
  119. swift/common/statsd_client.py +205 -0
  120. swift/common/storage_policy.py +49 -44
  121. swift/common/swob.py +86 -102
  122. swift/common/{utils.py → utils/__init__.py} +2191 -2762
  123. swift/common/utils/base.py +131 -0
  124. swift/common/utils/config.py +433 -0
  125. swift/common/utils/ipaddrs.py +256 -0
  126. swift/common/utils/libc.py +345 -0
  127. swift/common/utils/logs.py +859 -0
  128. swift/common/utils/timestamp.py +412 -0
  129. swift/common/wsgi.py +555 -536
  130. swift/container/auditor.py +14 -100
  131. swift/container/backend.py +552 -227
  132. swift/container/reconciler.py +126 -37
  133. swift/container/replicator.py +96 -22
  134. swift/container/server.py +397 -176
  135. swift/container/sharder.py +1580 -639
  136. swift/container/sync.py +94 -88
  137. swift/container/updater.py +53 -32
  138. swift/obj/auditor.py +153 -35
  139. swift/obj/diskfile.py +466 -217
  140. swift/obj/expirer.py +406 -124
  141. swift/obj/mem_diskfile.py +7 -4
  142. swift/obj/mem_server.py +1 -0
  143. swift/obj/reconstructor.py +523 -262
  144. swift/obj/replicator.py +249 -188
  145. swift/obj/server.py +213 -122
  146. swift/obj/ssync_receiver.py +145 -85
  147. swift/obj/ssync_sender.py +113 -54
  148. swift/obj/updater.py +653 -139
  149. swift/obj/watchers/__init__.py +0 -0
  150. swift/obj/watchers/dark_data.py +213 -0
  151. swift/proxy/controllers/account.py +11 -11
  152. swift/proxy/controllers/base.py +848 -604
  153. swift/proxy/controllers/container.py +452 -86
  154. swift/proxy/controllers/info.py +3 -2
  155. swift/proxy/controllers/obj.py +1009 -490
  156. swift/proxy/server.py +185 -112
  157. swift-2.35.0.dist-info/AUTHORS +501 -0
  158. swift-2.35.0.dist-info/LICENSE +202 -0
  159. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/METADATA +52 -61
  160. swift-2.35.0.dist-info/RECORD +201 -0
  161. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/WHEEL +1 -1
  162. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/entry_points.txt +43 -0
  163. swift-2.35.0.dist-info/pbr.json +1 -0
  164. swift/locale/de/LC_MESSAGES/swift.po +0 -1216
  165. swift/locale/en_GB/LC_MESSAGES/swift.po +0 -1207
  166. swift/locale/es/LC_MESSAGES/swift.po +0 -1085
  167. swift/locale/fr/LC_MESSAGES/swift.po +0 -909
  168. swift/locale/it/LC_MESSAGES/swift.po +0 -894
  169. swift/locale/ja/LC_MESSAGES/swift.po +0 -965
  170. swift/locale/ko_KR/LC_MESSAGES/swift.po +0 -964
  171. swift/locale/pt_BR/LC_MESSAGES/swift.po +0 -881
  172. swift/locale/ru/LC_MESSAGES/swift.po +0 -891
  173. swift/locale/tr_TR/LC_MESSAGES/swift.po +0 -832
  174. swift/locale/zh_CN/LC_MESSAGES/swift.po +0 -833
  175. swift/locale/zh_TW/LC_MESSAGES/swift.po +0 -838
  176. swift-2.23.2.data/scripts/swift-account-auditor +0 -23
  177. swift-2.23.2.data/scripts/swift-account-info +0 -51
  178. swift-2.23.2.data/scripts/swift-account-reaper +0 -23
  179. swift-2.23.2.data/scripts/swift-account-replicator +0 -34
  180. swift-2.23.2.data/scripts/swift-account-server +0 -23
  181. swift-2.23.2.data/scripts/swift-container-auditor +0 -23
  182. swift-2.23.2.data/scripts/swift-container-info +0 -51
  183. swift-2.23.2.data/scripts/swift-container-reconciler +0 -21
  184. swift-2.23.2.data/scripts/swift-container-replicator +0 -34
  185. swift-2.23.2.data/scripts/swift-container-sharder +0 -33
  186. swift-2.23.2.data/scripts/swift-container-sync +0 -23
  187. swift-2.23.2.data/scripts/swift-container-updater +0 -23
  188. swift-2.23.2.data/scripts/swift-dispersion-report +0 -24
  189. swift-2.23.2.data/scripts/swift-form-signature +0 -20
  190. swift-2.23.2.data/scripts/swift-init +0 -119
  191. swift-2.23.2.data/scripts/swift-object-auditor +0 -29
  192. swift-2.23.2.data/scripts/swift-object-expirer +0 -33
  193. swift-2.23.2.data/scripts/swift-object-info +0 -60
  194. swift-2.23.2.data/scripts/swift-object-reconstructor +0 -33
  195. swift-2.23.2.data/scripts/swift-object-relinker +0 -41
  196. swift-2.23.2.data/scripts/swift-object-replicator +0 -37
  197. swift-2.23.2.data/scripts/swift-object-server +0 -27
  198. swift-2.23.2.data/scripts/swift-object-updater +0 -23
  199. swift-2.23.2.data/scripts/swift-proxy-server +0 -23
  200. swift-2.23.2.data/scripts/swift-recon +0 -24
  201. swift-2.23.2.data/scripts/swift-ring-builder +0 -24
  202. swift-2.23.2.data/scripts/swift-ring-builder-analyzer +0 -22
  203. swift-2.23.2.data/scripts/swift-ring-composer +0 -22
  204. swift-2.23.2.dist-info/DESCRIPTION.rst +0 -166
  205. swift-2.23.2.dist-info/RECORD +0 -220
  206. swift-2.23.2.dist-info/metadata.json +0 -1
  207. swift-2.23.2.dist-info/pbr.json +0 -1
  208. {swift-2.23.2.dist-info → swift-2.35.0.dist-info}/top_level.txt +0 -0
@@ -155,30 +155,124 @@ All three steps may be performed with one sub-command::
155
155
  Run container-sharder on all nodes to shard the container.
156
156
 
157
157
  """
158
- from __future__ import print_function
159
158
  import argparse
160
159
  import json
160
+ import os.path
161
161
  import sys
162
162
  import time
163
+ from contextlib import contextmanager
163
164
 
164
- from six.moves import input
165
-
166
- from swift.common.utils import Timestamp, get_logger, ShardRange
165
+ from swift.common.utils import Timestamp, get_logger, ShardRange, readconf, \
166
+ ShardRangeList, non_negative_int, config_positive_int_value
167
167
  from swift.container.backend import ContainerBroker, UNSHARDED
168
168
  from swift.container.sharder import make_shard_ranges, sharding_enabled, \
169
- CleavingContext
169
+ CleavingContext, process_compactible_shard_sequences, \
170
+ find_compactible_shard_sequences, find_overlapping_ranges, \
171
+ find_paths, rank_paths, finalize_shrinking, DEFAULT_SHARDER_CONF, \
172
+ ContainerSharderConf, find_paths_with_gaps, combine_shard_ranges, \
173
+ update_own_shard_range_stats
170
174
 
175
+ EXIT_SUCCESS = 0
176
+ EXIT_ERROR = 1
177
+ EXIT_INVALID_ARGS = 2 # consistent with argparse exit code for invalid args
178
+ EXIT_USER_QUIT = 3
171
179
 
172
- def _load_and_validate_shard_data(args):
173
- try:
180
+ MIN_SHARD_RANGE_AGE_FOR_REPAIR = 4 * 3600
181
+
182
+ # Some CLI options derive their default values from DEFAULT_SHARDER_CONF if
183
+ # they have not been set. It is therefore important that the CLI parser
184
+ # provides None as a default so that we can detect that no value was set on the
185
+ # command line. We use this alias to act as a reminder.
186
+ USE_SHARDER_DEFAULT = object()
187
+
188
+
189
+ class ManageShardRangesException(Exception):
190
+ pass
191
+
192
+
193
+ class GapsFoundException(ManageShardRangesException):
194
+ pass
195
+
196
+
197
+ class InvalidStateException(ManageShardRangesException):
198
+ pass
199
+
200
+
201
+ class InvalidSolutionException(ManageShardRangesException):
202
+ def __init__(self, msg, acceptor_path, overlapping_donors):
203
+ super(InvalidSolutionException, self).__init__(msg)
204
+ self.acceptor_path = acceptor_path
205
+ self.overlapping_donors = overlapping_donors
206
+
207
+
208
+ def wrap_for_argparse(func, msg=None):
209
+ """
210
+ Wrap the given ``func`` to catch any ``ValueError`` and raise an
211
+ ``argparse.ArgumentTypeError`` instead.
212
+
213
+ :param func: a function.
214
+ :param msg: an optional message to use with any exception that is used; if
215
+ not given then the string representation of the ValueError will be
216
+ used.
217
+ :return: a function wrapper.
218
+ """
219
+ def wrapped_func(*args, **kwargs):
220
+ try:
221
+ return func(*args, **kwargs)
222
+ except ValueError as err:
223
+ raise argparse.ArgumentTypeError(str(err) if msg is None else msg)
224
+ return wrapped_func
225
+
226
+
227
+ def _proceed(args):
228
+ if args.dry_run:
229
+ choice = 'no'
230
+ elif args.yes:
231
+ choice = 'yes'
232
+ else:
233
+ try:
234
+ choice = input('Do you want to apply these changes to the '
235
+ 'container DB? [yes/N]')
236
+ except (EOFError, KeyboardInterrupt):
237
+ choice = 'no'
238
+ if choice != 'yes':
239
+ print('No changes applied')
240
+
241
+ return choice == 'yes'
242
+
243
+
244
+ def _print_shard_range(sr, level=0):
245
+ indent = ' ' * level
246
+ print(indent + '%r' % sr.name)
247
+ print(indent + ' objects: %9d, tombstones: %9d, lower: %r'
248
+ % (sr.object_count, sr.tombstones, sr.lower_str))
249
+ print(indent + ' state: %9s, deleted: %d upper: %r'
250
+ % (sr.state_text, sr.deleted, sr.upper_str))
251
+
252
+
253
+ @contextmanager
254
+ def _open_input(args):
255
+ if args.input == '-':
256
+ args.input = '<STDIN>'
257
+ yield sys.stdin
258
+ else:
174
259
  with open(args.input, 'r') as fd:
260
+ yield fd
261
+
262
+
263
+ def _load_and_validate_shard_data(args, require_index=True):
264
+ required_keys = ['lower', 'upper', 'object_count']
265
+ if require_index:
266
+ required_keys.append('index')
267
+ try:
268
+ with _open_input(args) as fd:
175
269
  try:
176
270
  data = json.load(fd)
177
271
  if not isinstance(data, list):
178
272
  raise ValueError('Shard data must be a list of dicts')
179
- for k in ('lower', 'upper', 'index', 'object_count'):
273
+ for k in required_keys:
180
274
  for shard in data:
181
- shard[k]
275
+ shard[k] # trigger KeyError for missing required key
182
276
  return data
183
277
  except (TypeError, ValueError, KeyError) as err:
184
278
  print('Failed to load valid shard range data: %r' % err,
@@ -208,7 +302,7 @@ def _check_shard_ranges(own_shard_range, shard_ranges):
208
302
  if reasons:
209
303
  print('WARNING: invalid shard ranges: %s.' % reasons)
210
304
  print('Aborting.')
211
- exit(2)
305
+ exit(EXIT_ERROR)
212
306
 
213
307
 
214
308
  def _check_own_shard_range(broker, args):
@@ -228,7 +322,8 @@ def _find_ranges(broker, args, status_file=None):
228
322
  start = last_report = time.time()
229
323
  limit = 5 if status_file else -1
230
324
  shard_data, last_found = broker.find_shard_ranges(
231
- args.rows_per_shard, limit=limit)
325
+ args.rows_per_shard, limit=limit,
326
+ minimum_shard_size=args.minimum_shard_size)
232
327
  if shard_data:
233
328
  while not last_found:
234
329
  if last_report + 10 < time.time():
@@ -238,7 +333,8 @@ def _find_ranges(broker, args, status_file=None):
238
333
  # prefix doesn't matter since we aren't persisting it
239
334
  found_ranges = make_shard_ranges(broker, shard_data, '.shards_')
240
335
  more_shard_data, last_found = broker.find_shard_ranges(
241
- args.rows_per_shard, existing_ranges=found_ranges, limit=5)
336
+ args.rows_per_shard, existing_ranges=found_ranges, limit=5,
337
+ minimum_shard_size=args.minimum_shard_size)
242
338
  shard_data.extend(more_shard_data)
243
339
  return shard_data, time.time() - start
244
340
 
@@ -250,11 +346,12 @@ def find_ranges(broker, args):
250
346
  (len(shard_data), delta_t,
251
347
  sum(r['object_count'] for r in shard_data)),
252
348
  file=sys.stderr)
253
- return 0
349
+ return EXIT_SUCCESS
254
350
 
255
351
 
256
352
  def show_shard_ranges(broker, args):
257
353
  shard_ranges = broker.get_shard_ranges(
354
+ includes=getattr(args, 'includes', None),
258
355
  include_deleted=getattr(args, 'include_deleted', False))
259
356
  shard_data = [dict(sr, state=sr.state_text)
260
357
  for sr in shard_ranges]
@@ -268,7 +365,7 @@ def show_shard_ranges(broker, args):
268
365
  else:
269
366
  print("Existing shard ranges:", file=sys.stderr)
270
367
  print(json.dumps(shard_data, sort_keys=True, indent=2))
271
- return 0
368
+ return EXIT_SUCCESS
272
369
 
273
370
 
274
371
  def db_info(broker, args):
@@ -280,6 +377,9 @@ def db_info(broker, args):
280
377
  if own_sr else None))
281
378
  db_state = broker.get_db_state()
282
379
  print('db_state = %s' % db_state)
380
+ info = broker.get_info()
381
+ print('object_count = %d' % info['object_count'])
382
+ print('bytes_used = %d' % info['bytes_used'])
283
383
  if db_state == 'sharding':
284
384
  print('Retiring db id: %s' % broker.get_brokers()[0].get_info()['id'])
285
385
  print('Cleaving context: %s' %
@@ -288,13 +388,14 @@ def db_info(broker, args):
288
388
  print('Metadata:')
289
389
  for k, (v, t) in broker.metadata.items():
290
390
  print(' %s = %s' % (k, v))
391
+ return EXIT_SUCCESS
291
392
 
292
393
 
293
394
  def delete_shard_ranges(broker, args):
294
395
  shard_ranges = broker.get_shard_ranges()
295
396
  if not shard_ranges:
296
397
  print("No shard ranges found to delete.")
297
- return 0
398
+ return EXIT_SUCCESS
298
399
 
299
400
  while not args.force:
300
401
  print('This will delete existing %d shard ranges.' % len(shard_ranges))
@@ -306,14 +407,18 @@ def delete_shard_ranges(broker, args):
306
407
  print(' - %d existing shard ranges have started sharding' %
307
408
  [sr.state != ShardRange.FOUND
308
409
  for sr in shard_ranges].count(True))
309
- choice = input('Do you want to show the existing ranges [s], '
310
- 'delete the existing ranges [yes] '
311
- 'or quit without deleting [q]? ')
410
+ try:
411
+ choice = input('Do you want to show the existing ranges [s], '
412
+ 'delete the existing ranges [yes] '
413
+ 'or quit without deleting [q]? ')
414
+ except (EOFError, KeyboardInterrupt):
415
+ choice = 'q'
416
+
312
417
  if choice == 's':
313
418
  show_shard_ranges(broker, args)
314
419
  continue
315
420
  elif choice == 'q':
316
- return 1
421
+ return EXIT_USER_QUIT
317
422
  elif choice == 'yes':
318
423
  break
319
424
  else:
@@ -326,7 +431,41 @@ def delete_shard_ranges(broker, args):
326
431
  sr.timestamp = now
327
432
  broker.merge_shard_ranges(shard_ranges)
328
433
  print('Deleted %s existing shard ranges.' % len(shard_ranges))
329
- return 0
434
+ return EXIT_SUCCESS
435
+
436
+
437
+ def merge_shard_ranges(broker, args):
438
+ _check_own_shard_range(broker, args)
439
+ shard_data = _load_and_validate_shard_data(args, require_index=False)
440
+ new_shard_ranges = ShardRangeList([ShardRange.from_dict(sr)
441
+ for sr in shard_data])
442
+ new_shard_ranges.sort(key=ShardRange.sort_key)
443
+
444
+ # do some checks before merging...
445
+ existing_shard_ranges = ShardRangeList(
446
+ broker.get_shard_ranges(include_deleted=True))
447
+ outcome = combine_shard_ranges(new_shard_ranges, existing_shard_ranges)
448
+ if args.verbose:
449
+ print('This change will result in the following shard ranges in the '
450
+ 'affected namespace:')
451
+ print(json.dumps([dict(sr) for sr in outcome], indent=2))
452
+ overlaps = find_overlapping_ranges(outcome)
453
+ if overlaps:
454
+ print('WARNING: this change will result in shard ranges overlaps!')
455
+ paths_with_gaps = find_paths_with_gaps(outcome)
456
+ gaps = [gap for start_path, gap, end_path in paths_with_gaps
457
+ if existing_shard_ranges.includes(gap)]
458
+ if gaps:
459
+ print('WARNING: this change will result in shard ranges gaps!')
460
+
461
+ if not _proceed(args):
462
+ return EXIT_USER_QUIT
463
+
464
+ with broker.updated_timeout(args.replace_timeout):
465
+ broker.merge_shard_ranges(new_shard_ranges)
466
+ print('Injected %d shard ranges.' % len(new_shard_ranges))
467
+ print('Run container-replicator to replicate them to other nodes.')
468
+ return EXIT_SUCCESS
330
469
 
331
470
 
332
471
  def _replace_shard_ranges(broker, args, shard_data, timeout=0):
@@ -342,7 +481,9 @@ def _replace_shard_ranges(broker, args, shard_data, timeout=0):
342
481
 
343
482
  # Crank up the timeout in an effort to *make sure* this succeeds
344
483
  with broker.updated_timeout(max(timeout, args.replace_timeout)):
345
- delete_shard_ranges(broker, args)
484
+ delete_status = delete_shard_ranges(broker, args)
485
+ if delete_status != EXIT_SUCCESS:
486
+ return delete_status
346
487
  broker.merge_shard_ranges(shard_ranges)
347
488
 
348
489
  print('Injected %d shard ranges.' % len(shard_ranges))
@@ -351,7 +492,7 @@ def _replace_shard_ranges(broker, args, shard_data, timeout=0):
351
492
  return enable_sharding(broker, args)
352
493
  else:
353
494
  print('Use the enable sub-command to enable sharding.')
354
- return 0
495
+ return EXIT_SUCCESS
355
496
 
356
497
 
357
498
  def replace_shard_ranges(broker, args):
@@ -370,6 +511,8 @@ def _enable_sharding(broker, own_shard_range, args):
370
511
  if own_shard_range.update_state(ShardRange.SHARDING):
371
512
  own_shard_range.epoch = Timestamp.now()
372
513
  own_shard_range.state_timestamp = own_shard_range.epoch
514
+ # initialise own_shard_range with current broker object stats...
515
+ update_own_shard_range_stats(broker, own_shard_range)
373
516
 
374
517
  with broker.updated_timeout(args.enable_timeout):
375
518
  broker.merge_shard_ranges([own_shard_range])
@@ -401,20 +544,381 @@ def enable_sharding(broker, args):
401
544
  print('WARNING: container in state %s (should be active or sharding).'
402
545
  % own_shard_range.state_text)
403
546
  print('Aborting.')
404
- return 2
547
+ return EXIT_ERROR
405
548
 
406
549
  print('Run container-sharder on all nodes to shard the container.')
407
- return 0
550
+ return EXIT_SUCCESS
551
+
552
+
553
+ def compact_shard_ranges(broker, args):
554
+ if not broker.is_root_container():
555
+ print('WARNING: Shard containers cannot be compacted.')
556
+ print('This command should be used on a root container.')
557
+ return EXIT_ERROR
558
+
559
+ if not broker.is_sharded():
560
+ print('WARNING: Container is not yet sharded so cannot be compacted.')
561
+ return EXIT_ERROR
562
+
563
+ shard_ranges = broker.get_shard_ranges()
564
+ if find_overlapping_ranges([sr for sr in shard_ranges if
565
+ sr.state != ShardRange.SHRINKING]):
566
+ print('WARNING: Container has overlapping shard ranges so cannot be '
567
+ 'compacted.')
568
+ return EXIT_ERROR
569
+
570
+ compactible = find_compactible_shard_sequences(broker,
571
+ args.shrink_threshold,
572
+ args.expansion_limit,
573
+ args.max_shrinking,
574
+ args.max_expanding)
575
+ if not compactible:
576
+ print('No shards identified for compaction.')
577
+ return EXIT_SUCCESS
578
+
579
+ for sequence in compactible:
580
+ if sequence[-1].state not in (ShardRange.ACTIVE, ShardRange.SHARDED):
581
+ print('ERROR: acceptor not in correct state: %s' % sequence[-1],
582
+ file=sys.stderr)
583
+ return EXIT_ERROR
584
+
585
+ for sequence in compactible:
586
+ acceptor = sequence[-1]
587
+ donors = sequence[:-1]
588
+ print('Donor shard range(s) with total of %d rows:'
589
+ % donors.row_count)
590
+ for donor in donors:
591
+ _print_shard_range(donor, level=1)
592
+ print('can be compacted into acceptor shard range:')
593
+ _print_shard_range(acceptor, level=1)
594
+ print('Total of %d shard sequences identified for compaction.'
595
+ % len(compactible))
596
+ print('Once applied to the broker these changes will result in shard '
597
+ 'range compaction the next time the sharder runs.')
598
+
599
+ if not _proceed(args):
600
+ return EXIT_USER_QUIT
601
+
602
+ process_compactible_shard_sequences(broker, compactible)
603
+ print('Updated %s shard sequences for compaction.' % len(compactible))
604
+ print('Run container-replicator to replicate the changes to other '
605
+ 'nodes.')
606
+ print('Run container-sharder on all nodes to compact shards.')
607
+ return EXIT_SUCCESS
608
+
609
+
610
+ def _remove_illegal_overlapping_donors(
611
+ acceptor_path, overlapping_donors, args):
612
+ # Check parent-children relationship in overlaps between acceptors and
613
+ # donors, remove any overlapping parent or child shard range from donors.
614
+ # Note: we can use set() here, since shard range object is hashed by
615
+ # id and all shard ranges in overlapping_donors are unique already.
616
+ parent_child_donors = set()
617
+ for acceptor in acceptor_path:
618
+ parent_child_donors.update(
619
+ [donor for donor in overlapping_donors
620
+ if acceptor.is_child_of(donor) or donor.is_child_of(acceptor)])
621
+ if parent_child_donors:
622
+ overlapping_donors = ShardRangeList(
623
+ [sr for sr in overlapping_donors
624
+ if sr not in parent_child_donors])
625
+ print('%d donor shards ignored due to parent-child relationship '
626
+ 'checks' % len(parent_child_donors))
627
+
628
+ # Check minimum age requirement in overlaps between acceptors and donors.
629
+ if args.min_shard_age == 0:
630
+ return acceptor_path, overlapping_donors
631
+ ts_now = Timestamp.now()
632
+ # Remove overlapping donor shard ranges who were created recently within
633
+ # 'min_shard_age' age limit.
634
+ qualified_donors = ShardRangeList(
635
+ [sr for sr in overlapping_donors
636
+ if float(sr.timestamp) + args.min_shard_age < float(ts_now)])
637
+ young_donors = len(overlapping_donors) - len(qualified_donors)
638
+ if young_donors > 0:
639
+ print('%d overlapping donor shards ignored due to minimum age '
640
+ 'limit' % young_donors)
641
+ if not qualified_donors:
642
+ return acceptor_path, None
643
+ # Remove those overlapping donors whose overlapping acceptors were created
644
+ # within age limit.
645
+ donors_with_young_overlap_acceptor = set()
646
+ for acceptor_sr in acceptor_path:
647
+ if float(acceptor_sr.timestamp) + args.min_shard_age < float(ts_now):
648
+ continue
649
+ donors_with_young_overlap_acceptor.update(
650
+ [sr for sr in qualified_donors if acceptor_sr.overlaps(sr)])
651
+ if donors_with_young_overlap_acceptor:
652
+ qualified_donors = ShardRangeList(
653
+ [sr for sr in qualified_donors
654
+ if sr not in donors_with_young_overlap_acceptor])
655
+ print('%d donor shards ignored due to existence of overlapping young '
656
+ 'acceptors' % len(donors_with_young_overlap_acceptor))
657
+
658
+ return acceptor_path, qualified_donors
659
+
660
+
661
+ def _find_overlapping_donors(shard_ranges, own_sr, args):
662
+ shard_ranges = ShardRangeList(shard_ranges)
663
+ if ShardRange.SHARDING in shard_ranges.states:
664
+ # This may be over-cautious, but for now we'll avoid dealing with
665
+ # SHARDING shards (which by design will temporarily overlap with their
666
+ # sub-shards) and require repair to be re-tried once sharding has
667
+ # completed. Note that once a shard ranges moves from SHARDING to
668
+ # SHARDED state and is deleted, some replicas of the shard may still be
669
+ # in the process of sharding but we cannot detect that at the root.
670
+ raise InvalidStateException('Found shard ranges in sharding state')
671
+ if ShardRange.SHRINKING in shard_ranges.states:
672
+ # Also stop now if there are SHRINKING shard ranges: we would need to
673
+ # ensure that these were not chosen as acceptors, but for now it is
674
+ # simpler to require repair to be re-tried once shrinking has
675
+ # completes.
676
+ raise InvalidStateException('Found shard ranges in shrinking state')
677
+
678
+ paths = find_paths(shard_ranges)
679
+ ranked_paths = rank_paths(paths, own_sr)
680
+ if not (ranked_paths and ranked_paths[0].includes(own_sr)):
681
+ # individual paths do not have gaps within them; if no path spans the
682
+ # entire namespace then there must be a gap in the shard_ranges
683
+ raise GapsFoundException
684
+
685
+ # simple repair strategy: choose the highest ranked complete sequence and
686
+ # shrink all other shard ranges into it
687
+ acceptor_path = ranked_paths[0]
688
+ acceptor_names = set(sr.name for sr in acceptor_path)
689
+ overlapping_donors = ShardRangeList([sr for sr in shard_ranges
690
+ if sr.name not in acceptor_names])
691
+
692
+ # check that the solution makes sense: if the acceptor path has the most
693
+ # progressed continuous cleaving, which has reached cleaved_upper, then we
694
+ # don't expect any shard ranges beyond cleaved_upper to be in states
695
+ # CLEAVED or ACTIVE, otherwise there should have been a better acceptor
696
+ # path that reached them.
697
+ cleaved_states = {ShardRange.CLEAVED, ShardRange.ACTIVE}
698
+ cleaved_upper = acceptor_path.find_lower(
699
+ lambda sr: sr.state not in cleaved_states)
700
+ beyond_cleaved = acceptor_path.filter(marker=cleaved_upper)
701
+ if beyond_cleaved.states.intersection(cleaved_states):
702
+ raise InvalidSolutionException(
703
+ 'Isolated cleaved and/or active shard ranges in acceptor path',
704
+ acceptor_path, overlapping_donors)
705
+ beyond_cleaved = overlapping_donors.filter(marker=cleaved_upper)
706
+ if beyond_cleaved.states.intersection(cleaved_states):
707
+ raise InvalidSolutionException(
708
+ 'Isolated cleaved and/or active shard ranges in donor ranges',
709
+ acceptor_path, overlapping_donors)
710
+
711
+ return _remove_illegal_overlapping_donors(
712
+ acceptor_path, overlapping_donors, args)
713
+
714
+
715
+ def _fix_gaps(broker, args, paths_with_gaps):
716
+ timestamp = Timestamp.now()
717
+ solutions = []
718
+ print('Found %d gaps:' % len(paths_with_gaps))
719
+ for start_path, gap_range, end_path in paths_with_gaps:
720
+ if end_path[0].state == ShardRange.ACTIVE:
721
+ expanding_range = end_path[0]
722
+ solutions.append((gap_range, expanding_range))
723
+ elif start_path[-1].state == ShardRange.ACTIVE:
724
+ expanding_range = start_path[-1]
725
+ solutions.append((gap_range, expanding_range))
726
+ else:
727
+ expanding_range = None
728
+ print(' gap: %r - %r'
729
+ % (gap_range.lower, gap_range.upper))
730
+ print(' apparent gap contents:')
731
+ for sr in broker.get_shard_ranges(marker=gap_range.lower,
732
+ end_marker=gap_range.upper,
733
+ include_deleted=True):
734
+ _print_shard_range(sr, 3)
735
+ if expanding_range:
736
+ print(' gap can be fixed by expanding neighbor range:')
737
+ _print_shard_range(expanding_range, 3)
738
+ else:
739
+ print('Warning: cannot fix gap: non-ACTIVE neighbors')
740
+
741
+ if args.max_expanding >= 0:
742
+ solutions = solutions[:args.max_expanding]
743
+
744
+ # it's possible that an expanding range is used twice, expanding both down
745
+ # and up; if so, we only want one copy of it in our merged shard ranges
746
+ expanding_ranges = {}
747
+ for gap_range, expanding_range in solutions:
748
+ expanding_range.expand([gap_range])
749
+ expanding_range.timestamp = timestamp
750
+ expanding_ranges[expanding_range.name] = expanding_range
751
+
752
+ print('')
753
+ print('Repairs necessary to fill gaps.')
754
+ print('The following expanded shard range(s) will be applied to the DB:')
755
+ for expanding_range in sorted(expanding_ranges.values(),
756
+ key=lambda s: s.lower):
757
+ _print_shard_range(expanding_range, 2)
758
+ print('')
759
+ print(
760
+ 'It is recommended that no other concurrent changes are made to the \n'
761
+ 'shard ranges while fixing gaps. If necessary, abort this change \n'
762
+ 'and stop any auto-sharding processes before repeating this command.'
763
+ )
764
+ print('')
765
+
766
+ if not _proceed(args):
767
+ return EXIT_USER_QUIT
768
+
769
+ broker.merge_shard_ranges(list(expanding_ranges.values()))
770
+ print('Run container-replicator to replicate the changes to other nodes.')
771
+ print('Run container-sharder on all nodes to fill gaps.')
772
+ return EXIT_SUCCESS
773
+
774
+
775
+ def repair_gaps(broker, args):
776
+ shard_ranges = broker.get_shard_ranges()
777
+ paths_with_gaps = find_paths_with_gaps(shard_ranges)
778
+ if paths_with_gaps:
779
+ return _fix_gaps(broker, args, paths_with_gaps)
780
+ else:
781
+ print('Found one complete sequence of %d shard ranges with no gaps.'
782
+ % len(shard_ranges))
783
+ print('No repairs necessary.')
784
+ return EXIT_SUCCESS
785
+
786
+
787
+ def print_repair_solution(acceptor_path, overlapping_donors):
788
+ print('Donors:')
789
+ for donor in sorted(overlapping_donors):
790
+ _print_shard_range(donor, level=1)
791
+ print('Acceptors:')
792
+ for acceptor in acceptor_path:
793
+ _print_shard_range(acceptor, level=1)
794
+
795
+
796
+ def find_repair_solution(shard_ranges, own_sr, args):
797
+ try:
798
+ acceptor_path, overlapping_donors = _find_overlapping_donors(
799
+ shard_ranges, own_sr, args)
800
+ except GapsFoundException:
801
+ print('Found no complete sequence of shard ranges.')
802
+ print('Repairs necessary to fill gaps.')
803
+ print('Gap filling not supported by this tool. No repairs performed.')
804
+ raise
805
+ except InvalidStateException as exc:
806
+ print('WARNING: %s' % exc)
807
+ print('No repairs performed.')
808
+ raise
809
+ except InvalidSolutionException as exc:
810
+ print('ERROR: %s' % exc)
811
+ print_repair_solution(exc.acceptor_path, exc.overlapping_donors)
812
+ print('No repairs performed.')
813
+ raise
814
+
815
+ if not overlapping_donors:
816
+ print('Found one complete sequence of %d shard ranges and no '
817
+ 'overlapping shard ranges.' % len(acceptor_path))
818
+ print('No repairs necessary.')
819
+ return None, None
820
+
821
+ print('Repairs necessary to remove overlapping shard ranges.')
822
+ print('Chosen a complete sequence of %d shard ranges with current total '
823
+ 'of %d object records to accept object records from %d overlapping '
824
+ 'donor shard ranges.' %
825
+ (len(acceptor_path), acceptor_path.object_count,
826
+ len(overlapping_donors)))
827
+ if args.verbose:
828
+ print_repair_solution(acceptor_path, overlapping_donors)
829
+
830
+ print('Once applied to the broker these changes will result in:')
831
+ print(' %d shard ranges being removed.' % len(overlapping_donors))
832
+ print(' %d object records being moved to the chosen shard ranges.'
833
+ % overlapping_donors.object_count)
834
+
835
+ return acceptor_path, overlapping_donors
836
+
837
+
838
+ def repair_overlaps(broker, args):
839
+ shard_ranges = broker.get_shard_ranges()
840
+ if not shard_ranges:
841
+ print('No shards found, nothing to do.')
842
+ return EXIT_SUCCESS
843
+
844
+ own_sr = broker.get_own_shard_range()
845
+ try:
846
+ acceptor_path, overlapping_donors = find_repair_solution(
847
+ shard_ranges, own_sr, args)
848
+ except ManageShardRangesException:
849
+ return EXIT_ERROR
850
+
851
+ if not acceptor_path:
852
+ return EXIT_SUCCESS
853
+
854
+ if not _proceed(args):
855
+ return EXIT_USER_QUIT
856
+
857
+ # merge changes to the broker...
858
+ # note: acceptors do not need to be modified since they already span the
859
+ # complete range
860
+ ts_now = Timestamp.now()
861
+ finalize_shrinking(broker, [], overlapping_donors, ts_now)
862
+ print('Updated %s donor shard ranges.' % len(overlapping_donors))
863
+ print('Run container-replicator to replicate the changes to other nodes.')
864
+ print('Run container-sharder on all nodes to repair shards.')
865
+ return EXIT_SUCCESS
866
+
867
+
868
+ def repair_shard_ranges(broker, args):
869
+ if not broker.is_root_container():
870
+ print('WARNING: Shard containers cannot be repaired.')
871
+ print('This command should be used on a root container.')
872
+ return EXIT_ERROR
873
+ if args.gaps:
874
+ return repair_gaps(broker, args)
875
+ else:
876
+ return repair_overlaps(broker, args)
877
+
878
+
879
+ def analyze_shard_ranges(args):
880
+ shard_data = _load_and_validate_shard_data(args, require_index=False)
881
+ for data in shard_data:
882
+ # allow for incomplete shard range data that may have been scraped from
883
+ # swift-container-info output
884
+ data.setdefault('epoch', None)
885
+ shard_ranges = [ShardRange.from_dict(data) for data in shard_data]
886
+ whole_sr = ShardRange('whole/namespace', 0)
887
+ try:
888
+ find_repair_solution(shard_ranges, whole_sr, args)
889
+ except ManageShardRangesException:
890
+ return EXIT_ERROR
891
+ return EXIT_SUCCESS
408
892
 
409
893
 
410
894
  def _add_find_args(parser):
411
- parser.add_argument('rows_per_shard', nargs='?', type=int, default=500000)
895
+ parser.add_argument(
896
+ 'rows_per_shard', nargs='?', type=int, default=USE_SHARDER_DEFAULT,
897
+ help='Target number of rows for newly created shards. '
898
+ 'Default is half of the shard_container_threshold value if that is '
899
+ 'given in a conf file specified with --config, otherwise %s.'
900
+ % DEFAULT_SHARDER_CONF['rows_per_shard'])
901
+ parser.add_argument(
902
+ '--minimum-shard-size',
903
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
904
+ default=USE_SHARDER_DEFAULT,
905
+ help='Minimum size of the final shard range. If this is greater than '
906
+ 'one then the final shard range may be extended to more than '
907
+ 'rows_per_shard in order to avoid a further shard range with less '
908
+ 'than minimum-shard-size rows.')
412
909
 
413
910
 
414
- def _add_replace_args(parser):
911
+ def _add_account_prefix_arg(parser):
415
912
  parser.add_argument(
416
913
  '--shards_account_prefix', metavar='shards_account_prefix', type=str,
417
- required=False, help='Prefix for shards account', default='.shards_')
914
+ required=False, default='.shards_',
915
+ help="Prefix for shards account. The default is '.shards_'. This "
916
+ "should only be changed if the auto_create_account_prefix option "
917
+ "has been similarly changed in swift.conf.")
918
+
919
+
920
+ def _add_replace_args(parser):
921
+ _add_account_prefix_arg(parser)
418
922
  parser.add_argument(
419
923
  '--replace-timeout', type=int, default=600,
420
924
  help='Minimum DB timeout to use when replacing shard ranges.')
@@ -432,11 +936,51 @@ def _add_enable_args(parser):
432
936
  help='DB timeout to use when enabling sharding.')
433
937
 
434
938
 
939
+ def _add_prompt_args(parser):
940
+ group = parser.add_mutually_exclusive_group()
941
+ group.add_argument(
942
+ '--yes', '-y', action='store_true', default=False,
943
+ help='Apply shard range changes to broker without prompting. '
944
+ 'Cannot be used with --dry-run option.')
945
+ group.add_argument(
946
+ '--dry-run', '-n', action='store_true', default=False,
947
+ help='Do not apply any shard range changes to broker. '
948
+ 'Cannot be used with --yes option.')
949
+
950
+
951
+ def _add_max_expanding_arg(parser):
952
+ parser.add_argument(
953
+ '--max-expanding', nargs='?',
954
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
955
+ default=USE_SHARDER_DEFAULT,
956
+ help='Maximum number of shards that should be '
957
+ 'expanded. Defaults to unlimited.')
958
+
959
+
435
960
  def _make_parser():
436
961
  parser = argparse.ArgumentParser(description='Manage shard ranges')
437
- parser.add_argument('container_db')
962
+ parser.add_argument('path_to_file',
963
+ help='Path to a container DB file or, for the analyze '
964
+ 'subcommand, a shard data file.')
965
+ parser.add_argument('--config', dest='conf_file', required=False,
966
+ help='Path to config file with [container-sharder] '
967
+ 'section. The following subcommand options will '
968
+ 'be loaded from a config file if they are not '
969
+ 'given on the command line: '
970
+ 'rows_per_shard, '
971
+ 'max_shrinking, '
972
+ 'max_expanding, '
973
+ 'shrink_threshold, '
974
+ 'expansion_limit')
438
975
  parser.add_argument('--verbose', '-v', action='count', default=0,
439
976
  help='Increase output verbosity')
977
+ # this is useful for probe tests that shard containers with unrealistically
978
+ # low numbers of objects, of which a significant proportion may still be in
979
+ # the pending file
980
+ parser.add_argument(
981
+ '--force-commits', action='store_true', default=False,
982
+ help='Force broker to commit pending object updates before finding '
983
+ 'shard ranges. By default the broker will skip commits.')
440
984
  subparsers = parser.add_subparsers(
441
985
  dest='subcommand', help='Sub-command help', title='Sub-commands')
442
986
 
@@ -463,6 +1007,8 @@ def _make_parser():
463
1007
  show_parser.add_argument(
464
1008
  '--brief', '-b', action='store_true', default=False,
465
1009
  help='Show only shard range bounds in output.')
1010
+ show_parser.add_argument('--includes',
1011
+ help='limit shard ranges to include key')
466
1012
  show_parser.set_defaults(func=show_shard_ranges)
467
1013
 
468
1014
  # info
@@ -470,6 +1016,22 @@ def _make_parser():
470
1016
  'info', help='Print container db info')
471
1017
  info_parser.set_defaults(func=db_info)
472
1018
 
1019
+ # merge
1020
+ merge_parser = subparsers.add_parser(
1021
+ 'merge',
1022
+ help='Merge shard range(s) from file with existing shard ranges. This '
1023
+ 'subcommand should only be used if you are confident that you '
1024
+ 'know what you are doing. Shard ranges should not typically be '
1025
+ 'modified in this way.')
1026
+ merge_parser.add_argument('input', metavar='input_file',
1027
+ type=str, help='Name of file')
1028
+ merge_parser.add_argument(
1029
+ '--replace-timeout', type=int, default=600,
1030
+ help='Minimum DB timeout to use when merging shard ranges.')
1031
+ _add_account_prefix_arg(merge_parser)
1032
+ _add_prompt_args(merge_parser)
1033
+ merge_parser.set_defaults(func=merge_shard_ranges)
1034
+
473
1035
  # replace
474
1036
  replace_parser = subparsers.add_parser(
475
1037
  'replace',
@@ -497,12 +1059,89 @@ def _make_parser():
497
1059
  _add_enable_args(enable_parser)
498
1060
  enable_parser.set_defaults(func=enable_sharding)
499
1061
  _add_replace_args(enable_parser)
1062
+
1063
+ # compact
1064
+ compact_parser = subparsers.add_parser(
1065
+ 'compact',
1066
+ help='Compact shard ranges with less than the shrink-threshold number '
1067
+ 'of rows. This command only works on root containers.')
1068
+ _add_prompt_args(compact_parser)
1069
+ compact_parser.add_argument(
1070
+ '--shrink-threshold', nargs='?',
1071
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1072
+ default=USE_SHARDER_DEFAULT,
1073
+ help='The number of rows below which a shard can qualify for '
1074
+ 'shrinking. '
1075
+ 'Defaults to %d' % DEFAULT_SHARDER_CONF['shrink_threshold'])
1076
+ compact_parser.add_argument(
1077
+ '--expansion-limit', nargs='?',
1078
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1079
+ default=USE_SHARDER_DEFAULT,
1080
+ help='Maximum number of rows for an expanding shard to have after '
1081
+ 'compaction has completed. '
1082
+ 'Defaults to %d' % DEFAULT_SHARDER_CONF['expansion_limit'])
1083
+ # If just one donor shard is chosen to shrink to an acceptor then the
1084
+ # expanded acceptor will handle object listings as soon as the donor shard
1085
+ # has shrunk. If more than one donor shard are chosen to shrink to an
1086
+ # acceptor then the acceptor may not handle object listings for some donor
1087
+ # shards that have shrunk until *all* donors have shrunk, resulting in
1088
+ # temporary gap(s) in object listings where the shrunk donors are missing.
1089
+ compact_parser.add_argument(
1090
+ '--max-shrinking', nargs='?',
1091
+ type=wrap_for_argparse(config_positive_int_value, 'must be > 0'),
1092
+ default=USE_SHARDER_DEFAULT,
1093
+ help='Maximum number of shards that should be '
1094
+ 'shrunk into each expanding shard. '
1095
+ 'Defaults to 1. Using values greater '
1096
+ 'than 1 may result in temporary gaps in '
1097
+ 'object listings until all selected '
1098
+ 'shards have shrunk.')
1099
+ _add_max_expanding_arg(compact_parser)
1100
+ compact_parser.set_defaults(func=compact_shard_ranges)
1101
+
1102
+ # repair
1103
+ repair_parser = subparsers.add_parser(
1104
+ 'repair',
1105
+ help='Repair overlapping shard ranges. No action will be taken '
1106
+ 'without user confirmation unless the -y option is used.')
1107
+ _add_prompt_args(repair_parser)
1108
+ repair_parser.add_argument(
1109
+ '--min-shard-age', nargs='?',
1110
+ type=wrap_for_argparse(non_negative_int, 'must be >= 0'),
1111
+ default=MIN_SHARD_RANGE_AGE_FOR_REPAIR,
1112
+ help='Minimum age of a shard for it to be considered as an overlap '
1113
+ 'that is due for repair. Overlapping shards younger than this '
1114
+ 'age will be ignored. Value of 0 means no recent shards will be '
1115
+ 'ignored. Defaults to %d.' % MIN_SHARD_RANGE_AGE_FOR_REPAIR)
1116
+ # TODO: maybe this should be a separate subcommand given that it needs
1117
+ # some extra options vs repairing overlaps?
1118
+ repair_parser.add_argument(
1119
+ '--gaps', action='store_true', default=False,
1120
+ help='Repair gaps in shard ranges.')
1121
+ _add_max_expanding_arg(repair_parser)
1122
+ repair_parser.set_defaults(func=repair_shard_ranges)
1123
+
1124
+ # analyze
1125
+ analyze_parser = subparsers.add_parser(
1126
+ 'analyze',
1127
+ help='Analyze shard range json data read from file. Use -v to see '
1128
+ 'more detailed analysis.')
1129
+ analyze_parser.add_argument(
1130
+ '--min-shard-age', nargs='?',
1131
+ type=wrap_for_argparse(non_negative_int, 'must be >= 0'),
1132
+ default=0,
1133
+ help='Minimum age of a shard for it to be considered as an overlap '
1134
+ 'that is due for repair. Overlapping shards younger than this '
1135
+ 'age will be ignored. Value of 0 means no recent shards will be '
1136
+ 'ignored. Defaults to 0.')
1137
+ analyze_parser.set_defaults(func=analyze_shard_ranges)
1138
+
500
1139
  return parser
501
1140
 
502
1141
 
503
- def main(args=None):
1142
+ def main(cli_args=None):
504
1143
  parser = _make_parser()
505
- args = parser.parse_args(args)
1144
+ args = parser.parse_args(cli_args)
506
1145
  if not args.subcommand:
507
1146
  # On py2, subparsers are required; on py3 they are not; see
508
1147
  # https://bugs.python.org/issue9253. py37 added a `required` kwarg
@@ -510,18 +1149,50 @@ def main(args=None):
510
1149
  # the matter. So, check whether the destination was set and bomb
511
1150
  # out if not.
512
1151
  parser.print_help()
513
- print('\nA sub-command is required.')
514
- return 1
1152
+ print('\nA sub-command is required.', file=sys.stderr)
1153
+ return EXIT_INVALID_ARGS
1154
+
1155
+ try:
1156
+ conf = {}
1157
+ if args.conf_file:
1158
+ conf = readconf(args.conf_file, 'container-sharder')
1159
+ conf.update(dict((k, v) for k, v in vars(args).items()
1160
+ if v != USE_SHARDER_DEFAULT))
1161
+ conf_args = ContainerSharderConf(conf)
1162
+ except (OSError, IOError) as exc:
1163
+ print('Error opening config file %s: %s' % (args.conf_file, exc),
1164
+ file=sys.stderr)
1165
+ return EXIT_ERROR
1166
+ except (TypeError, ValueError) as exc:
1167
+ print('Error loading config: %s' % exc, file=sys.stderr)
1168
+ return EXIT_INVALID_ARGS
1169
+
1170
+ for k, v in vars(args).items():
1171
+ # set any un-set cli args from conf_args
1172
+ if v is USE_SHARDER_DEFAULT:
1173
+ setattr(args, k, getattr(conf_args, k))
1174
+
1175
+ try:
1176
+ ContainerSharderConf.validate_conf(args)
1177
+ except ValueError as err:
1178
+ print('Invalid config: %s' % err, file=sys.stderr)
1179
+ return EXIT_INVALID_ARGS
1180
+
1181
+ if args.func in (analyze_shard_ranges,):
1182
+ args.input = args.path_to_file
1183
+ return args.func(args) or 0
1184
+
515
1185
  logger = get_logger({}, name='ContainerBroker', log_to_console=True)
516
- broker = ContainerBroker(args.container_db, logger=logger,
517
- skip_commits=True)
1186
+ broker = ContainerBroker(os.path.realpath(args.path_to_file),
1187
+ logger=logger,
1188
+ skip_commits=not args.force_commits)
518
1189
  try:
519
1190
  broker.get_info()
520
1191
  except Exception as exc:
521
- print('Error opening container DB %s: %s' % (args.container_db, exc),
1192
+ print('Error opening container DB %s: %s' % (args.path_to_file, exc),
522
1193
  file=sys.stderr)
523
- return 2
524
- print('Loaded db broker for %s.' % broker.path, file=sys.stderr)
1194
+ return EXIT_ERROR
1195
+ print('Loaded db broker for %s' % broker.path, file=sys.stderr)
525
1196
  return args.func(broker, args)
526
1197
 
527
1198