root / tools / cloud @ adee02b8
History | View | Annotate | Download (19.5 kB)
1 |
#!/usr/bin/env python |
---|---|
2 |
|
3 |
# Copyright 2011 GRNET S.A. All rights reserved. |
4 |
# |
5 |
# Redistribution and use in source and binary forms, with or |
6 |
# without modification, are permitted provided that the following |
7 |
# conditions are met: |
8 |
# |
9 |
# 1. Redistributions of source code must retain the above |
10 |
# copyright notice, this list of conditions and the following |
11 |
# disclaimer. |
12 |
# |
13 |
# 2. Redistributions in binary form must reproduce the above |
14 |
# copyright notice, this list of conditions and the following |
15 |
# disclaimer in the documentation and/or other materials |
16 |
# provided with the distribution. |
17 |
# |
18 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
19 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
20 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
21 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
22 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
23 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
24 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
25 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
26 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
27 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
28 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
29 |
# POSSIBILITY OF SUCH DAMAGE. |
30 |
# |
31 |
# The views and conclusions contained in the software and |
32 |
# documentation are those of the authors and should not be |
33 |
# interpreted as representing official policies, either expressed |
34 |
# or implied, of GRNET S.A. |
35 |
|
36 |
from httplib import HTTPConnection |
37 |
from optparse import OptionParser |
38 |
from os.path import basename |
39 |
from sys import argv, exit |
40 |
|
41 |
import json |
42 |
|
43 |
DEFAULT_HOST = '127.0.0.1:8000' |
44 |
DEFAULT_API = 'v1.1' |
45 |
|
46 |
TOKEN = '46e427d657b20defe352804f0eb6f8a2' |
47 |
|
48 |
|
49 |
commands = {} |
50 |
|
51 |
def command_name(name): |
52 |
def decorator(cls): |
53 |
commands[name] = cls |
54 |
return cls |
55 |
return decorator |
56 |
|
57 |
|
58 |
def print_addresses(networks): |
59 |
for i, net in enumerate(networks): |
60 |
key = 'addresses:'.rjust(13) if i == 0 else ' ' * 13 |
61 |
addr = '' |
62 |
if 'values' in net: |
63 |
addr = '[%s]' % ' '.join(ip['addr'] for ip in net['values']) |
64 |
|
65 |
val = '%s/%s %s %s' % (net['id'], net['name'], net['mac'], addr) |
66 |
print '%s %s' % (key, val) |
67 |
|
68 |
def print_dict(d, show_empty=True): |
69 |
for key, val in sorted(d.items()): |
70 |
if key == 'metadata': |
71 |
val = ', '.join('%s="%s"' % x for x in val['values'].items()) |
72 |
elif key == 'addresses': |
73 |
print_addresses(val['values']) |
74 |
continue |
75 |
elif key == 'servers': |
76 |
val = ', '.join(str(server_id) for server_id in val['values']) |
77 |
if val or show_empty: |
78 |
print '%s: %s' % (key.rjust(12), val) |
79 |
|
80 |
|
81 |
class Command(object): |
82 |
def __init__(self, argv): |
83 |
parser = OptionParser() |
84 |
parser.add_option('--host', dest='host', metavar='HOST', default=DEFAULT_HOST, |
85 |
help='use server HOST') |
86 |
parser.add_option('--api', dest='api', metavar='API', default=DEFAULT_API, |
87 |
help='use api API') |
88 |
parser.add_option('-v', action='store_true', dest='verbose', default=False, |
89 |
help='use verbose output') |
90 |
self.add_options(parser) |
91 |
options, args = parser.parse_args(argv) |
92 |
|
93 |
# Add options to self |
94 |
for opt in parser.option_list: |
95 |
key = opt.dest |
96 |
if key: |
97 |
val = getattr(options, key) |
98 |
setattr(self, key, val) |
99 |
|
100 |
self.execute(*args) |
101 |
|
102 |
def add_options(self, parser): |
103 |
pass |
104 |
|
105 |
def execute(self, *args): |
106 |
pass |
107 |
|
108 |
def http_cmd(self, method, path, body=None, expected_status=200): |
109 |
conn = HTTPConnection(self.host) |
110 |
|
111 |
kwargs = {} |
112 |
kwargs['headers'] = {'X-Auth-Token': TOKEN} |
113 |
if body: |
114 |
kwargs['headers']['Content-Type'] = 'application/json' |
115 |
kwargs['body'] = body |
116 |
conn.request(method, path, **kwargs) |
117 |
|
118 |
resp = conn.getresponse() |
119 |
if self.verbose: |
120 |
print '%d %s' % (resp.status, resp.reason) |
121 |
for key, val in resp.getheaders(): |
122 |
print '%s: %s' % (key.capitalize(), val) |
123 |
|
124 |
|
125 |
buf = resp.read() or '{}' |
126 |
try: |
127 |
reply = json.loads(buf) |
128 |
except ValueError: |
129 |
print 'Invalid response from the server.' |
130 |
if self.verbose: |
131 |
print buf |
132 |
exit(1) |
133 |
|
134 |
# If the response status is not the expected one, |
135 |
# assume an error has occured and treat the body |
136 |
# as a cloudfault. |
137 |
if resp.status != expected_status: |
138 |
if len(reply) == 1: |
139 |
key = reply.keys()[0] |
140 |
val = reply[key] |
141 |
print '%s: %s' % (key, val.get('message', '')) |
142 |
if self.verbose: |
143 |
print val.get('details', '') |
144 |
else: |
145 |
print 'Invalid response from the server.' |
146 |
exit(1) |
147 |
|
148 |
return reply |
149 |
|
150 |
def http_get(self, path, expected_status=200): |
151 |
return self.http_cmd('GET', path, None, expected_status) |
152 |
|
153 |
def http_post(self, path, body, expected_status=202): |
154 |
return self.http_cmd('POST', path, body, expected_status) |
155 |
|
156 |
def http_put(self, path, body, expected_status=204): |
157 |
return self.http_cmd('PUT', path, body, expected_status) |
158 |
|
159 |
def http_delete(self, path, expected_status=204): |
160 |
return self.http_cmd('DELETE', path, None, expected_status) |
161 |
|
162 |
|
163 |
@command_name('ls') |
164 |
class ListServers(Command): |
165 |
description = 'list servers' |
166 |
|
167 |
def add_options(self, parser): |
168 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
169 |
help='show detailed output') |
170 |
parser.add_option('-a', action='store_true', dest='show_empty', default=False, |
171 |
help='include empty values') |
172 |
|
173 |
def execute(self): |
174 |
path = '/api/%s/servers' % self.api |
175 |
if self.detail: |
176 |
path += '/detail' |
177 |
|
178 |
reply = self.http_get(path) |
179 |
|
180 |
for server in reply['servers']['values']: |
181 |
id = server.pop('id') |
182 |
name = server.pop('name') |
183 |
if self.detail: |
184 |
print '%d %s' % (id, name) |
185 |
print_dict(server, self.show_empty) |
186 |
|
187 |
else: |
188 |
print '%3d %s' % (id, name) |
189 |
|
190 |
|
191 |
@command_name('info') |
192 |
class GetServerDetails(Command): |
193 |
description = 'get server details' |
194 |
syntax = '<server id>' |
195 |
|
196 |
def add_options(self, parser): |
197 |
parser.add_option('-a', action='store_true', dest='show_empty', default=False, |
198 |
help='include empty values') |
199 |
|
200 |
def execute(self, server_id): |
201 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
202 |
reply = self.http_get(path) |
203 |
server = reply['server'] |
204 |
server.pop('id') |
205 |
print_dict(server, self.show_empty) |
206 |
|
207 |
|
208 |
@command_name('create') |
209 |
class CreateServer(Command): |
210 |
description = 'create server' |
211 |
syntax = '<server name>' |
212 |
|
213 |
def add_options(self, parser): |
214 |
parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1, |
215 |
help='use flavor FLAVOR_ID') |
216 |
parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1, |
217 |
help='use image IMAGE_ID') |
218 |
|
219 |
def execute(self, name): |
220 |
path = '/api/%s/servers' % self.api |
221 |
server = {'name': name, 'flavorRef': self.flavor, 'imageRef': self.image} |
222 |
body = json.dumps({'server': server}) |
223 |
reply = self.http_post(path, body) |
224 |
server = reply['server'] |
225 |
print_dict(server) |
226 |
|
227 |
|
228 |
@command_name('rename') |
229 |
class UpdateServerName(Command): |
230 |
description = 'update server name' |
231 |
syntax = '<server id> <new name>' |
232 |
|
233 |
def execute(self, server_id, name): |
234 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
235 |
body = json.dumps({'server': {'name': name}}) |
236 |
self.http_put(path, body) |
237 |
|
238 |
|
239 |
@command_name('delete') |
240 |
class DeleteServer(Command): |
241 |
description = 'delete server' |
242 |
syntax = '<server id>' |
243 |
|
244 |
def execute(self, server_id): |
245 |
path = '/api/%s/servers/%d' % (self.api, int(server_id)) |
246 |
self.http_delete(path) |
247 |
|
248 |
|
249 |
@command_name('reboot') |
250 |
class RebootServer(Command): |
251 |
description = 'reboot server' |
252 |
syntax = '<server id>' |
253 |
|
254 |
def add_options(self, parser): |
255 |
parser.add_option('-f', action='store_true', dest='hard', default=False, |
256 |
help='perform a hard reboot') |
257 |
|
258 |
def execute(self, server_id): |
259 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
260 |
type = 'HARD' if self.hard else 'SOFT' |
261 |
body = json.dumps({'reboot': {'type': type}}) |
262 |
self.http_post(path, body) |
263 |
|
264 |
|
265 |
@command_name('start') |
266 |
class StartServer(Command): |
267 |
description = 'start server' |
268 |
syntax = '<server id>' |
269 |
|
270 |
def execute(self, server_id): |
271 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
272 |
body = json.dumps({'start': {}}) |
273 |
self.http_post(path, body) |
274 |
|
275 |
|
276 |
@command_name('shutdown') |
277 |
class StartServer(Command): |
278 |
description = 'shutdown server' |
279 |
syntax = '<server id>' |
280 |
|
281 |
def execute(self, server_id): |
282 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
283 |
body = json.dumps({'shutdown': {}}) |
284 |
self.http_post(path, body) |
285 |
|
286 |
|
287 |
@command_name('console') |
288 |
class ServerConsole(Command): |
289 |
description = 'get VNC console' |
290 |
syntax = '<server id>' |
291 |
|
292 |
def add_options(self, parser): |
293 |
pass |
294 |
|
295 |
def execute(self, server_id): |
296 |
path = '/api/%s/servers/%d/action' % (self.api, int(server_id)) |
297 |
body = json.dumps({'console': {'type': 'vnc'}}) |
298 |
reply = self.http_cmd('POST', path, body, 200) |
299 |
print_dict(reply['console']) |
300 |
|
301 |
|
302 |
@command_name('lsaddr') |
303 |
class ListAddresses(Command): |
304 |
description = 'list server addresses' |
305 |
syntax = '<server id> [network]' |
306 |
|
307 |
def execute(self, server_id, network=None): |
308 |
path = '/api/%s/servers/%d/ips' % (self.api, int(server_id)) |
309 |
if network: |
310 |
path += '/%s' % network |
311 |
reply = self.http_get(path) |
312 |
|
313 |
addresses = [reply['network']] if network else reply['addresses']['values'] |
314 |
print_addresses(addresses) |
315 |
|
316 |
|
317 |
@command_name('lsflv') |
318 |
class ListFlavors(Command): |
319 |
description = 'list flavors' |
320 |
|
321 |
def add_options(self, parser): |
322 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
323 |
help='show detailed output') |
324 |
|
325 |
def execute(self): |
326 |
path = '/api/%s/flavors' % self.api |
327 |
if self.detail: |
328 |
path += '/detail' |
329 |
reply = self.http_get(path) |
330 |
|
331 |
for flavor in reply['flavors']['values']: |
332 |
id = flavor.pop('id') |
333 |
name = flavor.pop('name') |
334 |
details = ' '.join('%s=%s' % item for item in sorted(flavor.items())) |
335 |
print '%3d %s %s' % (id, name, details) |
336 |
|
337 |
|
338 |
@command_name('flvinfo') |
339 |
class GetFlavorDetails(Command): |
340 |
description = 'get flavor details' |
341 |
syntax = '<flavor id>' |
342 |
|
343 |
def execute(self, flavor_id): |
344 |
path = '/api/%s/flavors/%d' % (self.api, int(flavor_id)) |
345 |
reply = self.http_get(path) |
346 |
|
347 |
flavor = reply['flavor'] |
348 |
id = flavor.pop('id') |
349 |
name = flavor.pop('name') |
350 |
details = ' '.join('%s=%s' % item for item in sorted(flavor.items())) |
351 |
print '%3d %s %s' % (id, name, details) |
352 |
|
353 |
|
354 |
@command_name('lsimg') |
355 |
class ListImages(Command): |
356 |
description = 'list images' |
357 |
|
358 |
def add_options(self, parser): |
359 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
360 |
help='show detailed output') |
361 |
|
362 |
def execute(self): |
363 |
path = '/api/%s/images' % self.api |
364 |
if self.detail: |
365 |
path += '/detail' |
366 |
reply = self.http_get(path) |
367 |
|
368 |
for image in reply['images']['values']: |
369 |
id = image.pop('id') |
370 |
name = image.pop('name') |
371 |
if self.detail: |
372 |
print '%d %s' % (id, name) |
373 |
print_dict(image) |
374 |
|
375 |
else: |
376 |
print '%3d %s' % (id, name) |
377 |
|
378 |
|
379 |
@command_name('imginfo') |
380 |
class GetImageDetails(Command): |
381 |
description = 'get image details' |
382 |
syntax = '<image id>' |
383 |
|
384 |
def execute(self, image_id): |
385 |
path = '/api/%s/images/%d' % (self.api, int(image_id)) |
386 |
reply = self.http_get(path) |
387 |
image = reply['image'] |
388 |
image.pop('id') |
389 |
print_dict(image) |
390 |
|
391 |
|
392 |
@command_name('createimg') |
393 |
class CreateImage(Command): |
394 |
description = 'create image' |
395 |
syntax = '<server id> <image name>' |
396 |
|
397 |
def execute(self, server_id, name): |
398 |
path = '/api/%s/images' % self.api |
399 |
image = {'name': name, 'serverRef': int(server_id)} |
400 |
body = json.dumps({'image': image}) |
401 |
reply = self.http_post(path, body) |
402 |
print_dict(reply['image']) |
403 |
|
404 |
@command_name('deleteimg') |
405 |
class DeleteImage(Command): |
406 |
description = 'delete image' |
407 |
syntax = '<image id>' |
408 |
|
409 |
def execute(self, image_id): |
410 |
path = '/api/%s/images/%d' % (self.api, int(image_id)) |
411 |
self.http_delete(path) |
412 |
|
413 |
@command_name('lsmeta') |
414 |
class ListServerMeta(Command): |
415 |
description = 'list server meta' |
416 |
syntax = '<server id> [key]' |
417 |
|
418 |
def execute(self, server_id, key=None): |
419 |
path = '/api/%s/servers/%d/meta' % (self.api, int(server_id)) |
420 |
if key: |
421 |
path += '/' + key |
422 |
reply = self.http_get(path) |
423 |
if key: |
424 |
print_dict(reply['meta']) |
425 |
else: |
426 |
print_dict(reply['metadata']['values']) |
427 |
|
428 |
@command_name('setmeta') |
429 |
class UpdateServerMeta(Command): |
430 |
description = 'update server meta' |
431 |
syntax = '<server id> <key> <val>' |
432 |
|
433 |
def execute(self, server_id, key, val): |
434 |
path = '/api/%s/servers/%d/meta' % (self.api, int(server_id)) |
435 |
metadata = {key: val} |
436 |
body = json.dumps({'metadata': metadata}) |
437 |
reply = self.http_post(path, body, expected_status=201) |
438 |
print_dict(reply['metadata']) |
439 |
|
440 |
@command_name('addmeta') |
441 |
class CreateServerMeta(Command): |
442 |
description = 'add server meta' |
443 |
syntax = '<server id> <key> <val>' |
444 |
|
445 |
def execute(self, server_id, key, val): |
446 |
path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key) |
447 |
meta = {key: val} |
448 |
body = json.dumps({'meta': meta}) |
449 |
reply = self.http_put(path, body, expected_status=201) |
450 |
print_dict(reply['meta']) |
451 |
|
452 |
@command_name('delmeta') |
453 |
class DeleteServerMeta(Command): |
454 |
description = 'delete server meta' |
455 |
syntax = '<server id> <key>' |
456 |
|
457 |
def execute(self, server_id, key): |
458 |
path = '/api/%s/servers/%d/meta/%s' % (self.api, int(server_id), key) |
459 |
reply = self.http_delete(path) |
460 |
|
461 |
@command_name('lsimgmeta') |
462 |
class ListImageMeta(Command): |
463 |
description = 'list image meta' |
464 |
syntax = '<image id> [key]' |
465 |
|
466 |
def execute(self, image_id, key=None): |
467 |
path = '/api/%s/images/%d/meta' % (self.api, int(image_id)) |
468 |
if key: |
469 |
path += '/' + key |
470 |
reply = self.http_get(path) |
471 |
if key: |
472 |
print_dict(reply['meta']) |
473 |
else: |
474 |
print_dict(reply['metadata']['values']) |
475 |
|
476 |
@command_name('setimgmeta') |
477 |
class UpdateImageMeta(Command): |
478 |
description = 'update image meta' |
479 |
syntax = '<image id> <key> <val>' |
480 |
|
481 |
def execute(self, image_id, key, val): |
482 |
path = '/api/%s/images/%d/meta' % (self.api, int(image_id)) |
483 |
metadata = {key: val} |
484 |
body = json.dumps({'metadata': metadata}) |
485 |
reply = self.http_post(path, body, expected_status=201) |
486 |
print_dict(reply['metadata']) |
487 |
|
488 |
@command_name('addimgmeta') |
489 |
class CreateImageMeta(Command): |
490 |
description = 'add image meta' |
491 |
syntax = '<image id> <key> <val>' |
492 |
|
493 |
def execute(self, image_id, key, val): |
494 |
path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key) |
495 |
meta = {key: val} |
496 |
body = json.dumps({'meta': meta}) |
497 |
reply = self.http_put(path, body, expected_status=201) |
498 |
print_dict(reply['meta']) |
499 |
|
500 |
@command_name('delimgmeta') |
501 |
class DeleteImageMeta(Command): |
502 |
description = 'delete image meta' |
503 |
syntax = '<image id> <key>' |
504 |
|
505 |
def execute(self, image_id, key): |
506 |
path = '/api/%s/images/%d/meta/%s' % (self.api, int(image_id), key) |
507 |
reply = self.http_delete(path) |
508 |
|
509 |
|
510 |
@command_name('lsnet') |
511 |
class ListNetworks(Command): |
512 |
description = 'list networks' |
513 |
|
514 |
def add_options(self, parser): |
515 |
parser.add_option('-l', action='store_true', dest='detail', default=False, |
516 |
help='show detailed output') |
517 |
|
518 |
def execute(self): |
519 |
path = '/api/%s/networks' % self.api |
520 |
if self.detail: |
521 |
path += '/detail' |
522 |
reply = self.http_get(path) |
523 |
|
524 |
for network in reply['networks']['values']: |
525 |
id = network.pop('id') |
526 |
name = network.pop('name') |
527 |
if self.detail: |
528 |
print '%s %s' % (id, name) |
529 |
print_dict(network) |
530 |
|
531 |
else: |
532 |
print '%3s %s' % (id, name) |
533 |
|
534 |
|
535 |
@command_name('createnet') |
536 |
class CreateNetwork(Command): |
537 |
description = 'create network' |
538 |
syntax = '<network name>' |
539 |
|
540 |
def execute(self, name): |
541 |
path = '/api/%s/networks' % self.api |
542 |
network = {'name': name} |
543 |
body = json.dumps({'network': network}) |
544 |
reply = self.http_post(path, body) |
545 |
print_dict(reply['network']) |
546 |
|
547 |
|
548 |
@command_name('netinfo') |
549 |
class GetNetworkDetails(Command): |
550 |
description = 'get network details' |
551 |
syntax = '<network id>' |
552 |
|
553 |
def execute(self, network_id): |
554 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
555 |
reply = self.http_get(path) |
556 |
net = reply['network'] |
557 |
name = net.pop('id') |
558 |
print_dict(net) |
559 |
|
560 |
|
561 |
@command_name('renamenet') |
562 |
class UpdateNetworkName(Command): |
563 |
description = 'update network name' |
564 |
syntax = '<network_id> <new name>' |
565 |
|
566 |
def execute(self, network_id, name): |
567 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
568 |
body = json.dumps({'network': {'name': name}}) |
569 |
self.http_put(path, body) |
570 |
|
571 |
|
572 |
@command_name('deletenet') |
573 |
class DeleteNetwork(Command): |
574 |
description = 'delete network' |
575 |
syntax = '<network id>' |
576 |
|
577 |
def execute(self, network_id): |
578 |
path = '/api/%s/networks/%d' % (self.api, int(network_id)) |
579 |
self.http_delete(path) |
580 |
|
581 |
|
582 |
@command_name('connect') |
583 |
class AddNetwork(Command): |
584 |
description = 'connect a server to a network' |
585 |
syntax = '<server id> <network id>' |
586 |
|
587 |
def execute(self, server_id, network_id): |
588 |
path = '/api/%s/networks/%d/action' % (self.api, int(network_id)) |
589 |
body = json.dumps({'add': {'serverRef': server_id}}) |
590 |
self.http_post(path, body, expected_status=202) |
591 |
|
592 |
|
593 |
@command_name('disconnect') |
594 |
class RemoveNetwork(Command): |
595 |
description = 'disconnect a server from a network' |
596 |
syntax = '<server id> <network id>' |
597 |
|
598 |
def execute(self, server_id, network_id): |
599 |
path = '/api/%s/networks/%s/action' % (self.api, int(network_id)) |
600 |
body = json.dumps({'remove': {'serverRef': server_id}}) |
601 |
self.http_post(path, body, expected_status=202) |
602 |
|
603 |
|
604 |
def print_usage(): |
605 |
print 'Usage: %s <command>' % basename(argv[0]) |
606 |
|
607 |
print 'Commands:' |
608 |
for name, cls in sorted(commands.items()): |
609 |
description = getattr(cls, 'description', '') |
610 |
print ' %s %s' % (name.ljust(12), description) |
611 |
|
612 |
def main(): |
613 |
try: |
614 |
name = argv[1] |
615 |
cls = commands[name] |
616 |
except (IndexError, KeyError): |
617 |
print_usage() |
618 |
exit(1) |
619 |
|
620 |
try: |
621 |
cls(argv[2:]) |
622 |
except TypeError: |
623 |
syntax = getattr(cls, 'syntax', '') |
624 |
if syntax: |
625 |
print 'Syntax: %s %s' % (name, syntax) |
626 |
else: |
627 |
print 'Invalid syntax' |
628 |
exit(1) |
629 |
|
630 |
|
631 |
if __name__ == '__main__': |
632 |
main() |