Merge lp:~ttx/swift/1.4.1-proposed into lp:~hudson-openstack/swift/milestone-proposed
- 1.4.1-proposed
- Merge into milestone-proposed
Proposed by
Thierry Carrez
Status: | Merged |
---|---|
Approved by: | John Dickinson |
Approved revision: | 304 |
Merged at revision: | 304 |
Proposed branch: | lp:~ttx/swift/1.4.1-proposed |
Merge into: | lp:~hudson-openstack/swift/milestone-proposed |
Diff against target: |
11368 lines (+2932/-7158) 46 files modified
CHANGELOG (+16/-0) bin/st (+0/-1812) bin/swauth-add-account (+0/-68) bin/swauth-add-user (+0/-93) bin/swauth-cleanup-tokens (+0/-118) bin/swauth-delete-account (+0/-60) bin/swauth-delete-user (+0/-60) bin/swauth-list (+0/-86) bin/swauth-prep (+0/-59) bin/swauth-set-account-service (+0/-73) bin/swift (+1812/-0) doc/source/admin_guide.rst (+0/-16) doc/source/deployment_guide.rst (+37/-28) doc/source/development_auth.rst (+7/-7) doc/source/development_saio.rst (+12/-20) doc/source/howto_installmultinode.rst (+39/-80) doc/source/misc.rst (+6/-6) doc/source/overview_auth.rst (+8/-151) doc/source/overview_large_objects.rst (+12/-12) doc/source/overview_stats.rst (+6/-5) etc/log-processor.conf-sample (+1/-0) etc/proxy-server.conf-sample (+27/-17) setup.py (+2/-6) swift/__init__.py (+1/-1) swift/account/server.py (+3/-3) swift/common/bench.py (+4/-3) swift/common/daemon.py (+2/-1) swift/common/db.py (+38/-15) swift/common/db_replicator.py (+32/-3) swift/common/middleware/staticweb.py (+8/-8) swift/common/middleware/swauth.py (+0/-1374) swift/common/middleware/tempauth.py (+482/-0) swift/common/utils.py (+1/-1) swift/container/server.py (+2/-2) swift/proxy/server.py (+64/-15) swift/stats/db_stats_collector.py (+32/-7) swift/stats/log_uploader.py (+1/-1) test/functional/tests.py (+4/-4) test/probe/common.py (+19/-21) test/unit/account/test_server.py (+3/-0) test/unit/common/middleware/test_staticweb.py (+6/-6) test/unit/common/middleware/test_tempauth.py (+72/-2905) test/unit/common/test_utils.py (+4/-0) test/unit/container/test_server.py (+3/-0) test/unit/proxy/test_server.py (+75/-3) test/unit/stats/test_db_stats_collector.py (+91/-8) |
To merge this branch: | bzr merge lp:~ttx/swift/1.4.1-proposed |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Swift Core security contacts | Pending | ||
Review via email: mp+64648@code.launchpad.net |
Commit message
Merge 1.4.1 development from trunk (rev312)
Description of the change
Merge 1.4.1 development from trunk into milestone-proposed branch.
This was taken from rev312 in trunk (the last revision before 1.4.2 versioning)
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'CHANGELOG' |
2 | --- CHANGELOG 2011-05-27 13:48:41 +0000 |
3 | +++ CHANGELOG 2011-06-15 09:24:34 +0000 |
4 | @@ -1,3 +1,19 @@ |
5 | +swift (1.4.1) |
6 | + |
7 | + * st renamed to swift |
8 | + |
9 | + * swauth was separated froms swift. It is now its own project and can be |
10 | + found at https://github.com/gholt/swauth. |
11 | + |
12 | + * tempauth middleware added as an extremely limited auth system for dev |
13 | + work. |
14 | + |
15 | + * Account and container listings now properly labeled UTF-8 (previously the |
16 | + label was "utf8"). |
17 | + |
18 | + * Accounts are auto-created if an auth token is valid when the |
19 | + account_autocreate proxy config parameter is set to true. |
20 | + |
21 | swift (1.4.0) |
22 | |
23 | * swift-bench now cleans up containers it creates. |
24 | |
25 | === removed file 'bin/st' |
26 | --- bin/st 2011-05-19 14:48:15 +0000 |
27 | +++ bin/st 1970-01-01 00:00:00 +0000 |
28 | @@ -1,1812 +0,0 @@ |
29 | -#!/usr/bin/python -u |
30 | -# Copyright (c) 2010-2011 OpenStack, LLC. |
31 | -# |
32 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
33 | -# you may not use this file except in compliance with the License. |
34 | -# You may obtain a copy of the License at |
35 | -# |
36 | -# http://www.apache.org/licenses/LICENSE-2.0 |
37 | -# |
38 | -# Unless required by applicable law or agreed to in writing, software |
39 | -# distributed under the License is distributed on an "AS IS" BASIS, |
40 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
41 | -# implied. |
42 | -# See the License for the specific language governing permissions and |
43 | -# limitations under the License. |
44 | - |
45 | -from errno import EEXIST, ENOENT |
46 | -from hashlib import md5 |
47 | -from optparse import OptionParser |
48 | -from os import environ, listdir, makedirs, utime |
49 | -from os.path import basename, dirname, getmtime, getsize, isdir, join |
50 | -from Queue import Empty, Queue |
51 | -from sys import argv, exc_info, exit, stderr, stdout |
52 | -from threading import enumerate as threading_enumerate, Thread |
53 | -from time import sleep |
54 | -from traceback import format_exception |
55 | - |
56 | - |
57 | -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
58 | -# Inclusion of swift.common.client for convenience of single file distribution |
59 | - |
60 | -import socket |
61 | -from cStringIO import StringIO |
62 | -from re import compile, DOTALL |
63 | -from tokenize import generate_tokens, STRING, NAME, OP |
64 | -from urllib import quote as _quote, unquote |
65 | -from urlparse import urlparse, urlunparse |
66 | - |
67 | -try: |
68 | - from eventlet.green.httplib import HTTPException, HTTPSConnection |
69 | -except ImportError: |
70 | - from httplib import HTTPException, HTTPSConnection |
71 | - |
72 | -try: |
73 | - from eventlet import sleep |
74 | -except ImportError: |
75 | - from time import sleep |
76 | - |
77 | -try: |
78 | - from swift.common.bufferedhttp \ |
79 | - import BufferedHTTPConnection as HTTPConnection |
80 | -except ImportError: |
81 | - try: |
82 | - from eventlet.green.httplib import HTTPConnection |
83 | - except ImportError: |
84 | - from httplib import HTTPConnection |
85 | - |
86 | - |
87 | -def quote(value, safe='/'): |
88 | - """ |
89 | - Patched version of urllib.quote that encodes utf8 strings before quoting |
90 | - """ |
91 | - if isinstance(value, unicode): |
92 | - value = value.encode('utf8') |
93 | - return _quote(value, safe) |
94 | - |
95 | - |
96 | -# look for a real json parser first |
97 | -try: |
98 | - # simplejson is popular and pretty good |
99 | - from simplejson import loads as json_loads |
100 | -except ImportError: |
101 | - try: |
102 | - # 2.6 will have a json module in the stdlib |
103 | - from json import loads as json_loads |
104 | - except ImportError: |
105 | - # fall back on local parser otherwise |
106 | - comments = compile(r'/\*.*\*/|//[^\r\n]*', DOTALL) |
107 | - |
108 | - def json_loads(string): |
109 | - ''' |
110 | - Fairly competent json parser exploiting the python tokenizer and |
111 | - eval(). -- From python-cloudfiles |
112 | - |
113 | - _loads(serialized_json) -> object |
114 | - ''' |
115 | - try: |
116 | - res = [] |
117 | - consts = {'true': True, 'false': False, 'null': None} |
118 | - string = '(' + comments.sub('', string) + ')' |
119 | - for type, val, _junk, _junk, _junk in \ |
120 | - generate_tokens(StringIO(string).readline): |
121 | - if (type == OP and val not in '[]{}:,()-') or \ |
122 | - (type == NAME and val not in consts): |
123 | - raise AttributeError() |
124 | - elif type == STRING: |
125 | - res.append('u') |
126 | - res.append(val.replace('\\/', '/')) |
127 | - else: |
128 | - res.append(val) |
129 | - return eval(''.join(res), {}, consts) |
130 | - except Exception: |
131 | - raise AttributeError() |
132 | - |
133 | - |
134 | -class ClientException(Exception): |
135 | - |
136 | - def __init__(self, msg, http_scheme='', http_host='', http_port='', |
137 | - http_path='', http_query='', http_status=0, http_reason='', |
138 | - http_device=''): |
139 | - Exception.__init__(self, msg) |
140 | - self.msg = msg |
141 | - self.http_scheme = http_scheme |
142 | - self.http_host = http_host |
143 | - self.http_port = http_port |
144 | - self.http_path = http_path |
145 | - self.http_query = http_query |
146 | - self.http_status = http_status |
147 | - self.http_reason = http_reason |
148 | - self.http_device = http_device |
149 | - |
150 | - def __str__(self): |
151 | - a = self.msg |
152 | - b = '' |
153 | - if self.http_scheme: |
154 | - b += '%s://' % self.http_scheme |
155 | - if self.http_host: |
156 | - b += self.http_host |
157 | - if self.http_port: |
158 | - b += ':%s' % self.http_port |
159 | - if self.http_path: |
160 | - b += self.http_path |
161 | - if self.http_query: |
162 | - b += '?%s' % self.http_query |
163 | - if self.http_status: |
164 | - if b: |
165 | - b = '%s %s' % (b, self.http_status) |
166 | - else: |
167 | - b = str(self.http_status) |
168 | - if self.http_reason: |
169 | - if b: |
170 | - b = '%s %s' % (b, self.http_reason) |
171 | - else: |
172 | - b = '- %s' % self.http_reason |
173 | - if self.http_device: |
174 | - if b: |
175 | - b = '%s: device %s' % (b, self.http_device) |
176 | - else: |
177 | - b = 'device %s' % self.http_device |
178 | - return b and '%s: %s' % (a, b) or a |
179 | - |
180 | - |
181 | -def http_connection(url): |
182 | - """ |
183 | - Make an HTTPConnection or HTTPSConnection |
184 | - |
185 | - :param url: url to connect to |
186 | - :returns: tuple of (parsed url, connection object) |
187 | - :raises ClientException: Unable to handle protocol scheme |
188 | - """ |
189 | - parsed = urlparse(url) |
190 | - if parsed.scheme == 'http': |
191 | - conn = HTTPConnection(parsed.netloc) |
192 | - elif parsed.scheme == 'https': |
193 | - conn = HTTPSConnection(parsed.netloc) |
194 | - else: |
195 | - raise ClientException('Cannot handle protocol scheme %s for url %s' % |
196 | - (parsed.scheme, repr(url))) |
197 | - return parsed, conn |
198 | - |
199 | - |
200 | -def get_auth(url, user, key, snet=False): |
201 | - """ |
202 | - Get authentication/authorization credentials. |
203 | - |
204 | - The snet parameter is used for Rackspace's ServiceNet internal network |
205 | - implementation. In this function, it simply adds *snet-* to the beginning |
206 | - of the host name for the returned storage URL. With Rackspace Cloud Files, |
207 | - use of this network path causes no bandwidth charges but requires the |
208 | - client to be running on Rackspace's ServiceNet network. |
209 | - |
210 | - :param url: authentication/authorization URL |
211 | - :param user: user to authenticate as |
212 | - :param key: key or password for authorization |
213 | - :param snet: use SERVICENET internal network (see above), default is False |
214 | - :returns: tuple of (storage URL, auth token) |
215 | - :raises ClientException: HTTP GET request to auth URL failed |
216 | - """ |
217 | - parsed, conn = http_connection(url) |
218 | - conn.request('GET', parsed.path, '', |
219 | - {'X-Auth-User': user, 'X-Auth-Key': key}) |
220 | - resp = conn.getresponse() |
221 | - resp.read() |
222 | - if resp.status < 200 or resp.status >= 300: |
223 | - raise ClientException('Auth GET failed', http_scheme=parsed.scheme, |
224 | - http_host=conn.host, http_port=conn.port, |
225 | - http_path=parsed.path, http_status=resp.status, |
226 | - http_reason=resp.reason) |
227 | - url = resp.getheader('x-storage-url') |
228 | - if snet: |
229 | - parsed = list(urlparse(url)) |
230 | - # Second item in the list is the netloc |
231 | - parsed[1] = 'snet-' + parsed[1] |
232 | - url = urlunparse(parsed) |
233 | - return url, resp.getheader('x-storage-token', |
234 | - resp.getheader('x-auth-token')) |
235 | - |
236 | - |
237 | -def get_account(url, token, marker=None, limit=None, prefix=None, |
238 | - http_conn=None, full_listing=False): |
239 | - """ |
240 | - Get a listing of containers for the account. |
241 | - |
242 | - :param url: storage URL |
243 | - :param token: auth token |
244 | - :param marker: marker query |
245 | - :param limit: limit query |
246 | - :param prefix: prefix query |
247 | - :param http_conn: HTTP connection object (If None, it will create the |
248 | - conn object) |
249 | - :param full_listing: if True, return a full listing, else returns a max |
250 | - of 10000 listings |
251 | - :returns: a tuple of (response headers, a list of containers) The response |
252 | - headers will be a dict and all header names will be lowercase. |
253 | - :raises ClientException: HTTP GET request failed |
254 | - """ |
255 | - if not http_conn: |
256 | - http_conn = http_connection(url) |
257 | - if full_listing: |
258 | - rv = get_account(url, token, marker, limit, prefix, http_conn) |
259 | - listing = rv[1] |
260 | - while listing: |
261 | - marker = listing[-1]['name'] |
262 | - listing = \ |
263 | - get_account(url, token, marker, limit, prefix, http_conn)[1] |
264 | - if listing: |
265 | - rv[1].extend(listing) |
266 | - return rv |
267 | - parsed, conn = http_conn |
268 | - qs = 'format=json' |
269 | - if marker: |
270 | - qs += '&marker=%s' % quote(marker) |
271 | - if limit: |
272 | - qs += '&limit=%d' % limit |
273 | - if prefix: |
274 | - qs += '&prefix=%s' % quote(prefix) |
275 | - conn.request('GET', '%s?%s' % (parsed.path, qs), '', |
276 | - {'X-Auth-Token': token}) |
277 | - resp = conn.getresponse() |
278 | - resp_headers = {} |
279 | - for header, value in resp.getheaders(): |
280 | - resp_headers[header.lower()] = value |
281 | - if resp.status < 200 or resp.status >= 300: |
282 | - resp.read() |
283 | - raise ClientException('Account GET failed', http_scheme=parsed.scheme, |
284 | - http_host=conn.host, http_port=conn.port, |
285 | - http_path=parsed.path, http_query=qs, http_status=resp.status, |
286 | - http_reason=resp.reason) |
287 | - if resp.status == 204: |
288 | - resp.read() |
289 | - return resp_headers, [] |
290 | - return resp_headers, json_loads(resp.read()) |
291 | - |
292 | - |
293 | -def head_account(url, token, http_conn=None): |
294 | - """ |
295 | - Get account stats. |
296 | - |
297 | - :param url: storage URL |
298 | - :param token: auth token |
299 | - :param http_conn: HTTP connection object (If None, it will create the |
300 | - conn object) |
301 | - :returns: a dict containing the response's headers (all header names will |
302 | - be lowercase) |
303 | - :raises ClientException: HTTP HEAD request failed |
304 | - """ |
305 | - if http_conn: |
306 | - parsed, conn = http_conn |
307 | - else: |
308 | - parsed, conn = http_connection(url) |
309 | - conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) |
310 | - resp = conn.getresponse() |
311 | - resp.read() |
312 | - if resp.status < 200 or resp.status >= 300: |
313 | - raise ClientException('Account HEAD failed', http_scheme=parsed.scheme, |
314 | - http_host=conn.host, http_port=conn.port, |
315 | - http_path=parsed.path, http_status=resp.status, |
316 | - http_reason=resp.reason) |
317 | - resp_headers = {} |
318 | - for header, value in resp.getheaders(): |
319 | - resp_headers[header.lower()] = value |
320 | - return resp_headers |
321 | - |
322 | - |
323 | -def post_account(url, token, headers, http_conn=None): |
324 | - """ |
325 | - Update an account's metadata. |
326 | - |
327 | - :param url: storage URL |
328 | - :param token: auth token |
329 | - :param headers: additional headers to include in the request |
330 | - :param http_conn: HTTP connection object (If None, it will create the |
331 | - conn object) |
332 | - :raises ClientException: HTTP POST request failed |
333 | - """ |
334 | - if http_conn: |
335 | - parsed, conn = http_conn |
336 | - else: |
337 | - parsed, conn = http_connection(url) |
338 | - headers['X-Auth-Token'] = token |
339 | - conn.request('POST', parsed.path, '', headers) |
340 | - resp = conn.getresponse() |
341 | - resp.read() |
342 | - if resp.status < 200 or resp.status >= 300: |
343 | - raise ClientException('Account POST failed', |
344 | - http_scheme=parsed.scheme, http_host=conn.host, |
345 | - http_port=conn.port, http_path=path, http_status=resp.status, |
346 | - http_reason=resp.reason) |
347 | - |
348 | - |
349 | -def get_container(url, token, container, marker=None, limit=None, |
350 | - prefix=None, delimiter=None, http_conn=None, |
351 | - full_listing=False): |
352 | - """ |
353 | - Get a listing of objects for the container. |
354 | - |
355 | - :param url: storage URL |
356 | - :param token: auth token |
357 | - :param container: container name to get a listing for |
358 | - :param marker: marker query |
359 | - :param limit: limit query |
360 | - :param prefix: prefix query |
361 | - :param delimeter: string to delimit the queries on |
362 | - :param http_conn: HTTP connection object (If None, it will create the |
363 | - conn object) |
364 | - :param full_listing: if True, return a full listing, else returns a max |
365 | - of 10000 listings |
366 | - :returns: a tuple of (response headers, a list of objects) The response |
367 | - headers will be a dict and all header names will be lowercase. |
368 | - :raises ClientException: HTTP GET request failed |
369 | - """ |
370 | - if not http_conn: |
371 | - http_conn = http_connection(url) |
372 | - if full_listing: |
373 | - rv = get_container(url, token, container, marker, limit, prefix, |
374 | - delimiter, http_conn) |
375 | - listing = rv[1] |
376 | - while listing: |
377 | - if not delimiter: |
378 | - marker = listing[-1]['name'] |
379 | - else: |
380 | - marker = listing[-1].get('name', listing[-1].get('subdir')) |
381 | - listing = get_container(url, token, container, marker, limit, |
382 | - prefix, delimiter, http_conn)[1] |
383 | - if listing: |
384 | - rv[1].extend(listing) |
385 | - return rv |
386 | - parsed, conn = http_conn |
387 | - path = '%s/%s' % (parsed.path, quote(container)) |
388 | - qs = 'format=json' |
389 | - if marker: |
390 | - qs += '&marker=%s' % quote(marker) |
391 | - if limit: |
392 | - qs += '&limit=%d' % limit |
393 | - if prefix: |
394 | - qs += '&prefix=%s' % quote(prefix) |
395 | - if delimiter: |
396 | - qs += '&delimiter=%s' % quote(delimiter) |
397 | - conn.request('GET', '%s?%s' % (path, qs), '', {'X-Auth-Token': token}) |
398 | - resp = conn.getresponse() |
399 | - if resp.status < 200 or resp.status >= 300: |
400 | - resp.read() |
401 | - raise ClientException('Container GET failed', |
402 | - http_scheme=parsed.scheme, http_host=conn.host, |
403 | - http_port=conn.port, http_path=path, http_query=qs, |
404 | - http_status=resp.status, http_reason=resp.reason) |
405 | - resp_headers = {} |
406 | - for header, value in resp.getheaders(): |
407 | - resp_headers[header.lower()] = value |
408 | - if resp.status == 204: |
409 | - resp.read() |
410 | - return resp_headers, [] |
411 | - return resp_headers, json_loads(resp.read()) |
412 | - |
413 | - |
414 | -def head_container(url, token, container, http_conn=None): |
415 | - """ |
416 | - Get container stats. |
417 | - |
418 | - :param url: storage URL |
419 | - :param token: auth token |
420 | - :param container: container name to get stats for |
421 | - :param http_conn: HTTP connection object (If None, it will create the |
422 | - conn object) |
423 | - :returns: a dict containing the response's headers (all header names will |
424 | - be lowercase) |
425 | - :raises ClientException: HTTP HEAD request failed |
426 | - """ |
427 | - if http_conn: |
428 | - parsed, conn = http_conn |
429 | - else: |
430 | - parsed, conn = http_connection(url) |
431 | - path = '%s/%s' % (parsed.path, quote(container)) |
432 | - conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
433 | - resp = conn.getresponse() |
434 | - resp.read() |
435 | - if resp.status < 200 or resp.status >= 300: |
436 | - raise ClientException('Container HEAD failed', |
437 | - http_scheme=parsed.scheme, http_host=conn.host, |
438 | - http_port=conn.port, http_path=path, http_status=resp.status, |
439 | - http_reason=resp.reason) |
440 | - resp_headers = {} |
441 | - for header, value in resp.getheaders(): |
442 | - resp_headers[header.lower()] = value |
443 | - return resp_headers |
444 | - |
445 | - |
446 | -def put_container(url, token, container, headers=None, http_conn=None): |
447 | - """ |
448 | - Create a container |
449 | - |
450 | - :param url: storage URL |
451 | - :param token: auth token |
452 | - :param container: container name to create |
453 | - :param headers: additional headers to include in the request |
454 | - :param http_conn: HTTP connection object (If None, it will create the |
455 | - conn object) |
456 | - :raises ClientException: HTTP PUT request failed |
457 | - """ |
458 | - if http_conn: |
459 | - parsed, conn = http_conn |
460 | - else: |
461 | - parsed, conn = http_connection(url) |
462 | - path = '%s/%s' % (parsed.path, quote(container)) |
463 | - if not headers: |
464 | - headers = {} |
465 | - headers['X-Auth-Token'] = token |
466 | - conn.request('PUT', path, '', headers) |
467 | - resp = conn.getresponse() |
468 | - resp.read() |
469 | - if resp.status < 200 or resp.status >= 300: |
470 | - raise ClientException('Container PUT failed', |
471 | - http_scheme=parsed.scheme, http_host=conn.host, |
472 | - http_port=conn.port, http_path=path, http_status=resp.status, |
473 | - http_reason=resp.reason) |
474 | - |
475 | - |
476 | -def post_container(url, token, container, headers, http_conn=None): |
477 | - """ |
478 | - Update a container's metadata. |
479 | - |
480 | - :param url: storage URL |
481 | - :param token: auth token |
482 | - :param container: container name to update |
483 | - :param headers: additional headers to include in the request |
484 | - :param http_conn: HTTP connection object (If None, it will create the |
485 | - conn object) |
486 | - :raises ClientException: HTTP POST request failed |
487 | - """ |
488 | - if http_conn: |
489 | - parsed, conn = http_conn |
490 | - else: |
491 | - parsed, conn = http_connection(url) |
492 | - path = '%s/%s' % (parsed.path, quote(container)) |
493 | - headers['X-Auth-Token'] = token |
494 | - conn.request('POST', path, '', headers) |
495 | - resp = conn.getresponse() |
496 | - resp.read() |
497 | - if resp.status < 200 or resp.status >= 300: |
498 | - raise ClientException('Container POST failed', |
499 | - http_scheme=parsed.scheme, http_host=conn.host, |
500 | - http_port=conn.port, http_path=path, http_status=resp.status, |
501 | - http_reason=resp.reason) |
502 | - |
503 | - |
504 | -def delete_container(url, token, container, http_conn=None): |
505 | - """ |
506 | - Delete a container |
507 | - |
508 | - :param url: storage URL |
509 | - :param token: auth token |
510 | - :param container: container name to delete |
511 | - :param http_conn: HTTP connection object (If None, it will create the |
512 | - conn object) |
513 | - :raises ClientException: HTTP DELETE request failed |
514 | - """ |
515 | - if http_conn: |
516 | - parsed, conn = http_conn |
517 | - else: |
518 | - parsed, conn = http_connection(url) |
519 | - path = '%s/%s' % (parsed.path, quote(container)) |
520 | - conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
521 | - resp = conn.getresponse() |
522 | - resp.read() |
523 | - if resp.status < 200 or resp.status >= 300: |
524 | - raise ClientException('Container DELETE failed', |
525 | - http_scheme=parsed.scheme, http_host=conn.host, |
526 | - http_port=conn.port, http_path=path, http_status=resp.status, |
527 | - http_reason=resp.reason) |
528 | - |
529 | - |
530 | -def get_object(url, token, container, name, http_conn=None, |
531 | - resp_chunk_size=None): |
532 | - """ |
533 | - Get an object |
534 | - |
535 | - :param url: storage URL |
536 | - :param token: auth token |
537 | - :param container: container name that the object is in |
538 | - :param name: object name to get |
539 | - :param http_conn: HTTP connection object (If None, it will create the |
540 | - conn object) |
541 | - :param resp_chunk_size: if defined, chunk size of data to read. NOTE: If |
542 | - you specify a resp_chunk_size you must fully read |
543 | - the object's contents before making another |
544 | - request. |
545 | - :returns: a tuple of (response headers, the object's contents) The response |
546 | - headers will be a dict and all header names will be lowercase. |
547 | - :raises ClientException: HTTP GET request failed |
548 | - """ |
549 | - if http_conn: |
550 | - parsed, conn = http_conn |
551 | - else: |
552 | - parsed, conn = http_connection(url) |
553 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
554 | - conn.request('GET', path, '', {'X-Auth-Token': token}) |
555 | - resp = conn.getresponse() |
556 | - if resp.status < 200 or resp.status >= 300: |
557 | - resp.read() |
558 | - raise ClientException('Object GET failed', http_scheme=parsed.scheme, |
559 | - http_host=conn.host, http_port=conn.port, http_path=path, |
560 | - http_status=resp.status, http_reason=resp.reason) |
561 | - if resp_chunk_size: |
562 | - |
563 | - def _object_body(): |
564 | - buf = resp.read(resp_chunk_size) |
565 | - while buf: |
566 | - yield buf |
567 | - buf = resp.read(resp_chunk_size) |
568 | - object_body = _object_body() |
569 | - else: |
570 | - object_body = resp.read() |
571 | - resp_headers = {} |
572 | - for header, value in resp.getheaders(): |
573 | - resp_headers[header.lower()] = value |
574 | - return resp_headers, object_body |
575 | - |
576 | - |
577 | -def head_object(url, token, container, name, http_conn=None): |
578 | - """ |
579 | - Get object info |
580 | - |
581 | - :param url: storage URL |
582 | - :param token: auth token |
583 | - :param container: container name that the object is in |
584 | - :param name: object name to get info for |
585 | - :param http_conn: HTTP connection object (If None, it will create the |
586 | - conn object) |
587 | - :returns: a dict containing the response's headers (all header names will |
588 | - be lowercase) |
589 | - :raises ClientException: HTTP HEAD request failed |
590 | - """ |
591 | - if http_conn: |
592 | - parsed, conn = http_conn |
593 | - else: |
594 | - parsed, conn = http_connection(url) |
595 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
596 | - conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
597 | - resp = conn.getresponse() |
598 | - resp.read() |
599 | - if resp.status < 200 or resp.status >= 300: |
600 | - raise ClientException('Object HEAD failed', http_scheme=parsed.scheme, |
601 | - http_host=conn.host, http_port=conn.port, http_path=path, |
602 | - http_status=resp.status, http_reason=resp.reason) |
603 | - resp_headers = {} |
604 | - for header, value in resp.getheaders(): |
605 | - resp_headers[header.lower()] = value |
606 | - return resp_headers |
607 | - |
608 | - |
609 | -def put_object(url, token, container, name, contents, content_length=None, |
610 | - etag=None, chunk_size=65536, content_type=None, headers=None, |
611 | - http_conn=None): |
612 | - """ |
613 | - Put an object |
614 | - |
615 | - :param url: storage URL |
616 | - :param token: auth token |
617 | - :param container: container name that the object is in |
618 | - :param name: object name to put |
619 | - :param contents: a string or a file like object to read object data from |
620 | - :param content_length: value to send as content-length header; also limits |
621 | - the amount read from contents |
622 | - :param etag: etag of contents |
623 | - :param chunk_size: chunk size of data to write |
624 | - :param content_type: value to send as content-type header |
625 | - :param headers: additional headers to include in the request |
626 | - :param http_conn: HTTP connection object (If None, it will create the |
627 | - conn object) |
628 | - :returns: etag from server response |
629 | - :raises ClientException: HTTP PUT request failed |
630 | - """ |
631 | - if http_conn: |
632 | - parsed, conn = http_conn |
633 | - else: |
634 | - parsed, conn = http_connection(url) |
635 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
636 | - if not headers: |
637 | - headers = {} |
638 | - headers['X-Auth-Token'] = token |
639 | - if etag: |
640 | - headers['ETag'] = etag.strip('"') |
641 | - if content_length is not None: |
642 | - headers['Content-Length'] = str(content_length) |
643 | - if content_type is not None: |
644 | - headers['Content-Type'] = content_type |
645 | - if not contents: |
646 | - headers['Content-Length'] = '0' |
647 | - if hasattr(contents, 'read'): |
648 | - conn.putrequest('PUT', path) |
649 | - for header, value in headers.iteritems(): |
650 | - conn.putheader(header, value) |
651 | - if content_length is None: |
652 | - conn.putheader('Transfer-Encoding', 'chunked') |
653 | - conn.endheaders() |
654 | - chunk = contents.read(chunk_size) |
655 | - while chunk: |
656 | - conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
657 | - chunk = contents.read(chunk_size) |
658 | - conn.send('0\r\n\r\n') |
659 | - else: |
660 | - conn.endheaders() |
661 | - left = content_length |
662 | - while left > 0: |
663 | - size = chunk_size |
664 | - if size > left: |
665 | - size = left |
666 | - chunk = contents.read(size) |
667 | - conn.send(chunk) |
668 | - left -= len(chunk) |
669 | - else: |
670 | - conn.request('PUT', path, contents, headers) |
671 | - resp = conn.getresponse() |
672 | - resp.read() |
673 | - if resp.status < 200 or resp.status >= 300: |
674 | - raise ClientException('Object PUT failed', http_scheme=parsed.scheme, |
675 | - http_host=conn.host, http_port=conn.port, http_path=path, |
676 | - http_status=resp.status, http_reason=resp.reason) |
677 | - return resp.getheader('etag').strip('"') |
678 | - |
679 | - |
680 | -def post_object(url, token, container, name, headers, http_conn=None): |
681 | - """ |
682 | - Update object metadata |
683 | - |
684 | - :param url: storage URL |
685 | - :param token: auth token |
686 | - :param container: container name that the object is in |
687 | - :param name: name of the object to update |
688 | - :param headers: additional headers to include in the request |
689 | - :param http_conn: HTTP connection object (If None, it will create the |
690 | - conn object) |
691 | - :raises ClientException: HTTP POST request failed |
692 | - """ |
693 | - if http_conn: |
694 | - parsed, conn = http_conn |
695 | - else: |
696 | - parsed, conn = http_connection(url) |
697 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
698 | - headers['X-Auth-Token'] = token |
699 | - conn.request('POST', path, '', headers) |
700 | - resp = conn.getresponse() |
701 | - resp.read() |
702 | - if resp.status < 200 or resp.status >= 300: |
703 | - raise ClientException('Object POST failed', http_scheme=parsed.scheme, |
704 | - http_host=conn.host, http_port=conn.port, http_path=path, |
705 | - http_status=resp.status, http_reason=resp.reason) |
706 | - |
707 | - |
708 | -def delete_object(url, token, container, name, http_conn=None): |
709 | - """ |
710 | - Delete object |
711 | - |
712 | - :param url: storage URL |
713 | - :param token: auth token |
714 | - :param container: container name that the object is in |
715 | - :param name: object name to delete |
716 | - :param http_conn: HTTP connection object (If None, it will create the |
717 | - conn object) |
718 | - :raises ClientException: HTTP DELETE request failed |
719 | - """ |
720 | - if http_conn: |
721 | - parsed, conn = http_conn |
722 | - else: |
723 | - parsed, conn = http_connection(url) |
724 | - path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
725 | - conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
726 | - resp = conn.getresponse() |
727 | - resp.read() |
728 | - if resp.status < 200 or resp.status >= 300: |
729 | - raise ClientException('Object DELETE failed', |
730 | - http_scheme=parsed.scheme, http_host=conn.host, |
731 | - http_port=conn.port, http_path=path, http_status=resp.status, |
732 | - http_reason=resp.reason) |
733 | - |
734 | - |
735 | -class Connection(object): |
736 | - """Convenience class to make requests that will also retry the request""" |
737 | - |
738 | - def __init__(self, authurl, user, key, retries=5, preauthurl=None, |
739 | - preauthtoken=None, snet=False, starting_backoff=1): |
740 | - """ |
741 | - :param authurl: authenitcation URL |
742 | - :param user: user name to authenticate as |
743 | - :param key: key/password to authenticate with |
744 | - :param retries: Number of times to retry the request before failing |
745 | - :param preauthurl: storage URL (if you have already authenticated) |
746 | - :param preauthtoken: authentication token (if you have already |
747 | - authenticated) |
748 | - :param snet: use SERVICENET internal network default is False |
749 | - """ |
750 | - self.authurl = authurl |
751 | - self.user = user |
752 | - self.key = key |
753 | - self.retries = retries |
754 | - self.http_conn = None |
755 | - self.url = preauthurl |
756 | - self.token = preauthtoken |
757 | - self.attempts = 0 |
758 | - self.snet = snet |
759 | - self.starting_backoff = starting_backoff |
760 | - |
761 | - def get_auth(self): |
762 | - return get_auth(self.authurl, self.user, self.key, snet=self.snet) |
763 | - |
764 | - def http_connection(self): |
765 | - return http_connection(self.url) |
766 | - |
767 | - def _retry(self, reset_func, func, *args, **kwargs): |
768 | - self.attempts = 0 |
769 | - backoff = self.starting_backoff |
770 | - while self.attempts <= self.retries: |
771 | - self.attempts += 1 |
772 | - try: |
773 | - if not self.url or not self.token: |
774 | - self.url, self.token = self.get_auth() |
775 | - self.http_conn = None |
776 | - if not self.http_conn: |
777 | - self.http_conn = self.http_connection() |
778 | - kwargs['http_conn'] = self.http_conn |
779 | - rv = func(self.url, self.token, *args, **kwargs) |
780 | - return rv |
781 | - except (socket.error, HTTPException): |
782 | - if self.attempts > self.retries: |
783 | - raise |
784 | - self.http_conn = None |
785 | - except ClientException, err: |
786 | - if self.attempts > self.retries: |
787 | - raise |
788 | - if err.http_status == 401: |
789 | - self.url = self.token = None |
790 | - if self.attempts > 1: |
791 | - raise |
792 | - elif err.http_status == 408: |
793 | - self.http_conn = None |
794 | - elif 500 <= err.http_status <= 599: |
795 | - pass |
796 | - else: |
797 | - raise |
798 | - sleep(backoff) |
799 | - backoff *= 2 |
800 | - if reset_func: |
801 | - reset_func(func, *args, **kwargs) |
802 | - |
803 | - def head_account(self): |
804 | - """Wrapper for :func:`head_account`""" |
805 | - return self._retry(None, head_account) |
806 | - |
807 | - def get_account(self, marker=None, limit=None, prefix=None, |
808 | - full_listing=False): |
809 | - """Wrapper for :func:`get_account`""" |
810 | - # TODO(unknown): With full_listing=True this will restart the entire |
811 | - # listing with each retry. Need to make a better version that just |
812 | - # retries where it left off. |
813 | - return self._retry(None, get_account, marker=marker, limit=limit, |
814 | - prefix=prefix, full_listing=full_listing) |
815 | - |
816 | - def post_account(self, headers): |
817 | - """Wrapper for :func:`post_account`""" |
818 | - return self._retry(None, post_account, headers) |
819 | - |
820 | - def head_container(self, container): |
821 | - """Wrapper for :func:`head_container`""" |
822 | - return self._retry(None, head_container, container) |
823 | - |
824 | - def get_container(self, container, marker=None, limit=None, prefix=None, |
825 | - delimiter=None, full_listing=False): |
826 | - """Wrapper for :func:`get_container`""" |
827 | - # TODO(unknown): With full_listing=True this will restart the entire |
828 | - # listing with each retry. Need to make a better version that just |
829 | - # retries where it left off. |
830 | - return self._retry(None, get_container, container, marker=marker, |
831 | - limit=limit, prefix=prefix, delimiter=delimiter, |
832 | - full_listing=full_listing) |
833 | - |
834 | - def put_container(self, container, headers=None): |
835 | - """Wrapper for :func:`put_container`""" |
836 | - return self._retry(None, put_container, container, headers=headers) |
837 | - |
838 | - def post_container(self, container, headers): |
839 | - """Wrapper for :func:`post_container`""" |
840 | - return self._retry(None, post_container, container, headers) |
841 | - |
842 | - def delete_container(self, container): |
843 | - """Wrapper for :func:`delete_container`""" |
844 | - return self._retry(None, delete_container, container) |
845 | - |
846 | - def head_object(self, container, obj): |
847 | - """Wrapper for :func:`head_object`""" |
848 | - return self._retry(None, head_object, container, obj) |
849 | - |
850 | - def get_object(self, container, obj, resp_chunk_size=None): |
851 | - """Wrapper for :func:`get_object`""" |
852 | - return self._retry(None, get_object, container, obj, |
853 | - resp_chunk_size=resp_chunk_size) |
854 | - |
855 | - def put_object(self, container, obj, contents, content_length=None, |
856 | - etag=None, chunk_size=65536, content_type=None, |
857 | - headers=None): |
858 | - """Wrapper for :func:`put_object`""" |
859 | - |
860 | - def _default_reset(*args, **kwargs): |
861 | - raise ClientException('put_object(%r, %r, ...) failure and no ' |
862 | - 'ability to reset contents for reupload.' % (container, obj)) |
863 | - |
864 | - reset_func = _default_reset |
865 | - tell = getattr(contents, 'tell', None) |
866 | - seek = getattr(contents, 'seek', None) |
867 | - if tell and seek: |
868 | - orig_pos = tell() |
869 | - reset_func = lambda *a, **k: seek(orig_pos) |
870 | - elif not contents: |
871 | - reset_func = lambda *a, **k: None |
872 | - |
873 | - return self._retry(reset_func, put_object, container, obj, contents, |
874 | - content_length=content_length, etag=etag, chunk_size=chunk_size, |
875 | - content_type=content_type, headers=headers) |
876 | - |
877 | - def post_object(self, container, obj, headers): |
878 | - """Wrapper for :func:`post_object`""" |
879 | - return self._retry(None, post_object, container, obj, headers) |
880 | - |
881 | - def delete_object(self, container, obj): |
882 | - """Wrapper for :func:`delete_object`""" |
883 | - return self._retry(None, delete_object, container, obj) |
884 | - |
885 | -# End inclusion of swift.common.client |
886 | -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
887 | - |
888 | - |
889 | -def mkdirs(path): |
890 | - try: |
891 | - makedirs(path) |
892 | - except OSError, err: |
893 | - if err.errno != EEXIST: |
894 | - raise |
895 | - |
896 | - |
897 | -def put_errors_from_threads(threads, error_queue): |
898 | - """ |
899 | - Places any errors from the threads into error_queue. |
900 | - :param threads: A list of QueueFunctionThread instances. |
901 | - :param error_queue: A queue to put error strings into. |
902 | - :returns: True if any errors were found. |
903 | - """ |
904 | - was_error = False |
905 | - for thread in threads: |
906 | - for info in thread.exc_infos: |
907 | - was_error = True |
908 | - if isinstance(info[1], ClientException): |
909 | - error_queue.put(str(info[1])) |
910 | - else: |
911 | - error_queue.put(''.join(format_exception(*info))) |
912 | - return was_error |
913 | - |
914 | - |
915 | -class QueueFunctionThread(Thread): |
916 | - |
917 | - def __init__(self, queue, func, *args, **kwargs): |
918 | - """ Calls func for each item in queue; func is called with a queued |
919 | - item as the first arg followed by *args and **kwargs. Use the abort |
920 | - attribute to have the thread empty the queue (without processing) |
921 | - and exit. """ |
922 | - Thread.__init__(self) |
923 | - self.abort = False |
924 | - self.queue = queue |
925 | - self.func = func |
926 | - self.args = args |
927 | - self.kwargs = kwargs |
928 | - self.exc_infos = [] |
929 | - |
930 | - def run(self): |
931 | - try: |
932 | - while True: |
933 | - try: |
934 | - item = self.queue.get_nowait() |
935 | - if not self.abort: |
936 | - self.func(item, *self.args, **self.kwargs) |
937 | - self.queue.task_done() |
938 | - except Empty: |
939 | - if self.abort: |
940 | - break |
941 | - sleep(0.01) |
942 | - except Exception: |
943 | - self.exc_infos.append(exc_info()) |
944 | - |
945 | - |
946 | -st_delete_help = ''' |
947 | -delete --all OR delete container [--leave-segments] [object] [object] ... |
948 | - Deletes everything in the account (with --all), or everything in a |
949 | - container, or a list of objects depending on the args given. Segments of |
950 | - manifest objects will be deleted as well, unless you specify the |
951 | - --leave-segments option.'''.strip('\n') |
952 | - |
953 | - |
954 | -def st_delete(parser, args, print_queue, error_queue): |
955 | - parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
956 | - default=False, help='Indicates that you really want to delete ' |
957 | - 'everything in the account') |
958 | - parser.add_option('', '--leave-segments', action='store_true', |
959 | - dest='leave_segments', default=False, help='Indicates that you want ' |
960 | - 'the segments of manifest objects left alone') |
961 | - (options, args) = parse_args(parser, args) |
962 | - args = args[1:] |
963 | - if (not args and not options.yes_all) or (args and options.yes_all): |
964 | - error_queue.put('Usage: %s [options] %s' % |
965 | - (basename(argv[0]), st_delete_help)) |
966 | - return |
967 | - |
968 | - def _delete_segment((container, obj), conn): |
969 | - conn.delete_object(container, obj) |
970 | - if options.verbose: |
971 | - if conn.attempts > 2: |
972 | - print_queue.put('%s/%s [after %d attempts]' % |
973 | - (container, obj, conn.attempts)) |
974 | - else: |
975 | - print_queue.put('%s/%s' % (container, obj)) |
976 | - |
977 | - object_queue = Queue(10000) |
978 | - |
979 | - def _delete_object((container, obj), conn): |
980 | - try: |
981 | - old_manifest = None |
982 | - if not options.leave_segments: |
983 | - try: |
984 | - old_manifest = conn.head_object(container, obj).get( |
985 | - 'x-object-manifest') |
986 | - except ClientException, err: |
987 | - if err.http_status != 404: |
988 | - raise |
989 | - conn.delete_object(container, obj) |
990 | - if old_manifest: |
991 | - segment_queue = Queue(10000) |
992 | - scontainer, sprefix = old_manifest.split('/', 1) |
993 | - for delobj in conn.get_container(scontainer, |
994 | - prefix=sprefix)[1]: |
995 | - segment_queue.put((scontainer, delobj['name'])) |
996 | - if not segment_queue.empty(): |
997 | - segment_threads = [QueueFunctionThread(segment_queue, |
998 | - _delete_segment, create_connection()) for _junk in |
999 | - xrange(10)] |
1000 | - for thread in segment_threads: |
1001 | - thread.start() |
1002 | - while not segment_queue.empty(): |
1003 | - sleep(0.01) |
1004 | - for thread in segment_threads: |
1005 | - thread.abort = True |
1006 | - while thread.isAlive(): |
1007 | - thread.join(0.01) |
1008 | - put_errors_from_threads(segment_threads, error_queue) |
1009 | - if options.verbose: |
1010 | - path = options.yes_all and join(container, obj) or obj |
1011 | - if path[:1] in ('/', '\\'): |
1012 | - path = path[1:] |
1013 | - if conn.attempts > 1: |
1014 | - print_queue.put('%s [after %d attempts]' % |
1015 | - (path, conn.attempts)) |
1016 | - else: |
1017 | - print_queue.put(path) |
1018 | - except ClientException, err: |
1019 | - if err.http_status != 404: |
1020 | - raise |
1021 | - error_queue.put('Object %s not found' % |
1022 | - repr('%s/%s' % (container, obj))) |
1023 | - |
1024 | - container_queue = Queue(10000) |
1025 | - |
1026 | - def _delete_container(container, conn): |
1027 | - try: |
1028 | - marker = '' |
1029 | - while True: |
1030 | - objects = [o['name'] for o in |
1031 | - conn.get_container(container, marker=marker)[1]] |
1032 | - if not objects: |
1033 | - break |
1034 | - for obj in objects: |
1035 | - object_queue.put((container, obj)) |
1036 | - marker = objects[-1] |
1037 | - while not object_queue.empty(): |
1038 | - sleep(0.01) |
1039 | - attempts = 1 |
1040 | - while True: |
1041 | - try: |
1042 | - conn.delete_container(container) |
1043 | - break |
1044 | - except ClientException, err: |
1045 | - if err.http_status != 409: |
1046 | - raise |
1047 | - if attempts > 10: |
1048 | - raise |
1049 | - attempts += 1 |
1050 | - sleep(1) |
1051 | - except ClientException, err: |
1052 | - if err.http_status != 404: |
1053 | - raise |
1054 | - error_queue.put('Container %s not found' % repr(container)) |
1055 | - |
1056 | - url, token = get_auth(options.auth, options.user, options.key, |
1057 | - snet=options.snet) |
1058 | - create_connection = lambda: Connection(options.auth, options.user, |
1059 | - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
1060 | - object_threads = [QueueFunctionThread(object_queue, _delete_object, |
1061 | - create_connection()) for _junk in xrange(10)] |
1062 | - for thread in object_threads: |
1063 | - thread.start() |
1064 | - container_threads = [QueueFunctionThread(container_queue, |
1065 | - _delete_container, create_connection()) for _junk in xrange(10)] |
1066 | - for thread in container_threads: |
1067 | - thread.start() |
1068 | - if not args: |
1069 | - conn = create_connection() |
1070 | - try: |
1071 | - marker = '' |
1072 | - while True: |
1073 | - containers = \ |
1074 | - [c['name'] for c in conn.get_account(marker=marker)[1]] |
1075 | - if not containers: |
1076 | - break |
1077 | - for container in containers: |
1078 | - container_queue.put(container) |
1079 | - marker = containers[-1] |
1080 | - while not container_queue.empty(): |
1081 | - sleep(0.01) |
1082 | - while not object_queue.empty(): |
1083 | - sleep(0.01) |
1084 | - except ClientException, err: |
1085 | - if err.http_status != 404: |
1086 | - raise |
1087 | - error_queue.put('Account not found') |
1088 | - elif len(args) == 1: |
1089 | - if '/' in args[0]: |
1090 | - print >> stderr, 'WARNING: / in container name; you might have ' \ |
1091 | - 'meant %r instead of %r.' % \ |
1092 | - (args[0].replace('/', ' ', 1), args[0]) |
1093 | - conn = create_connection() |
1094 | - _delete_container(args[0], conn) |
1095 | - else: |
1096 | - for obj in args[1:]: |
1097 | - object_queue.put((args[0], obj)) |
1098 | - while not container_queue.empty(): |
1099 | - sleep(0.01) |
1100 | - for thread in container_threads: |
1101 | - thread.abort = True |
1102 | - while thread.isAlive(): |
1103 | - thread.join(0.01) |
1104 | - put_errors_from_threads(container_threads, error_queue) |
1105 | - while not object_queue.empty(): |
1106 | - sleep(0.01) |
1107 | - for thread in object_threads: |
1108 | - thread.abort = True |
1109 | - while thread.isAlive(): |
1110 | - thread.join(0.01) |
1111 | - put_errors_from_threads(object_threads, error_queue) |
1112 | - |
1113 | - |
1114 | -st_download_help = ''' |
1115 | -download --all OR download container [options] [object] [object] ... |
1116 | - Downloads everything in the account (with --all), or everything in a |
1117 | - container, or a list of objects depending on the args given. For a single |
1118 | - object download, you may use the -o [--output] <filename> option to |
1119 | - redirect the output to a specific file or if "-" then just redirect to |
1120 | - stdout.'''.strip('\n') |
1121 | - |
1122 | - |
1123 | -def st_download(options, args, print_queue, error_queue): |
1124 | - parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
1125 | - default=False, help='Indicates that you really want to download ' |
1126 | - 'everything in the account') |
1127 | - parser.add_option('-o', '--output', dest='out_file', help='For a single ' |
1128 | - 'file download, stream the output to an alternate location ') |
1129 | - (options, args) = parse_args(parser, args) |
1130 | - args = args[1:] |
1131 | - if options.out_file == '-': |
1132 | - options.verbose = 0 |
1133 | - if options.out_file and len(args) != 2: |
1134 | - exit('-o option only allowed for single file downloads') |
1135 | - if (not args and not options.yes_all) or (args and options.yes_all): |
1136 | - error_queue.put('Usage: %s [options] %s' % |
1137 | - (basename(argv[0]), st_download_help)) |
1138 | - return |
1139 | - |
1140 | - object_queue = Queue(10000) |
1141 | - |
1142 | - def _download_object(queue_arg, conn): |
1143 | - if len(queue_arg) == 2: |
1144 | - container, obj = queue_arg |
1145 | - out_file = None |
1146 | - elif len(queue_arg) == 3: |
1147 | - container, obj, out_file = queue_arg |
1148 | - else: |
1149 | - raise Exception("Invalid queue_arg length of %s" % len(queue_arg)) |
1150 | - try: |
1151 | - headers, body = \ |
1152 | - conn.get_object(container, obj, resp_chunk_size=65536) |
1153 | - content_type = headers.get('content-type') |
1154 | - if 'content-length' in headers: |
1155 | - content_length = int(headers.get('content-length')) |
1156 | - else: |
1157 | - content_length = None |
1158 | - etag = headers.get('etag') |
1159 | - path = options.yes_all and join(container, obj) or obj |
1160 | - if path[:1] in ('/', '\\'): |
1161 | - path = path[1:] |
1162 | - md5sum = None |
1163 | - make_dir = out_file != "-" |
1164 | - if content_type.split(';', 1)[0] == 'text/directory': |
1165 | - if make_dir and not isdir(path): |
1166 | - mkdirs(path) |
1167 | - read_length = 0 |
1168 | - if 'x-object-manifest' not in headers: |
1169 | - md5sum = md5() |
1170 | - for chunk in body: |
1171 | - read_length += len(chunk) |
1172 | - if md5sum: |
1173 | - md5sum.update(chunk) |
1174 | - else: |
1175 | - dirpath = dirname(path) |
1176 | - if make_dir and dirpath and not isdir(dirpath): |
1177 | - mkdirs(dirpath) |
1178 | - if out_file == "-": |
1179 | - fp = stdout |
1180 | - elif out_file: |
1181 | - fp = open(out_file, 'wb') |
1182 | - else: |
1183 | - fp = open(path, 'wb') |
1184 | - read_length = 0 |
1185 | - if 'x-object-manifest' not in headers: |
1186 | - md5sum = md5() |
1187 | - for chunk in body: |
1188 | - fp.write(chunk) |
1189 | - read_length += len(chunk) |
1190 | - if md5sum: |
1191 | - md5sum.update(chunk) |
1192 | - fp.close() |
1193 | - if md5sum and md5sum.hexdigest() != etag: |
1194 | - error_queue.put('%s: md5sum != etag, %s != %s' % |
1195 | - (path, md5sum.hexdigest(), etag)) |
1196 | - if content_length is not None and read_length != content_length: |
1197 | - error_queue.put('%s: read_length != content_length, %d != %d' % |
1198 | - (path, read_length, content_length)) |
1199 | - if 'x-object-meta-mtime' in headers and not options.out_file: |
1200 | - mtime = float(headers['x-object-meta-mtime']) |
1201 | - utime(path, (mtime, mtime)) |
1202 | - if options.verbose: |
1203 | - if conn.attempts > 1: |
1204 | - print_queue.put('%s [after %d attempts' % |
1205 | - (path, conn.attempts)) |
1206 | - else: |
1207 | - print_queue.put(path) |
1208 | - except ClientException, err: |
1209 | - if err.http_status != 404: |
1210 | - raise |
1211 | - error_queue.put('Object %s not found' % |
1212 | - repr('%s/%s' % (container, obj))) |
1213 | - |
1214 | - container_queue = Queue(10000) |
1215 | - |
1216 | - def _download_container(container, conn): |
1217 | - try: |
1218 | - marker = '' |
1219 | - while True: |
1220 | - objects = [o['name'] for o in |
1221 | - conn.get_container(container, marker=marker)[1]] |
1222 | - if not objects: |
1223 | - break |
1224 | - for obj in objects: |
1225 | - object_queue.put((container, obj)) |
1226 | - marker = objects[-1] |
1227 | - except ClientException, err: |
1228 | - if err.http_status != 404: |
1229 | - raise |
1230 | - error_queue.put('Container %s not found' % repr(container)) |
1231 | - |
1232 | - url, token = get_auth(options.auth, options.user, options.key, |
1233 | - snet=options.snet) |
1234 | - create_connection = lambda: Connection(options.auth, options.user, |
1235 | - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
1236 | - object_threads = [QueueFunctionThread(object_queue, _download_object, |
1237 | - create_connection()) for _junk in xrange(10)] |
1238 | - for thread in object_threads: |
1239 | - thread.start() |
1240 | - container_threads = [QueueFunctionThread(container_queue, |
1241 | - _download_container, create_connection()) for _junk in xrange(10)] |
1242 | - for thread in container_threads: |
1243 | - thread.start() |
1244 | - if not args: |
1245 | - conn = create_connection() |
1246 | - try: |
1247 | - marker = '' |
1248 | - while True: |
1249 | - containers = [c['name'] |
1250 | - for c in conn.get_account(marker=marker)[1]] |
1251 | - if not containers: |
1252 | - break |
1253 | - for container in containers: |
1254 | - container_queue.put(container) |
1255 | - marker = containers[-1] |
1256 | - except ClientException, err: |
1257 | - if err.http_status != 404: |
1258 | - raise |
1259 | - error_queue.put('Account not found') |
1260 | - elif len(args) == 1: |
1261 | - if '/' in args[0]: |
1262 | - print >> stderr, 'WARNING: / in container name; you might have ' \ |
1263 | - 'meant %r instead of %r.' % \ |
1264 | - (args[0].replace('/', ' ', 1), args[0]) |
1265 | - _download_container(args[0], create_connection()) |
1266 | - else: |
1267 | - if len(args) == 2: |
1268 | - obj = args[1] |
1269 | - object_queue.put((args[0], obj, options.out_file)) |
1270 | - else: |
1271 | - for obj in args[1:]: |
1272 | - object_queue.put((args[0], obj)) |
1273 | - while not container_queue.empty(): |
1274 | - sleep(0.01) |
1275 | - for thread in container_threads: |
1276 | - thread.abort = True |
1277 | - while thread.isAlive(): |
1278 | - thread.join(0.01) |
1279 | - put_errors_from_threads(container_threads, error_queue) |
1280 | - while not object_queue.empty(): |
1281 | - sleep(0.01) |
1282 | - for thread in object_threads: |
1283 | - thread.abort = True |
1284 | - while thread.isAlive(): |
1285 | - thread.join(0.01) |
1286 | - put_errors_from_threads(object_threads, error_queue) |
1287 | - |
1288 | - |
1289 | -st_list_help = ''' |
1290 | -list [options] [container] |
1291 | - Lists the containers for the account or the objects for a container. -p or |
1292 | - --prefix is an option that will only list items beginning with that prefix. |
1293 | - -d or --delimiter is option (for container listings only) that will roll up |
1294 | - items with the given delimiter (see Cloud Files general documentation for |
1295 | - what this means). |
1296 | -'''.strip('\n') |
1297 | - |
1298 | - |
1299 | -def st_list(options, args, print_queue, error_queue): |
1300 | - parser.add_option('-p', '--prefix', dest='prefix', help='Will only list ' |
1301 | - 'items beginning with the prefix') |
1302 | - parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll ' |
1303 | - 'up items with the given delimiter (see Cloud Files general ' |
1304 | - 'documentation for what this means)') |
1305 | - (options, args) = parse_args(parser, args) |
1306 | - args = args[1:] |
1307 | - if options.delimiter and not args: |
1308 | - exit('-d option only allowed for container listings') |
1309 | - if len(args) > 1: |
1310 | - error_queue.put('Usage: %s [options] %s' % |
1311 | - (basename(argv[0]), st_list_help)) |
1312 | - return |
1313 | - conn = Connection(options.auth, options.user, options.key, |
1314 | - snet=options.snet) |
1315 | - try: |
1316 | - marker = '' |
1317 | - while True: |
1318 | - if not args: |
1319 | - items = \ |
1320 | - conn.get_account(marker=marker, prefix=options.prefix)[1] |
1321 | - else: |
1322 | - items = conn.get_container(args[0], marker=marker, |
1323 | - prefix=options.prefix, delimiter=options.delimiter)[1] |
1324 | - if not items: |
1325 | - break |
1326 | - for item in items: |
1327 | - print_queue.put(item.get('name', item.get('subdir'))) |
1328 | - marker = items[-1].get('name', items[-1].get('subdir')) |
1329 | - except ClientException, err: |
1330 | - if err.http_status != 404: |
1331 | - raise |
1332 | - if not args: |
1333 | - error_queue.put('Account not found') |
1334 | - else: |
1335 | - error_queue.put('Container %s not found' % repr(args[0])) |
1336 | - |
1337 | - |
1338 | -st_stat_help = ''' |
1339 | -stat [container] [object] |
1340 | - Displays information for the account, container, or object depending on the |
1341 | - args given (if any).'''.strip('\n') |
1342 | - |
1343 | - |
1344 | -def st_stat(options, args, print_queue, error_queue): |
1345 | - (options, args) = parse_args(parser, args) |
1346 | - args = args[1:] |
1347 | - conn = Connection(options.auth, options.user, options.key) |
1348 | - if not args: |
1349 | - try: |
1350 | - headers = conn.head_account() |
1351 | - if options.verbose > 1: |
1352 | - print_queue.put(''' |
1353 | -StorageURL: %s |
1354 | -Auth Token: %s |
1355 | -'''.strip('\n') % (conn.url, conn.token)) |
1356 | - container_count = int(headers.get('x-account-container-count', 0)) |
1357 | - object_count = int(headers.get('x-account-object-count', 0)) |
1358 | - bytes_used = int(headers.get('x-account-bytes-used', 0)) |
1359 | - print_queue.put(''' |
1360 | - Account: %s |
1361 | -Containers: %d |
1362 | - Objects: %d |
1363 | - Bytes: %d'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], container_count, |
1364 | - object_count, bytes_used)) |
1365 | - for key, value in headers.items(): |
1366 | - if key.startswith('x-account-meta-'): |
1367 | - print_queue.put('%10s: %s' % ('Meta %s' % |
1368 | - key[len('x-account-meta-'):].title(), value)) |
1369 | - for key, value in headers.items(): |
1370 | - if not key.startswith('x-account-meta-') and key not in ( |
1371 | - 'content-length', 'date', 'x-account-container-count', |
1372 | - 'x-account-object-count', 'x-account-bytes-used'): |
1373 | - print_queue.put( |
1374 | - '%10s: %s' % (key.title(), value)) |
1375 | - except ClientException, err: |
1376 | - if err.http_status != 404: |
1377 | - raise |
1378 | - error_queue.put('Account not found') |
1379 | - elif len(args) == 1: |
1380 | - if '/' in args[0]: |
1381 | - print >> stderr, 'WARNING: / in container name; you might have ' \ |
1382 | - 'meant %r instead of %r.' % \ |
1383 | - (args[0].replace('/', ' ', 1), args[0]) |
1384 | - try: |
1385 | - headers = conn.head_container(args[0]) |
1386 | - object_count = int(headers.get('x-container-object-count', 0)) |
1387 | - bytes_used = int(headers.get('x-container-bytes-used', 0)) |
1388 | - print_queue.put(''' |
1389 | - Account: %s |
1390 | -Container: %s |
1391 | - Objects: %d |
1392 | - Bytes: %d |
1393 | - Read ACL: %s |
1394 | -Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], |
1395 | - object_count, bytes_used, |
1396 | - headers.get('x-container-read', ''), |
1397 | - headers.get('x-container-write', ''))) |
1398 | - for key, value in headers.items(): |
1399 | - if key.startswith('x-container-meta-'): |
1400 | - print_queue.put('%9s: %s' % ('Meta %s' % |
1401 | - key[len('x-container-meta-'):].title(), value)) |
1402 | - for key, value in headers.items(): |
1403 | - if not key.startswith('x-container-meta-') and key not in ( |
1404 | - 'content-length', 'date', 'x-container-object-count', |
1405 | - 'x-container-bytes-used', 'x-container-read', |
1406 | - 'x-container-write'): |
1407 | - print_queue.put( |
1408 | - '%9s: %s' % (key.title(), value)) |
1409 | - except ClientException, err: |
1410 | - if err.http_status != 404: |
1411 | - raise |
1412 | - error_queue.put('Container %s not found' % repr(args[0])) |
1413 | - elif len(args) == 2: |
1414 | - try: |
1415 | - headers = conn.head_object(args[0], args[1]) |
1416 | - print_queue.put(''' |
1417 | - Account: %s |
1418 | - Container: %s |
1419 | - Object: %s |
1420 | - Content Type: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], |
1421 | - args[1], headers.get('content-type'))) |
1422 | - if 'content-length' in headers: |
1423 | - print_queue.put('Content Length: %s' % |
1424 | - headers['content-length']) |
1425 | - if 'last-modified' in headers: |
1426 | - print_queue.put(' Last Modified: %s' % |
1427 | - headers['last-modified']) |
1428 | - if 'etag' in headers: |
1429 | - print_queue.put(' ETag: %s' % headers['etag']) |
1430 | - if 'x-object-manifest' in headers: |
1431 | - print_queue.put(' Manifest: %s' % |
1432 | - headers['x-object-manifest']) |
1433 | - for key, value in headers.items(): |
1434 | - if key.startswith('x-object-meta-'): |
1435 | - print_queue.put('%14s: %s' % ('Meta %s' % |
1436 | - key[len('x-object-meta-'):].title(), value)) |
1437 | - for key, value in headers.items(): |
1438 | - if not key.startswith('x-object-meta-') and key not in ( |
1439 | - 'content-type', 'content-length', 'last-modified', |
1440 | - 'etag', 'date', 'x-object-manifest'): |
1441 | - print_queue.put( |
1442 | - '%14s: %s' % (key.title(), value)) |
1443 | - except ClientException, err: |
1444 | - if err.http_status != 404: |
1445 | - raise |
1446 | - error_queue.put('Object %s not found' % |
1447 | - repr('%s/%s' % (args[0], args[1]))) |
1448 | - else: |
1449 | - error_queue.put('Usage: %s [options] %s' % |
1450 | - (basename(argv[0]), st_stat_help)) |
1451 | - |
1452 | - |
1453 | -st_post_help = ''' |
1454 | -post [options] [container] [object] |
1455 | - Updates meta information for the account, container, or object depending on |
1456 | - the args given. If the container is not found, it will be created |
1457 | - automatically; but this is not true for accounts and objects. Containers |
1458 | - also allow the -r (or --read-acl) and -w (or --write-acl) options. The -m |
1459 | - or --meta option is allowed on all and used to define the user meta data |
1460 | - items to set in the form Name:Value. This option can be repeated. Example: |
1461 | - post -m Color:Blue -m Size:Large'''.strip('\n') |
1462 | - |
1463 | - |
1464 | -def st_post(options, args, print_queue, error_queue): |
1465 | - parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the ' |
1466 | - 'Read ACL for containers. Quick summary of ACL syntax: .r:*, ' |
1467 | - '.r:-.example.com, .r:www.example.com, account1, account2:user2') |
1468 | - parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the ' |
1469 | - 'Write ACL for containers. Quick summary of ACL syntax: account1, ' |
1470 | - 'account2:user2') |
1471 | - parser.add_option('-m', '--meta', action='append', dest='meta', default=[], |
1472 | - help='Sets a meta data item with the syntax name:value. This option ' |
1473 | - 'may be repeated. Example: -m Color:Blue -m Size:Large') |
1474 | - (options, args) = parse_args(parser, args) |
1475 | - args = args[1:] |
1476 | - if (options.read_acl or options.write_acl) and not args: |
1477 | - exit('-r and -w options only allowed for containers') |
1478 | - conn = Connection(options.auth, options.user, options.key) |
1479 | - if not args: |
1480 | - headers = {} |
1481 | - for item in options.meta: |
1482 | - split_item = item.split(':') |
1483 | - headers['X-Account-Meta-' + split_item[0]] = \ |
1484 | - len(split_item) > 1 and split_item[1] |
1485 | - try: |
1486 | - conn.post_account(headers=headers) |
1487 | - except ClientException, err: |
1488 | - if err.http_status != 404: |
1489 | - raise |
1490 | - error_queue.put('Account not found') |
1491 | - elif len(args) == 1: |
1492 | - if '/' in args[0]: |
1493 | - print >> stderr, 'WARNING: / in container name; you might have ' \ |
1494 | - 'meant %r instead of %r.' % \ |
1495 | - (args[0].replace('/', ' ', 1), args[0]) |
1496 | - headers = {} |
1497 | - for item in options.meta: |
1498 | - split_item = item.split(':') |
1499 | - headers['X-Container-Meta-' + split_item[0]] = \ |
1500 | - len(split_item) > 1 and split_item[1] |
1501 | - if options.read_acl is not None: |
1502 | - headers['X-Container-Read'] = options.read_acl |
1503 | - if options.write_acl is not None: |
1504 | - headers['X-Container-Write'] = options.write_acl |
1505 | - try: |
1506 | - conn.post_container(args[0], headers=headers) |
1507 | - except ClientException, err: |
1508 | - if err.http_status != 404: |
1509 | - raise |
1510 | - conn.put_container(args[0], headers=headers) |
1511 | - elif len(args) == 2: |
1512 | - headers = {} |
1513 | - for item in options.meta: |
1514 | - split_item = item.split(':') |
1515 | - headers['X-Object-Meta-' + split_item[0]] = \ |
1516 | - len(split_item) > 1 and split_item[1] |
1517 | - try: |
1518 | - conn.post_object(args[0], args[1], headers=headers) |
1519 | - except ClientException, err: |
1520 | - if err.http_status != 404: |
1521 | - raise |
1522 | - error_queue.put('Object %s not found' % |
1523 | - repr('%s/%s' % (args[0], args[1]))) |
1524 | - else: |
1525 | - error_queue.put('Usage: %s [options] %s' % |
1526 | - (basename(argv[0]), st_post_help)) |
1527 | - |
1528 | - |
1529 | -st_upload_help = ''' |
1530 | -upload [options] container file_or_directory [file_or_directory] [...] |
1531 | - Uploads to the given container the files and directories specified by the |
1532 | - remaining args. -c or --changed is an option that will only upload files |
1533 | - that have changed since the last upload. -S <size> or --segment-size <size> |
1534 | - and --leave-segments are options as well (see --help for more). |
1535 | -'''.strip('\n') |
1536 | - |
1537 | - |
1538 | -def st_upload(options, args, print_queue, error_queue): |
1539 | - parser.add_option('-c', '--changed', action='store_true', dest='changed', |
1540 | - default=False, help='Will only upload files that have changed since ' |
1541 | - 'the last upload') |
1542 | - parser.add_option('-S', '--segment-size', dest='segment_size', help='Will ' |
1543 | - 'upload files in segments no larger than <size> and then create a ' |
1544 | - '"manifest" file that will download all the segments as if it were ' |
1545 | - 'the original file. The segments will be uploaded to a ' |
1546 | - '<container>_segments container so as to not pollute the main ' |
1547 | - '<container> listings.') |
1548 | - parser.add_option('', '--leave-segments', action='store_true', |
1549 | - dest='leave_segments', default=False, help='Indicates that you want ' |
1550 | - 'the older segments of manifest objects left alone (in the case of ' |
1551 | - 'overwrites)') |
1552 | - (options, args) = parse_args(parser, args) |
1553 | - args = args[1:] |
1554 | - if len(args) < 2: |
1555 | - error_queue.put('Usage: %s [options] %s' % |
1556 | - (basename(argv[0]), st_upload_help)) |
1557 | - return |
1558 | - object_queue = Queue(10000) |
1559 | - |
1560 | - def _segment_job(job, conn): |
1561 | - if job.get('delete', False): |
1562 | - conn.delete_object(job['container'], job['obj']) |
1563 | - else: |
1564 | - fp = open(job['path'], 'rb') |
1565 | - fp.seek(job['segment_start']) |
1566 | - conn.put_object(job.get('container', args[0] + '_segments'), |
1567 | - job['obj'], fp, content_length=job['segment_size']) |
1568 | - if options.verbose and 'log_line' in job: |
1569 | - if conn.attempts > 1: |
1570 | - print_queue.put('%s [after %d attempts]' % |
1571 | - (job['log_line'], conn.attempts)) |
1572 | - else: |
1573 | - print_queue.put(job['log_line']) |
1574 | - |
1575 | - def _object_job(job, conn): |
1576 | - path = job['path'] |
1577 | - container = job.get('container', args[0]) |
1578 | - dir_marker = job.get('dir_marker', False) |
1579 | - try: |
1580 | - obj = path |
1581 | - if obj.startswith('./') or obj.startswith('.\\'): |
1582 | - obj = obj[2:] |
1583 | - put_headers = {'x-object-meta-mtime': str(getmtime(path))} |
1584 | - if dir_marker: |
1585 | - if options.changed: |
1586 | - try: |
1587 | - headers = conn.head_object(container, obj) |
1588 | - ct = headers.get('content-type') |
1589 | - cl = int(headers.get('content-length')) |
1590 | - et = headers.get('etag') |
1591 | - mt = headers.get('x-object-meta-mtime') |
1592 | - if ct.split(';', 1)[0] == 'text/directory' and \ |
1593 | - cl == 0 and \ |
1594 | - et == 'd41d8cd98f00b204e9800998ecf8427e' and \ |
1595 | - mt == put_headers['x-object-meta-mtime']: |
1596 | - return |
1597 | - except ClientException, err: |
1598 | - if err.http_status != 404: |
1599 | - raise |
1600 | - conn.put_object(container, obj, '', content_length=0, |
1601 | - content_type='text/directory', |
1602 | - headers=put_headers) |
1603 | - else: |
1604 | - # We need to HEAD all objects now in case we're overwriting a |
1605 | - # manifest object and need to delete the old segments |
1606 | - # ourselves. |
1607 | - old_manifest = None |
1608 | - if options.changed or not options.leave_segments: |
1609 | - try: |
1610 | - headers = conn.head_object(container, obj) |
1611 | - cl = int(headers.get('content-length')) |
1612 | - mt = headers.get('x-object-meta-mtime') |
1613 | - if options.changed and cl == getsize(path) and \ |
1614 | - mt == put_headers['x-object-meta-mtime']: |
1615 | - return |
1616 | - if not options.leave_segments: |
1617 | - old_manifest = headers.get('x-object-manifest') |
1618 | - except ClientException, err: |
1619 | - if err.http_status != 404: |
1620 | - raise |
1621 | - if options.segment_size and \ |
1622 | - getsize(path) < options.segment_size: |
1623 | - full_size = getsize(path) |
1624 | - segment_queue = Queue(10000) |
1625 | - segment_threads = [QueueFunctionThread(segment_queue, |
1626 | - _segment_job, create_connection()) for _junk in |
1627 | - xrange(10)] |
1628 | - for thread in segment_threads: |
1629 | - thread.start() |
1630 | - segment = 0 |
1631 | - segment_start = 0 |
1632 | - while segment_start < full_size: |
1633 | - segment_size = int(options.segment_size) |
1634 | - if segment_start + segment_size > full_size: |
1635 | - segment_size = full_size - segment_start |
1636 | - segment_queue.put({'path': path, |
1637 | - 'obj': '%s/%s/%s/%08d' % (obj, |
1638 | - put_headers['x-object-meta-mtime'], full_size, |
1639 | - segment), |
1640 | - 'segment_start': segment_start, |
1641 | - 'segment_size': segment_size, |
1642 | - 'log_line': '%s segment %s' % (obj, segment)}) |
1643 | - segment += 1 |
1644 | - segment_start += segment_size |
1645 | - while not segment_queue.empty(): |
1646 | - sleep(0.01) |
1647 | - for thread in segment_threads: |
1648 | - thread.abort = True |
1649 | - while thread.isAlive(): |
1650 | - thread.join(0.01) |
1651 | - if put_errors_from_threads(segment_threads, error_queue): |
1652 | - raise ClientException('Aborting manifest creation ' |
1653 | - 'because not all segments could be uploaded. %s/%s' |
1654 | - % (container, obj)) |
1655 | - new_object_manifest = '%s_segments/%s/%s/%s/' % ( |
1656 | - container, obj, put_headers['x-object-meta-mtime'], |
1657 | - full_size) |
1658 | - if old_manifest == new_object_manifest: |
1659 | - old_manifest = None |
1660 | - put_headers['x-object-manifest'] = new_object_manifest |
1661 | - conn.put_object(container, obj, '', content_length=0, |
1662 | - headers=put_headers) |
1663 | - else: |
1664 | - conn.put_object(container, obj, open(path, 'rb'), |
1665 | - content_length=getsize(path), headers=put_headers) |
1666 | - if old_manifest: |
1667 | - segment_queue = Queue(10000) |
1668 | - scontainer, sprefix = old_manifest.split('/', 1) |
1669 | - for delobj in conn.get_container(scontainer, |
1670 | - prefix=sprefix)[1]: |
1671 | - segment_queue.put({'delete': True, |
1672 | - 'container': scontainer, 'obj': delobj['name']}) |
1673 | - if not segment_queue.empty(): |
1674 | - segment_threads = [QueueFunctionThread(segment_queue, |
1675 | - _segment_job, create_connection()) for _junk in |
1676 | - xrange(10)] |
1677 | - for thread in segment_threads: |
1678 | - thread.start() |
1679 | - while not segment_queue.empty(): |
1680 | - sleep(0.01) |
1681 | - for thread in segment_threads: |
1682 | - thread.abort = True |
1683 | - while thread.isAlive(): |
1684 | - thread.join(0.01) |
1685 | - put_errors_from_threads(segment_threads, error_queue) |
1686 | - if options.verbose: |
1687 | - if conn.attempts > 1: |
1688 | - print_queue.put( |
1689 | - '%s [after %d attempts]' % (obj, conn.attempts)) |
1690 | - else: |
1691 | - print_queue.put(obj) |
1692 | - except OSError, err: |
1693 | - if err.errno != ENOENT: |
1694 | - raise |
1695 | - error_queue.put('Local file %s not found' % repr(path)) |
1696 | - |
1697 | - def _upload_dir(path): |
1698 | - names = listdir(path) |
1699 | - if not names: |
1700 | - object_queue.put({'path': path, 'dir_marker': True}) |
1701 | - else: |
1702 | - for name in listdir(path): |
1703 | - subpath = join(path, name) |
1704 | - if isdir(subpath): |
1705 | - _upload_dir(subpath) |
1706 | - else: |
1707 | - object_queue.put({'path': subpath}) |
1708 | - |
1709 | - url, token = get_auth(options.auth, options.user, options.key, |
1710 | - snet=options.snet) |
1711 | - create_connection = lambda: Connection(options.auth, options.user, |
1712 | - options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
1713 | - object_threads = [QueueFunctionThread(object_queue, _object_job, |
1714 | - create_connection()) for _junk in xrange(10)] |
1715 | - for thread in object_threads: |
1716 | - thread.start() |
1717 | - conn = create_connection() |
1718 | - # Try to create the container, just in case it doesn't exist. If this |
1719 | - # fails, it might just be because the user doesn't have container PUT |
1720 | - # permissions, so we'll ignore any error. If there's really a problem, |
1721 | - # it'll surface on the first object PUT. |
1722 | - try: |
1723 | - conn.put_container(args[0]) |
1724 | - if options.segment_size is not None: |
1725 | - conn.put_container(args[0] + '_segments') |
1726 | - except Exception: |
1727 | - pass |
1728 | - try: |
1729 | - for arg in args[1:]: |
1730 | - if isdir(arg): |
1731 | - _upload_dir(arg) |
1732 | - else: |
1733 | - object_queue.put({'path': arg}) |
1734 | - while not object_queue.empty(): |
1735 | - sleep(0.01) |
1736 | - for thread in object_threads: |
1737 | - thread.abort = True |
1738 | - while thread.isAlive(): |
1739 | - thread.join(0.01) |
1740 | - put_errors_from_threads(object_threads, error_queue) |
1741 | - except ClientException, err: |
1742 | - if err.http_status != 404: |
1743 | - raise |
1744 | - error_queue.put('Account not found') |
1745 | - |
1746 | - |
1747 | -def parse_args(parser, args, enforce_requires=True): |
1748 | - if not args: |
1749 | - args = ['-h'] |
1750 | - (options, args) = parser.parse_args(args) |
1751 | - if enforce_requires and \ |
1752 | - not (options.auth and options.user and options.key): |
1753 | - exit(''' |
1754 | -Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or |
1755 | -overridden with -A, -U, or -K.'''.strip('\n')) |
1756 | - return options, args |
1757 | - |
1758 | - |
1759 | -if __name__ == '__main__': |
1760 | - parser = OptionParser(version='%prog 1.0', usage=''' |
1761 | -Usage: %%prog <command> [options] [args] |
1762 | - |
1763 | -Commands: |
1764 | - %(st_stat_help)s |
1765 | - %(st_list_help)s |
1766 | - %(st_upload_help)s |
1767 | - %(st_post_help)s |
1768 | - %(st_download_help)s |
1769 | - %(st_delete_help)s |
1770 | - |
1771 | -Example: |
1772 | - %%prog -A https://auth.api.rackspacecloud.com/v1.0 -U user -K key stat |
1773 | -'''.strip('\n') % globals()) |
1774 | - parser.add_option('-s', '--snet', action='store_true', dest='snet', |
1775 | - default=False, help='Use SERVICENET internal network') |
1776 | - parser.add_option('-v', '--verbose', action='count', dest='verbose', |
1777 | - default=1, help='Print more info') |
1778 | - parser.add_option('-q', '--quiet', action='store_const', dest='verbose', |
1779 | - const=0, default=1, help='Suppress status output') |
1780 | - parser.add_option('-A', '--auth', dest='auth', |
1781 | - default=environ.get('ST_AUTH'), |
1782 | - help='URL for obtaining an auth token') |
1783 | - parser.add_option('-U', '--user', dest='user', |
1784 | - default=environ.get('ST_USER'), |
1785 | - help='User name for obtaining an auth token') |
1786 | - parser.add_option('-K', '--key', dest='key', |
1787 | - default=environ.get('ST_KEY'), |
1788 | - help='Key for obtaining an auth token') |
1789 | - parser.disable_interspersed_args() |
1790 | - (options, args) = parse_args(parser, argv[1:], enforce_requires=False) |
1791 | - parser.enable_interspersed_args() |
1792 | - |
1793 | - commands = ('delete', 'download', 'list', 'post', 'stat', 'upload') |
1794 | - if not args or args[0] not in commands: |
1795 | - parser.print_usage() |
1796 | - if args: |
1797 | - exit('no such command: %s' % args[0]) |
1798 | - exit() |
1799 | - |
1800 | - print_queue = Queue(10000) |
1801 | - |
1802 | - def _print(item): |
1803 | - if isinstance(item, unicode): |
1804 | - item = item.encode('utf8') |
1805 | - print item |
1806 | - |
1807 | - print_thread = QueueFunctionThread(print_queue, _print) |
1808 | - print_thread.start() |
1809 | - |
1810 | - error_queue = Queue(10000) |
1811 | - |
1812 | - def _error(item): |
1813 | - if isinstance(item, unicode): |
1814 | - item = item.encode('utf8') |
1815 | - print >> stderr, item |
1816 | - |
1817 | - error_thread = QueueFunctionThread(error_queue, _error) |
1818 | - error_thread.start() |
1819 | - |
1820 | - try: |
1821 | - parser.usage = globals()['st_%s_help' % args[0]] |
1822 | - try: |
1823 | - globals()['st_%s' % args[0]](parser, argv[1:], print_queue, |
1824 | - error_queue) |
1825 | - except (ClientException, HTTPException, socket.error), err: |
1826 | - error_queue.put(str(err)) |
1827 | - while not print_queue.empty(): |
1828 | - sleep(0.01) |
1829 | - print_thread.abort = True |
1830 | - while print_thread.isAlive(): |
1831 | - print_thread.join(0.01) |
1832 | - while not error_queue.empty(): |
1833 | - sleep(0.01) |
1834 | - error_thread.abort = True |
1835 | - while error_thread.isAlive(): |
1836 | - error_thread.join(0.01) |
1837 | - except (SystemExit, Exception): |
1838 | - for thread in threading_enumerate(): |
1839 | - thread.abort = True |
1840 | - raise |
1841 | |
1842 | === removed file 'bin/swauth-add-account' |
1843 | --- bin/swauth-add-account 2011-04-18 16:08:48 +0000 |
1844 | +++ bin/swauth-add-account 1970-01-01 00:00:00 +0000 |
1845 | @@ -1,68 +0,0 @@ |
1846 | -#!/usr/bin/env python |
1847 | -# Copyright (c) 2010 OpenStack, LLC. |
1848 | -# |
1849 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
1850 | -# you may not use this file except in compliance with the License. |
1851 | -# You may obtain a copy of the License at |
1852 | -# |
1853 | -# http://www.apache.org/licenses/LICENSE-2.0 |
1854 | -# |
1855 | -# Unless required by applicable law or agreed to in writing, software |
1856 | -# distributed under the License is distributed on an "AS IS" BASIS, |
1857 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
1858 | -# implied. |
1859 | -# See the License for the specific language governing permissions and |
1860 | -# limitations under the License. |
1861 | - |
1862 | -import gettext |
1863 | -from optparse import OptionParser |
1864 | -from os.path import basename |
1865 | -from sys import argv, exit |
1866 | - |
1867 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
1868 | -from swift.common.utils import urlparse |
1869 | - |
1870 | - |
1871 | -if __name__ == '__main__': |
1872 | - gettext.install('swift', unicode=1) |
1873 | - parser = OptionParser(usage='Usage: %prog [options] <account>') |
1874 | - parser.add_option('-s', '--suffix', dest='suffix', |
1875 | - default='', help='The suffix to use with the reseller prefix as the ' |
1876 | - 'storage account name (default: <randomly-generated-uuid4>) Note: If ' |
1877 | - 'the account already exists, this will have no effect on existing ' |
1878 | - 'service URLs. Those will need to be updated with ' |
1879 | - 'swauth-set-account-service') |
1880 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
1881 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
1882 | - 'subsystem (default: http://127.0.0.1:8080/auth/)') |
1883 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
1884 | - default='.super_admin', help='The user with admin rights to add users ' |
1885 | - '(default: .super_admin).') |
1886 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
1887 | - help='The key for the user with admin rights to add users.') |
1888 | - args = argv[1:] |
1889 | - if not args: |
1890 | - args.append('-h') |
1891 | - (options, args) = parser.parse_args(args) |
1892 | - if len(args) != 1: |
1893 | - parser.parse_args(['-h']) |
1894 | - account = args[0] |
1895 | - parsed = urlparse(options.admin_url) |
1896 | - if parsed.scheme not in ('http', 'https'): |
1897 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
1898 | - (parsed.scheme, repr(options.admin_url))) |
1899 | - parsed_path = parsed.path |
1900 | - if not parsed_path: |
1901 | - parsed_path = '/' |
1902 | - elif parsed_path[-1] != '/': |
1903 | - parsed_path += '/' |
1904 | - path = '%sv2/%s' % (parsed_path, account) |
1905 | - headers = {'X-Auth-Admin-User': options.admin_user, |
1906 | - 'X-Auth-Admin-Key': options.admin_key} |
1907 | - if options.suffix: |
1908 | - headers['X-Account-Suffix'] = options.suffix |
1909 | - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers, |
1910 | - ssl=(parsed.scheme == 'https')) |
1911 | - resp = conn.getresponse() |
1912 | - if resp.status // 100 != 2: |
1913 | - exit('Account creation failed: %s %s' % (resp.status, resp.reason)) |
1914 | |
1915 | === removed file 'bin/swauth-add-user' |
1916 | --- bin/swauth-add-user 2011-04-18 16:08:48 +0000 |
1917 | +++ bin/swauth-add-user 1970-01-01 00:00:00 +0000 |
1918 | @@ -1,93 +0,0 @@ |
1919 | -#!/usr/bin/env python |
1920 | -# Copyright (c) 2010 OpenStack, LLC. |
1921 | -# |
1922 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
1923 | -# you may not use this file except in compliance with the License. |
1924 | -# You may obtain a copy of the License at |
1925 | -# |
1926 | -# http://www.apache.org/licenses/LICENSE-2.0 |
1927 | -# |
1928 | -# Unless required by applicable law or agreed to in writing, software |
1929 | -# distributed under the License is distributed on an "AS IS" BASIS, |
1930 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
1931 | -# implied. |
1932 | -# See the License for the specific language governing permissions and |
1933 | -# limitations under the License. |
1934 | - |
1935 | -import gettext |
1936 | -from optparse import OptionParser |
1937 | -from os.path import basename |
1938 | -from sys import argv, exit |
1939 | - |
1940 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
1941 | -from swift.common.utils import urlparse |
1942 | - |
1943 | - |
1944 | -if __name__ == '__main__': |
1945 | - gettext.install('swift', unicode=1) |
1946 | - parser = OptionParser( |
1947 | - usage='Usage: %prog [options] <account> <user> <password>') |
1948 | - parser.add_option('-a', '--admin', dest='admin', action='store_true', |
1949 | - default=False, help='Give the user administrator access; otherwise ' |
1950 | - 'the user will only have access to containers specifically allowed ' |
1951 | - 'with ACLs.') |
1952 | - parser.add_option('-r', '--reseller-admin', dest='reseller_admin', |
1953 | - action='store_true', default=False, help='Give the user full reseller ' |
1954 | - 'administrator access, giving them full access to all accounts within ' |
1955 | - 'the reseller, including the ability to create new accounts. Creating ' |
1956 | - 'a new reseller admin requires super_admin rights.') |
1957 | - parser.add_option('-s', '--suffix', dest='suffix', |
1958 | - default='', help='The suffix to use with the reseller prefix as the ' |
1959 | - 'storage account name (default: <randomly-generated-uuid4>) Note: If ' |
1960 | - 'the account already exists, this will have no effect on existing ' |
1961 | - 'service URLs. Those will need to be updated with ' |
1962 | - 'swauth-set-account-service') |
1963 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
1964 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
1965 | - 'subsystem (default: http://127.0.0.1:8080/auth/') |
1966 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
1967 | - default='.super_admin', help='The user with admin rights to add users ' |
1968 | - '(default: .super_admin).') |
1969 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
1970 | - help='The key for the user with admin rights to add users.') |
1971 | - args = argv[1:] |
1972 | - if not args: |
1973 | - args.append('-h') |
1974 | - (options, args) = parser.parse_args(args) |
1975 | - if len(args) != 3: |
1976 | - parser.parse_args(['-h']) |
1977 | - account, user, password = args |
1978 | - parsed = urlparse(options.admin_url) |
1979 | - if parsed.scheme not in ('http', 'https'): |
1980 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
1981 | - (parsed.scheme, repr(options.admin_url))) |
1982 | - parsed_path = parsed.path |
1983 | - if not parsed_path: |
1984 | - parsed_path = '/' |
1985 | - elif parsed_path[-1] != '/': |
1986 | - parsed_path += '/' |
1987 | - # Ensure the account exists |
1988 | - path = '%sv2/%s' % (parsed_path, account) |
1989 | - headers = {'X-Auth-Admin-User': options.admin_user, |
1990 | - 'X-Auth-Admin-Key': options.admin_key} |
1991 | - if options.suffix: |
1992 | - headers['X-Account-Suffix'] = options.suffix |
1993 | - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers, |
1994 | - ssl=(parsed.scheme == 'https')) |
1995 | - resp = conn.getresponse() |
1996 | - if resp.status // 100 != 2: |
1997 | - print 'Account creation failed: %s %s' % (resp.status, resp.reason) |
1998 | - # Add the user |
1999 | - path = '%sv2/%s/%s' % (parsed_path, account, user) |
2000 | - headers = {'X-Auth-Admin-User': options.admin_user, |
2001 | - 'X-Auth-Admin-Key': options.admin_key, |
2002 | - 'X-Auth-User-Key': password} |
2003 | - if options.admin: |
2004 | - headers['X-Auth-User-Admin'] = 'true' |
2005 | - if options.reseller_admin: |
2006 | - headers['X-Auth-User-Reseller-Admin'] = 'true' |
2007 | - conn = http_connect(parsed.hostname, parsed.port, 'PUT', path, headers, |
2008 | - ssl=(parsed.scheme == 'https')) |
2009 | - resp = conn.getresponse() |
2010 | - if resp.status // 100 != 2: |
2011 | - exit('User creation failed: %s %s' % (resp.status, resp.reason)) |
2012 | |
2013 | === removed file 'bin/swauth-cleanup-tokens' |
2014 | --- bin/swauth-cleanup-tokens 2011-04-18 16:08:48 +0000 |
2015 | +++ bin/swauth-cleanup-tokens 1970-01-01 00:00:00 +0000 |
2016 | @@ -1,118 +0,0 @@ |
2017 | -#!/usr/bin/env python |
2018 | -# Copyright (c) 2010 OpenStack, LLC. |
2019 | -# |
2020 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2021 | -# you may not use this file except in compliance with the License. |
2022 | -# You may obtain a copy of the License at |
2023 | -# |
2024 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2025 | -# |
2026 | -# Unless required by applicable law or agreed to in writing, software |
2027 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2028 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2029 | -# implied. |
2030 | -# See the License for the specific language governing permissions and |
2031 | -# limitations under the License. |
2032 | - |
2033 | -try: |
2034 | - import simplejson as json |
2035 | -except ImportError: |
2036 | - import json |
2037 | -import gettext |
2038 | -import re |
2039 | -from datetime import datetime, timedelta |
2040 | -from optparse import OptionParser |
2041 | -from sys import argv, exit |
2042 | -from time import sleep, time |
2043 | - |
2044 | -from swift.common.client import Connection, ClientException |
2045 | - |
2046 | - |
2047 | -if __name__ == '__main__': |
2048 | - gettext.install('swift', unicode=1) |
2049 | - parser = OptionParser(usage='Usage: %prog [options]') |
2050 | - parser.add_option('-t', '--token-life', dest='token_life', |
2051 | - default='86400', help='The expected life of tokens; token objects ' |
2052 | - 'modified more than this number of seconds ago will be checked for ' |
2053 | - 'expiration (default: 86400).') |
2054 | - parser.add_option('-s', '--sleep', dest='sleep', |
2055 | - default='0.1', help='The number of seconds to sleep between token ' |
2056 | - 'checks (default: 0.1)') |
2057 | - parser.add_option('-v', '--verbose', dest='verbose', action='store_true', |
2058 | - default=False, help='Outputs everything done instead of just the ' |
2059 | - 'deletions.') |
2060 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2061 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2062 | - 'subsystem (default: http://127.0.0.1:8080/auth/)') |
2063 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2064 | - help='The key for .super_admin.') |
2065 | - args = argv[1:] |
2066 | - if not args: |
2067 | - args.append('-h') |
2068 | - (options, args) = parser.parse_args(args) |
2069 | - if len(args) != 0: |
2070 | - parser.parse_args(['-h']) |
2071 | - options.admin_url = options.admin_url.rstrip('/') |
2072 | - if not options.admin_url.endswith('/v1.0'): |
2073 | - options.admin_url += '/v1.0' |
2074 | - options.admin_user = '.super_admin:.super_admin' |
2075 | - options.token_life = timedelta(0, float(options.token_life)) |
2076 | - options.sleep = float(options.sleep) |
2077 | - conn = Connection(options.admin_url, options.admin_user, options.admin_key) |
2078 | - for x in xrange(16): |
2079 | - container = '.token_%x' % x |
2080 | - marker = None |
2081 | - while True: |
2082 | - if options.verbose: |
2083 | - print 'GET %s?marker=%s' % (container, marker) |
2084 | - try: |
2085 | - objs = conn.get_container(container, marker=marker)[1] |
2086 | - except ClientException, e: |
2087 | - if e.http_status == 404: |
2088 | - exit('Container %s not found. swauth-prep needs to be ' |
2089 | - 'rerun' % (container)) |
2090 | - else: |
2091 | - exit('Object listing on container %s failed with status ' |
2092 | - 'code %d' % (container, e.http_status)) |
2093 | - if objs: |
2094 | - marker = objs[-1]['name'] |
2095 | - else: |
2096 | - if options.verbose: |
2097 | - print 'No more objects in %s' % container |
2098 | - break |
2099 | - for obj in objs: |
2100 | - last_modified = datetime(*map(int, re.split('[^\d]', |
2101 | - obj['last_modified'])[:-1])) |
2102 | - ago = datetime.utcnow() - last_modified |
2103 | - if ago > options.token_life: |
2104 | - if options.verbose: |
2105 | - print '%s/%s last modified %ss ago; investigating' % \ |
2106 | - (container, obj['name'], |
2107 | - ago.days * 86400 + ago.seconds) |
2108 | - print 'GET %s/%s' % (container, obj['name']) |
2109 | - detail = conn.get_object(container, obj['name'])[1] |
2110 | - detail = json.loads(detail) |
2111 | - if detail['expires'] < time(): |
2112 | - if options.verbose: |
2113 | - print '%s/%s expired %ds ago; deleting' % \ |
2114 | - (container, obj['name'], |
2115 | - time() - detail['expires']) |
2116 | - print 'DELETE %s/%s' % (container, obj['name']) |
2117 | - try: |
2118 | - conn.delete_object(container, obj['name']) |
2119 | - except ClientException, e: |
2120 | - if e.http_status != 404: |
2121 | - print 'DELETE of %s/%s failed with status ' \ |
2122 | - 'code %d' % (container, obj['name'], |
2123 | - e.http_status) |
2124 | - elif options.verbose: |
2125 | - print "%s/%s won't expire for %ds; skipping" % \ |
2126 | - (container, obj['name'], |
2127 | - detail['expires'] - time()) |
2128 | - elif options.verbose: |
2129 | - print '%s/%s last modified %ss ago; skipping' % \ |
2130 | - (container, obj['name'], |
2131 | - ago.days * 86400 + ago.seconds) |
2132 | - sleep(options.sleep) |
2133 | - if options.verbose: |
2134 | - print 'Done.' |
2135 | |
2136 | === removed file 'bin/swauth-delete-account' |
2137 | --- bin/swauth-delete-account 2011-04-18 16:08:48 +0000 |
2138 | +++ bin/swauth-delete-account 1970-01-01 00:00:00 +0000 |
2139 | @@ -1,60 +0,0 @@ |
2140 | -#!/usr/bin/env python |
2141 | -# Copyright (c) 2010 OpenStack, LLC. |
2142 | -# |
2143 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2144 | -# you may not use this file except in compliance with the License. |
2145 | -# You may obtain a copy of the License at |
2146 | -# |
2147 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2148 | -# |
2149 | -# Unless required by applicable law or agreed to in writing, software |
2150 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2151 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2152 | -# implied. |
2153 | -# See the License for the specific language governing permissions and |
2154 | -# limitations under the License. |
2155 | - |
2156 | -import gettext |
2157 | -from optparse import OptionParser |
2158 | -from os.path import basename |
2159 | -from sys import argv, exit |
2160 | - |
2161 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
2162 | -from swift.common.utils import urlparse |
2163 | - |
2164 | - |
2165 | -if __name__ == '__main__': |
2166 | - gettext.install('swift', unicode=1) |
2167 | - parser = OptionParser(usage='Usage: %prog [options] <account>') |
2168 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2169 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2170 | - 'subsystem (default: http://127.0.0.1:8080/auth/') |
2171 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
2172 | - default='.super_admin', help='The user with admin rights to add users ' |
2173 | - '(default: .super_admin).') |
2174 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2175 | - help='The key for the user with admin rights to add users.') |
2176 | - args = argv[1:] |
2177 | - if not args: |
2178 | - args.append('-h') |
2179 | - (options, args) = parser.parse_args(args) |
2180 | - if len(args) != 1: |
2181 | - parser.parse_args(['-h']) |
2182 | - account = args[0] |
2183 | - parsed = urlparse(options.admin_url) |
2184 | - if parsed.scheme not in ('http', 'https'): |
2185 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
2186 | - (parsed.scheme, repr(options.admin_url))) |
2187 | - parsed_path = parsed.path |
2188 | - if not parsed_path: |
2189 | - parsed_path = '/' |
2190 | - elif parsed_path[-1] != '/': |
2191 | - parsed_path += '/' |
2192 | - path = '%sv2/%s' % (parsed_path, account) |
2193 | - headers = {'X-Auth-Admin-User': options.admin_user, |
2194 | - 'X-Auth-Admin-Key': options.admin_key} |
2195 | - conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, |
2196 | - ssl=(parsed.scheme == 'https')) |
2197 | - resp = conn.getresponse() |
2198 | - if resp.status // 100 != 2: |
2199 | - exit('Account deletion failed: %s %s' % (resp.status, resp.reason)) |
2200 | |
2201 | === removed file 'bin/swauth-delete-user' |
2202 | --- bin/swauth-delete-user 2011-04-18 16:08:48 +0000 |
2203 | +++ bin/swauth-delete-user 1970-01-01 00:00:00 +0000 |
2204 | @@ -1,60 +0,0 @@ |
2205 | -#!/usr/bin/env python |
2206 | -# Copyright (c) 2010 OpenStack, LLC. |
2207 | -# |
2208 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2209 | -# you may not use this file except in compliance with the License. |
2210 | -# You may obtain a copy of the License at |
2211 | -# |
2212 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2213 | -# |
2214 | -# Unless required by applicable law or agreed to in writing, software |
2215 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2216 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2217 | -# implied. |
2218 | -# See the License for the specific language governing permissions and |
2219 | -# limitations under the License. |
2220 | - |
2221 | -import gettext |
2222 | -from optparse import OptionParser |
2223 | -from os.path import basename |
2224 | -from sys import argv, exit |
2225 | - |
2226 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
2227 | -from swift.common.utils import urlparse |
2228 | - |
2229 | - |
2230 | -if __name__ == '__main__': |
2231 | - gettext.install('swift', unicode=1) |
2232 | - parser = OptionParser(usage='Usage: %prog [options] <account> <user>') |
2233 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2234 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2235 | - 'subsystem (default: http://127.0.0.1:8080/auth/') |
2236 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
2237 | - default='.super_admin', help='The user with admin rights to add users ' |
2238 | - '(default: .super_admin).') |
2239 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2240 | - help='The key for the user with admin rights to add users.') |
2241 | - args = argv[1:] |
2242 | - if not args: |
2243 | - args.append('-h') |
2244 | - (options, args) = parser.parse_args(args) |
2245 | - if len(args) != 2: |
2246 | - parser.parse_args(['-h']) |
2247 | - account, user = args |
2248 | - parsed = urlparse(options.admin_url) |
2249 | - if parsed.scheme not in ('http', 'https'): |
2250 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
2251 | - (parsed.scheme, repr(options.admin_url))) |
2252 | - parsed_path = parsed.path |
2253 | - if not parsed_path: |
2254 | - parsed_path = '/' |
2255 | - elif parsed_path[-1] != '/': |
2256 | - parsed_path += '/' |
2257 | - path = '%sv2/%s/%s' % (parsed_path, account, user) |
2258 | - headers = {'X-Auth-Admin-User': options.admin_user, |
2259 | - 'X-Auth-Admin-Key': options.admin_key} |
2260 | - conn = http_connect(parsed.hostname, parsed.port, 'DELETE', path, headers, |
2261 | - ssl=(parsed.scheme == 'https')) |
2262 | - resp = conn.getresponse() |
2263 | - if resp.status // 100 != 2: |
2264 | - exit('User deletion failed: %s %s' % (resp.status, resp.reason)) |
2265 | |
2266 | === removed file 'bin/swauth-list' |
2267 | --- bin/swauth-list 2011-04-18 16:08:48 +0000 |
2268 | +++ bin/swauth-list 1970-01-01 00:00:00 +0000 |
2269 | @@ -1,86 +0,0 @@ |
2270 | -#!/usr/bin/env python |
2271 | -# Copyright (c) 2010 OpenStack, LLC. |
2272 | -# |
2273 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2274 | -# you may not use this file except in compliance with the License. |
2275 | -# You may obtain a copy of the License at |
2276 | -# |
2277 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2278 | -# |
2279 | -# Unless required by applicable law or agreed to in writing, software |
2280 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2281 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2282 | -# implied. |
2283 | -# See the License for the specific language governing permissions and |
2284 | -# limitations under the License. |
2285 | - |
2286 | -try: |
2287 | - import simplejson as json |
2288 | -except ImportError: |
2289 | - import json |
2290 | -import gettext |
2291 | -from optparse import OptionParser |
2292 | -from os.path import basename |
2293 | -from sys import argv, exit |
2294 | - |
2295 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
2296 | -from swift.common.utils import urlparse |
2297 | - |
2298 | - |
2299 | -if __name__ == '__main__': |
2300 | - gettext.install('swift', unicode=1) |
2301 | - parser = OptionParser(usage=''' |
2302 | -Usage: %prog [options] [account] [user] |
2303 | - |
2304 | -If [account] and [user] are omitted, a list of accounts will be output. |
2305 | - |
2306 | -If [account] is included but not [user], an account's information will be |
2307 | -output, including a list of users within the account. |
2308 | - |
2309 | -If [account] and [user] are included, the user's information will be output, |
2310 | -including a list of groups the user belongs to. |
2311 | - |
2312 | -If the [user] is '.groups', the active groups for the account will be listed. |
2313 | -'''.strip()) |
2314 | - parser.add_option('-p', '--plain-text', dest='plain_text', |
2315 | - action='store_true', default=False, help='Changes the output from ' |
2316 | - 'JSON to plain text. This will cause an account to list only the ' |
2317 | - 'users and a user to list only the groups.') |
2318 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2319 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2320 | - 'subsystem (default: http://127.0.0.1:8080/auth/') |
2321 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
2322 | - default='.super_admin', help='The user with admin rights to add users ' |
2323 | - '(default: .super_admin).') |
2324 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2325 | - help='The key for the user with admin rights to add users.') |
2326 | - args = argv[1:] |
2327 | - if not args: |
2328 | - args.append('-h') |
2329 | - (options, args) = parser.parse_args(args) |
2330 | - if len(args) > 2: |
2331 | - parser.parse_args(['-h']) |
2332 | - parsed = urlparse(options.admin_url) |
2333 | - if parsed.scheme not in ('http', 'https'): |
2334 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
2335 | - (parsed.scheme, repr(options.admin_url))) |
2336 | - parsed_path = parsed.path |
2337 | - if not parsed_path: |
2338 | - parsed_path = '/' |
2339 | - elif parsed_path[-1] != '/': |
2340 | - parsed_path += '/' |
2341 | - path = '%sv2/%s' % (parsed_path, '/'.join(args)) |
2342 | - headers = {'X-Auth-Admin-User': options.admin_user, |
2343 | - 'X-Auth-Admin-Key': options.admin_key} |
2344 | - conn = http_connect(parsed.hostname, parsed.port, 'GET', path, headers, |
2345 | - ssl=(parsed.scheme == 'https')) |
2346 | - resp = conn.getresponse() |
2347 | - body = resp.read() |
2348 | - if resp.status // 100 != 2: |
2349 | - exit('List failed: %s %s' % (resp.status, resp.reason)) |
2350 | - if options.plain_text: |
2351 | - info = json.loads(body) |
2352 | - for group in info[['accounts', 'users', 'groups'][len(args)]]: |
2353 | - print group['name'] |
2354 | - else: |
2355 | - print body |
2356 | |
2357 | === removed file 'bin/swauth-prep' |
2358 | --- bin/swauth-prep 2011-04-18 16:08:48 +0000 |
2359 | +++ bin/swauth-prep 1970-01-01 00:00:00 +0000 |
2360 | @@ -1,59 +0,0 @@ |
2361 | -#!/usr/bin/env python |
2362 | -# Copyright (c) 2010 OpenStack, LLC. |
2363 | -# |
2364 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2365 | -# you may not use this file except in compliance with the License. |
2366 | -# You may obtain a copy of the License at |
2367 | -# |
2368 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2369 | -# |
2370 | -# Unless required by applicable law or agreed to in writing, software |
2371 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2372 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2373 | -# implied. |
2374 | -# See the License for the specific language governing permissions and |
2375 | -# limitations under the License. |
2376 | - |
2377 | -import gettext |
2378 | -from optparse import OptionParser |
2379 | -from os.path import basename |
2380 | -from sys import argv, exit |
2381 | - |
2382 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
2383 | -from swift.common.utils import urlparse |
2384 | - |
2385 | - |
2386 | -if __name__ == '__main__': |
2387 | - gettext.install('swift', unicode=1) |
2388 | - parser = OptionParser(usage='Usage: %prog [options]') |
2389 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2390 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2391 | - 'subsystem (default: http://127.0.0.1:8080/auth/') |
2392 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
2393 | - default='.super_admin', help='The user with admin rights to add users ' |
2394 | - '(default: .super_admin).') |
2395 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2396 | - help='The key for the user with admin rights to add users.') |
2397 | - args = argv[1:] |
2398 | - if not args: |
2399 | - args.append('-h') |
2400 | - (options, args) = parser.parse_args(args) |
2401 | - if args: |
2402 | - parser.parse_args(['-h']) |
2403 | - parsed = urlparse(options.admin_url) |
2404 | - if parsed.scheme not in ('http', 'https'): |
2405 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
2406 | - (parsed.scheme, repr(options.admin_url))) |
2407 | - parsed_path = parsed.path |
2408 | - if not parsed_path: |
2409 | - parsed_path = '/' |
2410 | - elif parsed_path[-1] != '/': |
2411 | - parsed_path += '/' |
2412 | - path = '%sv2/.prep' % parsed_path |
2413 | - headers = {'X-Auth-Admin-User': options.admin_user, |
2414 | - 'X-Auth-Admin-Key': options.admin_key} |
2415 | - conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers, |
2416 | - ssl=(parsed.scheme == 'https')) |
2417 | - resp = conn.getresponse() |
2418 | - if resp.status // 100 != 2: |
2419 | - exit('Auth subsystem prep failed: %s %s' % (resp.status, resp.reason)) |
2420 | |
2421 | === removed file 'bin/swauth-set-account-service' |
2422 | --- bin/swauth-set-account-service 2011-04-18 16:08:48 +0000 |
2423 | +++ bin/swauth-set-account-service 1970-01-01 00:00:00 +0000 |
2424 | @@ -1,73 +0,0 @@ |
2425 | -#!/usr/bin/env python |
2426 | -# Copyright (c) 2010 OpenStack, LLC. |
2427 | -# |
2428 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
2429 | -# you may not use this file except in compliance with the License. |
2430 | -# You may obtain a copy of the License at |
2431 | -# |
2432 | -# http://www.apache.org/licenses/LICENSE-2.0 |
2433 | -# |
2434 | -# Unless required by applicable law or agreed to in writing, software |
2435 | -# distributed under the License is distributed on an "AS IS" BASIS, |
2436 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2437 | -# implied. |
2438 | -# See the License for the specific language governing permissions and |
2439 | -# limitations under the License. |
2440 | - |
2441 | -try: |
2442 | - import simplejson as json |
2443 | -except ImportError: |
2444 | - import json |
2445 | -import gettext |
2446 | -from optparse import OptionParser |
2447 | -from os.path import basename |
2448 | -from sys import argv, exit |
2449 | - |
2450 | -from swift.common.bufferedhttp import http_connect_raw as http_connect |
2451 | -from swift.common.utils import urlparse |
2452 | - |
2453 | - |
2454 | -if __name__ == '__main__': |
2455 | - gettext.install('swift', unicode=1) |
2456 | - parser = OptionParser(usage=''' |
2457 | -Usage: %prog [options] <account> <service> <name> <value> |
2458 | - |
2459 | -Sets a service URL for an account. Can only be set by a reseller admin. |
2460 | - |
2461 | -Example: %prog -K swauthkey test storage local http://127.0.0.1:8080/v1/AUTH_018c3946-23f8-4efb-a8fb-b67aae8e4162 |
2462 | -'''.strip()) |
2463 | - parser.add_option('-A', '--admin-url', dest='admin_url', |
2464 | - default='http://127.0.0.1:8080/auth/', help='The URL to the auth ' |
2465 | - 'subsystem (default: http://127.0.0.1:8080/auth/)') |
2466 | - parser.add_option('-U', '--admin-user', dest='admin_user', |
2467 | - default='.super_admin', help='The user with admin rights to add users ' |
2468 | - '(default: .super_admin).') |
2469 | - parser.add_option('-K', '--admin-key', dest='admin_key', |
2470 | - help='The key for the user with admin rights to add users.') |
2471 | - args = argv[1:] |
2472 | - if not args: |
2473 | - args.append('-h') |
2474 | - (options, args) = parser.parse_args(args) |
2475 | - if len(args) != 4: |
2476 | - parser.parse_args(['-h']) |
2477 | - account, service, name, url = args |
2478 | - parsed = urlparse(options.admin_url) |
2479 | - if parsed.scheme not in ('http', 'https'): |
2480 | - raise Exception('Cannot handle protocol scheme %s for url %s' % |
2481 | - (parsed.scheme, repr(options.admin_url))) |
2482 | - parsed_path = parsed.path |
2483 | - if not parsed_path: |
2484 | - parsed_path = '/' |
2485 | - elif parsed_path[-1] != '/': |
2486 | - parsed_path += '/' |
2487 | - path = '%sv2/%s/.services' % (parsed_path, account) |
2488 | - body = json.dumps({service: {name: url}}) |
2489 | - headers = {'Content-Length': str(len(body)), |
2490 | - 'X-Auth-Admin-User': options.admin_user, |
2491 | - 'X-Auth-Admin-Key': options.admin_key} |
2492 | - conn = http_connect(parsed.hostname, parsed.port, 'POST', path, headers, |
2493 | - ssl=(parsed.scheme == 'https')) |
2494 | - conn.send(body) |
2495 | - resp = conn.getresponse() |
2496 | - if resp.status // 100 != 2: |
2497 | - exit('Service set failed: %s %s' % (resp.status, resp.reason)) |
2498 | |
2499 | === added file 'bin/swift' |
2500 | --- bin/swift 1970-01-01 00:00:00 +0000 |
2501 | +++ bin/swift 2011-06-15 09:24:34 +0000 |
2502 | @@ -0,0 +1,1812 @@ |
2503 | +#!/usr/bin/python -u |
2504 | +# Copyright (c) 2010-2011 OpenStack, LLC. |
2505 | +# |
2506 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2507 | +# you may not use this file except in compliance with the License. |
2508 | +# You may obtain a copy of the License at |
2509 | +# |
2510 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2511 | +# |
2512 | +# Unless required by applicable law or agreed to in writing, software |
2513 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2514 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
2515 | +# implied. |
2516 | +# See the License for the specific language governing permissions and |
2517 | +# limitations under the License. |
2518 | + |
2519 | +from errno import EEXIST, ENOENT |
2520 | +from hashlib import md5 |
2521 | +from optparse import OptionParser |
2522 | +from os import environ, listdir, makedirs, utime |
2523 | +from os.path import basename, dirname, getmtime, getsize, isdir, join |
2524 | +from Queue import Empty, Queue |
2525 | +from sys import argv, exc_info, exit, stderr, stdout |
2526 | +from threading import enumerate as threading_enumerate, Thread |
2527 | +from time import sleep |
2528 | +from traceback import format_exception |
2529 | + |
2530 | + |
2531 | +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
2532 | +# Inclusion of swift.common.client for convenience of single file distribution |
2533 | + |
2534 | +import socket |
2535 | +from cStringIO import StringIO |
2536 | +from re import compile, DOTALL |
2537 | +from tokenize import generate_tokens, STRING, NAME, OP |
2538 | +from urllib import quote as _quote, unquote |
2539 | +from urlparse import urlparse, urlunparse |
2540 | + |
2541 | +try: |
2542 | + from eventlet.green.httplib import HTTPException, HTTPSConnection |
2543 | +except ImportError: |
2544 | + from httplib import HTTPException, HTTPSConnection |
2545 | + |
2546 | +try: |
2547 | + from eventlet import sleep |
2548 | +except ImportError: |
2549 | + from time import sleep |
2550 | + |
2551 | +try: |
2552 | + from swift.common.bufferedhttp \ |
2553 | + import BufferedHTTPConnection as HTTPConnection |
2554 | +except ImportError: |
2555 | + try: |
2556 | + from eventlet.green.httplib import HTTPConnection |
2557 | + except ImportError: |
2558 | + from httplib import HTTPConnection |
2559 | + |
2560 | + |
2561 | +def quote(value, safe='/'): |
2562 | + """ |
2563 | + Patched version of urllib.quote that encodes utf8 strings before quoting |
2564 | + """ |
2565 | + if isinstance(value, unicode): |
2566 | + value = value.encode('utf8') |
2567 | + return _quote(value, safe) |
2568 | + |
2569 | + |
2570 | +# look for a real json parser first |
2571 | +try: |
2572 | + # simplejson is popular and pretty good |
2573 | + from simplejson import loads as json_loads |
2574 | +except ImportError: |
2575 | + try: |
2576 | + # 2.6 will have a json module in the stdlib |
2577 | + from json import loads as json_loads |
2578 | + except ImportError: |
2579 | + # fall back on local parser otherwise |
2580 | + comments = compile(r'/\*.*\*/|//[^\r\n]*', DOTALL) |
2581 | + |
2582 | + def json_loads(string): |
2583 | + ''' |
2584 | + Fairly competent json parser exploiting the python tokenizer and |
2585 | + eval(). -- From python-cloudfiles |
2586 | + |
2587 | + _loads(serialized_json) -> object |
2588 | + ''' |
2589 | + try: |
2590 | + res = [] |
2591 | + consts = {'true': True, 'false': False, 'null': None} |
2592 | + string = '(' + comments.sub('', string) + ')' |
2593 | + for type, val, _junk, _junk, _junk in \ |
2594 | + generate_tokens(StringIO(string).readline): |
2595 | + if (type == OP and val not in '[]{}:,()-') or \ |
2596 | + (type == NAME and val not in consts): |
2597 | + raise AttributeError() |
2598 | + elif type == STRING: |
2599 | + res.append('u') |
2600 | + res.append(val.replace('\\/', '/')) |
2601 | + else: |
2602 | + res.append(val) |
2603 | + return eval(''.join(res), {}, consts) |
2604 | + except Exception: |
2605 | + raise AttributeError() |
2606 | + |
2607 | + |
2608 | +class ClientException(Exception): |
2609 | + |
2610 | + def __init__(self, msg, http_scheme='', http_host='', http_port='', |
2611 | + http_path='', http_query='', http_status=0, http_reason='', |
2612 | + http_device=''): |
2613 | + Exception.__init__(self, msg) |
2614 | + self.msg = msg |
2615 | + self.http_scheme = http_scheme |
2616 | + self.http_host = http_host |
2617 | + self.http_port = http_port |
2618 | + self.http_path = http_path |
2619 | + self.http_query = http_query |
2620 | + self.http_status = http_status |
2621 | + self.http_reason = http_reason |
2622 | + self.http_device = http_device |
2623 | + |
2624 | + def __str__(self): |
2625 | + a = self.msg |
2626 | + b = '' |
2627 | + if self.http_scheme: |
2628 | + b += '%s://' % self.http_scheme |
2629 | + if self.http_host: |
2630 | + b += self.http_host |
2631 | + if self.http_port: |
2632 | + b += ':%s' % self.http_port |
2633 | + if self.http_path: |
2634 | + b += self.http_path |
2635 | + if self.http_query: |
2636 | + b += '?%s' % self.http_query |
2637 | + if self.http_status: |
2638 | + if b: |
2639 | + b = '%s %s' % (b, self.http_status) |
2640 | + else: |
2641 | + b = str(self.http_status) |
2642 | + if self.http_reason: |
2643 | + if b: |
2644 | + b = '%s %s' % (b, self.http_reason) |
2645 | + else: |
2646 | + b = '- %s' % self.http_reason |
2647 | + if self.http_device: |
2648 | + if b: |
2649 | + b = '%s: device %s' % (b, self.http_device) |
2650 | + else: |
2651 | + b = 'device %s' % self.http_device |
2652 | + return b and '%s: %s' % (a, b) or a |
2653 | + |
2654 | + |
2655 | +def http_connection(url): |
2656 | + """ |
2657 | + Make an HTTPConnection or HTTPSConnection |
2658 | + |
2659 | + :param url: url to connect to |
2660 | + :returns: tuple of (parsed url, connection object) |
2661 | + :raises ClientException: Unable to handle protocol scheme |
2662 | + """ |
2663 | + parsed = urlparse(url) |
2664 | + if parsed.scheme == 'http': |
2665 | + conn = HTTPConnection(parsed.netloc) |
2666 | + elif parsed.scheme == 'https': |
2667 | + conn = HTTPSConnection(parsed.netloc) |
2668 | + else: |
2669 | + raise ClientException('Cannot handle protocol scheme %s for url %s' % |
2670 | + (parsed.scheme, repr(url))) |
2671 | + return parsed, conn |
2672 | + |
2673 | + |
2674 | +def get_auth(url, user, key, snet=False): |
2675 | + """ |
2676 | + Get authentication/authorization credentials. |
2677 | + |
2678 | + The snet parameter is used for Rackspace's ServiceNet internal network |
2679 | + implementation. In this function, it simply adds *snet-* to the beginning |
2680 | + of the host name for the returned storage URL. With Rackspace Cloud Files, |
2681 | + use of this network path causes no bandwidth charges but requires the |
2682 | + client to be running on Rackspace's ServiceNet network. |
2683 | + |
2684 | + :param url: authentication/authorization URL |
2685 | + :param user: user to authenticate as |
2686 | + :param key: key or password for authorization |
2687 | + :param snet: use SERVICENET internal network (see above), default is False |
2688 | + :returns: tuple of (storage URL, auth token) |
2689 | + :raises ClientException: HTTP GET request to auth URL failed |
2690 | + """ |
2691 | + parsed, conn = http_connection(url) |
2692 | + conn.request('GET', parsed.path, '', |
2693 | + {'X-Auth-User': user, 'X-Auth-Key': key}) |
2694 | + resp = conn.getresponse() |
2695 | + resp.read() |
2696 | + if resp.status < 200 or resp.status >= 300: |
2697 | + raise ClientException('Auth GET failed', http_scheme=parsed.scheme, |
2698 | + http_host=conn.host, http_port=conn.port, |
2699 | + http_path=parsed.path, http_status=resp.status, |
2700 | + http_reason=resp.reason) |
2701 | + url = resp.getheader('x-storage-url') |
2702 | + if snet: |
2703 | + parsed = list(urlparse(url)) |
2704 | + # Second item in the list is the netloc |
2705 | + parsed[1] = 'snet-' + parsed[1] |
2706 | + url = urlunparse(parsed) |
2707 | + return url, resp.getheader('x-storage-token', |
2708 | + resp.getheader('x-auth-token')) |
2709 | + |
2710 | + |
2711 | +def get_account(url, token, marker=None, limit=None, prefix=None, |
2712 | + http_conn=None, full_listing=False): |
2713 | + """ |
2714 | + Get a listing of containers for the account. |
2715 | + |
2716 | + :param url: storage URL |
2717 | + :param token: auth token |
2718 | + :param marker: marker query |
2719 | + :param limit: limit query |
2720 | + :param prefix: prefix query |
2721 | + :param http_conn: HTTP connection object (If None, it will create the |
2722 | + conn object) |
2723 | + :param full_listing: if True, return a full listing, else returns a max |
2724 | + of 10000 listings |
2725 | + :returns: a tuple of (response headers, a list of containers) The response |
2726 | + headers will be a dict and all header names will be lowercase. |
2727 | + :raises ClientException: HTTP GET request failed |
2728 | + """ |
2729 | + if not http_conn: |
2730 | + http_conn = http_connection(url) |
2731 | + if full_listing: |
2732 | + rv = get_account(url, token, marker, limit, prefix, http_conn) |
2733 | + listing = rv[1] |
2734 | + while listing: |
2735 | + marker = listing[-1]['name'] |
2736 | + listing = \ |
2737 | + get_account(url, token, marker, limit, prefix, http_conn)[1] |
2738 | + if listing: |
2739 | + rv[1].extend(listing) |
2740 | + return rv |
2741 | + parsed, conn = http_conn |
2742 | + qs = 'format=json' |
2743 | + if marker: |
2744 | + qs += '&marker=%s' % quote(marker) |
2745 | + if limit: |
2746 | + qs += '&limit=%d' % limit |
2747 | + if prefix: |
2748 | + qs += '&prefix=%s' % quote(prefix) |
2749 | + conn.request('GET', '%s?%s' % (parsed.path, qs), '', |
2750 | + {'X-Auth-Token': token}) |
2751 | + resp = conn.getresponse() |
2752 | + resp_headers = {} |
2753 | + for header, value in resp.getheaders(): |
2754 | + resp_headers[header.lower()] = value |
2755 | + if resp.status < 200 or resp.status >= 300: |
2756 | + resp.read() |
2757 | + raise ClientException('Account GET failed', http_scheme=parsed.scheme, |
2758 | + http_host=conn.host, http_port=conn.port, |
2759 | + http_path=parsed.path, http_query=qs, http_status=resp.status, |
2760 | + http_reason=resp.reason) |
2761 | + if resp.status == 204: |
2762 | + resp.read() |
2763 | + return resp_headers, [] |
2764 | + return resp_headers, json_loads(resp.read()) |
2765 | + |
2766 | + |
2767 | +def head_account(url, token, http_conn=None): |
2768 | + """ |
2769 | + Get account stats. |
2770 | + |
2771 | + :param url: storage URL |
2772 | + :param token: auth token |
2773 | + :param http_conn: HTTP connection object (If None, it will create the |
2774 | + conn object) |
2775 | + :returns: a dict containing the response's headers (all header names will |
2776 | + be lowercase) |
2777 | + :raises ClientException: HTTP HEAD request failed |
2778 | + """ |
2779 | + if http_conn: |
2780 | + parsed, conn = http_conn |
2781 | + else: |
2782 | + parsed, conn = http_connection(url) |
2783 | + conn.request('HEAD', parsed.path, '', {'X-Auth-Token': token}) |
2784 | + resp = conn.getresponse() |
2785 | + resp.read() |
2786 | + if resp.status < 200 or resp.status >= 300: |
2787 | + raise ClientException('Account HEAD failed', http_scheme=parsed.scheme, |
2788 | + http_host=conn.host, http_port=conn.port, |
2789 | + http_path=parsed.path, http_status=resp.status, |
2790 | + http_reason=resp.reason) |
2791 | + resp_headers = {} |
2792 | + for header, value in resp.getheaders(): |
2793 | + resp_headers[header.lower()] = value |
2794 | + return resp_headers |
2795 | + |
2796 | + |
2797 | +def post_account(url, token, headers, http_conn=None): |
2798 | + """ |
2799 | + Update an account's metadata. |
2800 | + |
2801 | + :param url: storage URL |
2802 | + :param token: auth token |
2803 | + :param headers: additional headers to include in the request |
2804 | + :param http_conn: HTTP connection object (If None, it will create the |
2805 | + conn object) |
2806 | + :raises ClientException: HTTP POST request failed |
2807 | + """ |
2808 | + if http_conn: |
2809 | + parsed, conn = http_conn |
2810 | + else: |
2811 | + parsed, conn = http_connection(url) |
2812 | + headers['X-Auth-Token'] = token |
2813 | + conn.request('POST', parsed.path, '', headers) |
2814 | + resp = conn.getresponse() |
2815 | + resp.read() |
2816 | + if resp.status < 200 or resp.status >= 300: |
2817 | + raise ClientException('Account POST failed', |
2818 | + http_scheme=parsed.scheme, http_host=conn.host, |
2819 | + http_port=conn.port, http_path=path, http_status=resp.status, |
2820 | + http_reason=resp.reason) |
2821 | + |
2822 | + |
2823 | +def get_container(url, token, container, marker=None, limit=None, |
2824 | + prefix=None, delimiter=None, http_conn=None, |
2825 | + full_listing=False): |
2826 | + """ |
2827 | + Get a listing of objects for the container. |
2828 | + |
2829 | + :param url: storage URL |
2830 | + :param token: auth token |
2831 | + :param container: container name to get a listing for |
2832 | + :param marker: marker query |
2833 | + :param limit: limit query |
2834 | + :param prefix: prefix query |
2835 | + :param delimeter: string to delimit the queries on |
2836 | + :param http_conn: HTTP connection object (If None, it will create the |
2837 | + conn object) |
2838 | + :param full_listing: if True, return a full listing, else returns a max |
2839 | + of 10000 listings |
2840 | + :returns: a tuple of (response headers, a list of objects) The response |
2841 | + headers will be a dict and all header names will be lowercase. |
2842 | + :raises ClientException: HTTP GET request failed |
2843 | + """ |
2844 | + if not http_conn: |
2845 | + http_conn = http_connection(url) |
2846 | + if full_listing: |
2847 | + rv = get_container(url, token, container, marker, limit, prefix, |
2848 | + delimiter, http_conn) |
2849 | + listing = rv[1] |
2850 | + while listing: |
2851 | + if not delimiter: |
2852 | + marker = listing[-1]['name'] |
2853 | + else: |
2854 | + marker = listing[-1].get('name', listing[-1].get('subdir')) |
2855 | + listing = get_container(url, token, container, marker, limit, |
2856 | + prefix, delimiter, http_conn)[1] |
2857 | + if listing: |
2858 | + rv[1].extend(listing) |
2859 | + return rv |
2860 | + parsed, conn = http_conn |
2861 | + path = '%s/%s' % (parsed.path, quote(container)) |
2862 | + qs = 'format=json' |
2863 | + if marker: |
2864 | + qs += '&marker=%s' % quote(marker) |
2865 | + if limit: |
2866 | + qs += '&limit=%d' % limit |
2867 | + if prefix: |
2868 | + qs += '&prefix=%s' % quote(prefix) |
2869 | + if delimiter: |
2870 | + qs += '&delimiter=%s' % quote(delimiter) |
2871 | + conn.request('GET', '%s?%s' % (path, qs), '', {'X-Auth-Token': token}) |
2872 | + resp = conn.getresponse() |
2873 | + if resp.status < 200 or resp.status >= 300: |
2874 | + resp.read() |
2875 | + raise ClientException('Container GET failed', |
2876 | + http_scheme=parsed.scheme, http_host=conn.host, |
2877 | + http_port=conn.port, http_path=path, http_query=qs, |
2878 | + http_status=resp.status, http_reason=resp.reason) |
2879 | + resp_headers = {} |
2880 | + for header, value in resp.getheaders(): |
2881 | + resp_headers[header.lower()] = value |
2882 | + if resp.status == 204: |
2883 | + resp.read() |
2884 | + return resp_headers, [] |
2885 | + return resp_headers, json_loads(resp.read()) |
2886 | + |
2887 | + |
2888 | +def head_container(url, token, container, http_conn=None): |
2889 | + """ |
2890 | + Get container stats. |
2891 | + |
2892 | + :param url: storage URL |
2893 | + :param token: auth token |
2894 | + :param container: container name to get stats for |
2895 | + :param http_conn: HTTP connection object (If None, it will create the |
2896 | + conn object) |
2897 | + :returns: a dict containing the response's headers (all header names will |
2898 | + be lowercase) |
2899 | + :raises ClientException: HTTP HEAD request failed |
2900 | + """ |
2901 | + if http_conn: |
2902 | + parsed, conn = http_conn |
2903 | + else: |
2904 | + parsed, conn = http_connection(url) |
2905 | + path = '%s/%s' % (parsed.path, quote(container)) |
2906 | + conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
2907 | + resp = conn.getresponse() |
2908 | + resp.read() |
2909 | + if resp.status < 200 or resp.status >= 300: |
2910 | + raise ClientException('Container HEAD failed', |
2911 | + http_scheme=parsed.scheme, http_host=conn.host, |
2912 | + http_port=conn.port, http_path=path, http_status=resp.status, |
2913 | + http_reason=resp.reason) |
2914 | + resp_headers = {} |
2915 | + for header, value in resp.getheaders(): |
2916 | + resp_headers[header.lower()] = value |
2917 | + return resp_headers |
2918 | + |
2919 | + |
2920 | +def put_container(url, token, container, headers=None, http_conn=None): |
2921 | + """ |
2922 | + Create a container |
2923 | + |
2924 | + :param url: storage URL |
2925 | + :param token: auth token |
2926 | + :param container: container name to create |
2927 | + :param headers: additional headers to include in the request |
2928 | + :param http_conn: HTTP connection object (If None, it will create the |
2929 | + conn object) |
2930 | + :raises ClientException: HTTP PUT request failed |
2931 | + """ |
2932 | + if http_conn: |
2933 | + parsed, conn = http_conn |
2934 | + else: |
2935 | + parsed, conn = http_connection(url) |
2936 | + path = '%s/%s' % (parsed.path, quote(container)) |
2937 | + if not headers: |
2938 | + headers = {} |
2939 | + headers['X-Auth-Token'] = token |
2940 | + conn.request('PUT', path, '', headers) |
2941 | + resp = conn.getresponse() |
2942 | + resp.read() |
2943 | + if resp.status < 200 or resp.status >= 300: |
2944 | + raise ClientException('Container PUT failed', |
2945 | + http_scheme=parsed.scheme, http_host=conn.host, |
2946 | + http_port=conn.port, http_path=path, http_status=resp.status, |
2947 | + http_reason=resp.reason) |
2948 | + |
2949 | + |
2950 | +def post_container(url, token, container, headers, http_conn=None): |
2951 | + """ |
2952 | + Update a container's metadata. |
2953 | + |
2954 | + :param url: storage URL |
2955 | + :param token: auth token |
2956 | + :param container: container name to update |
2957 | + :param headers: additional headers to include in the request |
2958 | + :param http_conn: HTTP connection object (If None, it will create the |
2959 | + conn object) |
2960 | + :raises ClientException: HTTP POST request failed |
2961 | + """ |
2962 | + if http_conn: |
2963 | + parsed, conn = http_conn |
2964 | + else: |
2965 | + parsed, conn = http_connection(url) |
2966 | + path = '%s/%s' % (parsed.path, quote(container)) |
2967 | + headers['X-Auth-Token'] = token |
2968 | + conn.request('POST', path, '', headers) |
2969 | + resp = conn.getresponse() |
2970 | + resp.read() |
2971 | + if resp.status < 200 or resp.status >= 300: |
2972 | + raise ClientException('Container POST failed', |
2973 | + http_scheme=parsed.scheme, http_host=conn.host, |
2974 | + http_port=conn.port, http_path=path, http_status=resp.status, |
2975 | + http_reason=resp.reason) |
2976 | + |
2977 | + |
2978 | +def delete_container(url, token, container, http_conn=None): |
2979 | + """ |
2980 | + Delete a container |
2981 | + |
2982 | + :param url: storage URL |
2983 | + :param token: auth token |
2984 | + :param container: container name to delete |
2985 | + :param http_conn: HTTP connection object (If None, it will create the |
2986 | + conn object) |
2987 | + :raises ClientException: HTTP DELETE request failed |
2988 | + """ |
2989 | + if http_conn: |
2990 | + parsed, conn = http_conn |
2991 | + else: |
2992 | + parsed, conn = http_connection(url) |
2993 | + path = '%s/%s' % (parsed.path, quote(container)) |
2994 | + conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
2995 | + resp = conn.getresponse() |
2996 | + resp.read() |
2997 | + if resp.status < 200 or resp.status >= 300: |
2998 | + raise ClientException('Container DELETE failed', |
2999 | + http_scheme=parsed.scheme, http_host=conn.host, |
3000 | + http_port=conn.port, http_path=path, http_status=resp.status, |
3001 | + http_reason=resp.reason) |
3002 | + |
3003 | + |
3004 | +def get_object(url, token, container, name, http_conn=None, |
3005 | + resp_chunk_size=None): |
3006 | + """ |
3007 | + Get an object |
3008 | + |
3009 | + :param url: storage URL |
3010 | + :param token: auth token |
3011 | + :param container: container name that the object is in |
3012 | + :param name: object name to get |
3013 | + :param http_conn: HTTP connection object (If None, it will create the |
3014 | + conn object) |
3015 | + :param resp_chunk_size: if defined, chunk size of data to read. NOTE: If |
3016 | + you specify a resp_chunk_size you must fully read |
3017 | + the object's contents before making another |
3018 | + request. |
3019 | + :returns: a tuple of (response headers, the object's contents) The response |
3020 | + headers will be a dict and all header names will be lowercase. |
3021 | + :raises ClientException: HTTP GET request failed |
3022 | + """ |
3023 | + if http_conn: |
3024 | + parsed, conn = http_conn |
3025 | + else: |
3026 | + parsed, conn = http_connection(url) |
3027 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
3028 | + conn.request('GET', path, '', {'X-Auth-Token': token}) |
3029 | + resp = conn.getresponse() |
3030 | + if resp.status < 200 or resp.status >= 300: |
3031 | + resp.read() |
3032 | + raise ClientException('Object GET failed', http_scheme=parsed.scheme, |
3033 | + http_host=conn.host, http_port=conn.port, http_path=path, |
3034 | + http_status=resp.status, http_reason=resp.reason) |
3035 | + if resp_chunk_size: |
3036 | + |
3037 | + def _object_body(): |
3038 | + buf = resp.read(resp_chunk_size) |
3039 | + while buf: |
3040 | + yield buf |
3041 | + buf = resp.read(resp_chunk_size) |
3042 | + object_body = _object_body() |
3043 | + else: |
3044 | + object_body = resp.read() |
3045 | + resp_headers = {} |
3046 | + for header, value in resp.getheaders(): |
3047 | + resp_headers[header.lower()] = value |
3048 | + return resp_headers, object_body |
3049 | + |
3050 | + |
3051 | +def head_object(url, token, container, name, http_conn=None): |
3052 | + """ |
3053 | + Get object info |
3054 | + |
3055 | + :param url: storage URL |
3056 | + :param token: auth token |
3057 | + :param container: container name that the object is in |
3058 | + :param name: object name to get info for |
3059 | + :param http_conn: HTTP connection object (If None, it will create the |
3060 | + conn object) |
3061 | + :returns: a dict containing the response's headers (all header names will |
3062 | + be lowercase) |
3063 | + :raises ClientException: HTTP HEAD request failed |
3064 | + """ |
3065 | + if http_conn: |
3066 | + parsed, conn = http_conn |
3067 | + else: |
3068 | + parsed, conn = http_connection(url) |
3069 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
3070 | + conn.request('HEAD', path, '', {'X-Auth-Token': token}) |
3071 | + resp = conn.getresponse() |
3072 | + resp.read() |
3073 | + if resp.status < 200 or resp.status >= 300: |
3074 | + raise ClientException('Object HEAD failed', http_scheme=parsed.scheme, |
3075 | + http_host=conn.host, http_port=conn.port, http_path=path, |
3076 | + http_status=resp.status, http_reason=resp.reason) |
3077 | + resp_headers = {} |
3078 | + for header, value in resp.getheaders(): |
3079 | + resp_headers[header.lower()] = value |
3080 | + return resp_headers |
3081 | + |
3082 | + |
3083 | +def put_object(url, token, container, name, contents, content_length=None, |
3084 | + etag=None, chunk_size=65536, content_type=None, headers=None, |
3085 | + http_conn=None): |
3086 | + """ |
3087 | + Put an object |
3088 | + |
3089 | + :param url: storage URL |
3090 | + :param token: auth token |
3091 | + :param container: container name that the object is in |
3092 | + :param name: object name to put |
3093 | + :param contents: a string or a file like object to read object data from |
3094 | + :param content_length: value to send as content-length header; also limits |
3095 | + the amount read from contents |
3096 | + :param etag: etag of contents |
3097 | + :param chunk_size: chunk size of data to write |
3098 | + :param content_type: value to send as content-type header |
3099 | + :param headers: additional headers to include in the request |
3100 | + :param http_conn: HTTP connection object (If None, it will create the |
3101 | + conn object) |
3102 | + :returns: etag from server response |
3103 | + :raises ClientException: HTTP PUT request failed |
3104 | + """ |
3105 | + if http_conn: |
3106 | + parsed, conn = http_conn |
3107 | + else: |
3108 | + parsed, conn = http_connection(url) |
3109 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
3110 | + if not headers: |
3111 | + headers = {} |
3112 | + headers['X-Auth-Token'] = token |
3113 | + if etag: |
3114 | + headers['ETag'] = etag.strip('"') |
3115 | + if content_length is not None: |
3116 | + headers['Content-Length'] = str(content_length) |
3117 | + if content_type is not None: |
3118 | + headers['Content-Type'] = content_type |
3119 | + if not contents: |
3120 | + headers['Content-Length'] = '0' |
3121 | + if hasattr(contents, 'read'): |
3122 | + conn.putrequest('PUT', path) |
3123 | + for header, value in headers.iteritems(): |
3124 | + conn.putheader(header, value) |
3125 | + if content_length is None: |
3126 | + conn.putheader('Transfer-Encoding', 'chunked') |
3127 | + conn.endheaders() |
3128 | + chunk = contents.read(chunk_size) |
3129 | + while chunk: |
3130 | + conn.send('%x\r\n%s\r\n' % (len(chunk), chunk)) |
3131 | + chunk = contents.read(chunk_size) |
3132 | + conn.send('0\r\n\r\n') |
3133 | + else: |
3134 | + conn.endheaders() |
3135 | + left = content_length |
3136 | + while left > 0: |
3137 | + size = chunk_size |
3138 | + if size > left: |
3139 | + size = left |
3140 | + chunk = contents.read(size) |
3141 | + conn.send(chunk) |
3142 | + left -= len(chunk) |
3143 | + else: |
3144 | + conn.request('PUT', path, contents, headers) |
3145 | + resp = conn.getresponse() |
3146 | + resp.read() |
3147 | + if resp.status < 200 or resp.status >= 300: |
3148 | + raise ClientException('Object PUT failed', http_scheme=parsed.scheme, |
3149 | + http_host=conn.host, http_port=conn.port, http_path=path, |
3150 | + http_status=resp.status, http_reason=resp.reason) |
3151 | + return resp.getheader('etag').strip('"') |
3152 | + |
3153 | + |
3154 | +def post_object(url, token, container, name, headers, http_conn=None): |
3155 | + """ |
3156 | + Update object metadata |
3157 | + |
3158 | + :param url: storage URL |
3159 | + :param token: auth token |
3160 | + :param container: container name that the object is in |
3161 | + :param name: name of the object to update |
3162 | + :param headers: additional headers to include in the request |
3163 | + :param http_conn: HTTP connection object (If None, it will create the |
3164 | + conn object) |
3165 | + :raises ClientException: HTTP POST request failed |
3166 | + """ |
3167 | + if http_conn: |
3168 | + parsed, conn = http_conn |
3169 | + else: |
3170 | + parsed, conn = http_connection(url) |
3171 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
3172 | + headers['X-Auth-Token'] = token |
3173 | + conn.request('POST', path, '', headers) |
3174 | + resp = conn.getresponse() |
3175 | + resp.read() |
3176 | + if resp.status < 200 or resp.status >= 300: |
3177 | + raise ClientException('Object POST failed', http_scheme=parsed.scheme, |
3178 | + http_host=conn.host, http_port=conn.port, http_path=path, |
3179 | + http_status=resp.status, http_reason=resp.reason) |
3180 | + |
3181 | + |
3182 | +def delete_object(url, token, container, name, http_conn=None): |
3183 | + """ |
3184 | + Delete object |
3185 | + |
3186 | + :param url: storage URL |
3187 | + :param token: auth token |
3188 | + :param container: container name that the object is in |
3189 | + :param name: object name to delete |
3190 | + :param http_conn: HTTP connection object (If None, it will create the |
3191 | + conn object) |
3192 | + :raises ClientException: HTTP DELETE request failed |
3193 | + """ |
3194 | + if http_conn: |
3195 | + parsed, conn = http_conn |
3196 | + else: |
3197 | + parsed, conn = http_connection(url) |
3198 | + path = '%s/%s/%s' % (parsed.path, quote(container), quote(name)) |
3199 | + conn.request('DELETE', path, '', {'X-Auth-Token': token}) |
3200 | + resp = conn.getresponse() |
3201 | + resp.read() |
3202 | + if resp.status < 200 or resp.status >= 300: |
3203 | + raise ClientException('Object DELETE failed', |
3204 | + http_scheme=parsed.scheme, http_host=conn.host, |
3205 | + http_port=conn.port, http_path=path, http_status=resp.status, |
3206 | + http_reason=resp.reason) |
3207 | + |
3208 | + |
3209 | +class Connection(object): |
3210 | + """Convenience class to make requests that will also retry the request""" |
3211 | + |
3212 | + def __init__(self, authurl, user, key, retries=5, preauthurl=None, |
3213 | + preauthtoken=None, snet=False, starting_backoff=1): |
3214 | + """ |
3215 | + :param authurl: authenitcation URL |
3216 | + :param user: user name to authenticate as |
3217 | + :param key: key/password to authenticate with |
3218 | + :param retries: Number of times to retry the request before failing |
3219 | + :param preauthurl: storage URL (if you have already authenticated) |
3220 | + :param preauthtoken: authentication token (if you have already |
3221 | + authenticated) |
3222 | + :param snet: use SERVICENET internal network default is False |
3223 | + """ |
3224 | + self.authurl = authurl |
3225 | + self.user = user |
3226 | + self.key = key |
3227 | + self.retries = retries |
3228 | + self.http_conn = None |
3229 | + self.url = preauthurl |
3230 | + self.token = preauthtoken |
3231 | + self.attempts = 0 |
3232 | + self.snet = snet |
3233 | + self.starting_backoff = starting_backoff |
3234 | + |
3235 | + def get_auth(self): |
3236 | + return get_auth(self.authurl, self.user, self.key, snet=self.snet) |
3237 | + |
3238 | + def http_connection(self): |
3239 | + return http_connection(self.url) |
3240 | + |
3241 | + def _retry(self, reset_func, func, *args, **kwargs): |
3242 | + self.attempts = 0 |
3243 | + backoff = self.starting_backoff |
3244 | + while self.attempts <= self.retries: |
3245 | + self.attempts += 1 |
3246 | + try: |
3247 | + if not self.url or not self.token: |
3248 | + self.url, self.token = self.get_auth() |
3249 | + self.http_conn = None |
3250 | + if not self.http_conn: |
3251 | + self.http_conn = self.http_connection() |
3252 | + kwargs['http_conn'] = self.http_conn |
3253 | + rv = func(self.url, self.token, *args, **kwargs) |
3254 | + return rv |
3255 | + except (socket.error, HTTPException): |
3256 | + if self.attempts > self.retries: |
3257 | + raise |
3258 | + self.http_conn = None |
3259 | + except ClientException, err: |
3260 | + if self.attempts > self.retries: |
3261 | + raise |
3262 | + if err.http_status == 401: |
3263 | + self.url = self.token = None |
3264 | + if self.attempts > 1: |
3265 | + raise |
3266 | + elif err.http_status == 408: |
3267 | + self.http_conn = None |
3268 | + elif 500 <= err.http_status <= 599: |
3269 | + pass |
3270 | + else: |
3271 | + raise |
3272 | + sleep(backoff) |
3273 | + backoff *= 2 |
3274 | + if reset_func: |
3275 | + reset_func(func, *args, **kwargs) |
3276 | + |
3277 | + def head_account(self): |
3278 | + """Wrapper for :func:`head_account`""" |
3279 | + return self._retry(None, head_account) |
3280 | + |
3281 | + def get_account(self, marker=None, limit=None, prefix=None, |
3282 | + full_listing=False): |
3283 | + """Wrapper for :func:`get_account`""" |
3284 | + # TODO(unknown): With full_listing=True this will restart the entire |
3285 | + # listing with each retry. Need to make a better version that just |
3286 | + # retries where it left off. |
3287 | + return self._retry(None, get_account, marker=marker, limit=limit, |
3288 | + prefix=prefix, full_listing=full_listing) |
3289 | + |
3290 | + def post_account(self, headers): |
3291 | + """Wrapper for :func:`post_account`""" |
3292 | + return self._retry(None, post_account, headers) |
3293 | + |
3294 | + def head_container(self, container): |
3295 | + """Wrapper for :func:`head_container`""" |
3296 | + return self._retry(None, head_container, container) |
3297 | + |
3298 | + def get_container(self, container, marker=None, limit=None, prefix=None, |
3299 | + delimiter=None, full_listing=False): |
3300 | + """Wrapper for :func:`get_container`""" |
3301 | + # TODO(unknown): With full_listing=True this will restart the entire |
3302 | + # listing with each retry. Need to make a better version that just |
3303 | + # retries where it left off. |
3304 | + return self._retry(None, get_container, container, marker=marker, |
3305 | + limit=limit, prefix=prefix, delimiter=delimiter, |
3306 | + full_listing=full_listing) |
3307 | + |
3308 | + def put_container(self, container, headers=None): |
3309 | + """Wrapper for :func:`put_container`""" |
3310 | + return self._retry(None, put_container, container, headers=headers) |
3311 | + |
3312 | + def post_container(self, container, headers): |
3313 | + """Wrapper for :func:`post_container`""" |
3314 | + return self._retry(None, post_container, container, headers) |
3315 | + |
3316 | + def delete_container(self, container): |
3317 | + """Wrapper for :func:`delete_container`""" |
3318 | + return self._retry(None, delete_container, container) |
3319 | + |
3320 | + def head_object(self, container, obj): |
3321 | + """Wrapper for :func:`head_object`""" |
3322 | + return self._retry(None, head_object, container, obj) |
3323 | + |
3324 | + def get_object(self, container, obj, resp_chunk_size=None): |
3325 | + """Wrapper for :func:`get_object`""" |
3326 | + return self._retry(None, get_object, container, obj, |
3327 | + resp_chunk_size=resp_chunk_size) |
3328 | + |
3329 | + def put_object(self, container, obj, contents, content_length=None, |
3330 | + etag=None, chunk_size=65536, content_type=None, |
3331 | + headers=None): |
3332 | + """Wrapper for :func:`put_object`""" |
3333 | + |
3334 | + def _default_reset(*args, **kwargs): |
3335 | + raise ClientException('put_object(%r, %r, ...) failure and no ' |
3336 | + 'ability to reset contents for reupload.' % (container, obj)) |
3337 | + |
3338 | + reset_func = _default_reset |
3339 | + tell = getattr(contents, 'tell', None) |
3340 | + seek = getattr(contents, 'seek', None) |
3341 | + if tell and seek: |
3342 | + orig_pos = tell() |
3343 | + reset_func = lambda *a, **k: seek(orig_pos) |
3344 | + elif not contents: |
3345 | + reset_func = lambda *a, **k: None |
3346 | + |
3347 | + return self._retry(reset_func, put_object, container, obj, contents, |
3348 | + content_length=content_length, etag=etag, chunk_size=chunk_size, |
3349 | + content_type=content_type, headers=headers) |
3350 | + |
3351 | + def post_object(self, container, obj, headers): |
3352 | + """Wrapper for :func:`post_object`""" |
3353 | + return self._retry(None, post_object, container, obj, headers) |
3354 | + |
3355 | + def delete_object(self, container, obj): |
3356 | + """Wrapper for :func:`delete_object`""" |
3357 | + return self._retry(None, delete_object, container, obj) |
3358 | + |
3359 | +# End inclusion of swift.common.client |
3360 | +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # |
3361 | + |
3362 | + |
3363 | +def mkdirs(path): |
3364 | + try: |
3365 | + makedirs(path) |
3366 | + except OSError, err: |
3367 | + if err.errno != EEXIST: |
3368 | + raise |
3369 | + |
3370 | + |
3371 | +def put_errors_from_threads(threads, error_queue): |
3372 | + """ |
3373 | + Places any errors from the threads into error_queue. |
3374 | + :param threads: A list of QueueFunctionThread instances. |
3375 | + :param error_queue: A queue to put error strings into. |
3376 | + :returns: True if any errors were found. |
3377 | + """ |
3378 | + was_error = False |
3379 | + for thread in threads: |
3380 | + for info in thread.exc_infos: |
3381 | + was_error = True |
3382 | + if isinstance(info[1], ClientException): |
3383 | + error_queue.put(str(info[1])) |
3384 | + else: |
3385 | + error_queue.put(''.join(format_exception(*info))) |
3386 | + return was_error |
3387 | + |
3388 | + |
3389 | +class QueueFunctionThread(Thread): |
3390 | + |
3391 | + def __init__(self, queue, func, *args, **kwargs): |
3392 | + """ Calls func for each item in queue; func is called with a queued |
3393 | + item as the first arg followed by *args and **kwargs. Use the abort |
3394 | + attribute to have the thread empty the queue (without processing) |
3395 | + and exit. """ |
3396 | + Thread.__init__(self) |
3397 | + self.abort = False |
3398 | + self.queue = queue |
3399 | + self.func = func |
3400 | + self.args = args |
3401 | + self.kwargs = kwargs |
3402 | + self.exc_infos = [] |
3403 | + |
3404 | + def run(self): |
3405 | + try: |
3406 | + while True: |
3407 | + try: |
3408 | + item = self.queue.get_nowait() |
3409 | + if not self.abort: |
3410 | + self.func(item, *self.args, **self.kwargs) |
3411 | + self.queue.task_done() |
3412 | + except Empty: |
3413 | + if self.abort: |
3414 | + break |
3415 | + sleep(0.01) |
3416 | + except Exception: |
3417 | + self.exc_infos.append(exc_info()) |
3418 | + |
3419 | + |
3420 | +st_delete_help = ''' |
3421 | +delete --all OR delete container [--leave-segments] [object] [object] ... |
3422 | + Deletes everything in the account (with --all), or everything in a |
3423 | + container, or a list of objects depending on the args given. Segments of |
3424 | + manifest objects will be deleted as well, unless you specify the |
3425 | + --leave-segments option.'''.strip('\n') |
3426 | + |
3427 | + |
3428 | +def st_delete(parser, args, print_queue, error_queue): |
3429 | + parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
3430 | + default=False, help='Indicates that you really want to delete ' |
3431 | + 'everything in the account') |
3432 | + parser.add_option('', '--leave-segments', action='store_true', |
3433 | + dest='leave_segments', default=False, help='Indicates that you want ' |
3434 | + 'the segments of manifest objects left alone') |
3435 | + (options, args) = parse_args(parser, args) |
3436 | + args = args[1:] |
3437 | + if (not args and not options.yes_all) or (args and options.yes_all): |
3438 | + error_queue.put('Usage: %s [options] %s' % |
3439 | + (basename(argv[0]), st_delete_help)) |
3440 | + return |
3441 | + |
3442 | + def _delete_segment((container, obj), conn): |
3443 | + conn.delete_object(container, obj) |
3444 | + if options.verbose: |
3445 | + if conn.attempts > 2: |
3446 | + print_queue.put('%s/%s [after %d attempts]' % |
3447 | + (container, obj, conn.attempts)) |
3448 | + else: |
3449 | + print_queue.put('%s/%s' % (container, obj)) |
3450 | + |
3451 | + object_queue = Queue(10000) |
3452 | + |
3453 | + def _delete_object((container, obj), conn): |
3454 | + try: |
3455 | + old_manifest = None |
3456 | + if not options.leave_segments: |
3457 | + try: |
3458 | + old_manifest = conn.head_object(container, obj).get( |
3459 | + 'x-object-manifest') |
3460 | + except ClientException, err: |
3461 | + if err.http_status != 404: |
3462 | + raise |
3463 | + conn.delete_object(container, obj) |
3464 | + if old_manifest: |
3465 | + segment_queue = Queue(10000) |
3466 | + scontainer, sprefix = old_manifest.split('/', 1) |
3467 | + for delobj in conn.get_container(scontainer, |
3468 | + prefix=sprefix)[1]: |
3469 | + segment_queue.put((scontainer, delobj['name'])) |
3470 | + if not segment_queue.empty(): |
3471 | + segment_threads = [QueueFunctionThread(segment_queue, |
3472 | + _delete_segment, create_connection()) for _junk in |
3473 | + xrange(10)] |
3474 | + for thread in segment_threads: |
3475 | + thread.start() |
3476 | + while not segment_queue.empty(): |
3477 | + sleep(0.01) |
3478 | + for thread in segment_threads: |
3479 | + thread.abort = True |
3480 | + while thread.isAlive(): |
3481 | + thread.join(0.01) |
3482 | + put_errors_from_threads(segment_threads, error_queue) |
3483 | + if options.verbose: |
3484 | + path = options.yes_all and join(container, obj) or obj |
3485 | + if path[:1] in ('/', '\\'): |
3486 | + path = path[1:] |
3487 | + if conn.attempts > 1: |
3488 | + print_queue.put('%s [after %d attempts]' % |
3489 | + (path, conn.attempts)) |
3490 | + else: |
3491 | + print_queue.put(path) |
3492 | + except ClientException, err: |
3493 | + if err.http_status != 404: |
3494 | + raise |
3495 | + error_queue.put('Object %s not found' % |
3496 | + repr('%s/%s' % (container, obj))) |
3497 | + |
3498 | + container_queue = Queue(10000) |
3499 | + |
3500 | + def _delete_container(container, conn): |
3501 | + try: |
3502 | + marker = '' |
3503 | + while True: |
3504 | + objects = [o['name'] for o in |
3505 | + conn.get_container(container, marker=marker)[1]] |
3506 | + if not objects: |
3507 | + break |
3508 | + for obj in objects: |
3509 | + object_queue.put((container, obj)) |
3510 | + marker = objects[-1] |
3511 | + while not object_queue.empty(): |
3512 | + sleep(0.01) |
3513 | + attempts = 1 |
3514 | + while True: |
3515 | + try: |
3516 | + conn.delete_container(container) |
3517 | + break |
3518 | + except ClientException, err: |
3519 | + if err.http_status != 409: |
3520 | + raise |
3521 | + if attempts > 10: |
3522 | + raise |
3523 | + attempts += 1 |
3524 | + sleep(1) |
3525 | + except ClientException, err: |
3526 | + if err.http_status != 404: |
3527 | + raise |
3528 | + error_queue.put('Container %s not found' % repr(container)) |
3529 | + |
3530 | + url, token = get_auth(options.auth, options.user, options.key, |
3531 | + snet=options.snet) |
3532 | + create_connection = lambda: Connection(options.auth, options.user, |
3533 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
3534 | + object_threads = [QueueFunctionThread(object_queue, _delete_object, |
3535 | + create_connection()) for _junk in xrange(10)] |
3536 | + for thread in object_threads: |
3537 | + thread.start() |
3538 | + container_threads = [QueueFunctionThread(container_queue, |
3539 | + _delete_container, create_connection()) for _junk in xrange(10)] |
3540 | + for thread in container_threads: |
3541 | + thread.start() |
3542 | + if not args: |
3543 | + conn = create_connection() |
3544 | + try: |
3545 | + marker = '' |
3546 | + while True: |
3547 | + containers = \ |
3548 | + [c['name'] for c in conn.get_account(marker=marker)[1]] |
3549 | + if not containers: |
3550 | + break |
3551 | + for container in containers: |
3552 | + container_queue.put(container) |
3553 | + marker = containers[-1] |
3554 | + while not container_queue.empty(): |
3555 | + sleep(0.01) |
3556 | + while not object_queue.empty(): |
3557 | + sleep(0.01) |
3558 | + except ClientException, err: |
3559 | + if err.http_status != 404: |
3560 | + raise |
3561 | + error_queue.put('Account not found') |
3562 | + elif len(args) == 1: |
3563 | + if '/' in args[0]: |
3564 | + print >> stderr, 'WARNING: / in container name; you might have ' \ |
3565 | + 'meant %r instead of %r.' % \ |
3566 | + (args[0].replace('/', ' ', 1), args[0]) |
3567 | + conn = create_connection() |
3568 | + _delete_container(args[0], conn) |
3569 | + else: |
3570 | + for obj in args[1:]: |
3571 | + object_queue.put((args[0], obj)) |
3572 | + while not container_queue.empty(): |
3573 | + sleep(0.01) |
3574 | + for thread in container_threads: |
3575 | + thread.abort = True |
3576 | + while thread.isAlive(): |
3577 | + thread.join(0.01) |
3578 | + put_errors_from_threads(container_threads, error_queue) |
3579 | + while not object_queue.empty(): |
3580 | + sleep(0.01) |
3581 | + for thread in object_threads: |
3582 | + thread.abort = True |
3583 | + while thread.isAlive(): |
3584 | + thread.join(0.01) |
3585 | + put_errors_from_threads(object_threads, error_queue) |
3586 | + |
3587 | + |
3588 | +st_download_help = ''' |
3589 | +download --all OR download container [options] [object] [object] ... |
3590 | + Downloads everything in the account (with --all), or everything in a |
3591 | + container, or a list of objects depending on the args given. For a single |
3592 | + object download, you may use the -o [--output] <filename> option to |
3593 | + redirect the output to a specific file or if "-" then just redirect to |
3594 | + stdout.'''.strip('\n') |
3595 | + |
3596 | + |
3597 | +def st_download(options, args, print_queue, error_queue): |
3598 | + parser.add_option('-a', '--all', action='store_true', dest='yes_all', |
3599 | + default=False, help='Indicates that you really want to download ' |
3600 | + 'everything in the account') |
3601 | + parser.add_option('-o', '--output', dest='out_file', help='For a single ' |
3602 | + 'file download, stream the output to an alternate location ') |
3603 | + (options, args) = parse_args(parser, args) |
3604 | + args = args[1:] |
3605 | + if options.out_file == '-': |
3606 | + options.verbose = 0 |
3607 | + if options.out_file and len(args) != 2: |
3608 | + exit('-o option only allowed for single file downloads') |
3609 | + if (not args and not options.yes_all) or (args and options.yes_all): |
3610 | + error_queue.put('Usage: %s [options] %s' % |
3611 | + (basename(argv[0]), st_download_help)) |
3612 | + return |
3613 | + |
3614 | + object_queue = Queue(10000) |
3615 | + |
3616 | + def _download_object(queue_arg, conn): |
3617 | + if len(queue_arg) == 2: |
3618 | + container, obj = queue_arg |
3619 | + out_file = None |
3620 | + elif len(queue_arg) == 3: |
3621 | + container, obj, out_file = queue_arg |
3622 | + else: |
3623 | + raise Exception("Invalid queue_arg length of %s" % len(queue_arg)) |
3624 | + try: |
3625 | + headers, body = \ |
3626 | + conn.get_object(container, obj, resp_chunk_size=65536) |
3627 | + content_type = headers.get('content-type') |
3628 | + if 'content-length' in headers: |
3629 | + content_length = int(headers.get('content-length')) |
3630 | + else: |
3631 | + content_length = None |
3632 | + etag = headers.get('etag') |
3633 | + path = options.yes_all and join(container, obj) or obj |
3634 | + if path[:1] in ('/', '\\'): |
3635 | + path = path[1:] |
3636 | + md5sum = None |
3637 | + make_dir = out_file != "-" |
3638 | + if content_type.split(';', 1)[0] == 'text/directory': |
3639 | + if make_dir and not isdir(path): |
3640 | + mkdirs(path) |
3641 | + read_length = 0 |
3642 | + if 'x-object-manifest' not in headers: |
3643 | + md5sum = md5() |
3644 | + for chunk in body: |
3645 | + read_length += len(chunk) |
3646 | + if md5sum: |
3647 | + md5sum.update(chunk) |
3648 | + else: |
3649 | + dirpath = dirname(path) |
3650 | + if make_dir and dirpath and not isdir(dirpath): |
3651 | + mkdirs(dirpath) |
3652 | + if out_file == "-": |
3653 | + fp = stdout |
3654 | + elif out_file: |
3655 | + fp = open(out_file, 'wb') |
3656 | + else: |
3657 | + fp = open(path, 'wb') |
3658 | + read_length = 0 |
3659 | + if 'x-object-manifest' not in headers: |
3660 | + md5sum = md5() |
3661 | + for chunk in body: |
3662 | + fp.write(chunk) |
3663 | + read_length += len(chunk) |
3664 | + if md5sum: |
3665 | + md5sum.update(chunk) |
3666 | + fp.close() |
3667 | + if md5sum and md5sum.hexdigest() != etag: |
3668 | + error_queue.put('%s: md5sum != etag, %s != %s' % |
3669 | + (path, md5sum.hexdigest(), etag)) |
3670 | + if content_length is not None and read_length != content_length: |
3671 | + error_queue.put('%s: read_length != content_length, %d != %d' % |
3672 | + (path, read_length, content_length)) |
3673 | + if 'x-object-meta-mtime' in headers and not options.out_file: |
3674 | + mtime = float(headers['x-object-meta-mtime']) |
3675 | + utime(path, (mtime, mtime)) |
3676 | + if options.verbose: |
3677 | + if conn.attempts > 1: |
3678 | + print_queue.put('%s [after %d attempts' % |
3679 | + (path, conn.attempts)) |
3680 | + else: |
3681 | + print_queue.put(path) |
3682 | + except ClientException, err: |
3683 | + if err.http_status != 404: |
3684 | + raise |
3685 | + error_queue.put('Object %s not found' % |
3686 | + repr('%s/%s' % (container, obj))) |
3687 | + |
3688 | + container_queue = Queue(10000) |
3689 | + |
3690 | + def _download_container(container, conn): |
3691 | + try: |
3692 | + marker = '' |
3693 | + while True: |
3694 | + objects = [o['name'] for o in |
3695 | + conn.get_container(container, marker=marker)[1]] |
3696 | + if not objects: |
3697 | + break |
3698 | + for obj in objects: |
3699 | + object_queue.put((container, obj)) |
3700 | + marker = objects[-1] |
3701 | + except ClientException, err: |
3702 | + if err.http_status != 404: |
3703 | + raise |
3704 | + error_queue.put('Container %s not found' % repr(container)) |
3705 | + |
3706 | + url, token = get_auth(options.auth, options.user, options.key, |
3707 | + snet=options.snet) |
3708 | + create_connection = lambda: Connection(options.auth, options.user, |
3709 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
3710 | + object_threads = [QueueFunctionThread(object_queue, _download_object, |
3711 | + create_connection()) for _junk in xrange(10)] |
3712 | + for thread in object_threads: |
3713 | + thread.start() |
3714 | + container_threads = [QueueFunctionThread(container_queue, |
3715 | + _download_container, create_connection()) for _junk in xrange(10)] |
3716 | + for thread in container_threads: |
3717 | + thread.start() |
3718 | + if not args: |
3719 | + conn = create_connection() |
3720 | + try: |
3721 | + marker = '' |
3722 | + while True: |
3723 | + containers = [c['name'] |
3724 | + for c in conn.get_account(marker=marker)[1]] |
3725 | + if not containers: |
3726 | + break |
3727 | + for container in containers: |
3728 | + container_queue.put(container) |
3729 | + marker = containers[-1] |
3730 | + except ClientException, err: |
3731 | + if err.http_status != 404: |
3732 | + raise |
3733 | + error_queue.put('Account not found') |
3734 | + elif len(args) == 1: |
3735 | + if '/' in args[0]: |
3736 | + print >> stderr, 'WARNING: / in container name; you might have ' \ |
3737 | + 'meant %r instead of %r.' % \ |
3738 | + (args[0].replace('/', ' ', 1), args[0]) |
3739 | + _download_container(args[0], create_connection()) |
3740 | + else: |
3741 | + if len(args) == 2: |
3742 | + obj = args[1] |
3743 | + object_queue.put((args[0], obj, options.out_file)) |
3744 | + else: |
3745 | + for obj in args[1:]: |
3746 | + object_queue.put((args[0], obj)) |
3747 | + while not container_queue.empty(): |
3748 | + sleep(0.01) |
3749 | + for thread in container_threads: |
3750 | + thread.abort = True |
3751 | + while thread.isAlive(): |
3752 | + thread.join(0.01) |
3753 | + put_errors_from_threads(container_threads, error_queue) |
3754 | + while not object_queue.empty(): |
3755 | + sleep(0.01) |
3756 | + for thread in object_threads: |
3757 | + thread.abort = True |
3758 | + while thread.isAlive(): |
3759 | + thread.join(0.01) |
3760 | + put_errors_from_threads(object_threads, error_queue) |
3761 | + |
3762 | + |
3763 | +st_list_help = ''' |
3764 | +list [options] [container] |
3765 | + Lists the containers for the account or the objects for a container. -p or |
3766 | + --prefix is an option that will only list items beginning with that prefix. |
3767 | + -d or --delimiter is option (for container listings only) that will roll up |
3768 | + items with the given delimiter (see Cloud Files general documentation for |
3769 | + what this means). |
3770 | +'''.strip('\n') |
3771 | + |
3772 | + |
3773 | +def st_list(options, args, print_queue, error_queue): |
3774 | + parser.add_option('-p', '--prefix', dest='prefix', help='Will only list ' |
3775 | + 'items beginning with the prefix') |
3776 | + parser.add_option('-d', '--delimiter', dest='delimiter', help='Will roll ' |
3777 | + 'up items with the given delimiter (see Cloud Files general ' |
3778 | + 'documentation for what this means)') |
3779 | + (options, args) = parse_args(parser, args) |
3780 | + args = args[1:] |
3781 | + if options.delimiter and not args: |
3782 | + exit('-d option only allowed for container listings') |
3783 | + if len(args) > 1: |
3784 | + error_queue.put('Usage: %s [options] %s' % |
3785 | + (basename(argv[0]), st_list_help)) |
3786 | + return |
3787 | + conn = Connection(options.auth, options.user, options.key, |
3788 | + snet=options.snet) |
3789 | + try: |
3790 | + marker = '' |
3791 | + while True: |
3792 | + if not args: |
3793 | + items = \ |
3794 | + conn.get_account(marker=marker, prefix=options.prefix)[1] |
3795 | + else: |
3796 | + items = conn.get_container(args[0], marker=marker, |
3797 | + prefix=options.prefix, delimiter=options.delimiter)[1] |
3798 | + if not items: |
3799 | + break |
3800 | + for item in items: |
3801 | + print_queue.put(item.get('name', item.get('subdir'))) |
3802 | + marker = items[-1].get('name', items[-1].get('subdir')) |
3803 | + except ClientException, err: |
3804 | + if err.http_status != 404: |
3805 | + raise |
3806 | + if not args: |
3807 | + error_queue.put('Account not found') |
3808 | + else: |
3809 | + error_queue.put('Container %s not found' % repr(args[0])) |
3810 | + |
3811 | + |
3812 | +st_stat_help = ''' |
3813 | +stat [container] [object] |
3814 | + Displays information for the account, container, or object depending on the |
3815 | + args given (if any).'''.strip('\n') |
3816 | + |
3817 | + |
3818 | +def st_stat(options, args, print_queue, error_queue): |
3819 | + (options, args) = parse_args(parser, args) |
3820 | + args = args[1:] |
3821 | + conn = Connection(options.auth, options.user, options.key) |
3822 | + if not args: |
3823 | + try: |
3824 | + headers = conn.head_account() |
3825 | + if options.verbose > 1: |
3826 | + print_queue.put(''' |
3827 | +StorageURL: %s |
3828 | +Auth Token: %s |
3829 | +'''.strip('\n') % (conn.url, conn.token)) |
3830 | + container_count = int(headers.get('x-account-container-count', 0)) |
3831 | + object_count = int(headers.get('x-account-object-count', 0)) |
3832 | + bytes_used = int(headers.get('x-account-bytes-used', 0)) |
3833 | + print_queue.put(''' |
3834 | + Account: %s |
3835 | +Containers: %d |
3836 | + Objects: %d |
3837 | + Bytes: %d'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], container_count, |
3838 | + object_count, bytes_used)) |
3839 | + for key, value in headers.items(): |
3840 | + if key.startswith('x-account-meta-'): |
3841 | + print_queue.put('%10s: %s' % ('Meta %s' % |
3842 | + key[len('x-account-meta-'):].title(), value)) |
3843 | + for key, value in headers.items(): |
3844 | + if not key.startswith('x-account-meta-') and key not in ( |
3845 | + 'content-length', 'date', 'x-account-container-count', |
3846 | + 'x-account-object-count', 'x-account-bytes-used'): |
3847 | + print_queue.put( |
3848 | + '%10s: %s' % (key.title(), value)) |
3849 | + except ClientException, err: |
3850 | + if err.http_status != 404: |
3851 | + raise |
3852 | + error_queue.put('Account not found') |
3853 | + elif len(args) == 1: |
3854 | + if '/' in args[0]: |
3855 | + print >> stderr, 'WARNING: / in container name; you might have ' \ |
3856 | + 'meant %r instead of %r.' % \ |
3857 | + (args[0].replace('/', ' ', 1), args[0]) |
3858 | + try: |
3859 | + headers = conn.head_container(args[0]) |
3860 | + object_count = int(headers.get('x-container-object-count', 0)) |
3861 | + bytes_used = int(headers.get('x-container-bytes-used', 0)) |
3862 | + print_queue.put(''' |
3863 | + Account: %s |
3864 | +Container: %s |
3865 | + Objects: %d |
3866 | + Bytes: %d |
3867 | + Read ACL: %s |
3868 | +Write ACL: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], |
3869 | + object_count, bytes_used, |
3870 | + headers.get('x-container-read', ''), |
3871 | + headers.get('x-container-write', ''))) |
3872 | + for key, value in headers.items(): |
3873 | + if key.startswith('x-container-meta-'): |
3874 | + print_queue.put('%9s: %s' % ('Meta %s' % |
3875 | + key[len('x-container-meta-'):].title(), value)) |
3876 | + for key, value in headers.items(): |
3877 | + if not key.startswith('x-container-meta-') and key not in ( |
3878 | + 'content-length', 'date', 'x-container-object-count', |
3879 | + 'x-container-bytes-used', 'x-container-read', |
3880 | + 'x-container-write'): |
3881 | + print_queue.put( |
3882 | + '%9s: %s' % (key.title(), value)) |
3883 | + except ClientException, err: |
3884 | + if err.http_status != 404: |
3885 | + raise |
3886 | + error_queue.put('Container %s not found' % repr(args[0])) |
3887 | + elif len(args) == 2: |
3888 | + try: |
3889 | + headers = conn.head_object(args[0], args[1]) |
3890 | + print_queue.put(''' |
3891 | + Account: %s |
3892 | + Container: %s |
3893 | + Object: %s |
3894 | + Content Type: %s'''.strip('\n') % (conn.url.rsplit('/', 1)[-1], args[0], |
3895 | + args[1], headers.get('content-type'))) |
3896 | + if 'content-length' in headers: |
3897 | + print_queue.put('Content Length: %s' % |
3898 | + headers['content-length']) |
3899 | + if 'last-modified' in headers: |
3900 | + print_queue.put(' Last Modified: %s' % |
3901 | + headers['last-modified']) |
3902 | + if 'etag' in headers: |
3903 | + print_queue.put(' ETag: %s' % headers['etag']) |
3904 | + if 'x-object-manifest' in headers: |
3905 | + print_queue.put(' Manifest: %s' % |
3906 | + headers['x-object-manifest']) |
3907 | + for key, value in headers.items(): |
3908 | + if key.startswith('x-object-meta-'): |
3909 | + print_queue.put('%14s: %s' % ('Meta %s' % |
3910 | + key[len('x-object-meta-'):].title(), value)) |
3911 | + for key, value in headers.items(): |
3912 | + if not key.startswith('x-object-meta-') and key not in ( |
3913 | + 'content-type', 'content-length', 'last-modified', |
3914 | + 'etag', 'date', 'x-object-manifest'): |
3915 | + print_queue.put( |
3916 | + '%14s: %s' % (key.title(), value)) |
3917 | + except ClientException, err: |
3918 | + if err.http_status != 404: |
3919 | + raise |
3920 | + error_queue.put('Object %s not found' % |
3921 | + repr('%s/%s' % (args[0], args[1]))) |
3922 | + else: |
3923 | + error_queue.put('Usage: %s [options] %s' % |
3924 | + (basename(argv[0]), st_stat_help)) |
3925 | + |
3926 | + |
3927 | +st_post_help = ''' |
3928 | +post [options] [container] [object] |
3929 | + Updates meta information for the account, container, or object depending on |
3930 | + the args given. If the container is not found, it will be created |
3931 | + automatically; but this is not true for accounts and objects. Containers |
3932 | + also allow the -r (or --read-acl) and -w (or --write-acl) options. The -m |
3933 | + or --meta option is allowed on all and used to define the user meta data |
3934 | + items to set in the form Name:Value. This option can be repeated. Example: |
3935 | + post -m Color:Blue -m Size:Large'''.strip('\n') |
3936 | + |
3937 | + |
3938 | +def st_post(options, args, print_queue, error_queue): |
3939 | + parser.add_option('-r', '--read-acl', dest='read_acl', help='Sets the ' |
3940 | + 'Read ACL for containers. Quick summary of ACL syntax: .r:*, ' |
3941 | + '.r:-.example.com, .r:www.example.com, account1, account2:user2') |
3942 | + parser.add_option('-w', '--write-acl', dest='write_acl', help='Sets the ' |
3943 | + 'Write ACL for containers. Quick summary of ACL syntax: account1, ' |
3944 | + 'account2:user2') |
3945 | + parser.add_option('-m', '--meta', action='append', dest='meta', default=[], |
3946 | + help='Sets a meta data item with the syntax name:value. This option ' |
3947 | + 'may be repeated. Example: -m Color:Blue -m Size:Large') |
3948 | + (options, args) = parse_args(parser, args) |
3949 | + args = args[1:] |
3950 | + if (options.read_acl or options.write_acl) and not args: |
3951 | + exit('-r and -w options only allowed for containers') |
3952 | + conn = Connection(options.auth, options.user, options.key) |
3953 | + if not args: |
3954 | + headers = {} |
3955 | + for item in options.meta: |
3956 | + split_item = item.split(':') |
3957 | + headers['X-Account-Meta-' + split_item[0]] = \ |
3958 | + len(split_item) > 1 and split_item[1] |
3959 | + try: |
3960 | + conn.post_account(headers=headers) |
3961 | + except ClientException, err: |
3962 | + if err.http_status != 404: |
3963 | + raise |
3964 | + error_queue.put('Account not found') |
3965 | + elif len(args) == 1: |
3966 | + if '/' in args[0]: |
3967 | + print >> stderr, 'WARNING: / in container name; you might have ' \ |
3968 | + 'meant %r instead of %r.' % \ |
3969 | + (args[0].replace('/', ' ', 1), args[0]) |
3970 | + headers = {} |
3971 | + for item in options.meta: |
3972 | + split_item = item.split(':') |
3973 | + headers['X-Container-Meta-' + split_item[0]] = \ |
3974 | + len(split_item) > 1 and split_item[1] |
3975 | + if options.read_acl is not None: |
3976 | + headers['X-Container-Read'] = options.read_acl |
3977 | + if options.write_acl is not None: |
3978 | + headers['X-Container-Write'] = options.write_acl |
3979 | + try: |
3980 | + conn.post_container(args[0], headers=headers) |
3981 | + except ClientException, err: |
3982 | + if err.http_status != 404: |
3983 | + raise |
3984 | + conn.put_container(args[0], headers=headers) |
3985 | + elif len(args) == 2: |
3986 | + headers = {} |
3987 | + for item in options.meta: |
3988 | + split_item = item.split(':') |
3989 | + headers['X-Object-Meta-' + split_item[0]] = \ |
3990 | + len(split_item) > 1 and split_item[1] |
3991 | + try: |
3992 | + conn.post_object(args[0], args[1], headers=headers) |
3993 | + except ClientException, err: |
3994 | + if err.http_status != 404: |
3995 | + raise |
3996 | + error_queue.put('Object %s not found' % |
3997 | + repr('%s/%s' % (args[0], args[1]))) |
3998 | + else: |
3999 | + error_queue.put('Usage: %s [options] %s' % |
4000 | + (basename(argv[0]), st_post_help)) |
4001 | + |
4002 | + |
4003 | +st_upload_help = ''' |
4004 | +upload [options] container file_or_directory [file_or_directory] [...] |
4005 | + Uploads to the given container the files and directories specified by the |
4006 | + remaining args. -c or --changed is an option that will only upload files |
4007 | + that have changed since the last upload. -S <size> or --segment-size <size> |
4008 | + and --leave-segments are options as well (see --help for more). |
4009 | +'''.strip('\n') |
4010 | + |
4011 | + |
4012 | +def st_upload(options, args, print_queue, error_queue): |
4013 | + parser.add_option('-c', '--changed', action='store_true', dest='changed', |
4014 | + default=False, help='Will only upload files that have changed since ' |
4015 | + 'the last upload') |
4016 | + parser.add_option('-S', '--segment-size', dest='segment_size', help='Will ' |
4017 | + 'upload files in segments no larger than <size> and then create a ' |
4018 | + '"manifest" file that will download all the segments as if it were ' |
4019 | + 'the original file. The segments will be uploaded to a ' |
4020 | + '<container>_segments container so as to not pollute the main ' |
4021 | + '<container> listings.') |
4022 | + parser.add_option('', '--leave-segments', action='store_true', |
4023 | + dest='leave_segments', default=False, help='Indicates that you want ' |
4024 | + 'the older segments of manifest objects left alone (in the case of ' |
4025 | + 'overwrites)') |
4026 | + (options, args) = parse_args(parser, args) |
4027 | + args = args[1:] |
4028 | + if len(args) < 2: |
4029 | + error_queue.put('Usage: %s [options] %s' % |
4030 | + (basename(argv[0]), st_upload_help)) |
4031 | + return |
4032 | + object_queue = Queue(10000) |
4033 | + |
4034 | + def _segment_job(job, conn): |
4035 | + if job.get('delete', False): |
4036 | + conn.delete_object(job['container'], job['obj']) |
4037 | + else: |
4038 | + fp = open(job['path'], 'rb') |
4039 | + fp.seek(job['segment_start']) |
4040 | + conn.put_object(job.get('container', args[0] + '_segments'), |
4041 | + job['obj'], fp, content_length=job['segment_size']) |
4042 | + if options.verbose and 'log_line' in job: |
4043 | + if conn.attempts > 1: |
4044 | + print_queue.put('%s [after %d attempts]' % |
4045 | + (job['log_line'], conn.attempts)) |
4046 | + else: |
4047 | + print_queue.put(job['log_line']) |
4048 | + |
4049 | + def _object_job(job, conn): |
4050 | + path = job['path'] |
4051 | + container = job.get('container', args[0]) |
4052 | + dir_marker = job.get('dir_marker', False) |
4053 | + try: |
4054 | + obj = path |
4055 | + if obj.startswith('./') or obj.startswith('.\\'): |
4056 | + obj = obj[2:] |
4057 | + put_headers = {'x-object-meta-mtime': str(getmtime(path))} |
4058 | + if dir_marker: |
4059 | + if options.changed: |
4060 | + try: |
4061 | + headers = conn.head_object(container, obj) |
4062 | + ct = headers.get('content-type') |
4063 | + cl = int(headers.get('content-length')) |
4064 | + et = headers.get('etag') |
4065 | + mt = headers.get('x-object-meta-mtime') |
4066 | + if ct.split(';', 1)[0] == 'text/directory' and \ |
4067 | + cl == 0 and \ |
4068 | + et == 'd41d8cd98f00b204e9800998ecf8427e' and \ |
4069 | + mt == put_headers['x-object-meta-mtime']: |
4070 | + return |
4071 | + except ClientException, err: |
4072 | + if err.http_status != 404: |
4073 | + raise |
4074 | + conn.put_object(container, obj, '', content_length=0, |
4075 | + content_type='text/directory', |
4076 | + headers=put_headers) |
4077 | + else: |
4078 | + # We need to HEAD all objects now in case we're overwriting a |
4079 | + # manifest object and need to delete the old segments |
4080 | + # ourselves. |
4081 | + old_manifest = None |
4082 | + if options.changed or not options.leave_segments: |
4083 | + try: |
4084 | + headers = conn.head_object(container, obj) |
4085 | + cl = int(headers.get('content-length')) |
4086 | + mt = headers.get('x-object-meta-mtime') |
4087 | + if options.changed and cl == getsize(path) and \ |
4088 | + mt == put_headers['x-object-meta-mtime']: |
4089 | + return |
4090 | + if not options.leave_segments: |
4091 | + old_manifest = headers.get('x-object-manifest') |
4092 | + except ClientException, err: |
4093 | + if err.http_status != 404: |
4094 | + raise |
4095 | + if options.segment_size and \ |
4096 | + getsize(path) < options.segment_size: |
4097 | + full_size = getsize(path) |
4098 | + segment_queue = Queue(10000) |
4099 | + segment_threads = [QueueFunctionThread(segment_queue, |
4100 | + _segment_job, create_connection()) for _junk in |
4101 | + xrange(10)] |
4102 | + for thread in segment_threads: |
4103 | + thread.start() |
4104 | + segment = 0 |
4105 | + segment_start = 0 |
4106 | + while segment_start < full_size: |
4107 | + segment_size = int(options.segment_size) |
4108 | + if segment_start + segment_size > full_size: |
4109 | + segment_size = full_size - segment_start |
4110 | + segment_queue.put({'path': path, |
4111 | + 'obj': '%s/%s/%s/%08d' % (obj, |
4112 | + put_headers['x-object-meta-mtime'], full_size, |
4113 | + segment), |
4114 | + 'segment_start': segment_start, |
4115 | + 'segment_size': segment_size, |
4116 | + 'log_line': '%s segment %s' % (obj, segment)}) |
4117 | + segment += 1 |
4118 | + segment_start += segment_size |
4119 | + while not segment_queue.empty(): |
4120 | + sleep(0.01) |
4121 | + for thread in segment_threads: |
4122 | + thread.abort = True |
4123 | + while thread.isAlive(): |
4124 | + thread.join(0.01) |
4125 | + if put_errors_from_threads(segment_threads, error_queue): |
4126 | + raise ClientException('Aborting manifest creation ' |
4127 | + 'because not all segments could be uploaded. %s/%s' |
4128 | + % (container, obj)) |
4129 | + new_object_manifest = '%s_segments/%s/%s/%s/' % ( |
4130 | + container, obj, put_headers['x-object-meta-mtime'], |
4131 | + full_size) |
4132 | + if old_manifest == new_object_manifest: |
4133 | + old_manifest = None |
4134 | + put_headers['x-object-manifest'] = new_object_manifest |
4135 | + conn.put_object(container, obj, '', content_length=0, |
4136 | + headers=put_headers) |
4137 | + else: |
4138 | + conn.put_object(container, obj, open(path, 'rb'), |
4139 | + content_length=getsize(path), headers=put_headers) |
4140 | + if old_manifest: |
4141 | + segment_queue = Queue(10000) |
4142 | + scontainer, sprefix = old_manifest.split('/', 1) |
4143 | + for delobj in conn.get_container(scontainer, |
4144 | + prefix=sprefix)[1]: |
4145 | + segment_queue.put({'delete': True, |
4146 | + 'container': scontainer, 'obj': delobj['name']}) |
4147 | + if not segment_queue.empty(): |
4148 | + segment_threads = [QueueFunctionThread(segment_queue, |
4149 | + _segment_job, create_connection()) for _junk in |
4150 | + xrange(10)] |
4151 | + for thread in segment_threads: |
4152 | + thread.start() |
4153 | + while not segment_queue.empty(): |
4154 | + sleep(0.01) |
4155 | + for thread in segment_threads: |
4156 | + thread.abort = True |
4157 | + while thread.isAlive(): |
4158 | + thread.join(0.01) |
4159 | + put_errors_from_threads(segment_threads, error_queue) |
4160 | + if options.verbose: |
4161 | + if conn.attempts > 1: |
4162 | + print_queue.put( |
4163 | + '%s [after %d attempts]' % (obj, conn.attempts)) |
4164 | + else: |
4165 | + print_queue.put(obj) |
4166 | + except OSError, err: |
4167 | + if err.errno != ENOENT: |
4168 | + raise |
4169 | + error_queue.put('Local file %s not found' % repr(path)) |
4170 | + |
4171 | + def _upload_dir(path): |
4172 | + names = listdir(path) |
4173 | + if not names: |
4174 | + object_queue.put({'path': path, 'dir_marker': True}) |
4175 | + else: |
4176 | + for name in listdir(path): |
4177 | + subpath = join(path, name) |
4178 | + if isdir(subpath): |
4179 | + _upload_dir(subpath) |
4180 | + else: |
4181 | + object_queue.put({'path': subpath}) |
4182 | + |
4183 | + url, token = get_auth(options.auth, options.user, options.key, |
4184 | + snet=options.snet) |
4185 | + create_connection = lambda: Connection(options.auth, options.user, |
4186 | + options.key, preauthurl=url, preauthtoken=token, snet=options.snet) |
4187 | + object_threads = [QueueFunctionThread(object_queue, _object_job, |
4188 | + create_connection()) for _junk in xrange(10)] |
4189 | + for thread in object_threads: |
4190 | + thread.start() |
4191 | + conn = create_connection() |
4192 | + # Try to create the container, just in case it doesn't exist. If this |
4193 | + # fails, it might just be because the user doesn't have container PUT |
4194 | + # permissions, so we'll ignore any error. If there's really a problem, |
4195 | + # it'll surface on the first object PUT. |
4196 | + try: |
4197 | + conn.put_container(args[0]) |
4198 | + if options.segment_size is not None: |
4199 | + conn.put_container(args[0] + '_segments') |
4200 | + except Exception: |
4201 | + pass |
4202 | + try: |
4203 | + for arg in args[1:]: |
4204 | + if isdir(arg): |
4205 | + _upload_dir(arg) |
4206 | + else: |
4207 | + object_queue.put({'path': arg}) |
4208 | + while not object_queue.empty(): |
4209 | + sleep(0.01) |
4210 | + for thread in object_threads: |
4211 | + thread.abort = True |
4212 | + while thread.isAlive(): |
4213 | + thread.join(0.01) |
4214 | + put_errors_from_threads(object_threads, error_queue) |
4215 | + except ClientException, err: |
4216 | + if err.http_status != 404: |
4217 | + raise |
4218 | + error_queue.put('Account not found') |
4219 | + |
4220 | + |
4221 | +def parse_args(parser, args, enforce_requires=True): |
4222 | + if not args: |
4223 | + args = ['-h'] |
4224 | + (options, args) = parser.parse_args(args) |
4225 | + if enforce_requires and \ |
4226 | + not (options.auth and options.user and options.key): |
4227 | + exit(''' |
4228 | +Requires ST_AUTH, ST_USER, and ST_KEY environment variables be set or |
4229 | +overridden with -A, -U, or -K.'''.strip('\n')) |
4230 | + return options, args |
4231 | + |
4232 | + |
4233 | +if __name__ == '__main__': |
4234 | + parser = OptionParser(version='%prog 1.0', usage=''' |
4235 | +Usage: %%prog <command> [options] [args] |
4236 | + |
4237 | +Commands: |
4238 | + %(st_stat_help)s |
4239 | + %(st_list_help)s |
4240 | + %(st_upload_help)s |
4241 | + %(st_post_help)s |
4242 | + %(st_download_help)s |
4243 | + %(st_delete_help)s |
4244 | + |
4245 | +Example: |
4246 | + %%prog -A https://auth.api.rackspacecloud.com/v1.0 -U user -K key stat |
4247 | +'''.strip('\n') % globals()) |
4248 | + parser.add_option('-s', '--snet', action='store_true', dest='snet', |
4249 | + default=False, help='Use SERVICENET internal network') |
4250 | + parser.add_option('-v', '--verbose', action='count', dest='verbose', |
4251 | + default=1, help='Print more info') |
4252 | + parser.add_option('-q', '--quiet', action='store_const', dest='verbose', |
4253 | + const=0, default=1, help='Suppress status output') |
4254 | + parser.add_option('-A', '--auth', dest='auth', |
4255 | + default=environ.get('ST_AUTH'), |
4256 | + help='URL for obtaining an auth token') |
4257 | + parser.add_option('-U', '--user', dest='user', |
4258 | + default=environ.get('ST_USER'), |
4259 | + help='User name for obtaining an auth token') |
4260 | + parser.add_option('-K', '--key', dest='key', |
4261 | + default=environ.get('ST_KEY'), |
4262 | + help='Key for obtaining an auth token') |
4263 | + parser.disable_interspersed_args() |
4264 | + (options, args) = parse_args(parser, argv[1:], enforce_requires=False) |
4265 | + parser.enable_interspersed_args() |
4266 | + |
4267 | + commands = ('delete', 'download', 'list', 'post', 'stat', 'upload') |
4268 | + if not args or args[0] not in commands: |
4269 | + parser.print_usage() |
4270 | + if args: |
4271 | + exit('no such command: %s' % args[0]) |
4272 | + exit() |
4273 | + |
4274 | + print_queue = Queue(10000) |
4275 | + |
4276 | + def _print(item): |
4277 | + if isinstance(item, unicode): |
4278 | + item = item.encode('utf8') |
4279 | + print item |
4280 | + |
4281 | + print_thread = QueueFunctionThread(print_queue, _print) |
4282 | + print_thread.start() |
4283 | + |
4284 | + error_queue = Queue(10000) |
4285 | + |
4286 | + def _error(item): |
4287 | + if isinstance(item, unicode): |
4288 | + item = item.encode('utf8') |
4289 | + print >> stderr, item |
4290 | + |
4291 | + error_thread = QueueFunctionThread(error_queue, _error) |
4292 | + error_thread.start() |
4293 | + |
4294 | + try: |
4295 | + parser.usage = globals()['st_%s_help' % args[0]] |
4296 | + try: |
4297 | + globals()['st_%s' % args[0]](parser, argv[1:], print_queue, |
4298 | + error_queue) |
4299 | + except (ClientException, HTTPException, socket.error), err: |
4300 | + error_queue.put(str(err)) |
4301 | + while not print_queue.empty(): |
4302 | + sleep(0.01) |
4303 | + print_thread.abort = True |
4304 | + while print_thread.isAlive(): |
4305 | + print_thread.join(0.01) |
4306 | + while not error_queue.empty(): |
4307 | + sleep(0.01) |
4308 | + error_thread.abort = True |
4309 | + while error_thread.isAlive(): |
4310 | + error_thread.join(0.01) |
4311 | + except (SystemExit, Exception): |
4312 | + for thread in threading_enumerate(): |
4313 | + thread.abort = True |
4314 | + raise |
4315 | |
4316 | === modified file 'doc/source/admin_guide.rst' |
4317 | --- doc/source/admin_guide.rst 2011-03-31 22:32:41 +0000 |
4318 | +++ doc/source/admin_guide.rst 2011-06-15 09:24:34 +0000 |
4319 | @@ -222,22 +222,6 @@ |
4320 | Sample represents 1.00% of the object partition space |
4321 | |
4322 | |
4323 | ------------------------------------- |
4324 | -Additional Cleanup Script for Swauth |
4325 | ------------------------------------- |
4326 | - |
4327 | -With Swauth, you'll want to install a cronjob to clean up any |
4328 | -orphaned expired tokens. These orphaned tokens can occur when a "stampede" |
4329 | -occurs where a single user authenticates several times concurrently. Generally, |
4330 | -these orphaned tokens don't pose much of an issue, but it's good to clean them |
4331 | -up once a "token life" period (default: 1 day or 86400 seconds). |
4332 | - |
4333 | -This should be as simple as adding `swauth-cleanup-tokens -A |
4334 | -https://<PROXY_HOSTNAME>:8080/auth/ -K swauthkey > /dev/null` to a crontab |
4335 | -entry on one of the proxies that is running Swauth; but run |
4336 | -`swauth-cleanup-tokens` with no arguments for detailed help on the options |
4337 | -available. |
4338 | - |
4339 | ------------------------ |
4340 | Debugging Tips and Tools |
4341 | ------------------------ |
4342 | |
4343 | === modified file 'doc/source/deployment_guide.rst' |
4344 | --- doc/source/deployment_guide.rst 2011-01-25 00:28:22 +0000 |
4345 | +++ doc/source/deployment_guide.rst 2011-06-15 09:24:34 +0000 |
4346 | @@ -547,37 +547,23 @@ |
4347 | node error limited |
4348 | allow_account_management false Whether account PUTs and DELETEs |
4349 | are even callable |
4350 | +account_autocreate false If set to 'true' authorized |
4351 | + accounts that do not yet exist |
4352 | + within the Swift cluster will |
4353 | + be automatically created. |
4354 | ============================ =============== ============================= |
4355 | |
4356 | -[auth] |
4357 | - |
4358 | -============ =================================== ======================== |
4359 | -Option Default Description |
4360 | ------------- ----------------------------------- ------------------------ |
4361 | -use Entry point for paste.deploy |
4362 | - to use for auth. To |
4363 | - use the swift dev auth, |
4364 | - set to: |
4365 | - `egg:swift#auth` |
4366 | -ip 127.0.0.1 IP address of auth |
4367 | - server |
4368 | -port 11000 Port of auth server |
4369 | -ssl False If True, use SSL to |
4370 | - connect to auth |
4371 | -node_timeout 10 Request timeout |
4372 | -============ =================================== ======================== |
4373 | - |
4374 | -[swauth] |
4375 | +[tempauth] |
4376 | |
4377 | ===================== =============================== ======================= |
4378 | Option Default Description |
4379 | --------------------- ------------------------------- ----------------------- |
4380 | use Entry point for |
4381 | paste.deploy to use for |
4382 | - auth. To use the swauth |
4383 | + auth. To use tempauth |
4384 | set to: |
4385 | - `egg:swift#swauth` |
4386 | -set log_name auth-server Label used when logging |
4387 | + `egg:swift#tempauth` |
4388 | +set log_name tempauth Label used when logging |
4389 | set log_facility LOG_LOCAL0 Syslog log facility |
4390 | set log_level INFO Log level |
4391 | set log_headers True If True, log headers in |
4392 | @@ -593,16 +579,39 @@ |
4393 | reserves anything |
4394 | beginning with the |
4395 | letter `v`. |
4396 | -default_swift_cluster local#http://127.0.0.1:8080/v1 The default Swift |
4397 | - cluster to place newly |
4398 | - created accounts on. |
4399 | token_life 86400 The number of seconds a |
4400 | token is valid. |
4401 | -node_timeout 10 Request timeout |
4402 | -super_admin_key None The key for the |
4403 | - .super_admin account. |
4404 | ===================== =============================== ======================= |
4405 | |
4406 | +Additionally, you need to list all the accounts/users you want here. The format |
4407 | +is:: |
4408 | + |
4409 | + user_<account>_<user> = <key> [group] [group] [...] [storage_url] |
4410 | + |
4411 | +There are special groups of:: |
4412 | + |
4413 | + .reseller_admin = can do anything to any account for this auth |
4414 | + .admin = can do anything within the account |
4415 | + |
4416 | +If neither of these groups are specified, the user can only access containers |
4417 | +that have been explicitly allowed for them by a .admin or .reseller_admin. |
4418 | + |
4419 | +The trailing optional storage_url allows you to specify an alternate url to |
4420 | +hand back to the user upon authentication. If not specified, this defaults to:: |
4421 | + |
4422 | + http[s]://<ip>:<port>/v1/<reseller_prefix>_<account> |
4423 | + |
4424 | +Where http or https depends on whether cert_file is specified in the [DEFAULT] |
4425 | +section, <ip> and <port> are based on the [DEFAULT] section's bind_ip and |
4426 | +bind_port (falling back to 127.0.0.1 and 8080), <reseller_prefix> is from this |
4427 | +section, and <account> is from the user_<account>_<user> name. |
4428 | + |
4429 | +Here are example entries, required for running the tests:: |
4430 | + |
4431 | + user_admin_admin = admin .admin .reseller_admin |
4432 | + user_test_tester = testing .admin |
4433 | + user_test2_tester2 = testing2 .admin |
4434 | + user_test_tester3 = testing3 |
4435 | |
4436 | ------------------------ |
4437 | Memcached Considerations |
4438 | |
4439 | === modified file 'doc/source/development_auth.rst' |
4440 | --- doc/source/development_auth.rst 2011-03-14 02:56:37 +0000 |
4441 | +++ doc/source/development_auth.rst 2011-06-15 09:24:34 +0000 |
4442 | @@ -6,7 +6,7 @@ |
4443 | Creating Your Own Auth Server and Middleware |
4444 | -------------------------------------------- |
4445 | |
4446 | -The included swift/common/middleware/swauth.py is a good example of how to |
4447 | +The included swift/common/middleware/tempauth.py is a good example of how to |
4448 | create an auth subsystem with proxy server auth middleware. The main points are |
4449 | that the auth middleware can reject requests up front, before they ever get to |
4450 | the Swift Proxy application, and afterwards when the proxy issues callbacks to |
4451 | @@ -27,7 +27,7 @@ |
4452 | environ['REMOTE_USER'] set to the authenticated user string but often more |
4453 | information is needed than just that. |
4454 | |
4455 | -The included Swauth will set the REMOTE_USER to a comma separated list of |
4456 | +The included TempAuth will set the REMOTE_USER to a comma separated list of |
4457 | groups the user belongs to. The first group will be the "user's group", a group |
4458 | that only the user belongs to. The second group will be the "account's group", |
4459 | a group that includes all users for that auth account (different than the |
4460 | @@ -37,7 +37,7 @@ |
4461 | |
4462 | It is highly recommended that authentication server implementers prefix their |
4463 | tokens and Swift storage accounts they create with a configurable reseller |
4464 | -prefix (`AUTH_` by default with the included Swauth). This prefix will avoid |
4465 | +prefix (`AUTH_` by default with the included TempAuth). This prefix will avoid |
4466 | conflicts with other authentication servers that might be using the same |
4467 | Swift cluster. Otherwise, the Swift cluster will have to try all the resellers |
4468 | until one validates a token or all fail. |
4469 | @@ -46,14 +46,14 @@ |
4470 | '.' as that is reserved for internal Swift use (such as the .r for referrer |
4471 | designations as you'll see later). |
4472 | |
4473 | -Example Authentication with Swauth: |
4474 | +Example Authentication with TempAuth: |
4475 | |
4476 | - * Token AUTH_tkabcd is given to the Swauth middleware in a request's |
4477 | + * Token AUTH_tkabcd is given to the TempAuth middleware in a request's |
4478 | X-Auth-Token header. |
4479 | - * The Swauth middleware validates the token AUTH_tkabcd and discovers |
4480 | + * The TempAuth middleware validates the token AUTH_tkabcd and discovers |
4481 | it matches the "tester" user within the "test" account for the storage |
4482 | account "AUTH_storage_xyz". |
4483 | - * The Swauth server sets the REMOTE_USER to |
4484 | + * The TempAuth middleware sets the REMOTE_USER to |
4485 | "test:tester,test,AUTH_storage_xyz" |
4486 | * Now this user will have full access (via authorization procedures later) |
4487 | to the AUTH_storage_xyz Swift storage account and access to containers in |
4488 | |
4489 | === modified file 'doc/source/development_saio.rst' |
4490 | --- doc/source/development_saio.rst 2011-05-18 15:26:52 +0000 |
4491 | +++ doc/source/development_saio.rst 2011-06-15 09:24:34 +0000 |
4492 | @@ -265,16 +265,18 @@ |
4493 | log_facility = LOG_LOCAL1 |
4494 | |
4495 | [pipeline:main] |
4496 | - pipeline = healthcheck cache swauth proxy-server |
4497 | + pipeline = healthcheck cache tempauth proxy-server |
4498 | |
4499 | [app:proxy-server] |
4500 | use = egg:swift#proxy |
4501 | allow_account_management = true |
4502 | |
4503 | - [filter:swauth] |
4504 | - use = egg:swift#swauth |
4505 | - # Highly recommended to change this. |
4506 | - super_admin_key = swauthkey |
4507 | + [filter:tempauth] |
4508 | + use = egg:swift#tempauth |
4509 | + user_admin_admin = admin .admin .reseller_admin |
4510 | + user_test_tester = testing .admin |
4511 | + user_test2_tester2 = testing2 .admin |
4512 | + user_test_tester3 = testing3 |
4513 | |
4514 | [filter:healthcheck] |
4515 | use = egg:swift#healthcheck |
4516 | @@ -558,8 +560,10 @@ |
4517 | ------------------------------------ |
4518 | |
4519 | #. Create `~/bin/resetswift.` |
4520 | - If you are using a loopback device substitute `/dev/sdb1` with `/srv/swift-disk`. |
4521 | - If you did not set up rsyslog for individual logging, remove the `find /var/log/swift...` line:: |
4522 | + |
4523 | + If you are using a loopback device substitute `/dev/sdb1` with `/srv/swift-disk`. |
4524 | + |
4525 | + If you did not set up rsyslog for individual logging, remove the `find /var/log/swift...` line:: |
4526 | |
4527 | #!/bin/bash |
4528 | |
4529 | @@ -608,18 +612,6 @@ |
4530 | |
4531 | swift-init main start |
4532 | |
4533 | - #. Create `~/bin/recreateaccounts`:: |
4534 | - |
4535 | - #!/bin/bash |
4536 | - |
4537 | - # Replace swauthkey with whatever your super_admin key is (recorded in |
4538 | - # /etc/swift/proxy-server.conf). |
4539 | - swauth-prep -K swauthkey |
4540 | - swauth-add-user -K swauthkey -a test tester testing |
4541 | - swauth-add-user -K swauthkey -a test2 tester2 testing2 |
4542 | - swauth-add-user -K swauthkey test tester3 testing3 |
4543 | - swauth-add-user -K swauthkey -a -r reseller reseller reseller |
4544 | - |
4545 | #. Create `~/bin/startrest`:: |
4546 | |
4547 | #!/bin/bash |
4548 | @@ -633,7 +625,7 @@ |
4549 | #. `recreateaccounts` |
4550 | #. Get an `X-Storage-Url` and `X-Auth-Token`: ``curl -v -H 'X-Storage-User: test:tester' -H 'X-Storage-Pass: testing' http://127.0.0.1:8080/auth/v1.0`` |
4551 | #. Check that you can GET account: ``curl -v -H 'X-Auth-Token: <token-from-x-auth-token-above>' <url-from-x-storage-url-above>`` |
4552 | - #. Check that `st` works: `st -A http://127.0.0.1:8080/auth/v1.0 -U test:tester -K testing stat` |
4553 | + #. Check that `swift` works: `swift -A http://127.0.0.1:8080/auth/v1.0 -U test:tester -K testing stat` |
4554 | #. `cp ~/swift/trunk/test/functional/sample.conf /etc/swift/func_test.conf` |
4555 | #. `cd ~/swift/trunk; ./.functests` (Note: functional tests will first delete |
4556 | everything in the configured accounts.) |
4557 | |
4558 | === modified file 'doc/source/howto_installmultinode.rst' |
4559 | --- doc/source/howto_installmultinode.rst 2011-05-17 03:59:57 +0000 |
4560 | +++ doc/source/howto_installmultinode.rst 2011-06-15 09:24:34 +0000 |
4561 | @@ -13,7 +13,7 @@ |
4562 | Basic architecture and terms |
4563 | ---------------------------- |
4564 | - *node* - a host machine running one or more Swift services |
4565 | -- *Proxy node* - node that runs Proxy services; also runs Swauth |
4566 | +- *Proxy node* - node that runs Proxy services; also runs TempAuth |
4567 | - *Storage node* - node that runs Account, Container, and Object services |
4568 | - *ring* - a set of mappings of Swift data to physical devices |
4569 | |
4570 | @@ -23,7 +23,7 @@ |
4571 | |
4572 | - Runs the swift-proxy-server processes which proxy requests to the |
4573 | appropriate Storage nodes. The proxy server will also contain |
4574 | - the Swauth service as WSGI middleware. |
4575 | + the TempAuth service as WSGI middleware. |
4576 | |
4577 | - five Storage nodes |
4578 | |
4579 | @@ -130,17 +130,15 @@ |
4580 | user = swift |
4581 | |
4582 | [pipeline:main] |
4583 | - pipeline = healthcheck cache swauth proxy-server |
4584 | + pipeline = healthcheck cache tempauth proxy-server |
4585 | |
4586 | [app:proxy-server] |
4587 | use = egg:swift#proxy |
4588 | allow_account_management = true |
4589 | |
4590 | - [filter:swauth] |
4591 | - use = egg:swift#swauth |
4592 | - default_swift_cluster = local#https://$PROXY_LOCAL_NET_IP:8080/v1 |
4593 | - # Highly recommended to change this key to something else! |
4594 | - super_admin_key = swauthkey |
4595 | + [filter:tempauth] |
4596 | + use = egg:swift#tempauth |
4597 | + user_system_root = testpass .admin https://$PROXY_LOCAL_NET_IP:8080/v1/AUTH_system |
4598 | |
4599 | [filter:healthcheck] |
4600 | use = egg:swift#healthcheck |
4601 | @@ -366,16 +364,6 @@ |
4602 | |
4603 | You run these commands from the Proxy node. |
4604 | |
4605 | -#. Create a user with administrative privileges (account = system, |
4606 | - username = root, password = testpass). Make sure to replace |
4607 | - ``swauthkey`` with whatever super_admin key you assigned in |
4608 | - the proxy-server.conf file |
4609 | - above. *Note: None of the values of |
4610 | - account, username, or password are special - they can be anything.*:: |
4611 | - |
4612 | - swauth-prep -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey |
4613 | - swauth-add-user -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey -a system root testpass |
4614 | - |
4615 | #. Get an X-Storage-Url and X-Auth-Token:: |
4616 | |
4617 | curl -k -v -H 'X-Storage-User: system:root' -H 'X-Storage-Pass: testpass' https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 |
4618 | @@ -384,34 +372,34 @@ |
4619 | |
4620 | curl -k -v -H 'X-Auth-Token: <token-from-x-auth-token-above>' <url-from-x-storage-url-above> |
4621 | |
4622 | -#. Check that ``st`` works (at this point, expect zero containers, zero objects, and zero bytes):: |
4623 | - |
4624 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass stat |
4625 | - |
4626 | -#. Use ``st`` to upload a few files named 'bigfile[1-2].tgz' to a container named 'myfiles':: |
4627 | - |
4628 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload myfiles bigfile1.tgz |
4629 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload myfiles bigfile2.tgz |
4630 | - |
4631 | -#. Use ``st`` to download all files from the 'myfiles' container:: |
4632 | - |
4633 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass download myfiles |
4634 | - |
4635 | -#. Use ``st`` to save a backup of your builder files to a container named 'builders'. Very important not to lose your builders!:: |
4636 | - |
4637 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload builders /etc/swift/*.builder |
4638 | - |
4639 | -#. Use ``st`` to list your containers:: |
4640 | - |
4641 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass list |
4642 | - |
4643 | -#. Use ``st`` to list the contents of your 'builders' container:: |
4644 | - |
4645 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass list builders |
4646 | - |
4647 | -#. Use ``st`` to download all files from the 'builders' container:: |
4648 | - |
4649 | - st -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass download builders |
4650 | +#. Check that ``swift`` works (at this point, expect zero containers, zero objects, and zero bytes):: |
4651 | + |
4652 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass stat |
4653 | + |
4654 | +#. Use ``swift`` to upload a few files named 'bigfile[1-2].tgz' to a container named 'myfiles':: |
4655 | + |
4656 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload myfiles bigfile1.tgz |
4657 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload myfiles bigfile2.tgz |
4658 | + |
4659 | +#. Use ``swift`` to download all files from the 'myfiles' container:: |
4660 | + |
4661 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass download myfiles |
4662 | + |
4663 | +#. Use ``swift`` to save a backup of your builder files to a container named 'builders'. Very important not to lose your builders!:: |
4664 | + |
4665 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass upload builders /etc/swift/*.builder |
4666 | + |
4667 | +#. Use ``swift`` to list your containers:: |
4668 | + |
4669 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass list |
4670 | + |
4671 | +#. Use ``swift`` to list the contents of your 'builders' container:: |
4672 | + |
4673 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass list builders |
4674 | + |
4675 | +#. Use ``swift`` to download all files from the 'builders' container:: |
4676 | + |
4677 | + swift -A https://$PROXY_LOCAL_NET_IP:8080/auth/v1.0 -U system:root -K testpass download builders |
4678 | |
4679 | .. _add-proxy-server: |
4680 | |
4681 | @@ -430,45 +418,16 @@ |
4682 | use = egg:swift#memcache |
4683 | memcache_servers = $PROXY_LOCAL_NET_IP:11211 |
4684 | |
4685 | -#. Change the default_cluster_url to point to the load balanced url, rather than the first proxy server you created in /etc/swift/proxy-server.conf:: |
4686 | - |
4687 | - [filter:swauth] |
4688 | - use = egg:swift#swauth |
4689 | - default_swift_cluster = local#http://<LOAD_BALANCER_HOSTNAME>/v1 |
4690 | - # Highly recommended to change this key to something else! |
4691 | - super_admin_key = swauthkey |
4692 | - |
4693 | -#. The above will make new accounts with the new default_swift_cluster URL, however it won't change any existing accounts. You can change a service URL for existing accounts with:: |
4694 | - |
4695 | - First retreve what the URL was:: |
4696 | - |
4697 | - swauth-list -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey <account> |
4698 | - |
4699 | - And then update it with:: |
4700 | - |
4701 | - swauth-set-account-service -A https://$PROXY_LOCAL_NET_IP:8080/auth/ -K swauthkey <account> storage local <new_url_for_the_account> |
4702 | - |
4703 | - Make the <new_url_for_the_account> look just like it's original URL but with the host:port update you want. |
4704 | +#. Change the storage url for any users to point to the load balanced url, rather than the first proxy server you created in /etc/swift/proxy-server.conf:: |
4705 | + |
4706 | + [filter:tempauth] |
4707 | + use = egg:swift#tempauth |
4708 | + user_system_root = testpass .admin http[s]://<LOAD_BALANCER_HOSTNAME>:<PORT>/v1/AUTH_system |
4709 | |
4710 | #. Next, copy all the ring information to all the nodes, including your new proxy nodes, and ensure the ring info gets to all the storage nodes as well. |
4711 | |
4712 | #. After you sync all the nodes, make sure the admin has the keys in /etc/swift and the ownership for the ring file is correct. |
4713 | |
4714 | -Additional Cleanup Script for Swauth |
4715 | ------------------------------------- |
4716 | - |
4717 | -With Swauth, you'll want to install a cronjob to clean up any |
4718 | -orphaned expired tokens. These orphaned tokens can occur when a "stampede" |
4719 | -occurs where a single user authenticates several times concurrently. Generally, |
4720 | -these orphaned tokens don't pose much of an issue, but it's good to clean them |
4721 | -up once a "token life" period (default: 1 day or 86400 seconds). |
4722 | - |
4723 | -This should be as simple as adding `swauth-cleanup-tokens -A |
4724 | -https://<PROXY_HOSTNAME>:8080/auth/ -K swauthkey > /dev/null` to a crontab |
4725 | -entry on one of the proxies that is running Swauth; but run |
4726 | -`swauth-cleanup-tokens` with no arguments for detailed help on the options |
4727 | -available. |
4728 | - |
4729 | Troubleshooting Notes |
4730 | --------------------- |
4731 | If you see problems, look in var/log/syslog (or messages on some distros). |
4732 | |
4733 | === modified file 'doc/source/misc.rst' |
4734 | --- doc/source/misc.rst 2011-03-24 03:37:07 +0000 |
4735 | +++ doc/source/misc.rst 2011-06-15 09:24:34 +0000 |
4736 | @@ -33,12 +33,12 @@ |
4737 | :members: |
4738 | :show-inheritance: |
4739 | |
4740 | -.. _common_swauth: |
4741 | - |
4742 | -Swauth |
4743 | -====== |
4744 | - |
4745 | -.. automodule:: swift.common.middleware.swauth |
4746 | +.. _common_tempauth: |
4747 | + |
4748 | +TempAuth |
4749 | +======== |
4750 | + |
4751 | +.. automodule:: swift.common.middleware.tempauth |
4752 | :members: |
4753 | :show-inheritance: |
4754 | |
4755 | |
4756 | === modified file 'doc/source/overview_auth.rst' |
4757 | --- doc/source/overview_auth.rst 2011-03-14 02:56:37 +0000 |
4758 | +++ doc/source/overview_auth.rst 2011-06-15 09:24:34 +0000 |
4759 | @@ -2,9 +2,9 @@ |
4760 | The Auth System |
4761 | =============== |
4762 | |
4763 | ------- |
4764 | -Swauth |
4765 | ------- |
4766 | +-------- |
4767 | +TempAuth |
4768 | +-------- |
4769 | |
4770 | The auth system for Swift is loosely based on the auth system from the existing |
4771 | Rackspace architecture -- actually from a few existing auth systems -- and is |
4772 | @@ -27,7 +27,7 @@ |
4773 | Swift will make calls to the auth system, giving the auth token to be |
4774 | validated. For a valid token, the auth system responds with an overall |
4775 | expiration in seconds from now. Swift will cache the token up to the expiration |
4776 | -time. The included Swauth also has the concept of admin and non-admin users |
4777 | +time. The included TempAuth also has the concept of admin and non-admin users |
4778 | within an account. Admin users can do anything within the account. Non-admin |
4779 | users can only perform operations per container based on the container's |
4780 | X-Container-Read and X-Container-Write ACLs. For more information on ACLs, see |
4781 | @@ -40,152 +40,9 @@ |
4782 | Extending Auth |
4783 | -------------- |
4784 | |
4785 | -Swauth is written as wsgi middleware, so implementing your own auth is as easy |
4786 | -as writing new wsgi middleware, and plugging it in to the proxy server. |
4787 | +TempAuth is written as wsgi middleware, so implementing your own auth is as |
4788 | +easy as writing new wsgi middleware, and plugging it in to the proxy server. |
4789 | +The KeyStone project and the Swauth project are examples of additional auth |
4790 | +services. |
4791 | |
4792 | Also, see :doc:`development_auth`. |
4793 | - |
4794 | - |
4795 | --------------- |
4796 | -Swauth Details |
4797 | --------------- |
4798 | - |
4799 | -The Swauth system is included at swift/common/middleware/swauth.py; a scalable |
4800 | -authentication and authorization system that uses Swift itself as its backing |
4801 | -store. This section will describe how it stores its data. |
4802 | - |
4803 | -At the topmost level, the auth system has its own Swift account it stores its |
4804 | -own account information within. This Swift account is known as |
4805 | -self.auth_account in the code and its name is in the format |
4806 | -self.reseller_prefix + ".auth". In this text, we'll refer to this account as |
4807 | -<auth_account>. |
4808 | - |
4809 | -The containers whose names do not begin with a period represent the accounts |
4810 | -within the auth service. For example, the <auth_account>/test container would |
4811 | -represent the "test" account. |
4812 | - |
4813 | -The objects within each container represent the users for that auth service |
4814 | -account. For example, the <auth_account>/test/bob object would represent the |
4815 | -user "bob" within the auth service account of "test". Each of these user |
4816 | -objects contain a JSON dictionary of the format:: |
4817 | - |
4818 | - {"auth": "<auth_type>:<auth_value>", "groups": <groups_array>} |
4819 | - |
4820 | -The `<auth_type>` can only be `plaintext` at this time, and the `<auth_value>` |
4821 | -is the plain text password itself. |
4822 | - |
4823 | -The `<groups_array>` contains at least two groups. The first is a unique group |
4824 | -identifying that user and it's name is of the format `<user>:<account>`. The |
4825 | -second group is the `<account>` itself. Additional groups of `.admin` for |
4826 | -account administrators and `.reseller_admin` for reseller administrators may |
4827 | -exist. Here's an example user JSON dictionary:: |
4828 | - |
4829 | - {"auth": "plaintext:testing", |
4830 | - "groups": ["name": "test:tester", "name": "test", "name": ".admin"]} |
4831 | - |
4832 | -To map an auth service account to a Swift storage account, the Service Account |
4833 | -Id string is stored in the `X-Container-Meta-Account-Id` header for the |
4834 | -<auth_account>/<account> container. To map back the other way, an |
4835 | -<auth_account>/.account_id/<account_id> object is created with the contents of |
4836 | -the corresponding auth service's account name. |
4837 | - |
4838 | -Also, to support a future where the auth service will support multiple Swift |
4839 | -clusters or even multiple services for the same auth service account, an |
4840 | -<auth_account>/<account>/.services object is created with its contents having a |
4841 | -JSON dictionary of the format:: |
4842 | - |
4843 | - {"storage": {"default": "local", "local": <url>}} |
4844 | - |
4845 | -The "default" is always "local" right now, and "local" is always the single |
4846 | -Swift cluster URL; but in the future there can be more than one cluster with |
4847 | -various names instead of just "local", and the "default" key's value will |
4848 | -contain the primary cluster to use for that account. Also, there may be more |
4849 | -services in addition to the current "storage" service right now. |
4850 | - |
4851 | -Here's an example .services dictionary at the moment:: |
4852 | - |
4853 | - {"storage": |
4854 | - {"default": "local", |
4855 | - "local": "http://127.0.0.1:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} |
4856 | - |
4857 | -But, here's an example of what the dictionary may look like in the future:: |
4858 | - |
4859 | - {"storage": |
4860 | - {"default": "dfw", |
4861 | - "dfw": "http://dfw.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", |
4862 | - "ord": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", |
4863 | - "sat": "http://ord.storage.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}, |
4864 | - "servers": |
4865 | - {"default": "dfw", |
4866 | - "dfw": "http://dfw.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", |
4867 | - "ord": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9", |
4868 | - "sat": "http://ord.servers.com:8080/v1/AUTH_8980f74b1cda41e483cbe0a925f448a9"}} |
4869 | - |
4870 | -Lastly, the tokens themselves are stored as objects in the |
4871 | -`<auth_account>/.token_[0-f]` containers. The names of the objects are the |
4872 | -token strings themselves, such as `AUTH_tked86bbd01864458aa2bd746879438d5a`. |
4873 | -The exact `.token_[0-f]` container chosen is based on the final digit of the |
4874 | -token name, such as `.token_a` for the token |
4875 | -`AUTH_tked86bbd01864458aa2bd746879438d5a`. The contents of the token objects |
4876 | -are JSON dictionaries of the format:: |
4877 | - |
4878 | - {"account": <account>, |
4879 | - "user": <user>, |
4880 | - "account_id": <account_id>, |
4881 | - "groups": <groups_array>, |
4882 | - "expires": <time.time() value>} |
4883 | - |
4884 | -The `<account>` is the auth service account's name for that token. The `<user>` |
4885 | -is the user within the account for that token. The `<account_id>` is the |
4886 | -same as the `X-Container-Meta-Account-Id` for the auth service's account, |
4887 | -as described above. The `<groups_array>` is the user's groups, as described |
4888 | -above with the user object. The "expires" value indicates when the token is no |
4889 | -longer valid, as compared to Python's time.time() value. |
4890 | - |
4891 | -Here's an example token object's JSON dictionary:: |
4892 | - |
4893 | - {"account": "test", |
4894 | - "user": "tester", |
4895 | - "account_id": "AUTH_8980f74b1cda41e483cbe0a925f448a9", |
4896 | - "groups": ["name": "test:tester", "name": "test", "name": ".admin"], |
4897 | - "expires": 1291273147.1624689} |
4898 | - |
4899 | -To easily map a user to an already issued token, the token name is stored in |
4900 | -the user object's `X-Object-Meta-Auth-Token` header. |
4901 | - |
4902 | -Here is an example full listing of an <auth_account>:: |
4903 | - |
4904 | - .account_id |
4905 | - AUTH_2282f516-559f-4966-b239-b5c88829e927 |
4906 | - AUTH_f6f57a3c-33b5-4e85-95a5-a801e67505c8 |
4907 | - AUTH_fea96a36-c177-4ca4-8c7e-b8c715d9d37b |
4908 | - .token_0 |
4909 | - .token_1 |
4910 | - .token_2 |
4911 | - .token_3 |
4912 | - .token_4 |
4913 | - .token_5 |
4914 | - .token_6 |
4915 | - AUTH_tk9d2941b13d524b268367116ef956dee6 |
4916 | - .token_7 |
4917 | - .token_8 |
4918 | - AUTH_tk93627c6324c64f78be746f1e6a4e3f98 |
4919 | - .token_9 |
4920 | - .token_a |
4921 | - .token_b |
4922 | - .token_c |
4923 | - .token_d |
4924 | - .token_e |
4925 | - AUTH_tk0d37d286af2c43ffad06e99112b3ec4e |
4926 | - .token_f |
4927 | - AUTH_tk766bbde93771489982d8dc76979d11cf |
4928 | - reseller |
4929 | - .services |
4930 | - reseller |
4931 | - test |
4932 | - .services |
4933 | - tester |
4934 | - tester3 |
4935 | - test2 |
4936 | - .services |
4937 | - tester2 |
4938 | |
4939 | === modified file 'doc/source/overview_large_objects.rst' |
4940 | --- doc/source/overview_large_objects.rst 2010-12-06 20:01:19 +0000 |
4941 | +++ doc/source/overview_large_objects.rst 2011-06-15 09:24:34 +0000 |
4942 | @@ -14,24 +14,24 @@ |
4943 | with the possibility of parallel uploads of the segments. |
4944 | |
4945 | ---------------------------------- |
4946 | -Using ``st`` for Segmented Objects |
4947 | +Using ``swift`` for Segmented Objects |
4948 | ---------------------------------- |
4949 | |
4950 | -The quickest way to try out this feature is use the included ``st`` Swift Tool. |
4951 | +The quickest way to try out this feature is use the included ``swift`` Swift Tool. |
4952 | You can use the ``-S`` option to specify the segment size to use when splitting |
4953 | a large file. For example:: |
4954 | |
4955 | - st upload test_container -S 1073741824 large_file |
4956 | + swift upload test_container -S 1073741824 large_file |
4957 | |
4958 | This would split the large_file into 1G segments and begin uploading those |
4959 | -segments in parallel. Once all the segments have been uploaded, ``st`` will |
4960 | +segments in parallel. Once all the segments have been uploaded, ``swift`` will |
4961 | then create the manifest file so the segments can be downloaded as one. |
4962 | |
4963 | -So now, the following ``st`` command would download the entire large object:: |
4964 | - |
4965 | - st download test_container large_file |
4966 | - |
4967 | -``st`` uses a strict convention for its segmented object support. In the above |
4968 | +So now, the following ``swift`` command would download the entire large object:: |
4969 | + |
4970 | + swift download test_container large_file |
4971 | + |
4972 | +``swift`` uses a strict convention for its segmented object support. In the above |
4973 | example it will upload all the segments into a second container named |
4974 | test_container_segments. These segments will have names like |
4975 | large_file/1290206778.25/21474836480/00000000, |
4976 | @@ -43,7 +43,7 @@ |
4977 | upload of a new file with the same name won't overwrite the contents of the |
4978 | first until the last moment when the manifest file is updated. |
4979 | |
4980 | -``st`` will manage these segment files for you, deleting old segments on |
4981 | +``swift`` will manage these segment files for you, deleting old segments on |
4982 | deletes and overwrites, etc. You can override this behavior with the |
4983 | ``--leave-segments`` option if desired; this is useful if you want to have |
4984 | multiple versions of the same large object available. |
4985 | @@ -53,14 +53,14 @@ |
4986 | ---------- |
4987 | |
4988 | You can also work with the segments and manifests directly with HTTP requests |
4989 | -instead of having ``st`` do that for you. You can just upload the segments like |
4990 | +instead of having ``swift`` do that for you. You can just upload the segments like |
4991 | you would any other object and the manifest is just a zero-byte file with an |
4992 | extra ``X-Object-Manifest`` header. |
4993 | |
4994 | All the object segments need to be in the same container, have a common object |
4995 | name prefix, and their names sort in the order they should be concatenated. |
4996 | They don't have to be in the same container as the manifest file will be, which |
4997 | -is useful to keep container listings clean as explained above with ``st``. |
4998 | +is useful to keep container listings clean as explained above with ``swift``. |
4999 | |
5000 | The manifest file is simply a zero-byte file with the extra |
The diff has been truncated for viewing.