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
swift/obj/expirer.py CHANGED
@@ -13,41 +13,156 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
- import six
16
+ import urllib
17
17
 
18
18
  from random import random
19
19
  from time import time
20
+ from optparse import OptionParser
20
21
  from os.path import join
21
- from swift import gettext_ as _
22
22
  from collections import defaultdict, deque
23
- import hashlib
24
23
 
25
24
  from eventlet import sleep, Timeout
26
25
  from eventlet.greenpool import GreenPool
27
26
 
28
- from swift.common.daemon import Daemon
27
+ from swift.common.constraints import AUTO_CREATE_ACCOUNT_PREFIX
28
+ from swift.common.daemon import Daemon, run_daemon
29
29
  from swift.common.internal_client import InternalClient, UnexpectedResponse
30
+ from swift.common import utils
30
31
  from swift.common.utils import get_logger, dump_recon_cache, split_path, \
31
- Timestamp, config_true_value, normalize_delete_at_timestamp
32
+ Timestamp, config_true_value, normalize_delete_at_timestamp, \
33
+ RateLimitedIterator, md5, non_negative_float, non_negative_int, \
34
+ parse_content_type, parse_options, config_positive_int_value
32
35
  from swift.common.http import HTTP_NOT_FOUND, HTTP_CONFLICT, \
33
36
  HTTP_PRECONDITION_FAILED
34
- from swift.common.swob import wsgi_quote, str_to_wsgi
37
+ from swift.common.recon import RECON_OBJECT_FILE, DEFAULT_RECON_CACHE_PATH
35
38
 
36
39
  from swift.container.reconciler import direct_delete_container_entry
37
40
 
38
41
  MAX_OBJECTS_TO_CACHE = 100000
42
+ X_DELETE_TYPE = 'text/plain'
39
43
  ASYNC_DELETE_TYPE = 'application/async-deleted'
40
44
 
45
+ # expiring_objects_account_name used to be a supported configuration across
46
+ # proxy/expirer configs, but AUTO_CREATE_ACCOUNT_PREFIX is configured in
47
+ # swift.conf constraints; neither should be changed
48
+ EXPIRER_ACCOUNT_NAME = AUTO_CREATE_ACCOUNT_PREFIX + 'expiring_objects'
49
+ # Most clusters use the default "expiring_objects_container_divisor" of 86400
50
+ EXPIRER_CONTAINER_DIVISOR = 86400
51
+ EXPIRER_CONTAINER_PER_DIVISOR = 100
52
+
53
+
54
+ class ExpirerConfig(object):
55
+
56
+ def __init__(self, conf, container_ring=None, logger=None):
57
+ """
58
+ Read the configurable object-expirer values consistently and issue
59
+ warnings appropriately when we encounter deprecated options.
60
+
61
+ This class is used in multiple contexts on proxy and object servers.
62
+
63
+ :param conf: a config dictionary
64
+ :param container_ring: optional, required in proxy context to lookup
65
+ task container (part, nodes)
66
+ :param logger: optional, will create one from the conf if not given
67
+ """
68
+ logger = logger or get_logger(conf)
69
+ if 'expiring_objects_container_divisor' in conf:
70
+ logger.warning(
71
+ 'expiring_objects_container_divisor is deprecated')
72
+ expirer_divisor = config_positive_int_value(
73
+ conf['expiring_objects_container_divisor'])
74
+ else:
75
+ expirer_divisor = EXPIRER_CONTAINER_DIVISOR
76
+
77
+ if 'expiring_objects_account_name' in conf:
78
+ logger.warning(
79
+ 'expiring_objects_account_name is deprecated; you need '
80
+ 'to migrate to the standard .expiring_objects account')
81
+ account_name = (AUTO_CREATE_ACCOUNT_PREFIX +
82
+ conf['expiring_objects_account_name'])
83
+ else:
84
+ account_name = EXPIRER_ACCOUNT_NAME
85
+ self.account_name = account_name
86
+ self.expirer_divisor = expirer_divisor
87
+ self.task_container_per_day = EXPIRER_CONTAINER_PER_DIVISOR
88
+ if self.task_container_per_day >= self.expirer_divisor:
89
+ msg = 'expiring_objects_container_divisor MUST be greater than 100'
90
+ if self.expirer_divisor != 86400:
91
+ msg += '; expiring_objects_container_divisor (%s) SHOULD be ' \
92
+ 'default value of %d' \
93
+ % (self.expirer_divisor, EXPIRER_CONTAINER_DIVISOR)
94
+ raise ValueError(msg)
95
+ self.container_ring = container_ring
96
+
97
+ def get_expirer_container(self, x_delete_at, acc, cont, obj):
98
+ """
99
+ Returns an expiring object task container name for given X-Delete-At
100
+ and (native string) a/c/o.
101
+ """
102
+ # offset backwards from the expected day is a hash of size "per day"
103
+ shard_int = (int(utils.hash_path(acc, cont, obj), 16) %
104
+ self.task_container_per_day)
105
+ # even though the attr is named "task_container_per_day" it's actually
106
+ # "task_container_per_divisor" if for some reason the deprecated config
107
+ # "expirer_divisor" option doesn't have the default value of 86400
108
+ return normalize_delete_at_timestamp(
109
+ int(x_delete_at) // self.expirer_divisor *
110
+ self.expirer_divisor - shard_int)
111
+
112
+ def get_expirer_account_and_container(self, x_delete_at, acc, cont, obj):
113
+ """
114
+ Calculates the expected expirer account and container for the target
115
+ given the current configuration.
116
+
117
+ :returns: a tuple, (account_name, task_container)
118
+ """
119
+ task_container = self.get_expirer_container(
120
+ x_delete_at, acc, cont, obj)
121
+ return self.account_name, task_container
122
+
123
+ def is_expected_task_container(self, task_container_int):
124
+ """
125
+ Validate the task_container timestamp as an expected value given the
126
+ current configuration. Changing the expirer configuration will lead to
127
+ orphaned x-delete-at task objects on overwrite, which may stick around
128
+ a whole reclaim age.
129
+
130
+ :params task_container_int: an int, all task_containers are expected
131
+ to be integer timestamps
132
+
133
+ :returns: a boolean, True if name fits with the given config
134
+ """
135
+ # calculate seconds offset into previous divisor window
136
+ r = (task_container_int - 1) % self.expirer_divisor
137
+ # seconds offset should be no more than task_container_per_day i.e.
138
+ # given % 86400, r==86359 is ok (because 41 is less than 100), but
139
+ # 49768 would be unexpected
140
+ return self.expirer_divisor - r <= self.task_container_per_day
141
+
142
+ def get_delete_at_nodes(self, x_delete_at, acc, cont, obj):
143
+ """
144
+ Get the task_container part, nodes, and name.
145
+
146
+ :returns: a tuple, (part, nodes, task_container_name)
147
+ """
148
+ if not self.container_ring:
149
+ raise RuntimeError('%s was not created with container_ring' % self)
150
+ account_name, task_container = self.get_expirer_account_and_container(
151
+ x_delete_at, acc, cont, obj)
152
+ part, nodes = self.container_ring.get_nodes(
153
+ account_name, task_container)
154
+ return part, nodes, task_container
155
+
41
156
 
42
157
  def build_task_obj(timestamp, target_account, target_container,
43
- target_obj):
158
+ target_obj, high_precision=False):
44
159
  """
45
160
  :return: a task object name in format of
46
161
  "<timestamp>-<target_account>/<target_container>/<target_obj>"
47
162
  """
48
163
  timestamp = Timestamp(timestamp)
49
164
  return '%s-%s/%s/%s' % (
50
- normalize_delete_at_timestamp(timestamp),
165
+ normalize_delete_at_timestamp(timestamp, high_precision),
51
166
  target_account, target_container, target_obj)
52
167
 
53
168
 
@@ -66,6 +181,80 @@ def parse_task_obj(task_obj):
66
181
  return timestamp, target_account, target_container, target_obj
67
182
 
68
183
 
184
+ def extract_expirer_bytes_from_ctype(content_type):
185
+ """
186
+ Parse a content-type and return the number of bytes.
187
+
188
+ :param content_type: a content-type string
189
+ :return: int or None
190
+ """
191
+ content_type, params = parse_content_type(content_type)
192
+ bytes_size = None
193
+ for k, v in params:
194
+ if k == 'swift_expirer_bytes':
195
+ bytes_size = int(v)
196
+ return bytes_size
197
+
198
+
199
+ def embed_expirer_bytes_in_ctype(content_type, metadata):
200
+ """
201
+ Embed number of bytes into content-type. The bytes should come from
202
+ content-length on regular objects, but future extensions to "bytes in
203
+ expirer queue" monitoring may want to more closely consider expiration of
204
+ large multipart object manifests.
205
+
206
+ :param content_type: a content-type string
207
+ :param metadata: a dict, from Diskfile metadata
208
+ :return: str
209
+ """
210
+ # as best I can tell this key is required by df.open
211
+ report_bytes = metadata['Content-Length']
212
+ return "%s;swift_expirer_bytes=%d" % (content_type, int(report_bytes))
213
+
214
+
215
+ def read_conf_for_delay_reaping_times(conf):
216
+ delay_reaping_times = {}
217
+ for conf_key in conf:
218
+ delay_reaping_prefix = "delay_reaping_"
219
+ if not conf_key.startswith(delay_reaping_prefix):
220
+ continue
221
+ delay_reaping_key = urllib.parse.unquote(
222
+ conf_key[len(delay_reaping_prefix):])
223
+ if delay_reaping_key.strip('/') != delay_reaping_key:
224
+ raise ValueError(
225
+ '%s '
226
+ 'should be in the form delay_reaping_<account> '
227
+ 'or delay_reaping_<account>/<container> '
228
+ '(leading or trailing "/" is not allowed)' % conf_key)
229
+ try:
230
+ # If split_path fails, have multiple '/' or
231
+ # account name is invalid
232
+ account, container = split_path(
233
+ '/' + delay_reaping_key, 1, 2
234
+ )
235
+ except ValueError:
236
+ raise ValueError(
237
+ '%s '
238
+ 'should be in the form delay_reaping_<account> '
239
+ 'or delay_reaping_<account>/<container> '
240
+ '(at most one "/" is allowed)' % conf_key)
241
+ try:
242
+ delay_reaping_times[(account, container)] = non_negative_float(
243
+ conf.get(conf_key)
244
+ )
245
+ except ValueError:
246
+ raise ValueError(
247
+ '%s must be a float '
248
+ 'greater than or equal to 0' % conf_key)
249
+ return delay_reaping_times
250
+
251
+
252
+ def get_delay_reaping(delay_reaping_times, target_account, target_container):
253
+ return delay_reaping_times.get(
254
+ (target_account, target_container),
255
+ delay_reaping_times.get((target_account, None), 0.0))
256
+
257
+
69
258
  class ObjectExpirer(Daemon):
70
259
  """
71
260
  Daemon that queries the internal hidden task accounts to discover objects
@@ -73,11 +262,14 @@ class ObjectExpirer(Daemon):
73
262
 
74
263
  :param conf: The daemon configuration.
75
264
  """
265
+ log_route = 'object-expirer'
76
266
 
77
267
  def __init__(self, conf, logger=None, swift=None):
78
268
  self.conf = conf
79
- self.logger = logger or get_logger(conf, log_route='object-expirer')
80
- self.interval = int(conf.get('interval') or 300)
269
+ self.logger = logger or get_logger(conf, log_route=self.log_route)
270
+ self.interval = float(conf.get('interval') or 300)
271
+ self.tasks_per_second = float(conf.get('tasks_per_second', 50.0))
272
+ self.expirer_config = ExpirerConfig(conf, logger=self.logger)
81
273
 
82
274
  self.conf_path = \
83
275
  self.conf.get('__file__') or '/etc/swift/object-expirer.conf'
@@ -87,22 +279,14 @@ class ObjectExpirer(Daemon):
87
279
  self.dequeue_from_legacy = \
88
280
  True if is_legacy_conf else \
89
281
  config_true_value(conf.get('dequeue_from_legacy', 'false'))
90
-
91
- if is_legacy_conf:
92
- self.ic_conf_path = self.conf_path
93
- else:
94
- self.ic_conf_path = \
95
- self.conf.get('internal_client_conf_path') or \
96
- '/etc/swift/internal-client.conf'
97
-
98
- self.read_conf_for_queue_access(swift)
99
-
100
- self.report_interval = int(conf.get('report_interval') or 300)
282
+ self.swift = swift or self._make_internal_client(is_legacy_conf)
283
+ self.read_conf_for_queue_access()
284
+ self.report_interval = float(conf.get('report_interval') or 300)
101
285
  self.report_first_time = self.report_last_time = time()
102
286
  self.report_objects = 0
103
287
  self.recon_cache_path = conf.get('recon_cache_path',
104
- '/var/cache/swift')
105
- self.rcache = join(self.recon_cache_path, 'object.recon')
288
+ DEFAULT_RECON_CACHE_PATH)
289
+ self.rcache = join(self.recon_cache_path, RECON_OBJECT_FILE)
106
290
  self.concurrency = int(conf.get('concurrency', 1))
107
291
  if self.concurrency < 1:
108
292
  raise ValueError("concurrency must be set to at least 1")
@@ -111,21 +295,33 @@ class ObjectExpirer(Daemon):
111
295
  # with the tombstone reclaim age in the consistency engine.
112
296
  self.reclaim_age = int(conf.get('reclaim_age', 604800))
113
297
 
114
- def read_conf_for_queue_access(self, swift):
115
- self.expiring_objects_account = \
116
- (self.conf.get('auto_create_account_prefix') or '.') + \
298
+ self.delay_reaping_times = read_conf_for_delay_reaping_times(conf)
299
+ self.round_robin_task_cache_size = int(
300
+ conf.get('round_robin_task_cache_size', MAX_OBJECTS_TO_CACHE))
301
+
302
+ def _make_internal_client(self, is_legacy_conf):
303
+ default_ic_conf_path = '/etc/swift/internal-client.conf'
304
+ if is_legacy_conf:
305
+ default_ic_conf_path = self.conf_path
306
+ ic_conf_path = self.conf.get(
307
+ 'internal_client_conf_path', default_ic_conf_path)
308
+ request_tries = int(self.conf.get('request_tries') or 3)
309
+ return InternalClient(
310
+ ic_conf_path, 'Swift Object Expirer', request_tries,
311
+ use_replication_network=True,
312
+ global_conf={'log_name': '%s-ic' % self.conf.get(
313
+ 'log_name', self.log_route)})
314
+
315
+ def read_conf_for_queue_access(self):
316
+ self.expiring_objects_account = AUTO_CREATE_ACCOUNT_PREFIX + \
117
317
  (self.conf.get('expiring_objects_account_name') or
118
318
  'expiring_objects')
119
319
 
120
320
  # This is for common parameter with general task queue in future
121
321
  self.task_container_prefix = ''
122
-
123
- request_tries = int(self.conf.get('request_tries') or 3)
124
- self.swift = swift or InternalClient(
125
- self.ic_conf_path, 'Swift Object Expirer', request_tries)
126
-
127
- self.processes = int(self.conf.get('processes', 0))
128
- self.process = int(self.conf.get('process', 0))
322
+ self.processes = non_negative_int(self.conf.get('processes', 0))
323
+ self.process = non_negative_int(self.conf.get('process', 0))
324
+ self._validate_processes_config()
129
325
 
130
326
  def report(self, final=False):
131
327
  """
@@ -137,17 +333,17 @@ class ObjectExpirer(Daemon):
137
333
  """
138
334
  if final:
139
335
  elapsed = time() - self.report_first_time
140
- self.logger.info(_('Pass completed in %(time)ds; '
141
- '%(objects)d objects expired') % {
142
- 'time': elapsed, 'objects': self.report_objects})
336
+ self.logger.info(
337
+ 'Pass completed in %(time)ds; %(objects)d objects expired', {
338
+ 'time': elapsed, 'objects': self.report_objects})
143
339
  dump_recon_cache({'object_expiration_pass': elapsed,
144
340
  'expired_last_pass': self.report_objects},
145
341
  self.rcache, self.logger)
146
342
  elif time() - self.report_last_time >= self.report_interval:
147
343
  elapsed = time() - self.report_first_time
148
- self.logger.info(_('Pass so far %(time)ds; '
149
- '%(objects)d objects expired') % {
150
- 'time': elapsed, 'objects': self.report_objects})
344
+ self.logger.info(
345
+ 'Pass so far %(time)ds; %(objects)d objects expired', {
346
+ 'time': elapsed, 'objects': self.report_objects})
151
347
  self.report_last_time = time()
152
348
 
153
349
  def parse_task_obj(self, task_obj):
@@ -186,7 +382,7 @@ class ObjectExpirer(Daemon):
186
382
  obj_cache[cache_key].append(delete_task)
187
383
  cnt += 1
188
384
 
189
- if cnt > MAX_OBJECTS_TO_CACHE:
385
+ if cnt > self.round_robin_task_cache_size:
190
386
  for task in dump_obj_cache_in_round_robin():
191
387
  yield task
192
388
  cnt = 0
@@ -203,7 +399,8 @@ class ObjectExpirer(Daemon):
203
399
  if not isinstance(name, bytes):
204
400
  name = name.encode('utf8')
205
401
  # md5 is only used for shuffling mod
206
- return int(hashlib.md5(name).hexdigest(), 16) % divisor
402
+ return int(md5(
403
+ name, usedforsecurity=False).hexdigest(), 16) % divisor
207
404
 
208
405
  def iter_task_accounts_to_expire(self):
209
406
  """
@@ -217,29 +414,112 @@ class ObjectExpirer(Daemon):
217
414
  only one expirer.
218
415
  """
219
416
  if self.processes > 0:
220
- yield self.expiring_objects_account, self.process, self.processes
417
+ yield (self.expirer_config.account_name,
418
+ self.process, self.processes)
221
419
  else:
222
- yield self.expiring_objects_account, 0, 1
223
-
224
- def delete_at_time_of_task_container(self, task_container):
225
- """
226
- get delete_at timestamp from task_container name
227
- """
228
- # task_container name is timestamp
229
- return Timestamp(task_container)
420
+ yield self.expirer_config.account_name, 0, 1
230
421
 
231
- def iter_task_containers_to_expire(self, task_account):
422
+ def get_task_containers_to_expire(self, task_account):
232
423
  """
233
- Yields task_container names under the task_account if the delete at
424
+ Collects task_container names under the task_account if the delete at
234
425
  timestamp of task_container is past.
235
426
  """
427
+ container_list = []
428
+ unexpected_task_containers = {
429
+ 'examples': [],
430
+ 'count': 0,
431
+ }
236
432
  for c in self.swift.iter_containers(task_account,
237
433
  prefix=self.task_container_prefix):
238
- task_container = str(c['name'])
239
- timestamp = self.delete_at_time_of_task_container(task_container)
240
- if timestamp > Timestamp.now():
434
+ try:
435
+ task_container_int = int(Timestamp(c['name']))
436
+ except ValueError:
437
+ self.logger.error('skipping invalid task container: %s/%s',
438
+ task_account, c['name'])
439
+ continue
440
+ if not self.expirer_config.is_expected_task_container(
441
+ task_container_int):
442
+ unexpected_task_containers['count'] += 1
443
+ if unexpected_task_containers['count'] < 5:
444
+ unexpected_task_containers['examples'].append(c['name'])
445
+ if task_container_int > Timestamp.now():
446
+ break
447
+ container_list.append(str(task_container_int))
448
+
449
+ if unexpected_task_containers['count']:
450
+ self.logger.info(
451
+ 'processing %s unexpected task containers (e.g. %s)',
452
+ unexpected_task_containers['count'],
453
+ ' '.join(unexpected_task_containers['examples']))
454
+ return container_list
455
+
456
+ def get_delay_reaping(self, target_account, target_container):
457
+ return get_delay_reaping(self.delay_reaping_times, target_account,
458
+ target_container)
459
+
460
+ def _iter_task_container(self, task_account, task_container,
461
+ my_index, divisor):
462
+ """
463
+ Iterates the input task container, yields a task expire info dict for
464
+ each delete task if it is assigned to this expirer process.
465
+
466
+ :raises UnexpectedResponse: if the task container listing is not
467
+ successful.
468
+ """
469
+ container_empty = True
470
+ for o in self.swift.iter_objects(task_account,
471
+ task_container,
472
+ acceptable_statuses=[2]):
473
+ container_empty = False
474
+ task_object = o['name']
475
+ try:
476
+ delete_timestamp, target_account, target_container, \
477
+ target_object = parse_task_obj(task_object)
478
+ except ValueError:
479
+ self.logger.exception('Unexcepted error handling task %r' %
480
+ task_object)
481
+ self.logger.increment('tasks.parse_errors')
482
+ continue
483
+ is_async = o.get('content_type') == ASYNC_DELETE_TYPE
484
+ delay_reaping = self.get_delay_reaping(target_account,
485
+ target_container)
486
+
487
+ if delete_timestamp > Timestamp.now():
488
+ # we shouldn't yield ANY more objects that can't reach
489
+ # the expiration date yet.
241
490
  break
242
- yield task_container
491
+
492
+ # Only one expirer daemon assigned for each task
493
+ if self.hash_mod('%s/%s' % (task_container, task_object),
494
+ divisor) != my_index:
495
+ self.logger.increment('tasks.skipped')
496
+ continue
497
+
498
+ if delete_timestamp > Timestamp(time() - delay_reaping) \
499
+ and not is_async:
500
+ # we shouldn't yield the object during the delay
501
+ self.logger.increment('tasks.delayed')
502
+ continue
503
+
504
+ self.logger.increment('tasks.assigned')
505
+ yield {'task_account': task_account,
506
+ 'task_container': task_container,
507
+ 'task_object': task_object,
508
+ 'target_path': '/'.join([
509
+ target_account, target_container, target_object]),
510
+ 'delete_timestamp': delete_timestamp,
511
+ 'is_async_delete': is_async}
512
+ if container_empty:
513
+ try:
514
+ self.swift.delete_container(
515
+ task_account, task_container,
516
+ acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT))
517
+ except (Exception, Timeout) as err:
518
+ self.logger.exception(
519
+ 'Exception while deleting container %(account)s '
520
+ '%(container)s %(err)s', {
521
+ 'account': task_account,
522
+ 'container': task_container, 'err': str(err)})
243
523
 
244
524
  def iter_task_to_expire(self, task_account_container_list,
245
525
  my_index, divisor):
@@ -248,36 +528,27 @@ class ObjectExpirer(Daemon):
248
528
  task_container, task_object, timestamp_to_delete, and target_path
249
529
  """
250
530
  for task_account, task_container in task_account_container_list:
251
- for o in self.swift.iter_objects(task_account, task_container):
252
- if six.PY2:
253
- task_object = o['name'].encode('utf8')
254
- else:
255
- task_object = o['name']
256
- try:
257
- delete_timestamp, target_account, target_container, \
258
- target_object = parse_task_obj(task_object)
259
- except ValueError:
260
- self.logger.exception('Unexcepted error handling task %r' %
261
- task_object)
262
- continue
263
- if delete_timestamp > Timestamp.now():
264
- # we shouldn't yield the object that doesn't reach
265
- # the expiration date yet.
266
- break
267
-
268
- # Only one expirer daemon assigned for one task
269
- if self.hash_mod('%s/%s' % (task_container, task_object),
270
- divisor) != my_index:
271
- continue
272
-
273
- is_async = o.get('content_type') == ASYNC_DELETE_TYPE
274
- yield {'task_account': task_account,
275
- 'task_container': task_container,
276
- 'task_object': task_object,
277
- 'target_path': '/'.join([
278
- target_account, target_container, target_object]),
279
- 'delete_timestamp': delete_timestamp,
280
- 'is_async_delete': is_async}
531
+ try:
532
+ for item in self._iter_task_container(
533
+ task_account, task_container, my_index, divisor):
534
+ yield item
535
+ except UnexpectedResponse as err:
536
+ if err.resp.status_int != 404:
537
+ self.logger.error(
538
+ 'Unexpected response while listing objects in '
539
+ 'container %(account)s %(container)s: %(err)s', {
540
+ 'account': task_account,
541
+ 'container': task_container,
542
+ 'err': str(err)
543
+ })
544
+ except (Exception, Timeout) as err:
545
+ self.logger.error(
546
+ 'Exception while listing objects in container %(account)s '
547
+ '%(container)s: %(err)s', {
548
+ 'account': task_account,
549
+ 'container': task_container,
550
+ 'err': str(err)
551
+ })
281
552
 
282
553
  def run_once(self, *args, **kwargs):
283
554
  """
@@ -290,6 +561,9 @@ class ObjectExpirer(Daemon):
290
561
  These will override the values from the config file if
291
562
  provided.
292
563
  """
564
+ # these config values are available to override at the command line,
565
+ # blow-up now if they're wrong
566
+ self.override_proceses_config_from_command_line(**kwargs)
293
567
  # This if-clause will be removed when general task queue feature is
294
568
  # implemented.
295
569
  if not self.dequeue_from_legacy:
@@ -300,13 +574,11 @@ class ObjectExpirer(Daemon):
300
574
  'with dequeue_from_legacy == true.')
301
575
  return
302
576
 
303
- self.get_process_values(kwargs)
304
577
  pool = GreenPool(self.concurrency)
305
578
  self.report_first_time = self.report_last_time = time()
306
579
  self.report_objects = 0
307
580
  try:
308
581
  self.logger.debug('Run begin')
309
- task_account_container_list_to_delete = list()
310
582
  for task_account, my_index, divisor in \
311
583
  self.iter_task_accounts_to_expire():
312
584
  container_count, obj_count = \
@@ -316,20 +588,17 @@ class ObjectExpirer(Daemon):
316
588
  if not container_count:
317
589
  continue
318
590
 
319
- self.logger.info(_(
591
+ self.logger.info(
320
592
  'Pass beginning for task account %(account)s; '
321
593
  '%(container_count)s possible containers; '
322
- '%(obj_count)s possible objects') % {
323
- 'account': task_account,
324
- 'container_count': container_count,
325
- 'obj_count': obj_count})
594
+ '%(obj_count)s possible objects', {
595
+ 'account': task_account,
596
+ 'container_count': container_count,
597
+ 'obj_count': obj_count})
326
598
 
327
599
  task_account_container_list = \
328
600
  [(task_account, task_container) for task_container in
329
- self.iter_task_containers_to_expire(task_account)]
330
-
331
- task_account_container_list_to_delete.extend(
332
- task_account_container_list)
601
+ self.get_task_containers_to_expire(task_account)]
333
602
 
334
603
  # delete_task_iter is a generator to yield a dict of
335
604
  # task_account, task_container, task_object, delete_timestamp,
@@ -338,27 +607,17 @@ class ObjectExpirer(Daemon):
338
607
  delete_task_iter = \
339
608
  self.round_robin_order(self.iter_task_to_expire(
340
609
  task_account_container_list, my_index, divisor))
341
-
342
- for delete_task in delete_task_iter:
610
+ rate_limited_iter = RateLimitedIterator(
611
+ delete_task_iter,
612
+ elements_per_second=self.tasks_per_second)
613
+ for delete_task in rate_limited_iter:
343
614
  pool.spawn_n(self.delete_object, **delete_task)
344
615
 
345
616
  pool.waitall()
346
- for task_account, task_container in \
347
- task_account_container_list_to_delete:
348
- try:
349
- self.swift.delete_container(
350
- task_account, task_container,
351
- acceptable_statuses=(2, HTTP_NOT_FOUND, HTTP_CONFLICT))
352
- except (Exception, Timeout) as err:
353
- self.logger.exception(
354
- _('Exception while deleting container %(account)s '
355
- '%(container)s %(err)s') % {
356
- 'account': task_account,
357
- 'container': task_container, 'err': str(err)})
358
617
  self.logger.debug('Run end')
359
618
  self.report(final=True)
360
619
  except (Exception, Timeout):
361
- self.logger.exception(_('Unhandled exception'))
620
+ self.logger.exception('Unhandled exception')
362
621
 
363
622
  def run_forever(self, *args, **kwargs):
364
623
  """
@@ -369,18 +628,21 @@ class ObjectExpirer(Daemon):
369
628
  :param kwargs: Extra keyword args to fulfill the Daemon interface; this
370
629
  daemon has no additional keyword args.
371
630
  """
631
+ # these config values are available to override at the command line
632
+ # blow-up now if they're wrong
633
+ self.override_proceses_config_from_command_line(**kwargs)
372
634
  sleep(random() * self.interval)
373
635
  while True:
374
636
  begin = time()
375
637
  try:
376
638
  self.run_once(*args, **kwargs)
377
639
  except (Exception, Timeout):
378
- self.logger.exception(_('Unhandled exception'))
640
+ self.logger.exception('Unhandled exception')
379
641
  elapsed = time() - begin
380
642
  if elapsed < self.interval:
381
643
  sleep(random() * (self.interval - elapsed))
382
644
 
383
- def get_process_values(self, kwargs):
645
+ def override_proceses_config_from_command_line(self, **kwargs):
384
646
  """
385
647
  Sets self.processes and self.process from the kwargs if those
386
648
  values exist, otherwise, leaves those values as they were set in
@@ -391,19 +653,20 @@ class ObjectExpirer(Daemon):
391
653
  line when the daemon is run.
392
654
  """
393
655
  if kwargs.get('processes') is not None:
394
- self.processes = int(kwargs['processes'])
656
+ self.processes = non_negative_int(kwargs['processes'])
395
657
 
396
658
  if kwargs.get('process') is not None:
397
- self.process = int(kwargs['process'])
659
+ self.process = non_negative_int(kwargs['process'])
398
660
 
399
- if self.process < 0:
400
- raise ValueError(
401
- 'process must be an integer greater than or equal to 0')
661
+ self._validate_processes_config()
402
662
 
403
- if self.processes < 0:
404
- raise ValueError(
405
- 'processes must be an integer greater than or equal to 0')
663
+ def _validate_processes_config(self):
664
+ """
665
+ Used in constructor and in override_proceses_config_from_command_line
666
+ to validate the processes configuration requirements.
406
667
 
668
+ :raiess: ValueError if processes config is invalid
669
+ """
407
670
  if self.processes and self.process >= self.processes:
408
671
  raise ValueError(
409
672
  'process must be less than processes')
@@ -469,7 +732,6 @@ class ObjectExpirer(Daemon):
469
732
  :raises UnexpectedResponse: if the delete was unsuccessful and
470
733
  should be retried later
471
734
  """
472
- path = '/v1/' + wsgi_quote(str_to_wsgi(actual_obj.lstrip('/')))
473
735
  if is_async_delete:
474
736
  headers = {'X-Timestamp': timestamp.normal}
475
737
  acceptable_statuses = (2, HTTP_CONFLICT, HTTP_NOT_FOUND)
@@ -478,4 +740,24 @@ class ObjectExpirer(Daemon):
478
740
  'X-If-Delete-At': timestamp.normal,
479
741
  'X-Backend-Clean-Expiring-Object-Queue': 'no'}
480
742
  acceptable_statuses = (2, HTTP_CONFLICT)
481
- self.swift.make_request('DELETE', path, headers, acceptable_statuses)
743
+ self.swift.delete_object(*split_path('/' + actual_obj, 3, 3, True),
744
+ headers=headers,
745
+ acceptable_statuses=acceptable_statuses)
746
+
747
+
748
+ def main():
749
+ parser = OptionParser("%prog CONFIG [options]")
750
+ parser.add_option('--processes', dest='processes',
751
+ help="Number of processes to use to do the work, don't "
752
+ "use this option to do all the work in one process")
753
+ parser.add_option('--process', dest='process',
754
+ help="Process number for this process, don't use "
755
+ "this option to do all the work in one process, this "
756
+ "is used to determine which part of the work this "
757
+ "process should do")
758
+ conf_file, options = parse_options(parser=parser, once=True)
759
+ run_daemon(ObjectExpirer, conf_file, **options)
760
+
761
+
762
+ if __name__ == '__main__':
763
+ main()