cwctl.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """the cubicweb-ctl tool, based on logilab.common.clcommands to
       
    19 provide a pluggable commands system.
       
    20 """
       
    21 from __future__ import print_function
       
    22 
       
    23 __docformat__ = "restructuredtext en"
       
    24 
       
    25 # *ctl module should limit the number of import to be imported as quickly as
       
    26 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
       
    27 # completion). So import locally in command helpers.
       
    28 import sys
       
    29 from warnings import warn, filterwarnings
       
    30 from os import remove, listdir, system, pathsep
       
    31 from os.path import exists, join, isfile, isdir, dirname, abspath
       
    32 
       
    33 try:
       
    34     from os import kill, getpgid
       
    35 except ImportError:
       
    36     def kill(*args):
       
    37         """win32 kill implementation"""
       
    38     def getpgid():
       
    39         """win32 getpgid implementation"""
       
    40 
       
    41 from six.moves.urllib.parse import urlparse
       
    42 
       
    43 from logilab.common.clcommands import CommandLine
       
    44 from logilab.common.shellutils import ASK
       
    45 from logilab.common.configuration import merge_options
       
    46 
       
    47 from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
       
    48 from cubicweb.utils import support_args
       
    49 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
       
    50 from cubicweb.toolsutils import Command, rm, create_dir, underline_title
       
    51 from cubicweb.__pkginfo__ import version
       
    52 
       
    53 # don't check duplicated commands, it occurs when reloading site_cubicweb
       
    54 CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
       
    55                     version=version, check_duplicated_command=False)
       
    56 
       
    57 def wait_process_end(pid, maxtry=10, waittime=1):
       
    58     """wait for a process to actually die"""
       
    59     import signal
       
    60     from time import sleep
       
    61     nbtry = 0
       
    62     while nbtry < maxtry:
       
    63         try:
       
    64             kill(pid, signal.SIGUSR1)
       
    65         except (OSError, AttributeError): # XXX win32
       
    66             break
       
    67         nbtry += 1
       
    68         sleep(waittime)
       
    69     else:
       
    70         raise ExecutionError('can\'t kill process %s' % pid)
       
    71 
       
    72 def list_instances(regdir):
       
    73     if isdir(regdir):
       
    74         return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
       
    75     else:
       
    76         return []
       
    77 
       
    78 def detect_available_modes(templdir):
       
    79     modes = []
       
    80     for fname in ('schema', 'schema.py'):
       
    81         if exists(join(templdir, fname)):
       
    82             modes.append('repository')
       
    83             break
       
    84     for fname in ('data', 'views', 'views.py'):
       
    85         if exists(join(templdir, fname)):
       
    86             modes.append('web ui')
       
    87             break
       
    88     return modes
       
    89 
       
    90 
       
    91 class InstanceCommand(Command):
       
    92     """base class for command taking 0 to n instance id as arguments
       
    93     (0 meaning all registered instances)
       
    94     """
       
    95     arguments = '[<instance>...]'
       
    96     options = (
       
    97         ("force",
       
    98          {'short': 'f', 'action' : 'store_true',
       
    99           'default': False,
       
   100           'help': 'force command without asking confirmation',
       
   101           }
       
   102          ),
       
   103         )
       
   104     actionverb = None
       
   105 
       
   106     def ordered_instances(self):
       
   107         """return instances in the order in which they should be started,
       
   108         considering $REGISTRY_DIR/startorder file if it exists (useful when
       
   109         some instances depends on another as external source).
       
   110 
       
   111         Instance used by another one should appears first in the file (one
       
   112         instance per line)
       
   113         """
       
   114         regdir = cwcfg.instances_dir()
       
   115         _allinstances = list_instances(regdir)
       
   116         if isfile(join(regdir, 'startorder')):
       
   117             allinstances = []
       
   118             for line in open(join(regdir, 'startorder')):
       
   119                 line = line.strip()
       
   120                 if line and not line.startswith('#'):
       
   121                     try:
       
   122                         _allinstances.remove(line)
       
   123                         allinstances.append(line)
       
   124                     except ValueError:
       
   125                         print('ERROR: startorder file contains unexistant '
       
   126                               'instance %s' % line)
       
   127             allinstances += _allinstances
       
   128         else:
       
   129             allinstances = _allinstances
       
   130         return allinstances
       
   131 
       
   132     def run(self, args):
       
   133         """run the <command>_method on each argument (a list of instance
       
   134         identifiers)
       
   135         """
       
   136         if not args:
       
   137             args = self.ordered_instances()
       
   138             try:
       
   139                 askconfirm = not self.config.force
       
   140             except AttributeError:
       
   141                 # no force option
       
   142                 askconfirm = False
       
   143         else:
       
   144             askconfirm = False
       
   145         self.run_args(args, askconfirm)
       
   146 
       
   147     def run_args(self, args, askconfirm):
       
   148         status = 0
       
   149         for appid in args:
       
   150             if askconfirm:
       
   151                 print('*'*72)
       
   152                 if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
       
   153                     continue
       
   154             try:
       
   155                 status = max(status, self.run_arg(appid))
       
   156             except (KeyboardInterrupt, SystemExit):
       
   157                 sys.stderr.write('%s aborted\n' % self.name)
       
   158                 return 2 # specific error code
       
   159         sys.exit(status)
       
   160 
       
   161     def run_arg(self, appid):
       
   162         cmdmeth = getattr(self, '%s_instance' % self.name)
       
   163         try:
       
   164             status = cmdmeth(appid)
       
   165         except (ExecutionError, ConfigurationError) as ex:
       
   166             sys.stderr.write('instance %s not %s: %s\n' % (
       
   167                     appid, self.actionverb, ex))
       
   168             status = 4
       
   169         except Exception as ex:
       
   170             import traceback
       
   171             traceback.print_exc()
       
   172             sys.stderr.write('instance %s not %s: %s\n' % (
       
   173                     appid, self.actionverb, ex))
       
   174             status = 8
       
   175         return status
       
   176 
       
   177 class InstanceCommandFork(InstanceCommand):
       
   178     """Same as `InstanceCommand`, but command is forked in a new environment
       
   179     for each argument
       
   180     """
       
   181 
       
   182     def run_args(self, args, askconfirm):
       
   183         if len(args) > 1:
       
   184             forkcmd = ' '.join(w for w in sys.argv if not w in args)
       
   185         else:
       
   186             forkcmd = None
       
   187         for appid in args:
       
   188             if askconfirm:
       
   189                 print('*'*72)
       
   190                 if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
       
   191                     continue
       
   192             if forkcmd:
       
   193                 status = system('%s %s' % (forkcmd, appid))
       
   194                 if status:
       
   195                     print('%s exited with status %s' % (forkcmd, status))
       
   196             else:
       
   197                 self.run_arg(appid)
       
   198 
       
   199 
       
   200 # base commands ###############################################################
       
   201 
       
   202 class ListCommand(Command):
       
   203     """List configurations, cubes and instances.
       
   204 
       
   205     List available configurations, installed cubes, and registered instances.
       
   206 
       
   207     If given, the optional argument allows to restrict listing only a category of items.
       
   208     """
       
   209     name = 'list'
       
   210     arguments = '[all|cubes|configurations|instances]'
       
   211     options = (
       
   212         ('verbose',
       
   213          {'short': 'v', 'action' : 'store_true',
       
   214           'help': "display more information."}),
       
   215         )
       
   216 
       
   217     def run(self, args):
       
   218         """run the command with its specific arguments"""
       
   219         if not args:
       
   220             mode = 'all'
       
   221         elif len(args) == 1:
       
   222             mode = args[0]
       
   223         else:
       
   224             raise BadCommandUsage('Too many arguments')
       
   225 
       
   226         from cubicweb.migration import ConfigurationProblem
       
   227 
       
   228         if mode == 'all':
       
   229             print('CubicWeb %s (%s mode)' % (cwcfg.cubicweb_version(), cwcfg.mode))
       
   230             print()
       
   231 
       
   232         if mode in ('all', 'config', 'configurations'):
       
   233             print('Available configurations:')
       
   234             for config in CONFIGURATIONS:
       
   235                 print('*', config.name)
       
   236                 for line in config.__doc__.splitlines():
       
   237                     line = line.strip()
       
   238                     if not line:
       
   239                         continue
       
   240                     print('   ', line)
       
   241             print()
       
   242 
       
   243         if mode in ('all', 'cubes'):
       
   244             cfgpb = ConfigurationProblem(cwcfg)
       
   245             try:
       
   246                 cubesdir = pathsep.join(cwcfg.cubes_search_path())
       
   247                 namesize = max(len(x) for x in cwcfg.available_cubes())
       
   248             except ConfigurationError as ex:
       
   249                 print('No cubes available:', ex)
       
   250             except ValueError:
       
   251                 print('No cubes available in %s' % cubesdir)
       
   252             else:
       
   253                 print('Available cubes (%s):' % cubesdir)
       
   254                 for cube in cwcfg.available_cubes():
       
   255                     try:
       
   256                         tinfo = cwcfg.cube_pkginfo(cube)
       
   257                         tversion = tinfo.version
       
   258                         cfgpb.add_cube(cube, tversion)
       
   259                     except (ConfigurationError, AttributeError) as ex:
       
   260                         tinfo = None
       
   261                         tversion = '[missing cube information: %s]' % ex
       
   262                     print('* %s %s' % (cube.ljust(namesize), tversion))
       
   263                     if self.config.verbose:
       
   264                         if tinfo:
       
   265                             descr = getattr(tinfo, 'description', '')
       
   266                             if not descr:
       
   267                                 descr = tinfo.__doc__
       
   268                             if descr:
       
   269                                 print('    '+ '    \n'.join(descr.splitlines()))
       
   270                         modes = detect_available_modes(cwcfg.cube_dir(cube))
       
   271                         print('    available modes: %s' % ', '.join(modes))
       
   272             print()
       
   273 
       
   274         if mode in ('all', 'instances'):
       
   275             try:
       
   276                 regdir = cwcfg.instances_dir()
       
   277             except ConfigurationError as ex:
       
   278                 print('No instance available:', ex)
       
   279                 print()
       
   280                 return
       
   281             instances = list_instances(regdir)
       
   282             if instances:
       
   283                 print('Available instances (%s):' % regdir)
       
   284                 for appid in instances:
       
   285                     modes = cwcfg.possible_configurations(appid)
       
   286                     if not modes:
       
   287                         print('* %s (BROKEN instance, no configuration found)' % appid)
       
   288                         continue
       
   289                     print('* %s (%s)' % (appid, ', '.join(modes)))
       
   290                     try:
       
   291                         config = cwcfg.config_for(appid, modes[0])
       
   292                     except Exception as exc:
       
   293                         print('    (BROKEN instance, %s)' % exc)
       
   294                         continue
       
   295             else:
       
   296                 print('No instance available in %s' % regdir)
       
   297             print()
       
   298 
       
   299         if mode == 'all':
       
   300             # configuration management problem solving
       
   301             cfgpb.solve()
       
   302             if cfgpb.warnings:
       
   303                 print('Warnings:\n', '\n'.join('* '+txt for txt in cfgpb.warnings))
       
   304             if cfgpb.errors:
       
   305                 print('Errors:')
       
   306                 for op, cube, version, src in cfgpb.errors:
       
   307                     if op == 'add':
       
   308                         print('* cube', cube, end=' ')
       
   309                         if version:
       
   310                             print(' version', version, end=' ')
       
   311                         print('is not installed, but required by %s' % src)
       
   312                     else:
       
   313                         print('* cube %s version %s is installed, but version %s is required by %s' % (
       
   314                             cube, cfgpb.cubes[cube], version, src))
       
   315 
       
   316 def check_options_consistency(config):
       
   317     if config.automatic and config.config_level > 0:
       
   318         raise BadCommandUsage('--automatic and --config-level should not be '
       
   319                               'used together')
       
   320 
       
   321 class CreateInstanceCommand(Command):
       
   322     """Create an instance from a cube. This is a unified
       
   323     command which can handle web / server / all-in-one installation
       
   324     according to available parts of the software library and of the
       
   325     desired cube.
       
   326 
       
   327     <cube>
       
   328       the name of cube to use (list available cube names using
       
   329       the "list" command). You can use several cubes by separating
       
   330       them using comma (e.g. 'jpl,email')
       
   331     <instance>
       
   332       an identifier for the instance to create
       
   333     """
       
   334     name = 'create'
       
   335     arguments = '<cube> <instance>'
       
   336     min_args = max_args = 2
       
   337     options = (
       
   338         ('automatic',
       
   339          {'short': 'a', 'action' : 'store_true',
       
   340           'default': False,
       
   341           'help': 'automatic mode: never ask and use default answer to every '
       
   342           'question. this may require that your login match a database super '
       
   343           'user (allowed to create database & all).',
       
   344           }),
       
   345         ('config-level',
       
   346          {'short': 'l', 'type' : 'int', 'metavar': '<level>',
       
   347           'default': 0,
       
   348           'help': 'configuration level (0..2): 0 will ask for essential '
       
   349           'configuration parameters only while 2 will ask for all parameters',
       
   350           }),
       
   351         ('config',
       
   352          {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
       
   353           'choices': ('all-in-one', 'repository'),
       
   354           'default': 'all-in-one',
       
   355           'help': 'installation type, telling which part of an instance '
       
   356           'should be installed. You can list available configurations using the'
       
   357           ' "list" command. Default to "all-in-one", e.g. an installation '
       
   358           'embedding both the RQL repository and the web server.',
       
   359           }),
       
   360         ('no-db-create',
       
   361          {'short': 'S',
       
   362           'action': 'store_true',
       
   363           'default': False,
       
   364           'help': 'stop after creation and do not continue with db-create',
       
   365           }),
       
   366         )
       
   367 
       
   368     def run(self, args):
       
   369         """run the command with its specific arguments"""
       
   370         from logilab.common.textutils import splitstrip
       
   371         check_options_consistency(self.config)
       
   372         configname = self.config.config
       
   373         cubes, appid = args
       
   374         cubes = splitstrip(cubes)
       
   375         # get the configuration and helper
       
   376         config = cwcfg.config_for(appid, configname, creating=True)
       
   377         cubes = config.expand_cubes(cubes)
       
   378         config.init_cubes(cubes)
       
   379         helper = self.config_helper(config)
       
   380         # check the cube exists
       
   381         try:
       
   382             templdirs = [cwcfg.cube_dir(cube)
       
   383                          for cube in cubes]
       
   384         except ConfigurationError as ex:
       
   385             print(ex)
       
   386             print('\navailable cubes:', end=' ')
       
   387             print(', '.join(cwcfg.available_cubes()))
       
   388             return
       
   389         # create the registry directory for this instance
       
   390         print('\n'+underline_title('Creating the instance %s' % appid))
       
   391         create_dir(config.apphome)
       
   392         # cubicweb-ctl configuration
       
   393         if not self.config.automatic:
       
   394             print('\n'+underline_title('Configuring the instance (%s.conf)'
       
   395                                        % configname))
       
   396             config.input_config('main', self.config.config_level)
       
   397         # configuration'specific stuff
       
   398         print()
       
   399         helper.bootstrap(cubes, self.config.automatic, self.config.config_level)
       
   400         # input for cubes specific options
       
   401         if not self.config.automatic:
       
   402             sections = set(sect.lower() for sect, opt, odict in config.all_options()
       
   403                            if 'type' in odict
       
   404                            and odict.get('level') <= self.config.config_level)
       
   405             for section in sections:
       
   406                 if section not in ('main', 'email', 'web'):
       
   407                     print('\n' + underline_title('%s options' % section))
       
   408                     config.input_config(section, self.config.config_level)
       
   409         # write down configuration
       
   410         config.save()
       
   411         self._handle_win32(config, appid)
       
   412         print('-> generated config %s' % config.main_config_file())
       
   413         # handle i18n files structure
       
   414         # in the first cube given
       
   415         from cubicweb import i18n
       
   416         langs = [lang for lang, _ in i18n.available_catalogs(join(templdirs[0], 'i18n'))]
       
   417         errors = config.i18ncompile(langs)
       
   418         if errors:
       
   419             print('\n'.join(errors))
       
   420             if self.config.automatic \
       
   421                    or not ASK.confirm('error while compiling message catalogs, '
       
   422                                       'continue anyway ?'):
       
   423                 print('creation not completed')
       
   424                 return
       
   425         # create the additional data directory for this instance
       
   426         if config.appdatahome != config.apphome: # true in dev mode
       
   427             create_dir(config.appdatahome)
       
   428         create_dir(join(config.appdatahome, 'backup'))
       
   429         if config['uid']:
       
   430             from logilab.common.shellutils import chown
       
   431             # this directory should be owned by the uid of the server process
       
   432             print('set %s as owner of the data directory' % config['uid'])
       
   433             chown(config.appdatahome, config['uid'])
       
   434         print('\n-> creation done for %s\n' % repr(config.apphome)[1:-1])
       
   435         if not self.config.no_db_create:
       
   436             helper.postcreate(self.config.automatic, self.config.config_level)
       
   437 
       
   438     def _handle_win32(self, config, appid):
       
   439         if sys.platform != 'win32':
       
   440             return
       
   441         service_template = """
       
   442 import sys
       
   443 import win32serviceutil
       
   444 sys.path.insert(0, r"%(CWPATH)s")
       
   445 
       
   446 from cubicweb.etwist.service import CWService
       
   447 
       
   448 classdict = {'_svc_name_': 'cubicweb-%(APPID)s',
       
   449              '_svc_display_name_': 'CubicWeb ' + '%(CNAME)s',
       
   450              'instance': '%(APPID)s'}
       
   451 %(CNAME)sService = type('%(CNAME)sService', (CWService,), classdict)
       
   452 
       
   453 if __name__ == '__main__':
       
   454     win32serviceutil.HandleCommandLine(%(CNAME)sService)
       
   455 """
       
   456         open(join(config.apphome, 'win32svc.py'), 'wb').write(
       
   457             service_template % {'APPID': appid,
       
   458                                 'CNAME': appid.capitalize(),
       
   459                                 'CWPATH': abspath(join(dirname(__file__), '..'))})
       
   460 
       
   461 
       
   462 class DeleteInstanceCommand(Command):
       
   463     """Delete an instance. Will remove instance's files and
       
   464     unregister it.
       
   465     """
       
   466     name = 'delete'
       
   467     arguments = '<instance>'
       
   468     min_args = max_args = 1
       
   469     options = ()
       
   470 
       
   471     def run(self, args):
       
   472         """run the command with its specific arguments"""
       
   473         appid = args[0]
       
   474         configs = [cwcfg.config_for(appid, configname)
       
   475                    for configname in cwcfg.possible_configurations(appid)]
       
   476         if not configs:
       
   477             raise ExecutionError('unable to guess configuration for %s' % appid)
       
   478         for config in configs:
       
   479             helper = self.config_helper(config, required=False)
       
   480             if helper:
       
   481                 helper.cleanup()
       
   482         # remove home
       
   483         rm(config.apphome)
       
   484         # remove instance data directory
       
   485         try:
       
   486             rm(config.appdatahome)
       
   487         except OSError as ex:
       
   488             import errno
       
   489             if ex.errno != errno.ENOENT:
       
   490                 raise
       
   491         confignames = ', '.join([config.name for config in configs])
       
   492         print('-> instance %s (%s) deleted.' % (appid, confignames))
       
   493 
       
   494 
       
   495 # instance commands ########################################################
       
   496 
       
   497 class StartInstanceCommand(InstanceCommandFork):
       
   498     """Start the given instances. If no instance is given, start them all.
       
   499 
       
   500     <instance>...
       
   501       identifiers of the instances to start. If no instance is
       
   502       given, start them all.
       
   503     """
       
   504     name = 'start'
       
   505     actionverb = 'started'
       
   506     options = (
       
   507         ("debug",
       
   508          {'short': 'D', 'action' : 'store_true',
       
   509           'help': 'start server in debug mode.'}),
       
   510         ("force",
       
   511          {'short': 'f', 'action' : 'store_true',
       
   512           'default': False,
       
   513           'help': 'start the instance even if it seems to be already \
       
   514 running.'}),
       
   515         ('profile',
       
   516          {'short': 'P', 'type' : 'string', 'metavar': '<stat file>',
       
   517           'default': None,
       
   518           'help': 'profile code and use the specified file to store stats',
       
   519           }),
       
   520         ('loglevel',
       
   521          {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
       
   522           'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
       
   523           'help': 'debug if -D is set, error otherwise',
       
   524           }),
       
   525         ('param',
       
   526          {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
       
   527           'default': {},
       
   528           'help': 'override <key> configuration file option with <value>.',
       
   529          }),
       
   530        )
       
   531 
       
   532     def start_instance(self, appid):
       
   533         """start the instance's server"""
       
   534         try:
       
   535             import twisted  # noqa
       
   536         except ImportError:
       
   537             msg = (
       
   538                 "Twisted is required by the 'start' command\n"
       
   539                 "Either install it, or use one of the alternative commands:\n"
       
   540                 "- '{ctl} wsgi {appid}'\n"
       
   541                 "- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
       
   542             raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
       
   543         config = cwcfg.config_for(appid, debugmode=self['debug'])
       
   544         # override config file values with cmdline options
       
   545         config.cmdline_options = self.config.param
       
   546         init_cmdline_log_threshold(config, self['loglevel'])
       
   547         if self['profile']:
       
   548             config.global_set_option('profile', self.config.profile)
       
   549         helper = self.config_helper(config, cmdname='start')
       
   550         pidf = config['pid-file']
       
   551         if exists(pidf) and not self['force']:
       
   552             msg = "%s seems to be running. Remove %s by hand if necessary or use \
       
   553 the --force option."
       
   554             raise ExecutionError(msg % (appid, pidf))
       
   555         if helper.start_server(config) == 1:
       
   556             print('instance %s started' % appid)
       
   557 
       
   558 
       
   559 def init_cmdline_log_threshold(config, loglevel):
       
   560     if loglevel is not None:
       
   561         config.global_set_option('log-threshold', loglevel.upper())
       
   562         config.init_log(config['log-threshold'], force=True)
       
   563 
       
   564 
       
   565 class StopInstanceCommand(InstanceCommand):
       
   566     """Stop the given instances.
       
   567 
       
   568     <instance>...
       
   569       identifiers of the instances to stop. If no instance is
       
   570       given, stop them all.
       
   571     """
       
   572     name = 'stop'
       
   573     actionverb = 'stopped'
       
   574 
       
   575     def ordered_instances(self):
       
   576         instances = super(StopInstanceCommand, self).ordered_instances()
       
   577         instances.reverse()
       
   578         return instances
       
   579 
       
   580     def stop_instance(self, appid):
       
   581         """stop the instance's server"""
       
   582         config = cwcfg.config_for(appid)
       
   583         helper = self.config_helper(config, cmdname='stop')
       
   584         helper.poststop() # do this anyway
       
   585         pidf = config['pid-file']
       
   586         if not exists(pidf):
       
   587             sys.stderr.write("%s doesn't exist.\n" % pidf)
       
   588             return
       
   589         import signal
       
   590         pid = int(open(pidf).read().strip())
       
   591         try:
       
   592             kill(pid, signal.SIGTERM)
       
   593         except Exception:
       
   594             sys.stderr.write("process %s seems already dead.\n" % pid)
       
   595         else:
       
   596             try:
       
   597                 wait_process_end(pid)
       
   598             except ExecutionError as ex:
       
   599                 sys.stderr.write('%s\ntrying SIGKILL\n' % ex)
       
   600                 try:
       
   601                     kill(pid, signal.SIGKILL)
       
   602                 except Exception:
       
   603                     # probably dead now
       
   604                     pass
       
   605                 wait_process_end(pid)
       
   606         try:
       
   607             remove(pidf)
       
   608         except OSError:
       
   609             # already removed by twistd
       
   610             pass
       
   611         print('instance %s stopped' % appid)
       
   612 
       
   613 
       
   614 class RestartInstanceCommand(StartInstanceCommand):
       
   615     """Restart the given instances.
       
   616 
       
   617     <instance>...
       
   618       identifiers of the instances to restart. If no instance is
       
   619       given, restart them all.
       
   620     """
       
   621     name = 'restart'
       
   622     actionverb = 'restarted'
       
   623 
       
   624     def run_args(self, args, askconfirm):
       
   625         regdir = cwcfg.instances_dir()
       
   626         if not isfile(join(regdir, 'startorder')) or len(args) <= 1:
       
   627             # no specific startorder
       
   628             super(RestartInstanceCommand, self).run_args(args, askconfirm)
       
   629             return
       
   630         print ('some specific start order is specified, will first stop all '
       
   631                'instances then restart them.')
       
   632         # get instances in startorder
       
   633         for appid in args:
       
   634             if askconfirm:
       
   635                 print('*'*72)
       
   636                 if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
       
   637                     continue
       
   638             StopInstanceCommand(self.logger).stop_instance(appid)
       
   639         forkcmd = [w for w in sys.argv if not w in args]
       
   640         forkcmd[1] = 'start'
       
   641         forkcmd = ' '.join(forkcmd)
       
   642         for appid in reversed(args):
       
   643             status = system('%s %s' % (forkcmd, appid))
       
   644             if status:
       
   645                 sys.exit(status)
       
   646 
       
   647     def restart_instance(self, appid):
       
   648         StopInstanceCommand(self.logger).stop_instance(appid)
       
   649         self.start_instance(appid)
       
   650 
       
   651 
       
   652 class ReloadConfigurationCommand(RestartInstanceCommand):
       
   653     """Reload the given instances. This command is equivalent to a
       
   654     restart for now.
       
   655 
       
   656     <instance>...
       
   657       identifiers of the instances to reload. If no instance is
       
   658       given, reload them all.
       
   659     """
       
   660     name = 'reload'
       
   661 
       
   662     def reload_instance(self, appid):
       
   663         self.restart_instance(appid)
       
   664 
       
   665 
       
   666 class StatusCommand(InstanceCommand):
       
   667     """Display status information about the given instances.
       
   668 
       
   669     <instance>...
       
   670       identifiers of the instances to status. If no instance is
       
   671       given, get status information about all registered instances.
       
   672     """
       
   673     name = 'status'
       
   674     options = ()
       
   675 
       
   676     @staticmethod
       
   677     def status_instance(appid):
       
   678         """print running status information for an instance"""
       
   679         status = 0
       
   680         for mode in cwcfg.possible_configurations(appid):
       
   681             config = cwcfg.config_for(appid, mode)
       
   682             print('[%s-%s]' % (appid, mode), end=' ')
       
   683             try:
       
   684                 pidf = config['pid-file']
       
   685             except KeyError:
       
   686                 print('buggy instance, pid file not specified')
       
   687                 continue
       
   688             if not exists(pidf):
       
   689                 print("doesn't seem to be running")
       
   690                 status = 1
       
   691                 continue
       
   692             pid = int(open(pidf).read().strip())
       
   693             # trick to guess whether or not the process is running
       
   694             try:
       
   695                 getpgid(pid)
       
   696             except OSError:
       
   697                 print("should be running with pid %s but the process can not be found" % pid)
       
   698                 status = 1
       
   699                 continue
       
   700             print("running with pid %s" % (pid))
       
   701         return status
       
   702 
       
   703 class UpgradeInstanceCommand(InstanceCommandFork):
       
   704     """Upgrade an instance after cubicweb and/or component(s) upgrade.
       
   705 
       
   706     For repository update, you will be prompted for a login / password to use
       
   707     to connect to the system database.  For some upgrades, the given user
       
   708     should have create or alter table permissions.
       
   709 
       
   710     <instance>...
       
   711       identifiers of the instances to upgrade. If no instance is
       
   712       given, upgrade them all.
       
   713     """
       
   714     name = 'upgrade'
       
   715     actionverb = 'upgraded'
       
   716     options = InstanceCommand.options + (
       
   717         ('force-cube-version',
       
   718          {'short': 't', 'type' : 'named', 'metavar': 'cube1:X.Y.Z,cube2:X.Y.Z',
       
   719           'default': None,
       
   720           'help': 'force migration from the indicated version for the specified cube(s).'}),
       
   721 
       
   722         ('force-cubicweb-version',
       
   723          {'short': 'e', 'type' : 'string', 'metavar': 'X.Y.Z',
       
   724           'default': None,
       
   725           'help': 'force migration from the indicated cubicweb version.'}),
       
   726 
       
   727         ('fs-only',
       
   728          {'short': 's', 'action' : 'store_true',
       
   729           'default': False,
       
   730           'help': 'only upgrade files on the file system, not the database.'}),
       
   731 
       
   732         ('nostartstop',
       
   733          {'short': 'n', 'action' : 'store_true',
       
   734           'default': False,
       
   735           'help': 'don\'t try to stop instance before migration and to restart it after.'}),
       
   736 
       
   737         ('verbosity',
       
   738          {'short': 'v', 'type' : 'int', 'metavar': '<0..2>',
       
   739           'default': 1,
       
   740           'help': "0: no confirmation, 1: only main commands confirmed, 2 ask \
       
   741 for everything."}),
       
   742 
       
   743         ('backup-db',
       
   744          {'short': 'b', 'type' : 'yn', 'metavar': '<y or n>',
       
   745           'default': None,
       
   746           'help': "Backup the instance database before upgrade.\n"\
       
   747           "If the option is ommitted, confirmation will be ask.",
       
   748           }),
       
   749 
       
   750         ('ext-sources',
       
   751          {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
       
   752           'default': None,
       
   753           'help': "For multisources instances, specify to which sources the \
       
   754 repository should connect to for upgrading. When unspecified or 'migration' is \
       
   755 given, appropriate sources for migration will be automatically selected \
       
   756 (recommended). If 'all' is given, will connect to all defined sources.",
       
   757           }),
       
   758         )
       
   759 
       
   760     def upgrade_instance(self, appid):
       
   761         print('\n' + underline_title('Upgrading the instance %s' % appid))
       
   762         from logilab.common.changelog import Version
       
   763         config = cwcfg.config_for(appid)
       
   764         instance_running = exists(config['pid-file'])
       
   765         config.repairing = True # notice we're not starting the server
       
   766         config.verbosity = self.config.verbosity
       
   767         set_sources_mode = getattr(config, 'set_sources_mode', None)
       
   768         if set_sources_mode is not None:
       
   769             set_sources_mode(self.config.ext_sources or ('migration',))
       
   770         # get instance and installed versions for the server and the componants
       
   771         mih = config.migration_handler()
       
   772         repo = mih.repo
       
   773         vcconf = repo.get_versions()
       
   774         helper = self.config_helper(config, required=False)
       
   775         if self.config.force_cube_version:
       
   776             for cube, version in self.config.force_cube_version.items():
       
   777                 vcconf[cube] = Version(version)
       
   778         toupgrade = []
       
   779         for cube in config.cubes():
       
   780             installedversion = config.cube_version(cube)
       
   781             try:
       
   782                 applversion = vcconf[cube]
       
   783             except KeyError:
       
   784                 config.error('no version information for %s' % cube)
       
   785                 continue
       
   786             if installedversion > applversion:
       
   787                 toupgrade.append( (cube, applversion, installedversion) )
       
   788         cubicwebversion = config.cubicweb_version()
       
   789         if self.config.force_cubicweb_version:
       
   790             applcubicwebversion = Version(self.config.force_cubicweb_version)
       
   791             vcconf['cubicweb'] = applcubicwebversion
       
   792         else:
       
   793             applcubicwebversion = vcconf.get('cubicweb')
       
   794         if cubicwebversion > applcubicwebversion:
       
   795             toupgrade.append(('cubicweb', applcubicwebversion, cubicwebversion))
       
   796         # only stop once we're sure we have something to do
       
   797         if instance_running and not (CWDEV or self.config.nostartstop):
       
   798             StopInstanceCommand(self.logger).stop_instance(appid)
       
   799         # run cubicweb/componants migration scripts
       
   800         if self.config.fs_only or toupgrade:
       
   801             for cube, fromversion, toversion in toupgrade:
       
   802                 print('-> migration needed from %s to %s for %s' % (fromversion, toversion, cube))
       
   803             with mih.cnx:
       
   804                 with mih.cnx.security_enabled(False, False):
       
   805                     mih.migrate(vcconf, reversed(toupgrade), self.config)
       
   806         else:
       
   807             print('-> no data migration needed for instance %s.' % appid)
       
   808         # rewrite main configuration file
       
   809         mih.rewrite_configuration()
       
   810         mih.shutdown()
       
   811         # handle i18n upgrade
       
   812         if not self.i18nupgrade(config):
       
   813             return
       
   814         print()
       
   815         if helper:
       
   816             helper.postupgrade(repo)
       
   817         print('-> instance migrated.')
       
   818         if instance_running and not (CWDEV or self.config.nostartstop):
       
   819             # restart instance through fork to get a proper environment, avoid
       
   820             # uicfg pb (and probably gettext catalogs, to check...)
       
   821             forkcmd = '%s start %s' % (sys.argv[0], appid)
       
   822             status = system(forkcmd)
       
   823             if status:
       
   824                 print('%s exited with status %s' % (forkcmd, status))
       
   825         print()
       
   826 
       
   827     def i18nupgrade(self, config):
       
   828         # handle i18n upgrade:
       
   829         # * install new languages
       
   830         # * recompile catalogs
       
   831         # XXX search available language in the first cube given
       
   832         from cubicweb import i18n
       
   833         templdir = cwcfg.cube_dir(config.cubes()[0])
       
   834         langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))]
       
   835         errors = config.i18ncompile(langs)
       
   836         if errors:
       
   837             print('\n'.join(errors))
       
   838             if not ASK.confirm('Error while compiling message catalogs, '
       
   839                                'continue anyway?'):
       
   840                 print('-> migration not completed.')
       
   841                 return False
       
   842         return True
       
   843 
       
   844 
       
   845 class ListVersionsInstanceCommand(InstanceCommand):
       
   846     """List versions used by an instance.
       
   847 
       
   848     <instance>...
       
   849       identifiers of the instances to list versions for.
       
   850     """
       
   851     name = 'versions'
       
   852 
       
   853     def versions_instance(self, appid):
       
   854         config = cwcfg.config_for(appid)
       
   855         # should not raise error if db versions don't match fs versions
       
   856         config.repairing = True
       
   857         # no need to load all appobjects and schema
       
   858         config.quick_start = True
       
   859         if hasattr(config, 'set_sources_mode'):
       
   860             config.set_sources_mode(('migration',))
       
   861         vcconf = config.repository().get_versions()
       
   862         for key in sorted(vcconf):
       
   863             print(key+': %s.%s.%s' % vcconf[key])
       
   864 
       
   865 class ShellCommand(Command):
       
   866     """Run an interactive migration shell on an instance. This is a python shell
       
   867     with enhanced migration commands predefined in the namespace. An additional
       
   868     argument may be given corresponding to a file containing commands to execute
       
   869     in batch mode.
       
   870 
       
   871     By default it will connect to a local instance using an in memory
       
   872     connection, unless a URL to a running instance is specified.
       
   873 
       
   874     Arguments after bare "--" string will not be processed by the shell command
       
   875     You can use it to pass extra arguments to your script and expect for
       
   876     them in '__args__' afterwards.
       
   877 
       
   878     <instance>
       
   879       the identifier of the instance to connect.
       
   880     """
       
   881     name = 'shell'
       
   882     arguments = '<instance> [batch command file(s)] [-- <script arguments>]'
       
   883     min_args = 1
       
   884     options = (
       
   885         ('system-only',
       
   886          {'short': 'S', 'action' : 'store_true',
       
   887           'help': 'only connect to the system source when the instance is '
       
   888           'using multiple sources. You can\'t use this option and the '
       
   889           '--ext-sources option at the same time.',
       
   890           'group': 'local'
       
   891          }),
       
   892 
       
   893         ('ext-sources',
       
   894          {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
       
   895           'help': "For multisources instances, specify to which sources the \
       
   896 repository should connect to for upgrading. When unspecified or 'all' given, \
       
   897 will connect to all defined sources. If 'migration' is given, appropriate \
       
   898 sources for migration will be automatically selected.",
       
   899           'group': 'local'
       
   900           }),
       
   901 
       
   902         ('force',
       
   903          {'short': 'f', 'action' : 'store_true',
       
   904           'help': 'don\'t check instance is up to date.',
       
   905           'group': 'local'
       
   906           }),
       
   907 
       
   908         ('repo-uri',
       
   909          {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
       
   910           'help': 'URI of the CubicWeb repository to connect to. URI can be \
       
   911 a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
       
   912 directly give URI as instance id instead',
       
   913           'group': 'remote'
       
   914           }),
       
   915         )
       
   916 
       
   917     def _handle_inmemory(self, appid):
       
   918         """ returns migration context handler & shutdown function """
       
   919         config = cwcfg.config_for(appid)
       
   920         if self.config.ext_sources:
       
   921             assert not self.config.system_only
       
   922             sources = self.config.ext_sources
       
   923         elif self.config.system_only:
       
   924             sources = ('system',)
       
   925         else:
       
   926             sources = ('all',)
       
   927         config.set_sources_mode(sources)
       
   928         config.repairing = self.config.force
       
   929         mih = config.migration_handler()
       
   930         return mih, lambda: mih.shutdown()
       
   931 
       
   932     def _handle_networked(self, appuri):
       
   933         """ returns migration context handler & shutdown function """
       
   934         from cubicweb import AuthenticationError
       
   935         from cubicweb.repoapi import connect, get_repository
       
   936         from cubicweb.server.utils import manager_userpasswd
       
   937         from cubicweb.server.migractions import ServerMigrationHelper
       
   938         while True:
       
   939             try:
       
   940                 login, pwd = manager_userpasswd(msg=None)
       
   941                 repo = get_repository(appuri)
       
   942                 cnx = connect(repo, login=login, password=pwd, mulcnx=False)
       
   943             except AuthenticationError as ex:
       
   944                 print(ex)
       
   945             except (KeyboardInterrupt, EOFError):
       
   946                 print()
       
   947                 sys.exit(0)
       
   948             else:
       
   949                 break
       
   950         cnx.load_appobjects()
       
   951         repo = cnx._repo
       
   952         mih = ServerMigrationHelper(None, repo=repo, cnx=cnx, verbosity=0,
       
   953                                     # hack so it don't try to load fs schema
       
   954                                     schema=1)
       
   955         return mih, lambda: cnx.close()
       
   956 
       
   957     def run(self, args):
       
   958         appuri = args.pop(0)
       
   959         if self.config.repo_uri:
       
   960             warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
       
   961                  DeprecationWarning)
       
   962             if urlparse(self.config.repo_uri).scheme == 'inmemory':
       
   963                 appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)
       
   964 
       
   965         from cubicweb.utils import parse_repo_uri
       
   966         protocol, hostport, appid = parse_repo_uri(appuri)
       
   967         if protocol == 'inmemory':
       
   968             mih, shutdown_callback = self._handle_inmemory(appid)
       
   969         else:
       
   970             mih, shutdown_callback = self._handle_networked(appuri)
       
   971         try:
       
   972             with mih.cnx:
       
   973                 with mih.cnx.security_enabled(False, False):
       
   974                     if args:
       
   975                         # use cmdline parser to access left/right attributes only
       
   976                         # remember that usage requires instance appid as first argument
       
   977                         scripts, args = self.cmdline_parser.largs[1:], self.cmdline_parser.rargs
       
   978                         for script in scripts:
       
   979                                 mih.cmd_process_script(script, scriptargs=args)
       
   980                                 mih.commit()
       
   981                     else:
       
   982                         mih.interactive_shell()
       
   983         finally:
       
   984             shutdown_callback()
       
   985 
       
   986 
       
   987 class RecompileInstanceCatalogsCommand(InstanceCommand):
       
   988     """Recompile i18n catalogs for instances.
       
   989 
       
   990     <instance>...
       
   991       identifiers of the instances to consider. If no instance is
       
   992       given, recompile for all registered instances.
       
   993     """
       
   994     name = 'i18ninstance'
       
   995 
       
   996     @staticmethod
       
   997     def i18ninstance_instance(appid):
       
   998         """recompile instance's messages catalogs"""
       
   999         config = cwcfg.config_for(appid)
       
  1000         config.quick_start = True # notify this is not a regular start
       
  1001         repo = config.repository()
       
  1002         if config._cubes is None:
       
  1003             # web only config
       
  1004             config.init_cubes(repo.get_cubes())
       
  1005         errors = config.i18ncompile()
       
  1006         if errors:
       
  1007             print('\n'.join(errors))
       
  1008 
       
  1009 
       
  1010 class ListInstancesCommand(Command):
       
  1011     """list available instances, useful for bash completion."""
       
  1012     name = 'listinstances'
       
  1013     hidden = True
       
  1014 
       
  1015     def run(self, args):
       
  1016         """run the command with its specific arguments"""
       
  1017         regdir = cwcfg.instances_dir()
       
  1018         for appid in sorted(listdir(regdir)):
       
  1019             print(appid)
       
  1020 
       
  1021 
       
  1022 class ListCubesCommand(Command):
       
  1023     """list available componants, useful for bash completion."""
       
  1024     name = 'listcubes'
       
  1025     hidden = True
       
  1026 
       
  1027     def run(self, args):
       
  1028         """run the command with its specific arguments"""
       
  1029         for cube in cwcfg.available_cubes():
       
  1030             print(cube)
       
  1031 
       
  1032 class ConfigureInstanceCommand(InstanceCommand):
       
  1033     """Configure instance.
       
  1034 
       
  1035     <instance>...
       
  1036       identifier of the instance to configure.
       
  1037     """
       
  1038     name = 'configure'
       
  1039     actionverb = 'configured'
       
  1040 
       
  1041     options = merge_options(InstanceCommand.options +
       
  1042                             (('param',
       
  1043                               {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
       
  1044                                'default': None,
       
  1045                                'help': 'set <key> to <value> in configuration file.',
       
  1046                                }),
       
  1047                              ))
       
  1048 
       
  1049     def configure_instance(self, appid):
       
  1050         if self.config.param is not None:
       
  1051             appcfg = cwcfg.config_for(appid)
       
  1052             for key, value in self.config.param.items():
       
  1053                 try:
       
  1054                     appcfg.global_set_option(key, value)
       
  1055                 except KeyError:
       
  1056                     raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name))
       
  1057             appcfg.save()
       
  1058 
       
  1059 
       
  1060 # WSGI #########
       
  1061 
       
  1062 WSGI_CHOICES = {}
       
  1063 from cubicweb.wsgi import server as stdlib_server
       
  1064 WSGI_CHOICES['stdlib'] = stdlib_server
       
  1065 try:
       
  1066     from cubicweb.wsgi import wz
       
  1067 except ImportError:
       
  1068     pass
       
  1069 else:
       
  1070     WSGI_CHOICES['werkzeug'] = wz
       
  1071 try:
       
  1072     from cubicweb.wsgi import tnd
       
  1073 except ImportError:
       
  1074     pass
       
  1075 else:
       
  1076     WSGI_CHOICES['tornado'] = tnd
       
  1077 
       
  1078 
       
  1079 def wsgichoices():
       
  1080     return tuple(WSGI_CHOICES)
       
  1081 
       
  1082 
       
  1083 class WSGIStartHandler(InstanceCommand):
       
  1084     """Start an interactive wsgi server """
       
  1085     name = 'wsgi'
       
  1086     actionverb = 'started'
       
  1087     arguments = '<instance>'
       
  1088 
       
  1089     @property
       
  1090     def options(self):
       
  1091         return (
       
  1092         ("debug",
       
  1093          {'short': 'D', 'action': 'store_true',
       
  1094           'default': False,
       
  1095           'help': 'start server in debug mode.'}),
       
  1096         ('method',
       
  1097          {'short': 'm',
       
  1098           'type': 'choice',
       
  1099           'metavar': '<method>',
       
  1100           'default': 'stdlib',
       
  1101           'choices': wsgichoices(),
       
  1102           'help': 'wsgi utility/method'}),
       
  1103         ('loglevel',
       
  1104          {'short': 'l',
       
  1105           'type': 'choice',
       
  1106           'metavar': '<log level>',
       
  1107           'default': None,
       
  1108           'choices': ('debug', 'info', 'warning', 'error'),
       
  1109           'help': 'debug if -D is set, error otherwise',
       
  1110           }),
       
  1111         )
       
  1112 
       
  1113     def wsgi_instance(self, appid):
       
  1114         config = cwcfg.config_for(appid, debugmode=self['debug'])
       
  1115         init_cmdline_log_threshold(config, self['loglevel'])
       
  1116         assert config.name == 'all-in-one'
       
  1117         meth = self['method']
       
  1118         server = WSGI_CHOICES[meth]
       
  1119         return server.run(config)
       
  1120 
       
  1121 
       
  1122 
       
  1123 for cmdcls in (ListCommand,
       
  1124                CreateInstanceCommand, DeleteInstanceCommand,
       
  1125                StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
       
  1126                WSGIStartHandler,
       
  1127                ReloadConfigurationCommand, StatusCommand,
       
  1128                UpgradeInstanceCommand,
       
  1129                ListVersionsInstanceCommand,
       
  1130                ShellCommand,
       
  1131                RecompileInstanceCatalogsCommand,
       
  1132                ListInstancesCommand, ListCubesCommand,
       
  1133                ConfigureInstanceCommand,
       
  1134                ):
       
  1135     CWCTL.register(cmdcls)
       
  1136 
       
  1137 
       
  1138 
       
  1139 def run(args):
       
  1140     """command line tool"""
       
  1141     import os
       
  1142     filterwarnings('default', category=DeprecationWarning)
       
  1143     cwcfg.load_cwctl_plugins()
       
  1144     try:
       
  1145         CWCTL.run(args)
       
  1146     except ConfigurationError as err:
       
  1147         print('ERROR: ', err)
       
  1148         sys.exit(1)
       
  1149     except ExecutionError as err:
       
  1150         print(err)
       
  1151         sys.exit(2)
       
  1152 
       
  1153 if __name__ == '__main__':
       
  1154     run(sys.argv[1:])