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