root / tools / store @ da62aafb
History | View | Annotate | Download (11 kB)
1 |
#!/usr/bin/env python |
---|---|
2 |
|
3 |
from getpass import getuser |
4 |
from httplib import HTTPConnection |
5 |
from optparse import OptionParser |
6 |
from os.path import basename |
7 |
from sys import argv, exit, stdin |
8 |
|
9 |
import json |
10 |
import logging |
11 |
|
12 |
|
13 |
DEFAULT_HOST = 'pithos.dev.grnet.gr' |
14 |
DEFAULT_API = 'v1' |
15 |
|
16 |
|
17 |
class Fault(Exception): |
18 |
def __init__(self, data=''): |
19 |
Exception.__init__(self, data) |
20 |
self.data = data |
21 |
|
22 |
|
23 |
class Client(object): |
24 |
def __init__(self, host, account, api='v1', verbose=False, debug=False): |
25 |
"""`host` can also include a port, e.g '127.0.0.1:8000'.""" |
26 |
|
27 |
self.host = host |
28 |
self.account = account |
29 |
self.api = api |
30 |
self.verbose = verbose or debug |
31 |
self.debug = debug |
32 |
|
33 |
def req(self, method, path, body=None, headers=None, format='text'): |
34 |
full_path = '/%s/%s%s?format=%s' % (self.api, self.account, path, format) |
35 |
conn = HTTPConnection(self.host) |
36 |
|
37 |
kwargs = {} |
38 |
kwargs['headers'] = headers or {} |
39 |
kwargs['headers']['Content-Length'] = len(body) if body else 0 |
40 |
if body: |
41 |
kwargs['body'] = body |
42 |
|
43 |
conn.request(method, full_path, **kwargs) |
44 |
resp = conn.getresponse() |
45 |
headers = dict(resp.getheaders()) |
46 |
|
47 |
if self.verbose: |
48 |
print '%d %s' % (resp.status, resp.reason) |
49 |
for key, val in headers.items(): |
50 |
print '%s: %s' % (key.capitalize(), val) |
51 |
|
52 |
|
53 |
data = resp.read() |
54 |
|
55 |
if self.debug: |
56 |
print data |
57 |
|
58 |
|
59 |
if format == 'json': |
60 |
data = json.loads(data) |
61 |
|
62 |
return resp.status, headers, data |
63 |
|
64 |
def delete(self, path, format='text'): |
65 |
return self.req('DELETE', path, format=format) |
66 |
|
67 |
def get(self, path, format='text'): |
68 |
return self.req('GET', path, format=format) |
69 |
|
70 |
def head(self, path, format='text'): |
71 |
return self.req('HEAD', path, format=format) |
72 |
|
73 |
def post(self, path, body=None, format='text', headers=None): |
74 |
return self.req('POST', path, body, headers=headers, format=format) |
75 |
|
76 |
def put(self, path, body=None, format='text', headers=None): |
77 |
return self.req('PUT', path, body, headers=headers, format=format) |
78 |
|
79 |
def _list(self, path, detail=False): |
80 |
format = 'json' if detail else 'text' |
81 |
status, headers, data = self.get(path, format=format) |
82 |
return data |
83 |
|
84 |
def _get_metadata(self, path, prefix): |
85 |
status, headers, data = self.head(path) |
86 |
prefixlen = len(prefix) |
87 |
meta = {} |
88 |
for key, val in headers.items(): |
89 |
if key.startswith(prefix): |
90 |
key = key[prefixlen:] |
91 |
meta[key] = val |
92 |
return meta |
93 |
|
94 |
|
95 |
# Storage Account Services |
96 |
|
97 |
def list_containers(self, detail=False): |
98 |
return self._list('', detail) |
99 |
|
100 |
def account_metadata(self): |
101 |
return self._get_metadata('', 'x-account-') |
102 |
|
103 |
|
104 |
# Storage Container Services |
105 |
|
106 |
def list_objects(self, container, detail=False): |
107 |
return self._list('/' + container, detail) |
108 |
|
109 |
def create_container(self, container): |
110 |
status, header, data = self.put('/' + container) |
111 |
if status == 202: |
112 |
return False |
113 |
elif status != 201: |
114 |
raise Fault(data) |
115 |
return True |
116 |
|
117 |
def delete_container(self, container): |
118 |
self.delete('/' + container) |
119 |
|
120 |
def retrieve_container_metadata(self, container): |
121 |
return self._get_metadata('/%s' % container, 'x-container-') |
122 |
|
123 |
|
124 |
# Storage Object Services |
125 |
|
126 |
def retrieve_object(self, container, object): |
127 |
path = '/%s/%s' % (container, object) |
128 |
status, headers, data = self.get(path) |
129 |
return data |
130 |
|
131 |
def create_object(self, container, object, data): |
132 |
path = '/%s/%s' % (container, object) |
133 |
self.put(path, data) |
134 |
|
135 |
def copy_object(self, src_container, src_object, dst_container, dst_object): |
136 |
path = '/%s/%s' % (dst_container, dst_object) |
137 |
headers = {} |
138 |
headers['X-Copy-From'] = '/%s/%s' % (src_container, src_object) |
139 |
headers['Content-Length'] = 0 |
140 |
self.put(path, headers=headers) |
141 |
|
142 |
def delete_object(self, container, object): |
143 |
self.delete('/%s/%s' % (container, object)) |
144 |
|
145 |
def retrieve_object_metadata(self, container, object): |
146 |
path = '/%s/%s' % (container, object) |
147 |
return self._get_metadata(path, 'x-object-meta-') |
148 |
|
149 |
def update_object_metadata(self, container, object, **meta): |
150 |
path = '/%s/%s' % (container, object) |
151 |
headers = {} |
152 |
for key, val in meta.items(): |
153 |
http_key = 'X-Object-Meta-' + key |
154 |
headers[http_key] = val |
155 |
self.post(path, headers=headers) |
156 |
|
157 |
|
158 |
_cli_commands = {} |
159 |
|
160 |
def cli_command(*args): |
161 |
def decorator(cls): |
162 |
cls.commands = args |
163 |
for name in args: |
164 |
_cli_commands[name] = cls |
165 |
return cls |
166 |
return decorator |
167 |
|
168 |
def class_for_cli_command(name): |
169 |
return _cli_commands[name] |
170 |
|
171 |
def print_dict(d, header='name'): |
172 |
if header: |
173 |
print d.pop(header) |
174 |
for key, val in sorted(d.items()): |
175 |
print '%s: %s' % (key.rjust(15), val) |
176 |
|
177 |
|
178 |
class Command(object): |
179 |
def __init__(self, argv): |
180 |
parser = OptionParser() |
181 |
parser.add_option('--host', dest='host', metavar='HOST', default=DEFAULT_HOST, |
182 |
help='use server HOST') |
183 |
parser.add_option('--user', dest='user', metavar='USERNAME', default=getuser(), |
184 |
help='use account USERNAME') |
185 |
parser.add_option('--api', dest='api', metavar='API', default=DEFAULT_API, |
186 |
help='use api API') |
187 |
parser.add_option('-v', action='store_true', dest='verbose', default=False, |
188 |
help='use verbose output') |
189 |
parser.add_option('-d', action='store_true', dest='debug', default=False, |
190 |
help='use debug output') |
191 |
self.add_options(parser) |
192 |
options, args = parser.parse_args(argv) |
193 |
|
194 |
# Add options to self |
195 |
for opt in parser.option_list: |
196 |
key = opt.dest |
197 |
if key: |
198 |
val = getattr(options, key) |
199 |
setattr(self, key, val) |
200 |
|
201 |
self.client = Client(self.host, self.user, self.api, self.verbose, self.debug) |
202 |
|
203 |
self.parser = parser |
204 |
self.args = args |
205 |
|
206 |
def add_options(self, parser): |
207 |
pass |
208 |
|
209 |
def execute(self, *args): |
210 |
pass |
211 |
|
212 |
|
213 |
@cli_command('list', 'ls') |
214 |
class List(Command): |
215 |
syntax = '[container]' |
216 |
description = 'list containers or objects' |
217 |
|
218 |
def add_options(self, parser): |
219 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
220 |
help='show detailed output') |
221 |
|
222 |
def execute(self, container=None): |
223 |
if container: |
224 |
self.list_objects(container) |
225 |
else: |
226 |
self.list_containers() |
227 |
|
228 |
def list_containers(self): |
229 |
if self.detail: |
230 |
for container in self.client.list_containers(detail=True): |
231 |
print_dict(container) |
232 |
|
233 |
else: |
234 |
print self.client.list_containers().strip() |
235 |
|
236 |
def list_objects(self, container): |
237 |
if self.detail: |
238 |
for obj in self.client.list_objects(container, detail=True): |
239 |
print_dict(obj) |
240 |
|
241 |
else: |
242 |
print self.client.list_objects(container).strip() |
243 |
|
244 |
|
245 |
@cli_command('meta') |
246 |
class Meta(Command): |
247 |
syntax = '[<container>[/<object>]]' |
248 |
description = 'get the metadata of an account, a container or an object' |
249 |
|
250 |
def execute(self, path=''): |
251 |
container, sep, object = path.partition('/') |
252 |
if object: |
253 |
meta = self.client.retrieve_object_metadata(container, object) |
254 |
elif container: |
255 |
meta = self.client.retrieve_container_metadata(container) |
256 |
else: |
257 |
meta = self.client.account_metadata() |
258 |
print_dict(meta, header=None) |
259 |
|
260 |
|
261 |
@cli_command('create') |
262 |
class CreateContainer(Command): |
263 |
syntax = '<container>' |
264 |
description = 'create a container' |
265 |
|
266 |
def execute(self, container): |
267 |
ret = self.client.create_container(container) |
268 |
if not ret: |
269 |
print 'Container already exists' |
270 |
|
271 |
|
272 |
@cli_command('delete', 'rm') |
273 |
class Delete(Command): |
274 |
syntax = '<container>[/<object>]' |
275 |
description = 'delete a container or an object' |
276 |
|
277 |
def execute(self, path): |
278 |
container, sep, object = path.partition('/') |
279 |
if object: |
280 |
self.client.delete_object(container, object) |
281 |
else: |
282 |
self.client.delete_container(container) |
283 |
|
284 |
|
285 |
@cli_command('get') |
286 |
class GetObject(Command): |
287 |
syntax = '<container>/<object>' |
288 |
description = 'get the data of an object' |
289 |
|
290 |
def execute(self, path): |
291 |
container, sep, object = path.partition('/') |
292 |
print self.client.retrieve_object(container, object) |
293 |
|
294 |
|
295 |
@cli_command('put') |
296 |
class PutObject(Command): |
297 |
syntax = '<container>/<object> <path>' |
298 |
description = 'create or update an object with contents of path' |
299 |
|
300 |
def execute(self, path, srcpath): |
301 |
container, sep, object = path.partition('/') |
302 |
f = open(srcpath) if srcpath != '-' else stdin |
303 |
data = f.read() |
304 |
self.client.create_object(container, object, data) |
305 |
f.close() |
306 |
|
307 |
|
308 |
@cli_command('copy', 'cp') |
309 |
class CopyObject(Command): |
310 |
syntax = '<src container>/<src object> [<dst container>/]<dst object>' |
311 |
description = 'copies an object to a different location' |
312 |
|
313 |
def execute(self, src, dst): |
314 |
src_container, sep, src_object = src.partition('/') |
315 |
dst_container, sep, dst_object = dst.partition('/') |
316 |
if not sep: |
317 |
dst_container = src_container |
318 |
dst_object = dst |
319 |
self.client.copy_object(src_container, src_object, dst_container, dst_object) |
320 |
|
321 |
|
322 |
@cli_command('set') |
323 |
class SetOjectMeta(Command): |
324 |
syntax = '<container>/<object> key=val [key=val] [...]' |
325 |
description = 'set object metadata' |
326 |
|
327 |
def execute(self, path, *args): |
328 |
container, sep, object = path.partition('/') |
329 |
meta = {} |
330 |
for arg in args: |
331 |
key, sep, val = arg.partition('=') |
332 |
meta[key.strip()] = val.strip() |
333 |
self.client.update_object_metadata(container, object, **meta) |
334 |
|
335 |
|
336 |
def print_usage(): |
337 |
cmd = Command([]) |
338 |
parser = cmd.parser |
339 |
parser.usage = '%prog <command> [options]' |
340 |
parser.print_help() |
341 |
|
342 |
commands = [] |
343 |
for cls in set(_cli_commands.values()): |
344 |
name = ', '.join(cls.commands) |
345 |
description = getattr(cls, 'description', '') |
346 |
commands.append(' %s %s' % (name.ljust(12), description)) |
347 |
print '\nCommands:\n' + '\n'.join(sorted(commands)) |
348 |
|
349 |
|
350 |
|
351 |
def main(): |
352 |
try: |
353 |
name = argv[1] |
354 |
cls = class_for_cli_command(name) |
355 |
except (IndexError, KeyError): |
356 |
print_usage() |
357 |
exit(1) |
358 |
|
359 |
cmd = cls(argv[2:]) |
360 |
|
361 |
try: |
362 |
cmd.execute(*cmd.args) |
363 |
except TypeError: |
364 |
cmd.parser.usage = '%%prog %s [options] %s' % (name, cmd.syntax) |
365 |
cmd.parser.print_help() |
366 |
exit(1) |
367 |
except Fault, f: |
368 |
print f.data |
369 |
|
370 |
|
371 |
if __name__ == '__main__': |
372 |
main() |