aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mchucarr...@apache.org
Subject git commit: Implement help message generation for the noun/verb framework.
Date Tue, 18 Feb 2014 20:38:24 GMT
Repository: incubator-aurora
Updated Branches:
  refs/heads/master 4eb308b85 -> 811fd0186


Implement help message generation for the noun/verb framework.

Bugs closed: aurora-202

Reviewed at https://reviews.apache.org/r/17948/


Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/811fd018
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/811fd018
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/811fd018

Branch: refs/heads/master
Commit: 811fd01863328b964dd7563d492a3f860e4744b6
Parents: 4eb308b
Author: Mark Chu-Carroll <mchucarroll@twopensource.com>
Authored: Tue Feb 18 15:34:26 2014 -0500
Committer: Mark Chu-Carroll <mchucarroll@twitter.com>
Committed: Tue Feb 18 15:34:26 2014 -0500

----------------------------------------------------------------------
 .../python/apache/aurora/client/cli/__init__.py | 126 ++++++++---
 .../python/apache/aurora/client/cli/client.py   |   6 +-
 .../python/apache/aurora/client/cli/context.py  |  12 +-
 .../python/apache/aurora/client/cli/jobs.py     | 213 +++++++------------
 .../python/apache/aurora/client/cli/options.py  |  69 +++++-
 .../python/apache/aurora/client/cli/quota.py    |  10 +-
 src/main/python/apache/aurora/client/cli/sla.py |  29 ++-
 .../python/apache/aurora/client/cli/task.py     |  37 ++--
 src/test/python/apache/aurora/client/cli/BUILD  |  11 +
 .../apache/aurora/client/cli/test_help.py       |  94 ++++++++
 10 files changed, 404 insertions(+), 203 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/__init__.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/__init__.py b/src/main/python/apache/aurora/client/cli/__init__.py
index 9eb5c52..d3bd119 100644
--- a/src/main/python/apache/aurora/client/cli/__init__.py
+++ b/src/main/python/apache/aurora/client/cli/__init__.py
@@ -72,51 +72,52 @@ class Context(object):
     self.options = options
 
 
-class CommandOption(object):
-  """A lightweight encapsulation of an argparse option specification, which can be used to
-  define options that can be reused by multiple commands.
-  """
-
-  def __init__(self, *args, **kwargs):
-    self.args = args
-    self.kwargs = kwargs
-
-  def add_to_parser(self, parser):
-    parser.add_argument(*self.args, **self.kwargs)
-
-
 class AuroraCommand(object):
   def setup_options_parser(self, argparser):
-    """Set up command line options parsing for this command.
+    """Sets up command line options parsing for this command.
     This is a thin veneer over the standard python argparse system.
     :param argparser: the argument parser where this command can add its arguments.
     """
     pass
 
   def add_option(self, argparser, option):
-    """Add a predefined argument encapsulated an a CommandOption to an argument parser."""
+    """Adds an option spec encapsulated an a CommandOption to this command's argument parser."""
     if not isinstance(option, CommandOption):
       raise TypeError('Command option object must be an instance of CommandOption')
     option.add_to_parser(argparser)
 
   @property
   def help(self):
-    """The help message for a command that will be used in the argparse help message"""
+    """Returns the help message for this command"""
+
+  @property
+  def usage(self):
+    """Returns a short usage description of the command"""
 
   @property
   def name(self):
-    """The command name"""
+    """Returns the command name"""
 
 
 class CommandLine(object):
   """The top-level object implementing a command-line application."""
 
+  @property
+  def name(self):
+    """Returns the name of this command-line tool"""
+
+  def print_out(self, str):
+    print(str)
+
+  def print_err(self, str):
+    print(str, file=sys.stderr)
+
   def __init__(self):
     self.nouns = None
     self.parser = None
 
   def register_noun(self, noun):
-    """Add a noun to the application"""
+    """Adds a noun to the application"""
     if self.nouns is None:
       self.nouns = {}
     if not isinstance(noun, Noun):
@@ -124,13 +125,59 @@ class CommandLine(object):
     self.nouns[noun.name] = noun
 
   def setup_options_parser(self):
-    """ Build the options parsing for the application."""
+    """ Builds the options parsing for the application."""
     self.parser = argparse.ArgumentParser()
     subparser = self.parser.add_subparsers(dest='noun')
     for (name, noun) in self.nouns.items():
       noun_parser = subparser.add_parser(name, help=noun.help)
       noun.internal_setup_options_parser(noun_parser)
 
+  def help_cmd(self, args):
+    """Generates a help message for a help request.
+    There are three kinds of help requests: a simple no-parameter request (help) which generates
+    a list of all of the commands; a one-parameter (help noun) request, which generates the
help
+    for a particular noun, and a two-parameter request (help noun verb) which generates the
help
+    for a particular verb.
+    """
+    if args is None or len(args) == 0:
+      self.print_out(self.composed_help)
+    elif len(args) == 1:
+      if args[0] in self.nouns:
+        self.print_out(self.nouns[args[0]].composed_help)
+        return EXIT_OK
+      else:
+        self.print_err('Unknown noun "%s"' % args[0])
+        self.print_err('Valid nouns are: %s' % [k for k in self.nouns])
+        return EXIT_INVALID_PARAMETER
+    elif len(args) == 2:
+      if args[0] in self.nouns:
+        if args[1] in self.nouns[args[0]].verbs:
+          self.print_out(self.nouns[args[0]].verbs[args[1]].composed_help)
+          return EXIT_OK
+        else:
+          self.print_err('Noun "%s" does not support a verb "%s"' % (args[0], args[1]))
+          verbs = [v for v in self.nouns[args[0]].verbs]
+          self.print_err('Valid verbs for "%s" are: %s' % (args[0], verbs))
+          return EXIT_INVALID_PARAMETER
+      else:
+        self.print_err('Unknown noun %s' % args[0])
+        return EXIT_INVALID_PARAMETER
+    else:
+      self.print_err('Unknown help command: %s' % (' '.join(args)))
+      self.print_err(self.composed_help)
+      return EXIT_INVALID_PARAMETER
+
+  @property
+  def composed_help(self):
+    """Get a fully composed, well-formatted help message"""
+    result = ["Usage:"]
+    for noun in self.registered_nouns:
+      result += ["==Commands for %ss" % noun]
+      result += ["  %s" % s for s in self.nouns[noun].usage] + [""]
+    result.append("\nRun 'help noun' or 'help noun verb' for help about a specific command")
+    return "\n".join(result)
+
+
   def register_nouns(self):
     """This method should overridden by applications to register the collection of nouns
     that they can manipulate.
@@ -159,6 +206,8 @@ class CommandLine(object):
         that should be parsed by the application; it does not include sys.argv[0].
     """
     nouns = self.registered_nouns
+    if args[0] == 'help':
+      return self.help_cmd(args[1:])
     self.setup_options_parser()
     options = self.parser.parse_args(args)
     if options.noun not in nouns:
@@ -194,7 +243,12 @@ class Noun(AuroraCommand):
     subparser = argparser.add_subparsers(dest='verb')
     for (name, verb) in self.verbs.items():
       vparser = subparser.add_parser(name, help=verb.help)
-      verb.setup_options_parser(vparser)
+      for opt in verb.get_options():
+        opt.add_to_parser(vparser)
+
+  @property
+  def usage(self):
+    return ["%s %s" % (self.name, ' '.join(self.verbs[verb].usage)) for verb in self.verbs]
 
   @classmethod
   def create_context(cls):
@@ -203,9 +257,12 @@ class Noun(AuroraCommand):
     """
     pass
 
-  @abstractmethod
-  def setup_options_parser(self, argparser):
-    pass
+  @property
+  def composed_help(self):
+    result = ['Usage for noun "%s":' % self.name]
+    result += ["    %s %s" % (self.name, self.verbs[verb].usage) for verb in self.verbs]
+    result += [self.help]
+    return '\n'.join(result)
 
   def execute(self, context):
     if context.options.verb not in self.verbs:
@@ -221,10 +278,31 @@ class Verb(AuroraCommand):
     """Create a link from a verb to its noun."""
     self.noun = noun
 
+  @property
+  def usage(self):
+    """Get a brief usage-description for the command.
+    A default usage string is automatically generated, but for commands with many options,
+    users may want to specify usage themselves.
+    """
+    result = [self.name]
+    result += [opt.render_usage() for opt in self.get_options()]
+    return " ".join(result)
+
   @abstractmethod
-  def setup_options_parser(self, argparser):
+  def get_options(self):
     pass
 
+  @property
+  def composed_help(self):
+    """Generate the composed help message shown when the user requests help about this verb"""
+    result = ['Usage for verb "%s %s":' % (self.noun.name, self.name)]
+    result += ["  " + s for s in self.usage]
+    result += ["Options:"]
+    for opt in self.get_options():
+      result += ["  " + s for s in opt.render_help()]
+    result += ["", self.help]
+    return "\n".join(result)
+
   def execute(self, context):
     pass
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/client.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/client.py b/src/main/python/apache/aurora/client/cli/client.py
index 604eb44..56d6366 100644
--- a/src/main/python/apache/aurora/client/cli/client.py
+++ b/src/main/python/apache/aurora/client/cli/client.py
@@ -27,8 +27,12 @@ class AuroraClientV2CommandProcessor(CommandProcessor):
   def __init__(self):
     self.commandline = AuroraCommandLine()
 
+  @property
+  def name(self):
+    return "aurora"
+
   def get_commands(self):
-    return self.commandline.registered_nouns
+    return self.commandline.registered_nouns + ["help"]
 
   def execute(self, args):
     return self.commandline.execute(args[1:])

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/context.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/context.py b/src/main/python/apache/aurora/client/cli/context.py
index 4d1de48..dad4fcb 100644
--- a/src/main/python/apache/aurora/client/cli/context.py
+++ b/src/main/python/apache/aurora/client/cli/context.py
@@ -73,17 +73,21 @@ class AuroraCommandContext(Context):
     except Exception as e:
       raise self.CommandError(EXIT_INVALID_CONFIGURATION, 'Error loading configuration: %s'
% e)
 
-  def print_out(self, str, indent=0):
+  def print_out(self, msg, indent=0):
     """Prints output. For debugging purposes, it's nice to be able to patch this
     and capture output.
     """
     indent_str = ' ' * indent
-    print('%s%s' % (indent_str, str))
+    lines = msg.split('\n')
+    for line in lines:
+      print('%s%s' % (indent_str, line))
 
-  def print_err(self, str, indent=0):
+  def print_err(self, msg, indent=0):
     """Prints output to standard error."""
     indent_str = ' ' * indent
-    print('%s%s' % (indent_str, str), file=sys.stderr)
+    lines = msg.split('\n')
+    for line in lines:
+      print('%s%s' % (indent_str, line), file=sys.stderr)
 
   def open_page(self, url):
     import webbrowser

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/jobs.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/jobs.py b/src/main/python/apache/aurora/client/cli/jobs.py
index 199d27f..3b327df 100644
--- a/src/main/python/apache/aurora/client/cli/jobs.py
+++ b/src/main/python/apache/aurora/client/cli/jobs.py
@@ -39,6 +39,7 @@ from apache.aurora.client.cli.options import (
     BATCH_OPTION,
     BIND_OPTION,
     BROWSER_OPTION,
+    CommandOption,
     CONFIG_ARGUMENT,
     FORCE_OPTION,
     HEALTHCHECK_OPTION,
@@ -73,16 +74,13 @@ class CancelUpdateCommand(Verb):
 
   @property
   def help(self):
-    return """Usage: aurora job cancel_update [--config_file=path [--json]] cluster/role/env/name
+    return "Cancel an in-progress update operation, releasing the update lock"
 
-    Cancels an in-progress update operation, releasing the update lock
-    """
-
-  def setup_options_parser(self, parser):
-    self.add_option(parser, JSON_READ_OPTION)
-    parser.add_argument('--config', type=str, default=None, dest='config_file',
-         help='Config file for the job, possibly containing hooks')
-    self.add_option(parser, JOBSPEC_ARGUMENT)
+  def get_options(self):
+    return [JSON_READ_OPTION,
+        CommandOption('--config', type=str, default=None, dest='config_file',
+            help='Config file for the job, possibly containing hooks'),
+        JOBSPEC_ARGUMENT]
 
   def execute(self, context):
     api = context.get_api(context.options.jobspec.cluster)
@@ -99,24 +97,18 @@ class CreateJobCommand(Verb):
 
   @property
   def help(self):
-    return """Usage: aurora create cluster/role/env/job config.aurora
-
-    Create a job using aurora
-    """
+    return "Create a job using aurora"
 
   CREATE_STATES = ('PENDING', 'RUNNING', 'FINISHED')
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, BIND_OPTION)
-    self.add_option(parser, BROWSER_OPTION)
-    self.add_option(parser, JSON_READ_OPTION)
-    parser.add_argument('--wait_until', choices=self.CREATE_STATES,
-        default='PENDING',
-        help=('Block the client until all the tasks have transitioned into the requested
state. '
-                        'Default: PENDING'))
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    self.add_option(parser, CONFIG_ARGUMENT)
-
+  def get_options(self):
+    return [BIND_OPTION, JSON_READ_OPTION,
+        CommandOption('--wait_until', choices=self.CREATE_STATES,
+            default='PENDING',
+            help=('Block the client until all the tasks have transitioned into the requested
'
+                'state. Default: PENDING')),
+        BROWSER_OPTION,
+        JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
   def execute(self, context):
     config = context.get_job_config(context.options.jobspec, context.options.config_file)
@@ -143,24 +135,19 @@ class DiffCommand(Verb):
 
   @property
   def help(self):
-    return """Usage: diff cluster/role/env/job config
-
-  Compares a job configuration against a running job.
-  By default the diff will be displayed using 'diff', though you may choose an alternate
-  diff program by setting the DIFF_VIEWER environment variable.
-  """
+    return """Compare a job configuration against a running job.
+By default the diff will be displayed using 'diff', though you may choose an
+alternate diff program by setting the DIFF_VIEWER environment variable."""
 
   @property
   def name(self):
     return 'diff'
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, BIND_OPTION)
-    self.add_option(parser, JSON_READ_OPTION)
-    parser.add_argument('--from', dest='rename_from', type=AuroraJobKey.from_path, default=None,
-        help='If specified, the job key to diff against.')
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    self.add_option(parser, CONFIG_ARGUMENT)
+  def get_options(self):
+    return [BIND_OPTION, JSON_READ_OPTION,
+        CommandOption('--from', dest='rename_from', type=AuroraJobKey.from_path, default=None,
+            help='If specified, the job key to diff against.'),
+        JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
   def pretty_print_task(self, task):
     task.configuration = None
@@ -214,27 +201,23 @@ class DiffCommand(Verb):
 
 
 class InspectCommand(Verb):
+
   @property
   def help(self):
-    return """Usage: inspect cluster/role/env/job config
-
-  Verifies that a job can be parsed from a configuration file, and displays
-  the parsed configuration.
-  """
+    return """Verify that a job can be parsed from a configuration file, and display
+the parsed configuration."""
 
   @property
   def name(self):
     return 'inspect'
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, BIND_OPTION)
-    self.add_option(parser, JSON_READ_OPTION)
-    parser.add_argument('--local', dest='local', default=False, action='store_true',
-        help='Inspect the configuration as would be created by the "spawn" command.')
-    parser.add_argument('--raw', dest='raw', default=False, action='store_true',
-        help='Show the raw configuration.')
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    self.add_option(parser, CONFIG_ARGUMENT)
+  def get_options(self):
+    return [BIND_OPTION, JSON_READ_OPTION,
+        CommandOption('--local', dest='local', default=False, action='store_true',
+            help='Inspect the configuration as would be created by the "spawn" command.'),
+        CommandOption('--raw', dest='raw', default=False, action='store_true',
+            help='Show the raw configuration.'),
+        JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
   def execute(self, context):
     config = context.get_job_config(context.options.jobspec, context.options.config_file)
@@ -295,17 +278,13 @@ class KillJobCommand(Verb):
 
   @property
   def help(self):
-    return """Usage: kill cluster/role/env/job
-
-    Kill a scheduled job
-    """
+    return "Kill a scheduled job"
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, BROWSER_OPTION)
-    self.add_option(parser, INSTANCES_OPTION)
-    parser.add_argument('--config', type=str, default=None, dest='config',
-         help='Config file for the job, possibly containing hooks')
-    self.add_option(parser, JOBSPEC_ARGUMENT)
+  def get_options(self):
+    return [BROWSER_OPTION, INSTANCES_OPTION,
+        CommandOption('--config', type=str, default=None, dest='config',
+            help='Config file for the job, possibly containing hooks'),
+        JOBSPEC_ARGUMENT]
 
   def execute(self, context):
     # TODO(mchucarroll): Check for wildcards; we don't allow wildcards for job kill.
@@ -319,19 +298,17 @@ class KillJobCommand(Verb):
 
 
 class ListJobsCommand(Verb):
+
   @property
   def help(self):
-    return """Usage: aurora job list jobspec
-
-    Lists jobs that match a jobkey or jobkey pattern.
-    """
+    return "List jobs that match a jobkey or jobkey pattern."
 
   @property
   def name(self):
     return 'list'
 
-  def setup_options_parser(self, parser):
-    parser.add_argument('jobspec', type=arg_type_jobkey)
+  def get_options(self):
+    return [CommandOption('jobspec', type=arg_type_jobkey)]
 
   def execute(self, context):
     jobs = context.get_jobs_matching_key(context.options.jobspec)
@@ -346,44 +323,25 @@ class RestartCommand(Verb):
   def name(self):
     return 'restart'
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, BATCH_OPTION)
-    self.add_option(parser, BIND_OPTION)
-    self.add_option(parser, BROWSER_OPTION)
-    self.add_option(parser, FORCE_OPTION)
-    self.add_option(parser, HEALTHCHECK_OPTION)
-    self.add_option(parser, INSTANCES_OPTION)
-    self.add_option(parser, JSON_READ_OPTION)
-    self.add_option(parser, WATCH_OPTION)
-    parser.add_argument('--max_per_instance_failures', type=int, default=0,
-        help='Maximum number of restarts per instance during restart. Increments total failure
'
-            'count when this limit is exceeded.')
-    parser.add_argument('--restart_threshold', type=int, default=60,
-        help='Maximum number of seconds before an instance must move into the RUNNING state
before '
-             'considered a failure.')
-    parser.add_argument('--max_total_failures', type=int, default=0,
-        help='Maximum number of instance failures to be tolerated in total during restart.')
-    parser.add_argument('--rollback_on_failure', type=bool, default=True,
-        help='If false, prevent update from performing a rollback.')
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    self.add_option(parser, CONFIG_ARGUMENT)
+  def get_options(self):
+    return [BATCH_OPTION, BIND_OPTION, BROWSER_OPTION, FORCE_OPTION, HEALTHCHECK_OPTION,
+        INSTANCES_OPTION, JSON_READ_OPTION, WATCH_OPTION,
+        CommandOption('--max_per_instance_failures', type=int, default=0,
+             help='Maximum number of restarts per instance during restart. Increments total
failure '
+                 'count when this limit is exceeded.'),
+        CommandOption('--restart_threshold', type=int, default=60,
+             help='Maximum number of seconds before a shard must move into the RUNNING state
'
+                 'before considered a failure.'),
+        CommandOption('--max_total_failures', type=int, default=0,
+             help='Maximum number of instance failures to be tolerated in total during restart.'),
+        CommandOption('--rollback_on_failure', default=True, action='store_false',
+            help='If false, prevent update from performing a rollback.'),
+        JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
   @property
   def help(self):
-    return """Usage: restart cluster/role/env/job
-    [--instances=INSTANCES]
-    [--batch_size=INT]
-    [--updater_health_check_interval_seconds=SECONDS]
-    [--max_per_instance_failures=INT]
-    [--max_total_failures=INT]
-    [--restart_threshold=INT]
-    [--watch_secs=SECONDS]
-    [--open_browser]
-
-  Performs a rolling restart of running task instances within a job.
-
-  Restarts are fully controlled client-side, so aborting halts the restart.
-  """
+    return """Perform a rolling restart of shards within a job.
+Restarts are fully controlled client-side, so aborting halts the restart."""
 
   def execute(self, context):
     api = context.get_api(context.options.jobspec.cluster)
@@ -405,21 +363,18 @@ class RestartCommand(Verb):
 
 
 class StatusCommand(Verb):
+
   @property
   def help(self):
-    return """Usage: aurora status jobspec
-
-    Get status information about a scheduled job or group of jobs. The
-    jobspec parameter can ommit parts of the jobkey, or use shell-style globs.
-    """
+    return """Get status information about a scheduled job or group of jobs.
+The jobspec parameter can omit parts of the jobkey, or use shell-style globs."""
 
   @property
   def name(self):
     return 'status'
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, JSON_WRITE_OPTION)
-    parser.add_argument('jobspec', type=arg_type_jobkey)
+  def get_options(self):
+    return [JSON_WRITE_OPTION, CommandOption('jobspec', type=arg_type_jobkey)]
 
   def render_tasks_json(self, jobkey, active_tasks, inactive_tasks):
     """Render the tasks running for a job in machine-processable JSON format."""
@@ -497,33 +452,26 @@ class UpdateCommand(Verb):
   def name(self):
     return 'update'
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, FORCE_OPTION)
-    self.add_option(parser, BIND_OPTION)
-    self.add_option(parser, JSON_READ_OPTION)
-    self.add_option(parser, INSTANCES_OPTION)
-    self.add_option(parser, HEALTHCHECK_OPTION)
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    self.add_option(parser, CONFIG_ARGUMENT)
+  def get_options(self):
+    return [FORCE_OPTION, BIND_OPTION, JSON_READ_OPTION, INSTANCES_OPTION, HEALTHCHECK_OPTION,
+        JOBSPEC_ARGUMENT, CONFIG_ARGUMENT]
 
   @property
   def help(self):
-    return """Usage: update cluster/role/env/job config
-
-  Performs a rolling upgrade on a running job, using the update configuration
-  within the config file as a control for update velocity and failure tolerance.
+    return """Perform a rolling upgrade on a running job, using the update configuration
+within the config file as a control for update velocity and failure tolerance.
 
-  Updates are fully controlled client-side, so aborting an update halts the
-  update and leaves the job in a 'locked' state on the scheduler.
-  Subsequent update attempts will fail until the update is 'unlocked' using the
-  'cancel_update' command.
+Updates are fully controlled client-side, so aborting an update halts the
+update and leaves the job in a 'locked' state on the scheduler.
+Subsequent update attempts will fail until the update is 'unlocked' using the
+'cancel_update' command.
 
-  The updater only takes action on instances in a job that have changed, meaning
-  that changing a single instance will only induce a restart on the changed task instance.
+The updater only takes action on instances in a job that have changed, meaning
+that changing a single instance will only induce a restart on the changed task instance.
 
-  You may want to consider using the 'diff' subcommand before updating,
-  to preview what changes will take effect.
-  """
+You may want to consider using the 'diff' subcommand before updating,
+to preview what changes will take effect.
+"""
 
   def warn_if_dangerous_change(self, context, api, job_spec, config):
     # Get the current job status, so that we can check if there's anything
@@ -545,7 +493,8 @@ class UpdateCommand(Verb):
     if (local_task_count >= 4 * remote_task_count or
         local_task_count <= 4 * remote_task_count or
         local_task_count == 0):
-      context.print_out('Warning: this update is a large change. Press ^C within 5 seconds
to abort')
+      context.print_out('Warning: this update is a large change. '
+          'Press ^C within 5 seconds to abort')
       time.sleep(5)
 
   def execute(self, context):

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/options.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/options.py b/src/main/python/apache/aurora/client/cli/options.py
index 37f416d..1cbde5e 100644
--- a/src/main/python/apache/aurora/client/cli/options.py
+++ b/src/main/python/apache/aurora/client/cli/options.py
@@ -15,13 +15,72 @@
 #
 
 from collections import namedtuple
+from types import IntType, StringType
 
-from apache.aurora.client.cli import CommandOption
 from apache.aurora.common.aurora_job_key import AuroraJobKey
 
 from twitter.common.quantity.parse_simple import parse_time
 
 
+class CommandOption(object):
+  """A lightweight encapsulation of an argparse option specification"""
+
+  def __init__(self, *args, **kwargs):
+    self.name = args[0]
+    self.args = args
+    self.kwargs = kwargs
+    self.type = kwargs['type'] if 'type' in kwargs else None
+    self.help = kwargs['help'] if 'help' in kwargs else ""
+
+  def is_mandatory(self):
+    return self.kwargs["required"] if "required" in self.kwargs else not self.name.startswith('--')
+
+  def get_displayname(self):
+    """Get a display name for a the expected format of a parameter value"""
+    if 'metavar' in self.kwargs:
+      displayname = self.kwargs['metavar']
+    elif self.type == str:
+      displayname = "str"
+    elif type(self.type) is StringType:
+      displayname = self.type
+    elif type(self.type) is IntType:
+      displayname = "int",
+    else:
+      displayname = "value"
+    return displayname
+
+  def render_usage(self):
+    """Create a usage string for this option"""
+    if not self.name.startswith('--'):
+      return self.get_displayname()
+    if "action" in self.kwargs:
+      if self.kwargs["action"] == "store_true":
+        return "[%s]" % self.name
+      elif self.kwargs["action"] == "store_false":
+        return "[--no-%s]" % self.name[2:]
+    if self.type is None and "choices" in self.kwargs:
+      return "[%s=%s]" % (self.name, self.kwargs["choices"])
+    else:
+      return "[%s=%s]" % (self.name, self.get_displayname())
+
+  def render_help(self):
+    """Render a full help message for this option"""
+    result = ""
+    if "action" in self.kwargs and self.kwargs["action"] == "store_true":
+      result = self.name
+    elif "action" in self.kwargs and self.kwargs["action"] == "store_false":
+      result = "--no-%s" % self.name[2:]
+    elif self.type is None and "choices" in self.kwargs:
+      result = "%s=%s" % (self.name, self.kwargs["choices"])
+    else:
+      result = "%s=%s" % (self.name, self.get_displayname())
+    return [result, "\t" + self.help]
+
+  def add_to_parser(self, parser):
+    """Add this option to an option parser"""
+    parser.add_argument(*self.args, **self.kwargs)
+
+
 def parse_qualified_role(rolestr):
   if rolestr is None:
     raise ValueError('Role argument cannot be empty!')
@@ -78,6 +137,7 @@ BATCH_OPTION = CommandOption('--batch_size', type=int, default=5,
 
 BIND_OPTION = CommandOption('--bind', type=str, default=[], dest='bindings',
     action='append',
+    metavar="pystachio-binding",
     help='Bind a thermos mustache variable name to a value. '
     'Multiple flags may be used to specify multiple values.')
 
@@ -105,13 +165,14 @@ HEALTHCHECK_OPTION = CommandOption('--healthcheck_interval_seconds',
type=int,
 
 
 INSTANCES_OPTION = CommandOption('--instances', type=parse_instances, dest='instances',
-    default=None,
+    default=None, metavar="inst,inst,inst...",
      help='A list of instance ids to act on. Can either be a comma-separated list (e.g. 0,1,2)
'
          'or a range (e.g. 0-2) or any combination of the two (e.g. 0-2,5,7-9). If not set,
'
          'all instances will be acted on.')
 
 
 JOBSPEC_ARGUMENT = CommandOption('jobspec', type=AuroraJobKey.from_path,
+    metavar="CLUSTER/ROLE/ENV/NAME",
     help='Fully specified job key, in CLUSTER/ROLE/ENV/NAME format')
 
 
@@ -125,8 +186,8 @@ JSON_WRITE_OPTION = CommandOption('--write_json', default=False, dest='write_jso
     help='Generate command output in JSON format')
 
 
-ROLE_ARGUMENT = CommandOption('role', type=parse_qualified_role,
-    help='Rolename to retrieve information about, in CLUSTER/NAME format')
+ROLE_ARGUMENT = CommandOption('role', type=parse_qualified_role, metavar='CLUSTER/NAME',
+    help='Rolename to retrieve information about')
 
 
 SSH_USER_OPTION = CommandOption('--ssh_user', '-l', default=None,

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/quota.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/quota.py b/src/main/python/apache/aurora/client/cli/quota.py
index a7bcfbe..d06f21a 100644
--- a/src/main/python/apache/aurora/client/cli/quota.py
+++ b/src/main/python/apache/aurora/client/cli/quota.py
@@ -41,14 +41,10 @@ class GetQuotaCmd(Verb):
 
   @property
   def help(self):
-    return """Usage: aurora quota get cluster/role
+    return "Print information about quotas for a role"
 
-    Prints information about quotas for a role
-    """
-
-  def setup_options_parser(self, parser):
-    self.add_option(parser, JSON_WRITE_OPTION)
-    self.add_option(parser, ROLE_ARGUMENT)
+  def get_options(self):
+    return [JSON_WRITE_OPTION, ROLE_ARGUMENT]
 
   def render_quota(self, write_json, quota_resp):
     def get_quota_json(quota):

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/sla.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/sla.py b/src/main/python/apache/aurora/client/cli/sla.py
index a3973df..0200270 100644
--- a/src/main/python/apache/aurora/client/cli/sla.py
+++ b/src/main/python/apache/aurora/client/cli/sla.py
@@ -20,7 +20,7 @@ from apache.aurora.client.cli import (
     Verb,
 )
 from apache.aurora.client.cli.context import AuroraCommandContext
-from apache.aurora.client.cli.options import JOBSPEC_ARGUMENT, parse_time_values
+from apache.aurora.client.cli.options import CommandOption, JOBSPEC_ARGUMENT, parse_time_values
 
 from twitter.common.quantity import Time
 
@@ -32,12 +32,10 @@ class GetTaskUpCountCmd(Verb):
 
   @property
   def help(self):
-    return """Usage: aurora sla get_task_up_count cluster/role/env/job [--duration]
-
-    Prints the percentage of tasks that stayed up within the last "duration" s|m|h|d.
-    If duration is not specified prints a histogram-like log-scale distribution
-    of task uptime percentages.
-    """
+    return """Print the percentage of tasks that stayed up within the last "duration" s|m|h|d.
+If duration is not specified prints a histogram-like log-scale distribution
+of task uptime percentages.
+"""
 
   @classmethod
   def render_get_task_up_count(cls, context, vector):
@@ -49,14 +47,15 @@ class GetTaskUpCountCmd(Verb):
     return '\n'.join(format_output(durations))
 
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    parser.add_argument('--durations', type=parse_time_values, default=None,
-        help='Durations to report uptime for.'
-             'Format: XdYhZmWs (each field optional but must be in that order.)'
-             'Examples: '
-             '  --durations=1d'
-             '  --durations=3m,10s,1h3m10s')
+  def get_options(self):
+    return [
+        CommandOption('--durations', type=parse_time_values, default=None,
+            help="""Durations to report uptime for.
+Format: XdYhZmWs (each field optional but must be in that order.)
+Examples:
+ --durations=1d'
+  --durations=3m,10s,1h3m10s"""),
+        JOBSPEC_ARGUMENT]
 
   def execute(self, context):
     api = context.get_api(context.options.jobspec.cluster)

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/main/python/apache/aurora/client/cli/task.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/cli/task.py b/src/main/python/apache/aurora/client/cli/task.py
index d7bf5cd..8d4d38e 100644
--- a/src/main/python/apache/aurora/client/cli/task.py
+++ b/src/main/python/apache/aurora/client/cli/task.py
@@ -42,6 +42,7 @@ from apache.aurora.client.cli.options import (
     BATCH_OPTION,
     BIND_OPTION,
     BROWSER_OPTION,
+    CommandOption,
     CONFIG_ARGUMENT,
     EXECUTOR_SANDBOX_OPTION,
     FORCE_OPTION,
@@ -87,13 +88,15 @@ class RunCommand(Verb):
   This means anything in the {{mesos.*}} and {{thermos.*}} namespaces.
   """
 
-  def setup_options_parser(self, parser):
-    parser.add_argument('--threads', '-t', type=int, default=1, dest='num_threads',
-        help='Number of threads to use')
-    self.add_option(parser, SSH_USER_OPTION)
-    self.add_option(parser, EXECUTOR_SANDBOX_OPTION)
-    self.add_option(parser, JOBSPEC_ARGUMENT)
-    parser.add_argument('cmd', type=str)
+  def get_options(self):
+    return [
+        CommandOption('--threads', '-t', type=int, default=1, dest='num_threads',
+            help='Number of threads to use'),
+        SSH_USER_OPTION,
+        EXECUTOR_SANDBOX_OPTION,
+        JOBSPEC_ARGUMENT,
+        CommandOption('cmd', type=str)
+    ]
 
   def execute(self, context):
     # TODO(mchucarroll): add options to specify which instances to run on (AURORA-198)
@@ -116,15 +119,17 @@ class SshCommand(Verb):
   Initiate an SSH session on the machine that a task instance is running on.
   """
 
-  def setup_options_parser(self, parser):
-    self.add_option(parser, SSH_USER_OPTION)
-    self.add_option(parser, EXECUTOR_SANDBOX_OPTION)
-    parser.add_argument('--tunnels', '-L', dest='tunnels', action='append', metavar='PORT:NAME',
-        default=[],
-        help="Add tunnel from local port PART to remote named port NAME")
-    parser.add_argument('--command', '-c', dest='command', type=str, default=None,
-        help="Command to execute through the ssh connection.")
-    self.add_option(parser, TASK_INSTANCE_ARGUMENT)
+  def get_options(self):
+    return [
+        SSH_USER_OPTION,
+        EXECUTOR_SANDBOX_OPTION,
+        CommandOption('--tunnels', '-L', dest='tunnels', action='append', metavar='PORT:NAME',
+            default=[],
+            help="Add tunnel from local port PART to remote named port NAME"),
+        CommandOption('--command', '-c', dest='command', type=str, default=None,
+            help="Command to execute through the ssh connection."),
+        TASK_INSTANCE_ARGUMENT
+    ]
 
   def execute(self, context):
     (cluster, role, env, name) = context.options.task_instance.jobkey

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/test/python/apache/aurora/client/cli/BUILD
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/BUILD b/src/test/python/apache/aurora/client/cli/BUILD
index 13a3d09..f9e6749 100644
--- a/src/test/python/apache/aurora/client/cli/BUILD
+++ b/src/test/python/apache/aurora/client/cli/BUILD
@@ -35,6 +35,17 @@ python_library(
 )
 
 python_tests(
+  name = 'help',
+  sources = [ 'test_help.py' ],
+  dependencies = [
+    pants('3rdparty/python:mock'),
+    pants('3rdparty/python:twitter.common.contextutil'),
+    pants('src/main/python/apache/aurora/client/cli'),
+    pants('src/main/python/apache/aurora/client/cli:client'),
+  ]
+)
+
+python_tests(
   name = 'bridge',
   sources = [ 'test_bridge.py' ],
   dependencies = [

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/811fd018/src/test/python/apache/aurora/client/cli/test_help.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_help.py b/src/test/python/apache/aurora/client/cli/test_help.py
new file mode 100644
index 0000000..6876bb7
--- /dev/null
+++ b/src/test/python/apache/aurora/client/cli/test_help.py
@@ -0,0 +1,94 @@
+#
+# Copyright 2013 Apache Software Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import contextlib
+import unittest
+
+from apache.aurora.client.cli import EXIT_INVALID_PARAMETER, EXIT_OK
+from apache.aurora.client.cli.client import AuroraCommandLine
+
+from mock import patch
+
+
+class TestHelp(unittest.TestCase):
+  """Tests of the help command for the Aurora v2 client framework"""
+
+  def setUp(self):
+    self.cmd = AuroraCommandLine()
+    self.transcript = []
+    self.err_transcript = []
+
+  def mock_print(self, str):
+    for str in str.split('\n'):
+      self.transcript.append(str)
+
+  def mock_print_err(self, str):
+    for str in str.split('\n'):
+      self.err_transcript.append(str)
+
+  def test_help(self):
+    with patch('apache.aurora.client.cli.client.AuroraCommandLine.print_out',
+        side_effect=self.mock_print):
+      self.cmd.execute(['help'])
+      assert len(self.transcript) > 10
+      assert self.transcript[0] == 'Usage:'
+      assert '==Commands for jobs' in self.transcript
+      assert '==Commands for quotas' in self.transcript
+
+  def test_help_noun(self):
+    with patch('apache.aurora.client.cli.client.AuroraCommandLine.print_out',
+        side_effect=self.mock_print):
+      self.cmd.execute(['help', 'job'])
+      assert len(self.transcript) > 10
+      assert self.transcript[0] == 'Usage for noun "job":' in self.transcript
+      assert not any('quota' in t for t in self.transcript)
+      assert any('job status' in t for t in self.transcript)
+      assert any('job list' in t for t in self.transcript)
+
+  def test_help_verb(self):
+    with patch('apache.aurora.client.cli.client.AuroraCommandLine.print_out',
+        side_effect=self.mock_print):
+      assert self.cmd.execute(['help', 'job', 'status']) == EXIT_OK
+      assert len(self.transcript) > 5
+      assert self.transcript[0] == 'Usage for verb "job status":' in self.transcript
+      assert not any('quota' in t for t in self.transcript)
+      assert not any('list' in t for t in self.transcript)
+      assert "Options:" in self.transcript
+      assert any('status' for t in self.transcript)
+
+  def test_help_unknown_noun(self):
+    with contextlib.nested(
+        patch('apache.aurora.client.cli.client.AuroraCommandLine.print_out',
+            side_effect=self.mock_print),
+        patch('apache.aurora.client.cli.client.AuroraCommandLine.print_err',
+            side_effect=self.mock_print_err)):
+      assert self.cmd.execute(['help', 'nothing']) == EXIT_INVALID_PARAMETER
+      assert len(self.transcript) == 0
+      assert len(self.err_transcript) == 2
+      assert 'Unknown noun "nothing"' == self.err_transcript[0]
+      assert "Valid nouns" in self.err_transcript[1]
+
+  def test_help_unknown_verb(self):
+    with contextlib.nested(
+        patch('apache.aurora.client.cli.client.AuroraCommandLine.print_out',
+            side_effect=self.mock_print),
+        patch('apache.aurora.client.cli.client.AuroraCommandLine.print_err',
+            side_effect=self.mock_print_err)):
+      assert self.cmd.execute(['help', 'job', 'nothing']) == EXIT_INVALID_PARAMETER
+      assert len(self.transcript) == 0
+      assert len(self.err_transcript) == 2
+      assert 'Noun "job" does not support a verb "nothing"' == self.err_transcript[0]
+      assert 'Valid verbs for "job" are' in self.err_transcript[1]


Mime
View raw message