starcluster.cli

Package starcluster :: Module cli |
[hide private]
[frames] | no frames]

# Source Code for Module starcluster.cli

      1  #!/usr/bin/env python
      2  """
      3  StarCluster Command Line Interface:
      4
      5  starcluster [global-opts] action [action-opts] [<action-args> ...]
      6  """
      7  import os
      8  import sys
      9  import shlex
     10  import socket
     11  import optparse
     12  import platform
     13  import traceback
     14
     15  from boto.exception import BotoServerError, EC2ResponseError, S3ResponseError
     16
     17  from starcluster import config
     18  from starcluster import static
     19  from starcluster import logger
     20  from starcluster import commands
     21  from starcluster import exception
     22  from starcluster import optcomplete
     23  from starcluster.logger import log, console, session
     24  from starcluster import __version__
     25
     26  __description__ = """
     27  StarCluster - (http://web.mit.edu/starcluster) (v. %s)
     28  Software Tools for Academics and Researchers (STAR)
     29  Please submit bug reports to starcluster@mit.edu
     30  """ % __version__
     31
     32
     33 -class StarClusterCLI(object):
     34      """
     35      StarCluster Command Line Interface
     36      """
     37 -    def __init__(self):
     38          self._gparser = None
     39          self.subcmds_map = {}
     40
     41      @property
     42 -    def gparser(self):
     43          if not self._gparser:
     44              self._gparser = self.create_global_parser()
     45          return self._gparser
     46
     47 -    def print_header(self):
     48          print __description__.replace('\n', '', 1)
     49
     50 -    def parse_subcommands(self, gparser=None):
     51          """
     52          Parse global arguments, find subcommand from list of subcommand
     53          objects, parse local subcommand arguments and return a tuple of
     54          global options, selected command object, command options, and
     55          command arguments.
     56
     57          Call execute() on the command object to run. The command object has
     58          members 'gopts' and 'opts' set for global and command options
     59          respectively, you don't need to call execute with those but you could
     60          if you wanted to.
     61          """
     62          gparser = gparser or self.gparser
     63          # parse global options.
     64          gopts, args = gparser.parse_args()
     65          if not args:
     66              gparser.print_help()
     67              raise SystemExit("\nError: you must specify an action.")
     68          # set debug level if specified
     69          if gopts.DEBUG:
     70              console.setLevel(logger.DEBUG)
     71          # load StarClusterConfig into global options
     72          try:
     73              cfg = config.StarClusterConfig(gopts.CONFIG)
     74              cfg.load()
     75          except exception.ConfigNotFound, e:
     76              log.error(e.msg)
     77              e.display_options()
     78              sys.exit(1)
     79          except exception.ConfigError, e:
     80              log.error(e.msg)
     81              sys.exit(1)
     82          gopts.CONFIG = cfg
     83          # Parse command arguments and invoke command.
     84          subcmdname, subargs = args[0], args[1:]
     85          try:
     86              sc = self.subcmds_map[subcmdname]
     87              lparser = optparse.OptionParser(sc.__doc__.strip())
     88              sc.gopts = gopts
     89              sc.parser = lparser
     90              sc.gparser = gparser
     91              sc.subcmds_map = self.subcmds_map
     92              sc.addopts(lparser)
     93              sc.opts, subsubargs = lparser.parse_args(subargs)
     94          except KeyError:
     95              raise SystemExit("Error: invalid command '%s'" % subcmdname)
     96          return gopts, sc, sc.opts, subsubargs
     97
     98 -    def create_global_parser(self, subcmds=None, no_usage=False,
     99                               add_help=True):
    100          if no_usage:
    101              gparser = optparse.OptionParser(usage=optparse.SUPPRESS_USAGE,
    102                                              add_help_option=add_help)
    103          else:
    104              gparser = optparse.OptionParser(__doc__.strip(),
    105                                              version=__version__,
    106                                              add_help_option=add_help)
    107              # Build map of name -> command and docstring.
    108              cmds_header = 'Available Commands:'
    109              gparser.usage += '\n\n%s\n' % cmds_header
    110              gparser.usage += '%s\n' % ('-' * len(cmds_header))
    111              gparser.usage += "NOTE: Pass --help to any command for a list of "
    112              gparser.usage += 'its options and detailed usage information\n\n'
    113              subcmds = subcmds or commands.all_cmds
    114              for sc in subcmds:
    115                  helptxt = sc.__doc__.splitlines()[3].strip()
    116                  gparser.usage += '- %s: %s\n' % (', '.join(sc.names), helptxt)
    117                  for n in sc.names:
    118                      assert n not in self.subcmds_map
    119                      self.subcmds_map[n] = sc
    120          gparser.add_option("-d", "--debug", dest="DEBUG",
    121                             action="store_true", default=False,
    122                             help="print debug messages (useful for "
    123                             "diagnosing problems)")
    124          gparser.add_option("-c", "--config", dest="CONFIG", action="store",
    125                             metavar="FILE",
    126                             help="use alternate config file (default: %s)" %
    127                             static.STARCLUSTER_CFG_FILE)
    128          gparser.add_option("-r", "--region", dest="REGION", action="store",
    129                             help="specify a region to use (default: us-east-1)")
    130          gparser.disable_interspersed_args()
    131          return gparser
    132
    133 -    def __write_module_version(self, modname, fp):
    134          """
    135          Write module version information to a file
    136          """
    137          try:
    138              mod = __import__(modname)
    139              fp.write("%s: %s\n" % (mod.__name__, mod.__version__))
    140          except Exception, e:
    141              print "error getting version for '%s' module: %s" % (modname, e)
    142
    143 -    def bug_found(self):
    144          """
    145          Builds a crash-report when StarCluster encounters an unhandled
    146          exception. Report includes system info, python version, dependency
    147          versions, and a full debug log and stack-trace of the crash.
    148          """
    149          dashes = '-' * 10
    150          header = dashes + ' %s ' + dashes + '\n'
    151          crashfile = open(static.CRASH_FILE, 'w')
    152          crashfile.write(header % "CRASH DETAILS")
    153          crashfile.write(session.stream.getvalue())
    154          crashfile.write(header % "SYSTEM INFO")
    155          crashfile.write("StarCluster: %s\n" % __version__)
    156          crashfile.write("Python: %s\n" % sys.version.replace('\n', ' '))
    157          crashfile.write("Platform: %s\n" % platform.platform())
    158          dependencies = ['boto', 'paramiko', 'Crypto', 'jinja2', 'decorator']
    159          for dep in dependencies:
    160              self.__write_module_version(dep, crashfile)
    161          crashfile.close()
    162          log.error("Oops! Looks like you've found a bug in StarCluster")
    163          log.error("Crash report written to: %s" % static.CRASH_FILE)
    164          log.error("Please remove any sensitive data from the crash report")
    165          log.error("and submit it to starcluster@mit.edu")
    166          sys.exit(1)
    167
    168 -    def get_global_opts(self):
    169          """
    170          Parse and return global options. This method will silently return None
    171          if any errors are encountered during parsing.
    172          """
    173          gparser = self.create_global_parser(no_usage=True, add_help=False)
    174          try:
    175              sys.stdout = open(os.devnull, 'w')
    176              sys.stderr = open(os.devnull, 'w')
    177              gopts, _ = gparser.parse_args()
    178              return gopts
    179          except SystemExit:
    180              pass
    181          finally:
    182              sys.stdout = sys.__stdout__
    183              sys.stderr = sys.__stderr__
    184
    185 -    def is_completion_active(self):
    186          return 'OPTPARSE_AUTO_COMPLETE' in os.environ
    187
    188 -    def _init_completion(self):
    189          """
    190          Restore original sys.argv from COMP_LINE in the case that starcluster
    191          is being called by Bash/ZSH for completion options. Bash/ZSH will
    192          simply call 'starcluster' with COMP_LINE environment variable set to
    193          the current (partial) argv for completion.
    194
    195          StarCluster's Bash/ZSH completion code needs to read the global config
    196          option in case an alternate config is specified at the command line
    197          when completing options. StarCluster's comletion code uses the config
    198          to generate completion options. Setting sys.argv to $COMP_LINE in this
    199          case allows the global option parser to be used to extract the global
    200          -c option (if specified) and load the proper config in the completion
    201          code.
    202          """
    203          if 'COMP_LINE' in os.environ:
    204              newargv = shlex.split(os.environ.get('COMP_LINE'))
    205              for i, arg in enumerate(newargv):
    206                  arg = os.path.expanduser(arg)
    207                  newargv[i] = os.path.expandvars(arg)
    208              sys.argv = newargv
    209
    210 -    def handle_completion(self):
    211          if self.is_completion_active():
    212              gparser = self.create_global_parser(no_usage=True, add_help=False)
    213              # set sys.path to COMP_LINE if it exists
    214              self._init_completion()
    215              # fetch the global options
    216              gopts = self.get_global_opts()
    217              # try to load StarClusterConfig into global options
    218              if gopts:
    219                  try:
    220                      cfg = config.StarClusterConfig(gopts.CONFIG)
    221                      cfg.load()
    222                  except exception.ConfigError:
    223                      cfg = None
    224                  gopts.CONFIG = cfg
    225              scmap = {}
    226              for sc in commands.all_cmds:
    227                  sc.gopts = gopts
    228                  for n in sc.names:
    229                      scmap[n] = sc
    230              listcter = optcomplete.ListCompleter(scmap.keys())
    231              subcter = optcomplete.NoneCompleter()
    232              optcomplete.autocomplete(gparser, listcter, None, subcter,
    233                                       subcommands=scmap)
    234              sys.exit(1)
    235
    236 -    def main(self):
    237          """
    238          StarCluster main
    239          """
    240          # Handle Bash/ZSH completion if necessary
    241          self.handle_completion()
    242          # Show StarCluster header
    243          self.print_header()
    244          # Parse subcommand options and args
    245          gopts, sc, opts, args = self.parse_subcommands()
    246          if args and args[0] == 'help':
    247              # make 'help' subcommand act like --help option
    248              sc.parser.print_help()
    249              sys.exit(0)
    250          # run the subcommand and handle exceptions
    251          try:
    252              sc.execute(args)
    253          except (EC2ResponseError, S3ResponseError, BotoServerError), e:
    254              log.error("%s: %s" % (e.error_code, e.error_message))
    255              sys.exit(1)
    256          except socket.error, e:
    257              log.error("Unable to connect: %s" % e)
    258              log.error("Check your internet connection?")
    259              sys.exit(1)
    260          except exception.ThreadPoolException, e:
    261              if not gopts.DEBUG:
    262                  e.print_excs()
    263              log.debug(e.format_excs())
    264              print
    265              self.bug_found()
    266          except exception.ClusterDoesNotExist, e:
    267              cm = gopts.CONFIG.get_cluster_manager()
    268              cls = cm.get_clusters()
    269              log.error(e.msg)
    270              if cls:
    271                  taglist = ', '.join([c.cluster_tag for c in cls])
    272                  active_clusters = "(active clusters: %s)" % taglist
    273                  log.error(active_clusters)
    274              sys.exit(1)
    275          except exception.BaseException, e:
    276              log.error(e.msg, extra={'__textwrap__': True})
    277              sys.exit(1)
    278          except SystemExit, e:
    279              # re-raise SystemExit to avoid the bug-catcher below
    280              raise e
    281          except Exception, e:
    282              if not gopts.DEBUG:
    283                  traceback.print_exc()
    284              log.debug(traceback.format_exc())
    285              print
    286              self.bug_found()
    287
    288
    289 -def warn_debug_file_moved():
    290      old_file = os.path.join(static.TMP_DIR, 'starcluster-debug-%s.log' %
    291                              static.CURRENT_USER)
    292      if os.path.exists(old_file):
    293          stars = '*' * 50
    294          log.warn(stars)
    295          log.warn("The default log file location is now:")
    296          log.warn("")
    297          log.warn(static.DEBUG_FILE)
    298          log.warn("")
    299          log.warn("Please delete or move the old log file located at:")
    300          log.warn("")
    301          log.warn(old_file)
    302          log.warn(stars)
    303
    304
    305 -def main():
    306      static.create_sc_config_dirs()
    307      logger.configure_sc_logging()
    308      warn_debug_file_moved()
    309      StarClusterCLI().main()
    310
    311  if __name__ == '__main__':
    312      try:
    313          main()
    314      except KeyboardInterrupt:
    315          print "Interrupted, exiting."
    316