cwctl.py
changeset 0 b97547f5f1fa
child 151 343e7a18675d
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """%%prog %s [options] %s
       
     2 
       
     3 CubicWeb main applications controller. 
       
     4 %s"""
       
     5 
       
     6 import sys
       
     7 from os import remove, listdir, system, kill, getpgid
       
     8 from os.path import exists, join, isfile, isdir
       
     9 
       
    10 from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
       
    11 from cubicweb.cwconfig import CubicWebConfiguration, CONFIGURATIONS
       
    12 from cubicweb.toolsutils import (Command, register_commands, main_run, 
       
    13                               rm, create_dir, pop_arg, confirm)
       
    14     
       
    15 def wait_process_end(pid, maxtry=10, waittime=1):
       
    16     """wait for a process to actually die"""
       
    17     import signal
       
    18     from time import sleep
       
    19     nbtry = 0
       
    20     while nbtry < maxtry:
       
    21         try:
       
    22             kill(pid, signal.SIGUSR1)
       
    23         except OSError:
       
    24             break
       
    25         nbtry += 1
       
    26         sleep(waittime)
       
    27     else:
       
    28         raise ExecutionError('can\'t kill process %s' % pid)
       
    29 
       
    30 def list_instances(regdir):
       
    31     return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
       
    32 
       
    33 def detect_available_modes(templdir):
       
    34     modes = []
       
    35     for fname in ('schema', 'schema.py'):
       
    36         if exists(join(templdir, fname)):
       
    37             modes.append('repository')
       
    38             break
       
    39     for fname in ('data', 'views', 'views.py'):
       
    40         if exists(join(templdir, fname)):
       
    41             modes.append('web ui')
       
    42             break
       
    43     return modes
       
    44     
       
    45     
       
    46 class ApplicationCommand(Command):
       
    47     """base class for command taking 0 to n application id as arguments
       
    48     (0 meaning all registered applications)
       
    49     """
       
    50     arguments = '[<application>...]'    
       
    51     options = (
       
    52         ("force",
       
    53          {'short': 'f', 'action' : 'store_true',
       
    54           'default': False,
       
    55           'help': 'force command without asking confirmation',
       
    56           }
       
    57          ),
       
    58         )
       
    59     actionverb = None
       
    60     
       
    61     def ordered_instances(self):
       
    62         """return instances in the order in which they should be started,
       
    63         considering $REGISTRY_DIR/startorder file if it exists (useful when
       
    64         some instances depends on another as external source
       
    65         """
       
    66         regdir = CubicWebConfiguration.registry_dir()
       
    67         _allinstances = list_instances(regdir)
       
    68         if isfile(join(regdir, 'startorder')):
       
    69             allinstances = []
       
    70             for line in file(join(regdir, 'startorder')):
       
    71                 line = line.strip()
       
    72                 if line and not line.startswith('#'):
       
    73                     try:
       
    74                         _allinstances.remove(line)
       
    75                         allinstances.append(line)
       
    76                     except ValueError:
       
    77                         print 'ERROR: startorder file contains unexistant instance %s' % line
       
    78             allinstances += _allinstances
       
    79         else:
       
    80             allinstances = _allinstances
       
    81         return allinstances
       
    82     
       
    83     def run(self, args):
       
    84         """run the <command>_method on each argument (a list of application
       
    85         identifiers)
       
    86         """
       
    87         if not args:
       
    88             args = self.ordered_instances()
       
    89             try:
       
    90                 askconfirm = not self.config.force
       
    91             except AttributeError:
       
    92                 # no force option
       
    93                 askconfirm = False
       
    94         else:
       
    95             askconfirm = False
       
    96         self.run_args(args, askconfirm)
       
    97         
       
    98     def run_args(self, args, askconfirm):
       
    99         for appid in args:
       
   100             if askconfirm:
       
   101                 print '*'*72
       
   102                 if not confirm('%s application %r ?' % (self.name, appid)):
       
   103                     continue
       
   104             self.run_arg(appid)
       
   105             
       
   106     def run_arg(self, appid):
       
   107         cmdmeth = getattr(self, '%s_application' % self.name)
       
   108         try:
       
   109             cmdmeth(appid)
       
   110         except (KeyboardInterrupt, SystemExit):
       
   111             print >> sys.stderr, '%s aborted' % self.name
       
   112             sys.exit(2) # specific error code
       
   113         except (ExecutionError, ConfigurationError), ex:
       
   114             print >> sys.stderr, 'application %s not %s: %s' % (
       
   115                 appid, self.actionverb, ex)
       
   116         except Exception, ex:
       
   117             import traceback
       
   118             traceback.print_exc()
       
   119             print >> sys.stderr, 'application %s not %s: %s' % (
       
   120                 appid, self.actionverb, ex)
       
   121 
       
   122 
       
   123 class ApplicationCommandFork(ApplicationCommand):
       
   124     """Same as `ApplicationCommand`, but command is forked in a new environment
       
   125     for each argument
       
   126     """
       
   127 
       
   128     def run_args(self, args, askconfirm):
       
   129         if len(args) > 1:
       
   130             forkcmd = ' '.join(w for w in sys.argv if not w in args)
       
   131         else:
       
   132             forkcmd = None
       
   133         for appid in args:
       
   134             if askconfirm:
       
   135                 print '*'*72
       
   136                 if not confirm('%s application %r ?' % (self.name, appid)):
       
   137                     continue
       
   138             if forkcmd:
       
   139                 status = system('%s %s' % (forkcmd, appid))
       
   140                 if status:
       
   141                     sys.exit(status)
       
   142             else:
       
   143                 self.run_arg(appid)
       
   144     
       
   145 # base commands ###############################################################
       
   146 
       
   147 class ListCommand(Command):
       
   148     """List configurations, componants and applications.
       
   149 
       
   150     list available configurations, installed web and server componants, and
       
   151     registered applications
       
   152     """
       
   153     name = 'list'
       
   154     options = (
       
   155         ('verbose',
       
   156          {'short': 'v', 'action' : 'store_true', 
       
   157           'help': "display more information."}),        
       
   158         )
       
   159     
       
   160     def run(self, args):
       
   161         """run the command with its specific arguments"""
       
   162         if args:
       
   163             raise BadCommandUsage('Too much arguments')
       
   164         print 'CubicWeb version:', CubicWebConfiguration.cubicweb_version()
       
   165         print 'Detected mode:', CubicWebConfiguration.mode
       
   166         print
       
   167         print 'Available configurations:'
       
   168         for config in CONFIGURATIONS:
       
   169             print '*', config.name
       
   170             for line in config.__doc__.splitlines():
       
   171                 line = line.strip()
       
   172                 if not line:
       
   173                     continue
       
   174                 print '   ', line
       
   175         print 
       
   176         try:
       
   177             cubesdir = CubicWebConfiguration.cubes_dir()
       
   178             namesize = max(len(x) for x in CubicWebConfiguration.available_cubes())
       
   179         except ConfigurationError, ex:
       
   180             print 'No cubes available:', ex
       
   181         except ValueError:
       
   182             print 'No cubes available in %s' % cubesdir
       
   183         else:
       
   184             print 'Available cubes (%s):' % cubesdir
       
   185             for cube in CubicWebConfiguration.available_cubes():
       
   186                 if cube in ('CVS', '.svn', 'shared', '.hg'):
       
   187                     continue
       
   188                 templdir = join(cubesdir, cube)
       
   189                 try:
       
   190                     tinfo = CubicWebConfiguration.cube_pkginfo(cube)
       
   191                     tversion = tinfo.version
       
   192                 except ConfigurationError:
       
   193                     tinfo = None
       
   194                     tversion = '[missing cube information]'
       
   195                 print '* %s %s' % (cube.ljust(namesize), tversion)
       
   196                 if self.config.verbose:
       
   197                     shortdesc = tinfo and (getattr(tinfo, 'short_desc', '')
       
   198                                            or tinfo.__doc__)
       
   199                     if shortdesc:
       
   200                         print '    '+ '    \n'.join(shortdesc.splitlines())
       
   201                     modes = detect_available_modes(templdir)
       
   202                     print '    available modes: %s' % ', '.join(modes)
       
   203         print
       
   204         try:
       
   205             regdir = CubicWebConfiguration.registry_dir()
       
   206         except ConfigurationError, ex:
       
   207             print 'No application available:', ex
       
   208             print
       
   209             return
       
   210         instances = list_instances(regdir)
       
   211         if instances:
       
   212             print 'Available applications (%s):' % regdir
       
   213             for appid in instances:
       
   214                 modes = CubicWebConfiguration.possible_configurations(appid)
       
   215                 if not modes:
       
   216                     print '* %s (BROKEN application, no configuration found)' % appid
       
   217                     continue
       
   218                 print '* %s (%s)' % (appid, ', '.join(modes))
       
   219                 try:
       
   220                     config = CubicWebConfiguration.config_for(appid, modes[0])
       
   221                 except Exception, exc: 
       
   222                     print '    (BROKEN application, %s)' % exc
       
   223                     continue
       
   224         else:
       
   225             print 'No application available in %s' % regdir
       
   226         print
       
   227 
       
   228 
       
   229 class CreateApplicationCommand(Command):
       
   230     """Create an application from a cube. This is an unified
       
   231     command which can handle web / server / all-in-one installation
       
   232     according to available parts of the software library and of the
       
   233     desired cube.
       
   234 
       
   235     <cube>
       
   236       the name of cube to use (list available cube names using
       
   237       the "list" command). You can use several cubes by separating
       
   238       them using comma (e.g. 'jpl,eemail')
       
   239     <application>
       
   240       an identifier for the application to create
       
   241     """
       
   242     name = 'create'
       
   243     arguments = '<cube> <application>'
       
   244     options = (
       
   245         ("config-level",
       
   246          {'short': 'l', 'type' : 'int', 'metavar': '<level>',
       
   247           'default': 0,
       
   248           'help': 'configuration level (0..2): 0 will ask for essential \
       
   249 configuration parameters only while 2 will ask for all parameters',
       
   250           }
       
   251          ),
       
   252         ("config",
       
   253          {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
       
   254           'choices': ('all-in-one', 'repository', 'twisted'),
       
   255           'default': 'all-in-one',
       
   256           'help': 'installation type, telling which part of an application \
       
   257 should be installed. You can list available configurations using the "list" \
       
   258 command. Default to "all-in-one", e.g. an installation embedding both the RQL \
       
   259 repository and the web server.',
       
   260           }
       
   261          ),
       
   262         )
       
   263     
       
   264     def run(self, args):
       
   265         """run the command with its specific arguments"""
       
   266         from logilab.common.textutils import get_csv
       
   267         configname = self.config.config
       
   268         cubes = get_csv(pop_arg(args, 1))
       
   269         appid = pop_arg(args)
       
   270         # get the configuration and helper
       
   271         CubicWebConfiguration.creating = True
       
   272         config = CubicWebConfiguration.config_for(appid, configname)
       
   273         config.set_language = False
       
   274         config.init_cubes(config.expand_cubes(cubes))
       
   275         helper = self.config_helper(config)
       
   276         # check the cube exists
       
   277         try:
       
   278             templdirs = [CubicWebConfiguration.cube_dir(cube)
       
   279                          for cube in cubes]
       
   280         except ConfigurationError, ex:
       
   281             print ex
       
   282             print '\navailable cubes:',
       
   283             print ', '.join(CubicWebConfiguration.available_cubes())
       
   284             return
       
   285         # create the registry directory for this application
       
   286         create_dir(config.apphome)
       
   287         # load site_cubicweb from the cubes dir (if any)
       
   288         config.load_site_cubicweb()
       
   289         # cubicweb-ctl configuration
       
   290         print '** application\'s %s configuration' % configname
       
   291         print '-' * 72
       
   292         config.input_config('main', self.config.config_level)
       
   293         # configuration'specific stuff
       
   294         print
       
   295         helper.bootstrap(cubes, self.config.config_level)
       
   296         # write down configuration
       
   297         config.save()
       
   298         # handle i18n files structure
       
   299         # XXX currently available languages are guessed from translations found
       
   300         # in the first cube given
       
   301         from cubicweb.common import i18n
       
   302         langs = [lang for lang, _ in i18n.available_catalogs(join(templdirs[0], 'i18n'))]
       
   303         errors = config.i18ncompile(langs)
       
   304         if errors:
       
   305             print '\n'.join(errors)
       
   306             if not confirm('error while compiling message catalogs, '
       
   307                            'continue anyway ?'):
       
   308                 print 'creation not completed'
       
   309                 return
       
   310         # create the additional data directory for this application
       
   311         if config.appdatahome != config.apphome: # true in dev mode
       
   312             create_dir(config.appdatahome)
       
   313         if config['uid']:
       
   314             from logilab.common.shellutils import chown
       
   315             # this directory should be owned by the uid of the server process
       
   316             print 'set %s as owner of the data directory' % config['uid']
       
   317             chown(config.appdatahome, config['uid'])
       
   318         print
       
   319         print
       
   320         print '*' * 72
       
   321         print 'application %s (%s) created in %r' % (appid, configname,
       
   322                                                      config.apphome)
       
   323         print
       
   324         helper.postcreate()
       
   325 
       
   326     
       
   327 class DeleteApplicationCommand(Command):
       
   328     """Delete an application. Will remove application's files and
       
   329     unregister it.
       
   330     """
       
   331     name = 'delete'
       
   332     arguments = '<application>'
       
   333     
       
   334     options = ()
       
   335 
       
   336     def run(self, args):
       
   337         """run the command with its specific arguments"""
       
   338         appid = pop_arg(args, msg="No application specified !")
       
   339         configs = [CubicWebConfiguration.config_for(appid, configname)
       
   340                    for configname in CubicWebConfiguration.possible_configurations(appid)]
       
   341         if not configs:
       
   342             raise ExecutionError('unable to guess configuration for %s' % appid)
       
   343         for config in configs:
       
   344             helper = self.config_helper(config, required=False)
       
   345             if helper:
       
   346                 helper.cleanup()
       
   347         # remove home
       
   348         rm(config.apphome)
       
   349         # remove instance data directory
       
   350         try:
       
   351             rm(config.appdatahome)
       
   352         except OSError, ex:
       
   353             import errno
       
   354             if ex.errno != errno.ENOENT:
       
   355                 raise
       
   356         confignames = ', '.join([config.name for config in configs])
       
   357         print 'application %s (%s) deleted' % (appid, confignames)
       
   358 
       
   359 
       
   360 # application commands ########################################################
       
   361 
       
   362 class StartApplicationCommand(ApplicationCommand):
       
   363     """Start the given applications. If no application is given, start them all.
       
   364     
       
   365     <application>...
       
   366       identifiers of the applications to start. If no application is
       
   367       given, start them all.
       
   368     """
       
   369     name = 'start'
       
   370     actionverb = 'started'
       
   371     options = (
       
   372         ("debug",
       
   373          {'short': 'D', 'action' : 'store_true',
       
   374           'help': 'start server in debug mode.'}),
       
   375         ("force",
       
   376          {'short': 'f', 'action' : 'store_true',
       
   377           'default': False,
       
   378           'help': 'start the application even if it seems to be already \
       
   379 running.'}),
       
   380         ('profile',
       
   381          {'short': 'P', 'type' : 'string', 'metavar': '<stat file>',
       
   382           'default': None,
       
   383           'help': 'profile code and use the specified file to store stats',
       
   384           }),
       
   385         )
       
   386 
       
   387     def start_application(self, appid):
       
   388         """start the application's server"""
       
   389         # use get() since start may be used from other commands (eg upgrade)
       
   390         # without all options defined
       
   391         debug = self.get('debug')
       
   392         force = self.get('force')
       
   393         config = CubicWebConfiguration.config_for(appid)
       
   394         if self.get('profile'):
       
   395             config.global_set_option('profile', self.config.profile)
       
   396         helper = self.config_helper(config, cmdname='start')
       
   397         pidf = config['pid-file']
       
   398         if exists(pidf) and not force:
       
   399             msg = "%s seems to be running. Remove %s by hand if necessary or use \
       
   400 the --force option."
       
   401             raise ExecutionError(msg % (appid, pidf))
       
   402         command = helper.start_command(config, debug)
       
   403         if debug:
       
   404             print "starting server with command :"
       
   405             print command
       
   406         if system(command):
       
   407             print 'an error occured while starting the application, not started'
       
   408             print
       
   409             return False
       
   410         if not debug:
       
   411             print 'application %s started' % appid
       
   412         return True
       
   413 
       
   414 
       
   415 class StopApplicationCommand(ApplicationCommand):
       
   416     """Stop the given applications.
       
   417     
       
   418     <application>...
       
   419       identifiers of the applications to stop. If no application is
       
   420       given, stop them all.
       
   421     """
       
   422     name = 'stop'
       
   423     actionverb = 'stopped'
       
   424     
       
   425     def ordered_instances(self):
       
   426         instances = super(StopApplicationCommand, self).ordered_instances()
       
   427         instances.reverse()
       
   428         return instances
       
   429     
       
   430     def stop_application(self, appid):
       
   431         """stop the application's server"""
       
   432         config = CubicWebConfiguration.config_for(appid)
       
   433         helper = self.config_helper(config, cmdname='stop')
       
   434         helper.poststop() # do this anyway
       
   435         pidf = config['pid-file']
       
   436         if not exists(pidf):
       
   437             print >> sys.stderr, "%s doesn't exist." % pidf
       
   438             return
       
   439         import signal
       
   440         pid = int(open(pidf).read().strip())
       
   441         try:
       
   442             kill(pid, signal.SIGTERM)
       
   443         except:
       
   444             print >> sys.stderr, "process %s seems already dead." % pid
       
   445         else:
       
   446             try:
       
   447                 wait_process_end(pid)
       
   448             except ExecutionError, ex:
       
   449                 print >> sys.stderr, ex
       
   450                 print >> sys.stderr, 'trying SIGKILL'
       
   451                 try:
       
   452                     kill(pid, signal.SIGKILL)
       
   453                 except:
       
   454                     # probably dead now
       
   455                     pass
       
   456                 wait_process_end(pid)
       
   457         try:
       
   458             remove(pidf)
       
   459         except OSError:
       
   460             # already removed by twistd
       
   461             pass
       
   462         print 'application %s stopped' % appid
       
   463     
       
   464 
       
   465 class RestartApplicationCommand(StartApplicationCommand,
       
   466                                 StopApplicationCommand):
       
   467     """Restart the given applications.
       
   468     
       
   469     <application>...
       
   470       identifiers of the applications to restart. If no application is
       
   471       given, restart them all.
       
   472     """
       
   473     name = 'restart'
       
   474     actionverb = 'restarted'
       
   475 
       
   476     def run_args(self, args, askconfirm):
       
   477         regdir = CubicWebConfiguration.registry_dir()
       
   478         if not isfile(join(regdir, 'startorder')) or len(args) <= 1:
       
   479             # no specific startorder
       
   480             super(RestartApplicationCommand, self).run_args(args, askconfirm)
       
   481             return
       
   482         print ('some specific start order is specified, will first stop all '
       
   483                'applications then restart them.')
       
   484         # get instances in startorder
       
   485         stopped = []
       
   486         for appid in args:
       
   487             if askconfirm:
       
   488                 print '*'*72
       
   489                 if not confirm('%s application %r ?' % (self.name, appid)):
       
   490                     continue
       
   491             self.stop_application(appid)
       
   492             stopped.append(appid)
       
   493         forkcmd = [w for w in sys.argv if not w in args]
       
   494         forkcmd[1] = 'start'
       
   495         forkcmd = ' '.join(forkcmd)
       
   496         for appid in reversed(args):
       
   497             status = system('%s %s' % (forkcmd, appid))
       
   498             if status:
       
   499                 sys.exit(status)
       
   500     
       
   501     def restart_application(self, appid):
       
   502         self.stop_application(appid)
       
   503         if self.start_application(appid):
       
   504             print 'application %s %s' % (appid, self.actionverb)
       
   505 
       
   506         
       
   507 class ReloadConfigurationCommand(RestartApplicationCommand):
       
   508     """Reload the given applications. This command is equivalent to a
       
   509     restart for now.
       
   510     
       
   511     <application>...
       
   512       identifiers of the applications to reload. If no application is
       
   513       given, reload them all.
       
   514     """
       
   515     name = 'reload'
       
   516     
       
   517     def reload_application(self, appid):
       
   518         self.restart_application(appid)
       
   519     
       
   520 
       
   521 class StatusCommand(ApplicationCommand):
       
   522     """Display status information about the given applications.
       
   523     
       
   524     <application>...
       
   525       identifiers of the applications to status. If no application is
       
   526       given, get status information about all registered applications.
       
   527     """
       
   528     name = 'status'
       
   529     options = ()
       
   530 
       
   531     def status_application(self, appid):
       
   532         """print running status information for an application"""
       
   533         for mode in CubicWebConfiguration.possible_configurations(appid):
       
   534             config = CubicWebConfiguration.config_for(appid, mode)
       
   535             print '[%s-%s]' % (appid, mode),
       
   536             try:
       
   537                 pidf = config['pid-file']
       
   538             except KeyError:
       
   539                 print 'buggy application, pid file not specified'
       
   540                 continue
       
   541             if not exists(pidf):
       
   542                 print "doesn't seem to be running"
       
   543                 continue
       
   544             pid = int(open(pidf).read().strip())
       
   545             # trick to guess whether or not the process is running
       
   546             try:
       
   547                 getpgid(pid)
       
   548             except OSError:
       
   549                 print "should be running with pid %s but the process can not be found" % pid
       
   550                 continue
       
   551             print "running with pid %s" % (pid)
       
   552 
       
   553 
       
   554 class UpgradeApplicationCommand(ApplicationCommandFork,
       
   555                                 StartApplicationCommand,
       
   556                                 StopApplicationCommand):
       
   557     """Upgrade an application after cubicweb and/or component(s) upgrade.
       
   558 
       
   559     For repository update, you will be prompted for a login / password to use
       
   560     to connect to the system database.  For some upgrades, the given user
       
   561     should have create or alter table permissions.
       
   562 
       
   563     <application>...
       
   564       identifiers of the applications to upgrade. If no application is
       
   565       given, upgrade them all.
       
   566     """
       
   567     name = 'upgrade'
       
   568     actionverb = 'upgraded'
       
   569     options = ApplicationCommand.options + (
       
   570         ('force-componant-version',
       
   571          {'short': 't', 'type' : 'csv', 'metavar': 'cube1=X.Y.Z,cube2=X.Y.Z',
       
   572           'default': None,
       
   573           'help': 'force migration from the indicated  version for the specified cube.'}),
       
   574         ('force-cubicweb-version',
       
   575          {'short': 'e', 'type' : 'string', 'metavar': 'X.Y.Z',
       
   576           'default': None,
       
   577           'help': 'force migration from the indicated cubicweb version.'}),
       
   578         
       
   579         ('fs-only',
       
   580          {'short': 's', 'action' : 'store_true',
       
   581           'default': False,
       
   582           'help': 'only upgrade files on the file system, not the database.'}),
       
   583 
       
   584         ('nostartstop',
       
   585          {'short': 'n', 'action' : 'store_true',
       
   586           'default': False,
       
   587           'help': 'don\'t try to stop application before migration and to restart it after.'}),
       
   588         
       
   589         ('verbosity',
       
   590          {'short': 'v', 'type' : 'int', 'metavar': '<0..2>',
       
   591           'default': 1,
       
   592           'help': "0: no confirmation, 1: only main commands confirmed, 2 ask \
       
   593 for everything."}),
       
   594         
       
   595         ('backup-db',
       
   596          {'short': 'b', 'type' : 'yn', 'metavar': '<y or n>',
       
   597           'default': None,
       
   598           'help': "Backup the application database before upgrade.\n"\
       
   599           "If the option is ommitted, confirmation will be ask.",
       
   600           }),
       
   601 
       
   602         ('ext-sources',
       
   603          {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
       
   604           'default': None,
       
   605           'help': "For multisources instances, specify to which sources the \
       
   606 repository should connect to for upgrading. When unspecified or 'migration' is \
       
   607 given, appropriate sources for migration will be automatically selected \
       
   608 (recommended). If 'all' is given, will connect to all defined sources.",
       
   609           }),
       
   610         )
       
   611 
       
   612     def ordered_instances(self):
       
   613         # need this since mro return StopApplicationCommand implementation
       
   614         return ApplicationCommand.ordered_instances(self)
       
   615     
       
   616     def upgrade_application(self, appid):
       
   617         from logilab.common.changelog import Version
       
   618         if not (CubicWebConfiguration.mode == 'dev' or self.config.nostartstop):
       
   619             self.stop_application(appid)
       
   620         config = CubicWebConfiguration.config_for(appid)
       
   621         config.creating = True # notice we're not starting the server
       
   622         config.verbosity = self.config.verbosity
       
   623         config.set_sources_mode(self.config.ext_sources or ('migration',))
       
   624         # get application and installed versions for the server and the componants
       
   625         print 'getting versions configuration from the repository...'
       
   626         mih = config.migration_handler()
       
   627         repo = mih.repo_connect()
       
   628         vcconf = repo.get_versions()
       
   629         print 'done'
       
   630         if self.config.force_componant_version:
       
   631             packversions = {}
       
   632             for vdef in self.config.force_componant_version:
       
   633                 componant, version = vdef.split('=')
       
   634                 packversions[componant] = Version(version)
       
   635             vcconf.update(packversions)
       
   636         toupgrade = []
       
   637         for cube in config.cubes():
       
   638             installedversion = config.cube_version(cube)
       
   639             try:
       
   640                 applversion = vcconf[cube]
       
   641             except KeyError:
       
   642                 config.error('no version information for %s' % cube)
       
   643                 continue
       
   644             if installedversion > applversion:
       
   645                 toupgrade.append( (cube, applversion, installedversion) )
       
   646         cubicwebversion = config.cubicweb_version()           
       
   647         if self.config.force_cubicweb_version:
       
   648             applcubicwebversion = Version(self.config.force_cubicweb_version)
       
   649             vcconf['cubicweb'] = applcubicwebversion
       
   650         else:
       
   651             applcubicwebversion = vcconf.get('cubicweb')
       
   652         if cubicwebversion > applcubicwebversion:
       
   653             toupgrade.append( ('cubicweb', applcubicwebversion, cubicwebversion) )
       
   654         if not self.config.fs_only and not toupgrade:
       
   655             print 'no software migration needed for application %s' % appid
       
   656             return
       
   657         for cube, fromversion, toversion in toupgrade:
       
   658             print '**** %s migration %s -> %s' % (cube, fromversion, toversion)
       
   659         # run cubicweb/componants migration scripts
       
   660         mih.migrate(vcconf, reversed(toupgrade), self.config)
       
   661         # rewrite main configuration file
       
   662         mih.rewrite_configuration()
       
   663         # handle i18n upgrade:
       
   664         # * install new languages
       
   665         # * recompile catalogs
       
   666         # XXX currently available languages are guessed from translations found
       
   667         # in the first componant given
       
   668         from cubicweb.common import i18n
       
   669         templdir = CubicWebConfiguration.cube_dir(config.cubes()[0])
       
   670         langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))]
       
   671         errors = config.i18ncompile(langs)
       
   672         if errors:
       
   673             print '\n'.join(errors)
       
   674             if not confirm('error while compiling message catalogs, '
       
   675                            'continue anyway ?'):
       
   676                 print 'migration not completed'
       
   677                 return
       
   678         mih.rewrite_vcconfiguration()
       
   679         mih.shutdown()
       
   680         print
       
   681         print 'application migrated'
       
   682         if not (CubicWebConfiguration.mode == 'dev' or self.config.nostartstop):
       
   683             self.start_application(appid)
       
   684         print
       
   685 
       
   686 
       
   687 class ShellCommand(Command):
       
   688     """Run an interactive migration shell. This is a python shell with
       
   689     enhanced migration commands predefined in the namespace. An additional
       
   690     argument may be given corresponding to a file containing commands to
       
   691     execute in batch mode.
       
   692 
       
   693     <application>
       
   694       the identifier of the application to connect.
       
   695     """
       
   696     name = 'shell'
       
   697     arguments = '<application> [batch command file]'
       
   698     options = (
       
   699         ('system-only',
       
   700          {'short': 'S', 'action' : 'store_true',
       
   701           'default': False,
       
   702           'help': 'only connect to the system source when the instance is '
       
   703           'using multiple sources. You can\'t use this option and the '
       
   704           '--ext-sources option at the same time.'}),
       
   705         
       
   706         ('ext-sources',
       
   707          {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
       
   708           'default': None,
       
   709           'help': "For multisources instances, specify to which sources the \
       
   710 repository should connect to for upgrading. When unspecified or 'all' given, \
       
   711 will connect to all defined sources. If 'migration' is given, appropriate \
       
   712 sources for migration will be automatically selected.",
       
   713           }),
       
   714         
       
   715         )
       
   716     def run(self, args):
       
   717         appid = pop_arg(args, 99, msg="No application specified !")
       
   718         config = CubicWebConfiguration.config_for(appid)
       
   719         if self.config.ext_sources:
       
   720             assert not self.config.system_only
       
   721             sources = self.config.ext_sources
       
   722         elif self.config.system_only:
       
   723             sources = ('system',)
       
   724         else:
       
   725             sources = ('all',)
       
   726         config.set_sources_mode(sources)
       
   727         mih = config.migration_handler()
       
   728         if args:
       
   729             mih.scripts_session(args)
       
   730         else:
       
   731             mih.interactive_shell()
       
   732         mih.shutdown() 
       
   733 
       
   734 
       
   735 class RecompileApplicationCatalogsCommand(ApplicationCommand):
       
   736     """Recompile i18n catalogs for applications.
       
   737     
       
   738     <application>...
       
   739       identifiers of the applications to consider. If no application is
       
   740       given, recompile for all registered applications.
       
   741     """
       
   742     name = 'i18ncompile'
       
   743     
       
   744     def i18ncompile_application(self, appid):
       
   745         """recompile application's messages catalogs"""
       
   746         config = CubicWebConfiguration.config_for(appid)
       
   747         try:
       
   748             config.bootstrap_cubes()
       
   749         except IOError, ex:
       
   750             import errno
       
   751             if ex.errno != errno.ENOENT:
       
   752                 raise
       
   753             # bootstrap_cubes files doesn't exist
       
   754             # set creating to notify this is not a regular start
       
   755             config.creating = True
       
   756             # create an in-memory repository, will call config.init_cubes()
       
   757             config.repository()
       
   758         except AttributeError:
       
   759             # web only config
       
   760             config.init_cubes(config.repository().get_cubes())
       
   761         errors = config.i18ncompile()
       
   762         if errors:
       
   763             print '\n'.join(errors)
       
   764 
       
   765 
       
   766 class ListInstancesCommand(Command):
       
   767     """list available instances, useful for bash completion."""
       
   768     name = 'listinstances'
       
   769     hidden = True
       
   770     
       
   771     def run(self, args):
       
   772         """run the command with its specific arguments"""
       
   773         regdir = CubicWebConfiguration.registry_dir()
       
   774         for appid in sorted(listdir(regdir)):
       
   775             print appid
       
   776 
       
   777 
       
   778 class ListCubesCommand(Command):
       
   779     """list available componants, useful for bash completion."""
       
   780     name = 'listcubes'
       
   781     hidden = True
       
   782     
       
   783     def run(self, args):
       
   784         """run the command with its specific arguments"""
       
   785         for cube in CubicWebConfiguration.available_cubes():
       
   786             print cube
       
   787 
       
   788 register_commands((ListCommand,
       
   789                    CreateApplicationCommand,
       
   790                    DeleteApplicationCommand,
       
   791                    StartApplicationCommand,
       
   792                    StopApplicationCommand,
       
   793                    RestartApplicationCommand,
       
   794                    ReloadConfigurationCommand,
       
   795                    StatusCommand,
       
   796                    UpgradeApplicationCommand,
       
   797                    ShellCommand,
       
   798                    RecompileApplicationCatalogsCommand,
       
   799                    ListInstancesCommand, ListCubesCommand,
       
   800                    ))
       
   801 
       
   802                 
       
   803 def run(args):
       
   804     """command line tool"""
       
   805     CubicWebConfiguration.load_cwctl_plugins()
       
   806     main_run(args, __doc__)
       
   807 
       
   808 if __name__ == '__main__':
       
   809     run(sys.argv[1:])