Commit 56d84a4e authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Syntax check required arguments (incomplete)

Refs: #4596
parent 25f9a991
......@@ -64,15 +64,28 @@ def _arg2syntax(arg):
'_', ' ')
def _required_syntax(arguments, required):
if isinstance(required, tuple):
return ' '.join([_required_syntax(arguments, k) for k in required])
elif isinstance(required, list):
return '(%s)' % ' | '.join([
_required_syntax(arguments, k) for k in required])
return '/'.join(arguments[required].parsed_name)
def _construct_command_syntax(cls):
spec = getargspec(cls.main.im_func)
args = spec.args[1:]
n = len(args) - len(spec.defaults or ())
required = ' '.join(['<%s>' % _arg2syntax(x) for x in args[:n]])
optional = ' '.join(['[%s]' % _arg2syntax(x) for x in args[n:]])
cls.syntax = ' '.join(x for x in [required, optional] if x)
cls.syntax = ' '.join([required, optional])
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
required = getattr(cls, 'required', None)
if required:
arguments = getattr(cls, 'arguments', dict())
cls.syntax += ' %s' % _required_syntax(arguments, required)
def _num_of_matching_terms(basic_list, attack_list):
......@@ -549,7 +562,8 @@ def main():
if parser.unparsed:
run_one_cmd(exe, parser, cloud)
elif _help:
parser.parser.print_help()
#parser.parser.print_help()
parser.print_help()
_groups_help(parser.arguments)
else:
run_shell(exe, parser, cloud)
......
......@@ -37,6 +37,7 @@ from kamaki.cli.utils import split_input
from datetime import datetime as dtm
from time import mktime
from sys import stderr
from logging import getLogger
from argparse import ArgumentParser, ArgumentError
......@@ -393,16 +394,30 @@ _arguments = dict(
class ArgumentParseManager(object):
"""Manage (initialize and update) an ArgumentParser object"""
def __init__(self, exe, arguments=None):
def __init__(self, exe, arguments=None, required=None):
"""
:param exe: (str) the basic command (e.g. 'kamaki')
:param arguments: (dict) if given, overrides the global _argument as
the parsers arguments specification
:param required: (list or tuple) an iterable of argument keys, denoting
which arguments are required. A tuple denoted an AND relation,
while a list denotes an OR relation e.g., ['a', 'b'] means that
either 'a' or 'b' is required, while ('a', 'b') means that both 'a'
and 'b' ar required.
Nesting is allowed e.g., ['a', ('b', 'c'), ['d', 'e']] means that
this command required either 'a', or both 'b' and 'c', or one of
'd', 'e'.
Repeated arguments are also allowed e.g., [('a', 'b'), ('a', 'c'),
['b', 'c']] means that the command required either 'a' and 'b' or
'a' and 'c' or at least one of 'b', 'c' and could be written as
[('a', ['b', 'c']), ['b', 'c']]
"""
self.parser = ArgumentParser(
add_help=False, formatter_class=RawDescriptionHelpFormatter)
self._exe = exe
self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
self.required = required
if arguments:
self.arguments = arguments
else:
......@@ -411,6 +426,56 @@ class ArgumentParseManager(object):
self._parser_modified, self._parsed, self._unparsed = False, None, None
self.parse()
@staticmethod
def required2list(required):
if isinstance(required, list) or isinstance(required, tuple):
terms = []
for r in required:
terms.append(ArgumentParseManager.required2list(r))
return list(set(terms).union())
return required
@staticmethod
def required2str(required, arguments, tab=''):
if isinstance(required, list):
return ' %sat least one:\n%s' % (tab, ''.join(
[ArgumentParseManager.required2str(
r, arguments, tab + ' ') for r in required]))
elif isinstance(required, tuple):
return ' %sall:\n%s' % (tab, ''.join(
[ArgumentParseManager.required2str(
r, arguments, tab + ' ') for r in required]))
else:
lt_pn, lt_all, arg = 23, 80, arguments[required]
tab2 = ' ' * lt_pn
ret = '%s%s' % (tab, ', '.join(arg.parsed_name))
if arg.arity != 0:
ret += ' %s' % required.upper()
ret = ('{:<%s}' % lt_pn).format(ret)
prefix = ('\n%s' % tab2) if len(ret) < lt_pn else ' '
step, cur = (len(arg.help) / (lt_all - lt_pn)) or len(arg.help), 0
while arg.help[cur:]:
next = cur + step
ret += prefix
ret += ('{:<%s}' % (lt_all - lt_pn)).format(arg.help[cur:next])
cur, finish = next, '\n%s' % tab2
return ret + '\n'
def print_help(self, out=stderr):
if self.required:
tmp_args = dict(self.arguments)
for term in self.required2list(self.required):
tmp_args.pop(term)
tmp_parser = ArgumentParseManager(self._exe, tmp_args)
tmp_parser.syntax = self.syntax
tmp_parser.parser.description = '%s\n\nrequired arguments:\n%s' % (
self.parser.description,
self.required2str(self.required, self.arguments))
tmp_parser.update_parser()
tmp_parser.parser.print_help()
else:
self.parser.print_help()
@property
def syntax(self):
"""The command syntax (useful for help messages, descriptions, etc)"""
......@@ -465,7 +530,7 @@ class ArgumentParseManager(object):
:param new_arguments: (dict)
"""
if new_arguments:
assert isinstance(new_arguments, dict)
assert isinstance(new_arguments, dict), 'Arguments not in dict !!!'
self._arguments.update(new_arguments)
self.update_parser()
......@@ -474,6 +539,14 @@ class ArgumentParseManager(object):
try:
pkargs = (new_args,) if new_args else ()
self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
pdict = vars(self._parsed)
diff = set(self.required or []).difference(
[k for k in pdict if pdict[k] != None])
if diff:
self.print_help()
miss = ['/'.join(self.arguments[k].parsed_name) for k in diff]
raise CLISyntaxError(
'Missing required arguments (%s)' % ', '.join(miss))
except SystemExit:
raiseCLIError(CLISyntaxError('Argument Syntax Error'))
for name, arg in self.arguments.items():
......
......@@ -33,7 +33,7 @@
from cmd import Cmd
from os import popen
from sys import stdout
from sys import stdout, stderr
from kamaki.cli import exec_cmd, print_error_message, print_subcommands_help
from kamaki.cli.argument import ArgumentParseManager
......@@ -167,7 +167,7 @@ class Shell(Cmd):
self.__dict__ = oldcontext
@staticmethod
def _create_help_method(cmd_name, args, descr, syntax):
def _create_help_method(cmd_name, args, required, descr, syntax):
tmp_args = dict(args)
#tmp_args.pop('options', None)
tmp_args.pop('cloud', None)
......@@ -175,10 +175,11 @@ class Shell(Cmd):
tmp_args.pop('verbose', None)
tmp_args.pop('silent', None)
tmp_args.pop('config', None)
help_parser = ArgumentParseManager(cmd_name, tmp_args)
help_parser = ArgumentParseManager(cmd_name, tmp_args, required)
help_parser.parser.description = descr
help_parser.syntax = syntax
return help_parser.parser.print_help
#return help_parser.parser.print_help
return help_parser.print_help
def _register_command(self, cmd_path):
cmd = self.cmd_tree.get_command(cmd_path)
......@@ -200,6 +201,7 @@ class Shell(Cmd):
if subcmd.is_command: # exec command
try:
cls = subcmd.cmd_class
cmd_parser.required = getattr(cls, 'required', None)
ldescr = getattr(cls, 'long_description', '')
if subcmd.path == 'history_run':
instance = cls(
......@@ -214,7 +216,7 @@ class Shell(Cmd):
cmd_parser.syntax = '%s %s' % (
subcmd.path.replace('_', ' '), cls.syntax)
help_method = self._create_help_method(
cmd.name, cmd_parser.arguments,
cmd.name, cmd_parser.arguments, cmd_parser.required,
subcmd.help, cmd_parser.syntax)
if '-h' in cmd_args or '--help' in cmd_args:
help_method()
......
......@@ -63,6 +63,13 @@ def addLogSettings(foo):
class _command_init(object):
# self.arguments (dict) contains all non-positional arguments
# self.required (list or tuple) contains required argument keys
# if it is a list, at least one of these arguments is required
# if it is a tuple, all arguments are required
# Lists and tuples can nest other lists and/or tuples
required = None
def __init__(
self,
arguments={}, auth_base=None, cloud=None,
......
......@@ -372,14 +372,12 @@ class PersonalityArgument(KeyValueArgument):
@command(server_cmds)
class server_create(_init_cyclades, _optional_json, _server_wait):
"""Create a server (aka Virtual Machine)
Parameters:
- name: (single quoted text)
- flavor id: Hardware flavor. Pick one from: /flavor list
- image id: OS images. Pick one from: /image list
"""
"""Create a server (aka Virtual Machine)"""
arguments = dict(
server_name=ValueArgument('The name of the new server', '--name'),
flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
image_id=IntArgument('The ID of the hardware image', '--image-id'),
personality=PersonalityArgument(
(80 * ' ').join(howto_personality), ('-p', '--personality')),
wait=FlagArgument('Wait server to build', ('-w', '--wait')),
......@@ -389,6 +387,7 @@ class server_create(_init_cyclades, _optional_json, _server_wait):
'srv1, srv2, etc.',
'--cluster-size')
)
required = ('server_name', 'flavor_id', 'image_id')
@errors.cyclades.cluster_size
def _create_cluster(self, prefix, flavor_id, image_id, size):
......@@ -439,28 +438,38 @@ class server_create(_init_cyclades, _optional_json, _server_wait):
self._wait(r['id'], r['status'])
self.writeln(' ')
def main(self, name, flavor_id, image_id):
def main(self):
super(self.__class__, self)._run()
self._run(name=name, flavor_id=flavor_id, image_id=image_id)
self._run(
name=self['server_name'],
flavor_id=self['flavor_id'],
image_id=self['image_id'])
@command(server_cmds)
class server_rename(_init_cyclades, _optional_output_cmd):
"""Set/update a virtual server name
virtual server names are not unique, therefore multiple servers may share
the same name
"""
class server_modify(_init_cyclades, _optional_output_cmd):
"""Modify attributes of a virtual server"""
arguments = dict(
server_name=ValueArgument('The new name', '--name'),
flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
)
required = ['server_name', 'flavor_id']
@errors.generic.all
@errors.cyclades.connection
@errors.cyclades.server_id
def _run(self, server_id, new_name):
self._optional_output(
self.client.update_server_name(int(server_id), new_name))
def _run(self, server_id):
if self['server_name']:
self.client.update_server_name((server_id), self['server_name'])
if self['flavor_id']:
self.client.resize_server(server_id, self['flavor_id'])
if self['with_output']:
self._optional_output(self.client.get_server_details(server_id))
def main(self, server_id, new_name):
def main(self, server_id):
super(self.__class__, self)._run()
self._run(server_id=server_id, new_name=new_name)
self._run(server_id=server_id)
@command(server_cmds)
......@@ -628,26 +637,6 @@ class server_console(_init_cyclades, _optional_json):
self._run(server_id=server_id)
@command(server_cmds)
class server_resize(_init_cyclades, _optional_output_cmd):
"""Set a different flavor for an existing server
To get server ids and flavor ids:
/server list
/flavor list
"""
@errors.generic.all
@errors.cyclades.connection
@errors.cyclades.server_id
@errors.cyclades.flavor_id
def _run(self, server_id, flavor_id):
self._optional_output(self.client.resize_server(server_id, flavor_id))
def main(self, server_id, flavor_id):
super(self.__class__, self)._run()
self._run(server_id=server_id, flavor_id=flavor_id)
@command(server_cmds)
class server_firewall(_init_cyclades):
"""Manage virtual server firewall profiles for public networks"""
......
......@@ -456,11 +456,11 @@ class port_create(_init_network, _optional_json):
@errors.cyclades.connection
@errors.cyclades.network_id
def _run(self, network_id, device_id):
if not (bool(self['subnet_id']) ^ bool(self['ip_address'])):
if bool(self['subnet_id']) != bool(self['ip_address']):
raise CLIInvalidArgument('Invalid use of arguments', details=[
'--subned-id and --ip-address should be used together'])
fixed_ips = dict(
subnet_id=self['subnet_id'], ip_address=self['ip_address']) if (
'--subnet-id and --ip-address should be used together'])
fixed_ips = [dict(
subnet_id=self['subnet_id'], ip_address=self['ip_address'])] if (
self['subnet_id']) else None
r = self.client.create_port(
network_id, device_id,
......
......@@ -58,7 +58,8 @@ def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
def run(cloud, parser, _help):
group = get_command_group(list(parser.unparsed), parser.arguments)
if not group:
parser.parser.print_help()
#parser.parser.print_help()
parser.print_help()
_groups_help(parser.arguments)
exit(0)
......@@ -92,7 +93,10 @@ def run(cloud, parser, _help):
update_parser_help(parser, cmd)
if _help or not cmd.is_command:
parser.parser.print_help()
#parser.parser.print_help()
if cmd.cmd_class:
parser.required = getattr(cmd.cmd_class, 'required', None)
parser.print_help()
if getattr(cmd, 'long_help', False):
print 'Details:\n', cmd.long_help
print_subcommands_help(cmd)
......@@ -102,7 +106,9 @@ def run(cloud, parser, _help):
auth_base = init_cached_authenticator(_cnf, cloud, kloger) if (
cloud) else None
executable = cls(parser.arguments, auth_base, cloud)
parser.required = getattr(cls, 'required', None)
parser.update_arguments(executable.arguments)
for term in _best_match:
parser.unparsed.remove(term)
exec_cmd(executable, parser.unparsed, parser.parser.print_help)
#exec_cmd(executable, parser.unparsed, parser.parser.print_help)
exec_cmd(executable, parser.unparsed, parser.print_help)
......@@ -534,11 +534,12 @@ class CycladesNetworkClient(NetworkClient):
port['security_groups'] = security_groups
if name:
port['name'] = name
if fixed_ips:
diff = set(['subnet_id', 'ip_address']).difference(fixed_ips)
for fixed_ip in fixed_ips:
diff = set(['subnet_id', 'ip_address']).difference(fixed_ip)
if diff:
raise ValueError(
'Invalid format for "fixed_ips", %s missing' % diff)
if fixed_ips:
port['fixed_ips'] = fixed_ips
r = self.ports_post(json_data=dict(port=port), success=201)
return r.json['port']
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment