Revision 56d84a4e
b/kamaki/cli/__init__.py | ||
---|---|---|
64 | 64 |
'_', ' ') |
65 | 65 |
|
66 | 66 |
|
67 |
def _required_syntax(arguments, required): |
|
68 |
if isinstance(required, tuple): |
|
69 |
return ' '.join([_required_syntax(arguments, k) for k in required]) |
|
70 |
elif isinstance(required, list): |
|
71 |
return '(%s)' % ' | '.join([ |
|
72 |
_required_syntax(arguments, k) for k in required]) |
|
73 |
return '/'.join(arguments[required].parsed_name) |
|
74 |
|
|
75 |
|
|
67 | 76 |
def _construct_command_syntax(cls): |
68 | 77 |
spec = getargspec(cls.main.im_func) |
69 | 78 |
args = spec.args[1:] |
70 | 79 |
n = len(args) - len(spec.defaults or ()) |
71 | 80 |
required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]]) |
72 | 81 |
optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]]) |
73 |
cls.syntax = ' '.join(x for x in [required, optional] if x)
|
|
82 |
cls.syntax = ' '.join([required, optional])
|
|
74 | 83 |
if spec.varargs: |
75 | 84 |
cls.syntax += ' <%s ...>' % spec.varargs |
85 |
required = getattr(cls, 'required', None) |
|
86 |
if required: |
|
87 |
arguments = getattr(cls, 'arguments', dict()) |
|
88 |
cls.syntax += ' %s' % _required_syntax(arguments, required) |
|
76 | 89 |
|
77 | 90 |
|
78 | 91 |
def _num_of_matching_terms(basic_list, attack_list): |
... | ... | |
549 | 562 |
if parser.unparsed: |
550 | 563 |
run_one_cmd(exe, parser, cloud) |
551 | 564 |
elif _help: |
552 |
parser.parser.print_help() |
|
565 |
#parser.parser.print_help() |
|
566 |
parser.print_help() |
|
553 | 567 |
_groups_help(parser.arguments) |
554 | 568 |
else: |
555 | 569 |
run_shell(exe, parser, cloud) |
b/kamaki/cli/argument/__init__.py | ||
---|---|---|
37 | 37 |
|
38 | 38 |
from datetime import datetime as dtm |
39 | 39 |
from time import mktime |
40 |
from sys import stderr |
|
40 | 41 |
|
41 | 42 |
from logging import getLogger |
42 | 43 |
from argparse import ArgumentParser, ArgumentError |
... | ... | |
393 | 394 |
class ArgumentParseManager(object): |
394 | 395 |
"""Manage (initialize and update) an ArgumentParser object""" |
395 | 396 |
|
396 |
def __init__(self, exe, arguments=None): |
|
397 |
def __init__(self, exe, arguments=None, required=None):
|
|
397 | 398 |
""" |
398 | 399 |
:param exe: (str) the basic command (e.g. 'kamaki') |
399 | 400 |
|
400 | 401 |
:param arguments: (dict) if given, overrides the global _argument as |
401 | 402 |
the parsers arguments specification |
403 |
:param required: (list or tuple) an iterable of argument keys, denoting |
|
404 |
which arguments are required. A tuple denoted an AND relation, |
|
405 |
while a list denotes an OR relation e.g., ['a', 'b'] means that |
|
406 |
either 'a' or 'b' is required, while ('a', 'b') means that both 'a' |
|
407 |
and 'b' ar required. |
|
408 |
Nesting is allowed e.g., ['a', ('b', 'c'), ['d', 'e']] means that |
|
409 |
this command required either 'a', or both 'b' and 'c', or one of |
|
410 |
'd', 'e'. |
|
411 |
Repeated arguments are also allowed e.g., [('a', 'b'), ('a', 'c'), |
|
412 |
['b', 'c']] means that the command required either 'a' and 'b' or |
|
413 |
'a' and 'c' or at least one of 'b', 'c' and could be written as |
|
414 |
[('a', ['b', 'c']), ['b', 'c']] |
|
402 | 415 |
""" |
403 | 416 |
self.parser = ArgumentParser( |
404 | 417 |
add_help=False, formatter_class=RawDescriptionHelpFormatter) |
418 |
self._exe = exe |
|
405 | 419 |
self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe |
420 |
self.required = required |
|
406 | 421 |
if arguments: |
407 | 422 |
self.arguments = arguments |
408 | 423 |
else: |
... | ... | |
411 | 426 |
self._parser_modified, self._parsed, self._unparsed = False, None, None |
412 | 427 |
self.parse() |
413 | 428 |
|
429 |
@staticmethod |
|
430 |
def required2list(required): |
|
431 |
if isinstance(required, list) or isinstance(required, tuple): |
|
432 |
terms = [] |
|
433 |
for r in required: |
|
434 |
terms.append(ArgumentParseManager.required2list(r)) |
|
435 |
return list(set(terms).union()) |
|
436 |
return required |
|
437 |
|
|
438 |
@staticmethod |
|
439 |
def required2str(required, arguments, tab=''): |
|
440 |
if isinstance(required, list): |
|
441 |
return ' %sat least one:\n%s' % (tab, ''.join( |
|
442 |
[ArgumentParseManager.required2str( |
|
443 |
r, arguments, tab + ' ') for r in required])) |
|
444 |
elif isinstance(required, tuple): |
|
445 |
return ' %sall:\n%s' % (tab, ''.join( |
|
446 |
[ArgumentParseManager.required2str( |
|
447 |
r, arguments, tab + ' ') for r in required])) |
|
448 |
else: |
|
449 |
lt_pn, lt_all, arg = 23, 80, arguments[required] |
|
450 |
tab2 = ' ' * lt_pn |
|
451 |
ret = '%s%s' % (tab, ', '.join(arg.parsed_name)) |
|
452 |
if arg.arity != 0: |
|
453 |
ret += ' %s' % required.upper() |
|
454 |
ret = ('{:<%s}' % lt_pn).format(ret) |
|
455 |
prefix = ('\n%s' % tab2) if len(ret) < lt_pn else ' ' |
|
456 |
step, cur = (len(arg.help) / (lt_all - lt_pn)) or len(arg.help), 0 |
|
457 |
while arg.help[cur:]: |
|
458 |
next = cur + step |
|
459 |
ret += prefix |
|
460 |
ret += ('{:<%s}' % (lt_all - lt_pn)).format(arg.help[cur:next]) |
|
461 |
cur, finish = next, '\n%s' % tab2 |
|
462 |
return ret + '\n' |
|
463 |
|
|
464 |
def print_help(self, out=stderr): |
|
465 |
if self.required: |
|
466 |
tmp_args = dict(self.arguments) |
|
467 |
for term in self.required2list(self.required): |
|
468 |
tmp_args.pop(term) |
|
469 |
tmp_parser = ArgumentParseManager(self._exe, tmp_args) |
|
470 |
tmp_parser.syntax = self.syntax |
|
471 |
tmp_parser.parser.description = '%s\n\nrequired arguments:\n%s' % ( |
|
472 |
self.parser.description, |
|
473 |
self.required2str(self.required, self.arguments)) |
|
474 |
tmp_parser.update_parser() |
|
475 |
tmp_parser.parser.print_help() |
|
476 |
else: |
|
477 |
self.parser.print_help() |
|
478 |
|
|
414 | 479 |
@property |
415 | 480 |
def syntax(self): |
416 | 481 |
"""The command syntax (useful for help messages, descriptions, etc)""" |
... | ... | |
465 | 530 |
:param new_arguments: (dict) |
466 | 531 |
""" |
467 | 532 |
if new_arguments: |
468 |
assert isinstance(new_arguments, dict) |
|
533 |
assert isinstance(new_arguments, dict), 'Arguments not in dict !!!'
|
|
469 | 534 |
self._arguments.update(new_arguments) |
470 | 535 |
self.update_parser() |
471 | 536 |
|
... | ... | |
474 | 539 |
try: |
475 | 540 |
pkargs = (new_args,) if new_args else () |
476 | 541 |
self._parsed, unparsed = self.parser.parse_known_args(*pkargs) |
542 |
pdict = vars(self._parsed) |
|
543 |
diff = set(self.required or []).difference( |
|
544 |
[k for k in pdict if pdict[k] != None]) |
|
545 |
if diff: |
|
546 |
self.print_help() |
|
547 |
miss = ['/'.join(self.arguments[k].parsed_name) for k in diff] |
|
548 |
raise CLISyntaxError( |
|
549 |
'Missing required arguments (%s)' % ', '.join(miss)) |
|
477 | 550 |
except SystemExit: |
478 | 551 |
raiseCLIError(CLISyntaxError('Argument Syntax Error')) |
479 | 552 |
for name, arg in self.arguments.items(): |
b/kamaki/cli/command_shell.py | ||
---|---|---|
33 | 33 |
|
34 | 34 |
from cmd import Cmd |
35 | 35 |
from os import popen |
36 |
from sys import stdout |
|
36 |
from sys import stdout, stderr
|
|
37 | 37 |
|
38 | 38 |
from kamaki.cli import exec_cmd, print_error_message, print_subcommands_help |
39 | 39 |
from kamaki.cli.argument import ArgumentParseManager |
... | ... | |
167 | 167 |
self.__dict__ = oldcontext |
168 | 168 |
|
169 | 169 |
@staticmethod |
170 |
def _create_help_method(cmd_name, args, descr, syntax): |
|
170 |
def _create_help_method(cmd_name, args, required, descr, syntax):
|
|
171 | 171 |
tmp_args = dict(args) |
172 | 172 |
#tmp_args.pop('options', None) |
173 | 173 |
tmp_args.pop('cloud', None) |
... | ... | |
175 | 175 |
tmp_args.pop('verbose', None) |
176 | 176 |
tmp_args.pop('silent', None) |
177 | 177 |
tmp_args.pop('config', None) |
178 |
help_parser = ArgumentParseManager(cmd_name, tmp_args) |
|
178 |
help_parser = ArgumentParseManager(cmd_name, tmp_args, required)
|
|
179 | 179 |
help_parser.parser.description = descr |
180 | 180 |
help_parser.syntax = syntax |
181 |
return help_parser.parser.print_help |
|
181 |
#return help_parser.parser.print_help |
|
182 |
return help_parser.print_help |
|
182 | 183 |
|
183 | 184 |
def _register_command(self, cmd_path): |
184 | 185 |
cmd = self.cmd_tree.get_command(cmd_path) |
... | ... | |
200 | 201 |
if subcmd.is_command: # exec command |
201 | 202 |
try: |
202 | 203 |
cls = subcmd.cmd_class |
204 |
cmd_parser.required = getattr(cls, 'required', None) |
|
203 | 205 |
ldescr = getattr(cls, 'long_description', '') |
204 | 206 |
if subcmd.path == 'history_run': |
205 | 207 |
instance = cls( |
... | ... | |
214 | 216 |
cmd_parser.syntax = '%s %s' % ( |
215 | 217 |
subcmd.path.replace('_', ' '), cls.syntax) |
216 | 218 |
help_method = self._create_help_method( |
217 |
cmd.name, cmd_parser.arguments, |
|
219 |
cmd.name, cmd_parser.arguments, cmd_parser.required,
|
|
218 | 220 |
subcmd.help, cmd_parser.syntax) |
219 | 221 |
if '-h' in cmd_args or '--help' in cmd_args: |
220 | 222 |
help_method() |
b/kamaki/cli/commands/__init__.py | ||
---|---|---|
63 | 63 |
|
64 | 64 |
class _command_init(object): |
65 | 65 |
|
66 |
# self.arguments (dict) contains all non-positional arguments |
|
67 |
# self.required (list or tuple) contains required argument keys |
|
68 |
# if it is a list, at least one of these arguments is required |
|
69 |
# if it is a tuple, all arguments are required |
|
70 |
# Lists and tuples can nest other lists and/or tuples |
|
71 |
required = None |
|
72 |
|
|
66 | 73 |
def __init__( |
67 | 74 |
self, |
68 | 75 |
arguments={}, auth_base=None, cloud=None, |
b/kamaki/cli/commands/cyclades.py | ||
---|---|---|
372 | 372 |
|
373 | 373 |
@command(server_cmds) |
374 | 374 |
class server_create(_init_cyclades, _optional_json, _server_wait): |
375 |
"""Create a server (aka Virtual Machine) |
|
376 |
Parameters: |
|
377 |
- name: (single quoted text) |
|
378 |
- flavor id: Hardware flavor. Pick one from: /flavor list |
|
379 |
- image id: OS images. Pick one from: /image list |
|
380 |
""" |
|
375 |
"""Create a server (aka Virtual Machine)""" |
|
381 | 376 |
|
382 | 377 |
arguments = dict( |
378 |
server_name=ValueArgument('The name of the new server', '--name'), |
|
379 |
flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'), |
|
380 |
image_id=IntArgument('The ID of the hardware image', '--image-id'), |
|
383 | 381 |
personality=PersonalityArgument( |
384 | 382 |
(80 * ' ').join(howto_personality), ('-p', '--personality')), |
385 | 383 |
wait=FlagArgument('Wait server to build', ('-w', '--wait')), |
... | ... | |
389 | 387 |
'srv1, srv2, etc.', |
390 | 388 |
'--cluster-size') |
391 | 389 |
) |
390 |
required = ('server_name', 'flavor_id', 'image_id') |
|
392 | 391 |
|
393 | 392 |
@errors.cyclades.cluster_size |
394 | 393 |
def _create_cluster(self, prefix, flavor_id, image_id, size): |
... | ... | |
439 | 438 |
self._wait(r['id'], r['status']) |
440 | 439 |
self.writeln(' ') |
441 | 440 |
|
442 |
def main(self, name, flavor_id, image_id):
|
|
441 |
def main(self): |
|
443 | 442 |
super(self.__class__, self)._run() |
444 |
self._run(name=name, flavor_id=flavor_id, image_id=image_id) |
|
443 |
self._run( |
|
444 |
name=self['server_name'], |
|
445 |
flavor_id=self['flavor_id'], |
|
446 |
image_id=self['image_id']) |
|
445 | 447 |
|
446 | 448 |
|
447 | 449 |
@command(server_cmds) |
448 |
class server_rename(_init_cyclades, _optional_output_cmd): |
|
449 |
"""Set/update a virtual server name |
|
450 |
virtual server names are not unique, therefore multiple servers may share |
|
451 |
the same name |
|
452 |
""" |
|
450 |
class server_modify(_init_cyclades, _optional_output_cmd): |
|
451 |
"""Modify attributes of a virtual server""" |
|
452 |
|
|
453 |
arguments = dict( |
|
454 |
server_name=ValueArgument('The new name', '--name'), |
|
455 |
flavor_id=IntArgument('Set a different flavor', '--flavor-id'), |
|
456 |
) |
|
457 |
required = ['server_name', 'flavor_id'] |
|
453 | 458 |
|
454 | 459 |
@errors.generic.all |
455 | 460 |
@errors.cyclades.connection |
456 | 461 |
@errors.cyclades.server_id |
457 |
def _run(self, server_id, new_name): |
|
458 |
self._optional_output( |
|
459 |
self.client.update_server_name(int(server_id), new_name)) |
|
462 |
def _run(self, server_id): |
|
463 |
if self['server_name']: |
|
464 |
self.client.update_server_name((server_id), self['server_name']) |
|
465 |
if self['flavor_id']: |
|
466 |
self.client.resize_server(server_id, self['flavor_id']) |
|
467 |
if self['with_output']: |
|
468 |
self._optional_output(self.client.get_server_details(server_id)) |
|
460 | 469 |
|
461 |
def main(self, server_id, new_name):
|
|
470 |
def main(self, server_id): |
|
462 | 471 |
super(self.__class__, self)._run() |
463 |
self._run(server_id=server_id, new_name=new_name)
|
|
472 |
self._run(server_id=server_id) |
|
464 | 473 |
|
465 | 474 |
|
466 | 475 |
@command(server_cmds) |
... | ... | |
629 | 638 |
|
630 | 639 |
|
631 | 640 |
@command(server_cmds) |
632 |
class server_resize(_init_cyclades, _optional_output_cmd): |
|
633 |
"""Set a different flavor for an existing server |
|
634 |
To get server ids and flavor ids: |
|
635 |
/server list |
|
636 |
/flavor list |
|
637 |
""" |
|
638 |
|
|
639 |
@errors.generic.all |
|
640 |
@errors.cyclades.connection |
|
641 |
@errors.cyclades.server_id |
|
642 |
@errors.cyclades.flavor_id |
|
643 |
def _run(self, server_id, flavor_id): |
|
644 |
self._optional_output(self.client.resize_server(server_id, flavor_id)) |
|
645 |
|
|
646 |
def main(self, server_id, flavor_id): |
|
647 |
super(self.__class__, self)._run() |
|
648 |
self._run(server_id=server_id, flavor_id=flavor_id) |
|
649 |
|
|
650 |
|
|
651 |
@command(server_cmds) |
|
652 | 641 |
class server_firewall(_init_cyclades): |
653 | 642 |
"""Manage virtual server firewall profiles for public networks""" |
654 | 643 |
|
b/kamaki/cli/commands/network.py | ||
---|---|---|
456 | 456 |
@errors.cyclades.connection |
457 | 457 |
@errors.cyclades.network_id |
458 | 458 |
def _run(self, network_id, device_id): |
459 |
if not (bool(self['subnet_id']) ^ bool(self['ip_address'])):
|
|
459 |
if bool(self['subnet_id']) != bool(self['ip_address']):
|
|
460 | 460 |
raise CLIInvalidArgument('Invalid use of arguments', details=[ |
461 |
'--subned-id and --ip-address should be used together'])
|
|
462 |
fixed_ips = dict( |
|
463 |
subnet_id=self['subnet_id'], ip_address=self['ip_address']) if ( |
|
461 |
'--subnet-id and --ip-address should be used together'])
|
|
462 |
fixed_ips = [dict(
|
|
463 |
subnet_id=self['subnet_id'], ip_address=self['ip_address'])] if (
|
|
464 | 464 |
self['subnet_id']) else None |
465 | 465 |
r = self.client.create_port( |
466 | 466 |
network_id, device_id, |
b/kamaki/cli/one_command.py | ||
---|---|---|
58 | 58 |
def run(cloud, parser, _help): |
59 | 59 |
group = get_command_group(list(parser.unparsed), parser.arguments) |
60 | 60 |
if not group: |
61 |
parser.parser.print_help() |
|
61 |
#parser.parser.print_help() |
|
62 |
parser.print_help() |
|
62 | 63 |
_groups_help(parser.arguments) |
63 | 64 |
exit(0) |
64 | 65 |
|
... | ... | |
92 | 93 |
update_parser_help(parser, cmd) |
93 | 94 |
|
94 | 95 |
if _help or not cmd.is_command: |
95 |
parser.parser.print_help() |
|
96 |
#parser.parser.print_help() |
|
97 |
if cmd.cmd_class: |
|
98 |
parser.required = getattr(cmd.cmd_class, 'required', None) |
|
99 |
parser.print_help() |
|
96 | 100 |
if getattr(cmd, 'long_help', False): |
97 | 101 |
print 'Details:\n', cmd.long_help |
98 | 102 |
print_subcommands_help(cmd) |
... | ... | |
102 | 106 |
auth_base = init_cached_authenticator(_cnf, cloud, kloger) if ( |
103 | 107 |
cloud) else None |
104 | 108 |
executable = cls(parser.arguments, auth_base, cloud) |
109 |
parser.required = getattr(cls, 'required', None) |
|
105 | 110 |
parser.update_arguments(executable.arguments) |
106 | 111 |
for term in _best_match: |
107 | 112 |
parser.unparsed.remove(term) |
108 |
exec_cmd(executable, parser.unparsed, parser.parser.print_help) |
|
113 |
#exec_cmd(executable, parser.unparsed, parser.parser.print_help) |
|
114 |
exec_cmd(executable, parser.unparsed, parser.print_help) |
b/kamaki/clients/cyclades/__init__.py | ||
---|---|---|
534 | 534 |
port['security_groups'] = security_groups |
535 | 535 |
if name: |
536 | 536 |
port['name'] = name |
537 |
if fixed_ips:
|
|
538 |
diff = set(['subnet_id', 'ip_address']).difference(fixed_ips)
|
|
537 |
for fixed_ip in fixed_ips:
|
|
538 |
diff = set(['subnet_id', 'ip_address']).difference(fixed_ip) |
|
539 | 539 |
if diff: |
540 | 540 |
raise ValueError( |
541 | 541 |
'Invalid format for "fixed_ips", %s missing' % diff) |
542 |
if fixed_ips: |
|
542 | 543 |
port['fixed_ips'] = fixed_ips |
543 | 544 |
r = self.ports_post(json_data=dict(port=port), success=201) |
544 | 545 |
return r.json['port'] |
Also available in: Unified diff