--- a/cwctl.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1154 +0,0 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""the cubicweb-ctl tool, based on logilab.common.clcommands to
-provide a pluggable commands system.
-"""
-from __future__ import print_function
-
-__docformat__ = "restructuredtext en"
-
-# *ctl module should limit the number of import to be imported as quickly as
-# possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
-# completion). So import locally in command helpers.
-import sys
-from warnings import warn, filterwarnings
-from os import remove, listdir, system, pathsep
-from os.path import exists, join, isfile, isdir, dirname, abspath
-
-try:
- from os import kill, getpgid
-except ImportError:
- def kill(*args):
- """win32 kill implementation"""
- def getpgid():
- """win32 getpgid implementation"""
-
-from six.moves.urllib.parse import urlparse
-
-from logilab.common.clcommands import CommandLine
-from logilab.common.shellutils import ASK
-from logilab.common.configuration import merge_options
-
-from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
-from cubicweb.utils import support_args
-from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS
-from cubicweb.toolsutils import Command, rm, create_dir, underline_title
-from cubicweb.__pkginfo__ import version
-
-# don't check duplicated commands, it occurs when reloading site_cubicweb
-CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
- version=version, check_duplicated_command=False)
-
-def wait_process_end(pid, maxtry=10, waittime=1):
- """wait for a process to actually die"""
- import signal
- from time import sleep
- nbtry = 0
- while nbtry < maxtry:
- try:
- kill(pid, signal.SIGUSR1)
- except (OSError, AttributeError): # XXX win32
- break
- nbtry += 1
- sleep(waittime)
- else:
- raise ExecutionError('can\'t kill process %s' % pid)
-
-def list_instances(regdir):
- if isdir(regdir):
- return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
- else:
- return []
-
-def detect_available_modes(templdir):
- modes = []
- for fname in ('schema', 'schema.py'):
- if exists(join(templdir, fname)):
- modes.append('repository')
- break
- for fname in ('data', 'views', 'views.py'):
- if exists(join(templdir, fname)):
- modes.append('web ui')
- break
- return modes
-
-
-class InstanceCommand(Command):
- """base class for command taking 0 to n instance id as arguments
- (0 meaning all registered instances)
- """
- arguments = '[<instance>...]'
- options = (
- ("force",
- {'short': 'f', 'action' : 'store_true',
- 'default': False,
- 'help': 'force command without asking confirmation',
- }
- ),
- )
- actionverb = None
-
- def ordered_instances(self):
- """return instances in the order in which they should be started,
- considering $REGISTRY_DIR/startorder file if it exists (useful when
- some instances depends on another as external source).
-
- Instance used by another one should appears first in the file (one
- instance per line)
- """
- regdir = cwcfg.instances_dir()
- _allinstances = list_instances(regdir)
- if isfile(join(regdir, 'startorder')):
- allinstances = []
- for line in open(join(regdir, 'startorder')):
- line = line.strip()
- if line and not line.startswith('#'):
- try:
- _allinstances.remove(line)
- allinstances.append(line)
- except ValueError:
- print('ERROR: startorder file contains unexistant '
- 'instance %s' % line)
- allinstances += _allinstances
- else:
- allinstances = _allinstances
- return allinstances
-
- def run(self, args):
- """run the <command>_method on each argument (a list of instance
- identifiers)
- """
- if not args:
- args = self.ordered_instances()
- try:
- askconfirm = not self.config.force
- except AttributeError:
- # no force option
- askconfirm = False
- else:
- askconfirm = False
- self.run_args(args, askconfirm)
-
- def run_args(self, args, askconfirm):
- status = 0
- for appid in args:
- if askconfirm:
- print('*'*72)
- if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
- continue
- try:
- status = max(status, self.run_arg(appid))
- except (KeyboardInterrupt, SystemExit):
- sys.stderr.write('%s aborted\n' % self.name)
- return 2 # specific error code
- sys.exit(status)
-
- def run_arg(self, appid):
- cmdmeth = getattr(self, '%s_instance' % self.name)
- try:
- status = cmdmeth(appid)
- except (ExecutionError, ConfigurationError) as ex:
- sys.stderr.write('instance %s not %s: %s\n' % (
- appid, self.actionverb, ex))
- status = 4
- except Exception as ex:
- import traceback
- traceback.print_exc()
- sys.stderr.write('instance %s not %s: %s\n' % (
- appid, self.actionverb, ex))
- status = 8
- return status
-
-class InstanceCommandFork(InstanceCommand):
- """Same as `InstanceCommand`, but command is forked in a new environment
- for each argument
- """
-
- def run_args(self, args, askconfirm):
- if len(args) > 1:
- forkcmd = ' '.join(w for w in sys.argv if not w in args)
- else:
- forkcmd = None
- for appid in args:
- if askconfirm:
- print('*'*72)
- if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
- continue
- if forkcmd:
- status = system('%s %s' % (forkcmd, appid))
- if status:
- print('%s exited with status %s' % (forkcmd, status))
- else:
- self.run_arg(appid)
-
-
-# base commands ###############################################################
-
-class ListCommand(Command):
- """List configurations, cubes and instances.
-
- List available configurations, installed cubes, and registered instances.
-
- If given, the optional argument allows to restrict listing only a category of items.
- """
- name = 'list'
- arguments = '[all|cubes|configurations|instances]'
- options = (
- ('verbose',
- {'short': 'v', 'action' : 'store_true',
- 'help': "display more information."}),
- )
-
- def run(self, args):
- """run the command with its specific arguments"""
- if not args:
- mode = 'all'
- elif len(args) == 1:
- mode = args[0]
- else:
- raise BadCommandUsage('Too many arguments')
-
- from cubicweb.migration import ConfigurationProblem
-
- if mode == 'all':
- print('CubicWeb %s (%s mode)' % (cwcfg.cubicweb_version(), cwcfg.mode))
- print()
-
- if mode in ('all', 'config', 'configurations'):
- print('Available configurations:')
- for config in CONFIGURATIONS:
- print('*', config.name)
- for line in config.__doc__.splitlines():
- line = line.strip()
- if not line:
- continue
- print(' ', line)
- print()
-
- if mode in ('all', 'cubes'):
- cfgpb = ConfigurationProblem(cwcfg)
- try:
- cubesdir = pathsep.join(cwcfg.cubes_search_path())
- namesize = max(len(x) for x in cwcfg.available_cubes())
- except ConfigurationError as ex:
- print('No cubes available:', ex)
- except ValueError:
- print('No cubes available in %s' % cubesdir)
- else:
- print('Available cubes (%s):' % cubesdir)
- for cube in cwcfg.available_cubes():
- try:
- tinfo = cwcfg.cube_pkginfo(cube)
- tversion = tinfo.version
- cfgpb.add_cube(cube, tversion)
- except (ConfigurationError, AttributeError) as ex:
- tinfo = None
- tversion = '[missing cube information: %s]' % ex
- print('* %s %s' % (cube.ljust(namesize), tversion))
- if self.config.verbose:
- if tinfo:
- descr = getattr(tinfo, 'description', '')
- if not descr:
- descr = tinfo.__doc__
- if descr:
- print(' '+ ' \n'.join(descr.splitlines()))
- modes = detect_available_modes(cwcfg.cube_dir(cube))
- print(' available modes: %s' % ', '.join(modes))
- print()
-
- if mode in ('all', 'instances'):
- try:
- regdir = cwcfg.instances_dir()
- except ConfigurationError as ex:
- print('No instance available:', ex)
- print()
- return
- instances = list_instances(regdir)
- if instances:
- print('Available instances (%s):' % regdir)
- for appid in instances:
- modes = cwcfg.possible_configurations(appid)
- if not modes:
- print('* %s (BROKEN instance, no configuration found)' % appid)
- continue
- print('* %s (%s)' % (appid, ', '.join(modes)))
- try:
- config = cwcfg.config_for(appid, modes[0])
- except Exception as exc:
- print(' (BROKEN instance, %s)' % exc)
- continue
- else:
- print('No instance available in %s' % regdir)
- print()
-
- if mode == 'all':
- # configuration management problem solving
- cfgpb.solve()
- if cfgpb.warnings:
- print('Warnings:\n', '\n'.join('* '+txt for txt in cfgpb.warnings))
- if cfgpb.errors:
- print('Errors:')
- for op, cube, version, src in cfgpb.errors:
- if op == 'add':
- print('* cube', cube, end=' ')
- if version:
- print(' version', version, end=' ')
- print('is not installed, but required by %s' % src)
- else:
- print('* cube %s version %s is installed, but version %s is required by %s' % (
- cube, cfgpb.cubes[cube], version, src))
-
-def check_options_consistency(config):
- if config.automatic and config.config_level > 0:
- raise BadCommandUsage('--automatic and --config-level should not be '
- 'used together')
-
-class CreateInstanceCommand(Command):
- """Create an instance from a cube. This is a unified
- command which can handle web / server / all-in-one installation
- according to available parts of the software library and of the
- desired cube.
-
- <cube>
- the name of cube to use (list available cube names using
- the "list" command). You can use several cubes by separating
- them using comma (e.g. 'jpl,email')
- <instance>
- an identifier for the instance to create
- """
- name = 'create'
- arguments = '<cube> <instance>'
- min_args = max_args = 2
- options = (
- ('automatic',
- {'short': 'a', 'action' : 'store_true',
- 'default': False,
- 'help': 'automatic mode: never ask and use default answer to every '
- 'question. this may require that your login match a database super '
- 'user (allowed to create database & all).',
- }),
- ('config-level',
- {'short': 'l', 'type' : 'int', 'metavar': '<level>',
- 'default': 0,
- 'help': 'configuration level (0..2): 0 will ask for essential '
- 'configuration parameters only while 2 will ask for all parameters',
- }),
- ('config',
- {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
- 'choices': ('all-in-one', 'repository'),
- 'default': 'all-in-one',
- 'help': 'installation type, telling which part of an instance '
- 'should be installed. You can list available configurations using the'
- ' "list" command. Default to "all-in-one", e.g. an installation '
- 'embedding both the RQL repository and the web server.',
- }),
- ('no-db-create',
- {'short': 'S',
- 'action': 'store_true',
- 'default': False,
- 'help': 'stop after creation and do not continue with db-create',
- }),
- )
-
- def run(self, args):
- """run the command with its specific arguments"""
- from logilab.common.textutils import splitstrip
- check_options_consistency(self.config)
- configname = self.config.config
- cubes, appid = args
- cubes = splitstrip(cubes)
- # get the configuration and helper
- config = cwcfg.config_for(appid, configname, creating=True)
- cubes = config.expand_cubes(cubes)
- config.init_cubes(cubes)
- helper = self.config_helper(config)
- # check the cube exists
- try:
- templdirs = [cwcfg.cube_dir(cube)
- for cube in cubes]
- except ConfigurationError as ex:
- print(ex)
- print('\navailable cubes:', end=' ')
- print(', '.join(cwcfg.available_cubes()))
- return
- # create the registry directory for this instance
- print('\n'+underline_title('Creating the instance %s' % appid))
- create_dir(config.apphome)
- # cubicweb-ctl configuration
- if not self.config.automatic:
- print('\n'+underline_title('Configuring the instance (%s.conf)'
- % configname))
- config.input_config('main', self.config.config_level)
- # configuration'specific stuff
- print()
- helper.bootstrap(cubes, self.config.automatic, self.config.config_level)
- # input for cubes specific options
- if not self.config.automatic:
- sections = set(sect.lower() for sect, opt, odict in config.all_options()
- if 'type' in odict
- and odict.get('level') <= self.config.config_level)
- for section in sections:
- if section not in ('main', 'email', 'web'):
- print('\n' + underline_title('%s options' % section))
- config.input_config(section, self.config.config_level)
- # write down configuration
- config.save()
- self._handle_win32(config, appid)
- print('-> generated config %s' % config.main_config_file())
- # handle i18n files structure
- # in the first cube given
- from cubicweb import i18n
- langs = [lang for lang, _ in i18n.available_catalogs(join(templdirs[0], 'i18n'))]
- errors = config.i18ncompile(langs)
- if errors:
- print('\n'.join(errors))
- if self.config.automatic \
- or not ASK.confirm('error while compiling message catalogs, '
- 'continue anyway ?'):
- print('creation not completed')
- return
- # create the additional data directory for this instance
- if config.appdatahome != config.apphome: # true in dev mode
- create_dir(config.appdatahome)
- create_dir(join(config.appdatahome, 'backup'))
- if config['uid']:
- from logilab.common.shellutils import chown
- # this directory should be owned by the uid of the server process
- print('set %s as owner of the data directory' % config['uid'])
- chown(config.appdatahome, config['uid'])
- print('\n-> creation done for %s\n' % repr(config.apphome)[1:-1])
- if not self.config.no_db_create:
- helper.postcreate(self.config.automatic, self.config.config_level)
-
- def _handle_win32(self, config, appid):
- if sys.platform != 'win32':
- return
- service_template = """
-import sys
-import win32serviceutil
-sys.path.insert(0, r"%(CWPATH)s")
-
-from cubicweb.etwist.service import CWService
-
-classdict = {'_svc_name_': 'cubicweb-%(APPID)s',
- '_svc_display_name_': 'CubicWeb ' + '%(CNAME)s',
- 'instance': '%(APPID)s'}
-%(CNAME)sService = type('%(CNAME)sService', (CWService,), classdict)
-
-if __name__ == '__main__':
- win32serviceutil.HandleCommandLine(%(CNAME)sService)
-"""
- open(join(config.apphome, 'win32svc.py'), 'wb').write(
- service_template % {'APPID': appid,
- 'CNAME': appid.capitalize(),
- 'CWPATH': abspath(join(dirname(__file__), '..'))})
-
-
-class DeleteInstanceCommand(Command):
- """Delete an instance. Will remove instance's files and
- unregister it.
- """
- name = 'delete'
- arguments = '<instance>'
- min_args = max_args = 1
- options = ()
-
- def run(self, args):
- """run the command with its specific arguments"""
- appid = args[0]
- configs = [cwcfg.config_for(appid, configname)
- for configname in cwcfg.possible_configurations(appid)]
- if not configs:
- raise ExecutionError('unable to guess configuration for %s' % appid)
- for config in configs:
- helper = self.config_helper(config, required=False)
- if helper:
- helper.cleanup()
- # remove home
- rm(config.apphome)
- # remove instance data directory
- try:
- rm(config.appdatahome)
- except OSError as ex:
- import errno
- if ex.errno != errno.ENOENT:
- raise
- confignames = ', '.join([config.name for config in configs])
- print('-> instance %s (%s) deleted.' % (appid, confignames))
-
-
-# instance commands ########################################################
-
-class StartInstanceCommand(InstanceCommandFork):
- """Start the given instances. If no instance is given, start them all.
-
- <instance>...
- identifiers of the instances to start. If no instance is
- given, start them all.
- """
- name = 'start'
- actionverb = 'started'
- options = (
- ("debug",
- {'short': 'D', 'action' : 'store_true',
- 'help': 'start server in debug mode.'}),
- ("force",
- {'short': 'f', 'action' : 'store_true',
- 'default': False,
- 'help': 'start the instance even if it seems to be already \
-running.'}),
- ('profile',
- {'short': 'P', 'type' : 'string', 'metavar': '<stat file>',
- 'default': None,
- 'help': 'profile code and use the specified file to store stats',
- }),
- ('loglevel',
- {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
- 'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
- 'help': 'debug if -D is set, error otherwise',
- }),
- ('param',
- {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
- 'default': {},
- 'help': 'override <key> configuration file option with <value>.',
- }),
- )
-
- def start_instance(self, appid):
- """start the instance's server"""
- try:
- import twisted # noqa
- except ImportError:
- msg = (
- "Twisted is required by the 'start' command\n"
- "Either install it, or use one of the alternative commands:\n"
- "- '{ctl} wsgi {appid}'\n"
- "- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
- raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
- config = cwcfg.config_for(appid, debugmode=self['debug'])
- # override config file values with cmdline options
- config.cmdline_options = self.config.param
- init_cmdline_log_threshold(config, self['loglevel'])
- if self['profile']:
- config.global_set_option('profile', self.config.profile)
- helper = self.config_helper(config, cmdname='start')
- pidf = config['pid-file']
- if exists(pidf) and not self['force']:
- msg = "%s seems to be running. Remove %s by hand if necessary or use \
-the --force option."
- raise ExecutionError(msg % (appid, pidf))
- if helper.start_server(config) == 1:
- print('instance %s started' % appid)
-
-
-def init_cmdline_log_threshold(config, loglevel):
- if loglevel is not None:
- config.global_set_option('log-threshold', loglevel.upper())
- config.init_log(config['log-threshold'], force=True)
-
-
-class StopInstanceCommand(InstanceCommand):
- """Stop the given instances.
-
- <instance>...
- identifiers of the instances to stop. If no instance is
- given, stop them all.
- """
- name = 'stop'
- actionverb = 'stopped'
-
- def ordered_instances(self):
- instances = super(StopInstanceCommand, self).ordered_instances()
- instances.reverse()
- return instances
-
- def stop_instance(self, appid):
- """stop the instance's server"""
- config = cwcfg.config_for(appid)
- helper = self.config_helper(config, cmdname='stop')
- helper.poststop() # do this anyway
- pidf = config['pid-file']
- if not exists(pidf):
- sys.stderr.write("%s doesn't exist.\n" % pidf)
- return
- import signal
- pid = int(open(pidf).read().strip())
- try:
- kill(pid, signal.SIGTERM)
- except Exception:
- sys.stderr.write("process %s seems already dead.\n" % pid)
- else:
- try:
- wait_process_end(pid)
- except ExecutionError as ex:
- sys.stderr.write('%s\ntrying SIGKILL\n' % ex)
- try:
- kill(pid, signal.SIGKILL)
- except Exception:
- # probably dead now
- pass
- wait_process_end(pid)
- try:
- remove(pidf)
- except OSError:
- # already removed by twistd
- pass
- print('instance %s stopped' % appid)
-
-
-class RestartInstanceCommand(StartInstanceCommand):
- """Restart the given instances.
-
- <instance>...
- identifiers of the instances to restart. If no instance is
- given, restart them all.
- """
- name = 'restart'
- actionverb = 'restarted'
-
- def run_args(self, args, askconfirm):
- regdir = cwcfg.instances_dir()
- if not isfile(join(regdir, 'startorder')) or len(args) <= 1:
- # no specific startorder
- super(RestartInstanceCommand, self).run_args(args, askconfirm)
- return
- print ('some specific start order is specified, will first stop all '
- 'instances then restart them.')
- # get instances in startorder
- for appid in args:
- if askconfirm:
- print('*'*72)
- if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
- continue
- StopInstanceCommand(self.logger).stop_instance(appid)
- forkcmd = [w for w in sys.argv if not w in args]
- forkcmd[1] = 'start'
- forkcmd = ' '.join(forkcmd)
- for appid in reversed(args):
- status = system('%s %s' % (forkcmd, appid))
- if status:
- sys.exit(status)
-
- def restart_instance(self, appid):
- StopInstanceCommand(self.logger).stop_instance(appid)
- self.start_instance(appid)
-
-
-class ReloadConfigurationCommand(RestartInstanceCommand):
- """Reload the given instances. This command is equivalent to a
- restart for now.
-
- <instance>...
- identifiers of the instances to reload. If no instance is
- given, reload them all.
- """
- name = 'reload'
-
- def reload_instance(self, appid):
- self.restart_instance(appid)
-
-
-class StatusCommand(InstanceCommand):
- """Display status information about the given instances.
-
- <instance>...
- identifiers of the instances to status. If no instance is
- given, get status information about all registered instances.
- """
- name = 'status'
- options = ()
-
- @staticmethod
- def status_instance(appid):
- """print running status information for an instance"""
- status = 0
- for mode in cwcfg.possible_configurations(appid):
- config = cwcfg.config_for(appid, mode)
- print('[%s-%s]' % (appid, mode), end=' ')
- try:
- pidf = config['pid-file']
- except KeyError:
- print('buggy instance, pid file not specified')
- continue
- if not exists(pidf):
- print("doesn't seem to be running")
- status = 1
- continue
- pid = int(open(pidf).read().strip())
- # trick to guess whether or not the process is running
- try:
- getpgid(pid)
- except OSError:
- print("should be running with pid %s but the process can not be found" % pid)
- status = 1
- continue
- print("running with pid %s" % (pid))
- return status
-
-class UpgradeInstanceCommand(InstanceCommandFork):
- """Upgrade an instance after cubicweb and/or component(s) upgrade.
-
- For repository update, you will be prompted for a login / password to use
- to connect to the system database. For some upgrades, the given user
- should have create or alter table permissions.
-
- <instance>...
- identifiers of the instances to upgrade. If no instance is
- given, upgrade them all.
- """
- name = 'upgrade'
- actionverb = 'upgraded'
- options = InstanceCommand.options + (
- ('force-cube-version',
- {'short': 't', 'type' : 'named', 'metavar': 'cube1:X.Y.Z,cube2:X.Y.Z',
- 'default': None,
- 'help': 'force migration from the indicated version for the specified cube(s).'}),
-
- ('force-cubicweb-version',
- {'short': 'e', 'type' : 'string', 'metavar': 'X.Y.Z',
- 'default': None,
- 'help': 'force migration from the indicated cubicweb version.'}),
-
- ('fs-only',
- {'short': 's', 'action' : 'store_true',
- 'default': False,
- 'help': 'only upgrade files on the file system, not the database.'}),
-
- ('nostartstop',
- {'short': 'n', 'action' : 'store_true',
- 'default': False,
- 'help': 'don\'t try to stop instance before migration and to restart it after.'}),
-
- ('verbosity',
- {'short': 'v', 'type' : 'int', 'metavar': '<0..2>',
- 'default': 1,
- 'help': "0: no confirmation, 1: only main commands confirmed, 2 ask \
-for everything."}),
-
- ('backup-db',
- {'short': 'b', 'type' : 'yn', 'metavar': '<y or n>',
- 'default': None,
- 'help': "Backup the instance database before upgrade.\n"\
- "If the option is ommitted, confirmation will be ask.",
- }),
-
- ('ext-sources',
- {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
- 'default': None,
- 'help': "For multisources instances, specify to which sources the \
-repository should connect to for upgrading. When unspecified or 'migration' is \
-given, appropriate sources for migration will be automatically selected \
-(recommended). If 'all' is given, will connect to all defined sources.",
- }),
- )
-
- def upgrade_instance(self, appid):
- print('\n' + underline_title('Upgrading the instance %s' % appid))
- from logilab.common.changelog import Version
- config = cwcfg.config_for(appid)
- instance_running = exists(config['pid-file'])
- config.repairing = True # notice we're not starting the server
- config.verbosity = self.config.verbosity
- set_sources_mode = getattr(config, 'set_sources_mode', None)
- if set_sources_mode is not None:
- set_sources_mode(self.config.ext_sources or ('migration',))
- # get instance and installed versions for the server and the componants
- mih = config.migration_handler()
- repo = mih.repo
- vcconf = repo.get_versions()
- helper = self.config_helper(config, required=False)
- if self.config.force_cube_version:
- for cube, version in self.config.force_cube_version.items():
- vcconf[cube] = Version(version)
- toupgrade = []
- for cube in config.cubes():
- installedversion = config.cube_version(cube)
- try:
- applversion = vcconf[cube]
- except KeyError:
- config.error('no version information for %s' % cube)
- continue
- if installedversion > applversion:
- toupgrade.append( (cube, applversion, installedversion) )
- cubicwebversion = config.cubicweb_version()
- if self.config.force_cubicweb_version:
- applcubicwebversion = Version(self.config.force_cubicweb_version)
- vcconf['cubicweb'] = applcubicwebversion
- else:
- applcubicwebversion = vcconf.get('cubicweb')
- if cubicwebversion > applcubicwebversion:
- toupgrade.append(('cubicweb', applcubicwebversion, cubicwebversion))
- # only stop once we're sure we have something to do
- if instance_running and not (CWDEV or self.config.nostartstop):
- StopInstanceCommand(self.logger).stop_instance(appid)
- # run cubicweb/componants migration scripts
- if self.config.fs_only or toupgrade:
- for cube, fromversion, toversion in toupgrade:
- print('-> migration needed from %s to %s for %s' % (fromversion, toversion, cube))
- with mih.cnx:
- with mih.cnx.security_enabled(False, False):
- mih.migrate(vcconf, reversed(toupgrade), self.config)
- else:
- print('-> no data migration needed for instance %s.' % appid)
- # rewrite main configuration file
- mih.rewrite_configuration()
- mih.shutdown()
- # handle i18n upgrade
- if not self.i18nupgrade(config):
- return
- print()
- if helper:
- helper.postupgrade(repo)
- print('-> instance migrated.')
- if instance_running and not (CWDEV or self.config.nostartstop):
- # restart instance through fork to get a proper environment, avoid
- # uicfg pb (and probably gettext catalogs, to check...)
- forkcmd = '%s start %s' % (sys.argv[0], appid)
- status = system(forkcmd)
- if status:
- print('%s exited with status %s' % (forkcmd, status))
- print()
-
- def i18nupgrade(self, config):
- # handle i18n upgrade:
- # * install new languages
- # * recompile catalogs
- # XXX search available language in the first cube given
- from cubicweb import i18n
- templdir = cwcfg.cube_dir(config.cubes()[0])
- langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))]
- errors = config.i18ncompile(langs)
- if errors:
- print('\n'.join(errors))
- if not ASK.confirm('Error while compiling message catalogs, '
- 'continue anyway?'):
- print('-> migration not completed.')
- return False
- return True
-
-
-class ListVersionsInstanceCommand(InstanceCommand):
- """List versions used by an instance.
-
- <instance>...
- identifiers of the instances to list versions for.
- """
- name = 'versions'
-
- def versions_instance(self, appid):
- config = cwcfg.config_for(appid)
- # should not raise error if db versions don't match fs versions
- config.repairing = True
- # no need to load all appobjects and schema
- config.quick_start = True
- if hasattr(config, 'set_sources_mode'):
- config.set_sources_mode(('migration',))
- vcconf = config.repository().get_versions()
- for key in sorted(vcconf):
- print(key+': %s.%s.%s' % vcconf[key])
-
-class ShellCommand(Command):
- """Run an interactive migration shell on an instance. This is a python shell
- with enhanced migration commands predefined in the namespace. An additional
- argument may be given corresponding to a file containing commands to execute
- in batch mode.
-
- By default it will connect to a local instance using an in memory
- connection, unless a URL to a running instance is specified.
-
- Arguments after bare "--" string will not be processed by the shell command
- You can use it to pass extra arguments to your script and expect for
- them in '__args__' afterwards.
-
- <instance>
- the identifier of the instance to connect.
- """
- name = 'shell'
- arguments = '<instance> [batch command file(s)] [-- <script arguments>]'
- min_args = 1
- options = (
- ('system-only',
- {'short': 'S', 'action' : 'store_true',
- 'help': 'only connect to the system source when the instance is '
- 'using multiple sources. You can\'t use this option and the '
- '--ext-sources option at the same time.',
- 'group': 'local'
- }),
-
- ('ext-sources',
- {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
- 'help': "For multisources instances, specify to which sources the \
-repository should connect to for upgrading. When unspecified or 'all' given, \
-will connect to all defined sources. If 'migration' is given, appropriate \
-sources for migration will be automatically selected.",
- 'group': 'local'
- }),
-
- ('force',
- {'short': 'f', 'action' : 'store_true',
- 'help': 'don\'t check instance is up to date.',
- 'group': 'local'
- }),
-
- ('repo-uri',
- {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
- 'help': 'URI of the CubicWeb repository to connect to. URI can be \
-a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
-directly give URI as instance id instead',
- 'group': 'remote'
- }),
- )
-
- def _handle_inmemory(self, appid):
- """ returns migration context handler & shutdown function """
- config = cwcfg.config_for(appid)
- if self.config.ext_sources:
- assert not self.config.system_only
- sources = self.config.ext_sources
- elif self.config.system_only:
- sources = ('system',)
- else:
- sources = ('all',)
- config.set_sources_mode(sources)
- config.repairing = self.config.force
- mih = config.migration_handler()
- return mih, lambda: mih.shutdown()
-
- def _handle_networked(self, appuri):
- """ returns migration context handler & shutdown function """
- from cubicweb import AuthenticationError
- from cubicweb.repoapi import connect, get_repository
- from cubicweb.server.utils import manager_userpasswd
- from cubicweb.server.migractions import ServerMigrationHelper
- while True:
- try:
- login, pwd = manager_userpasswd(msg=None)
- repo = get_repository(appuri)
- cnx = connect(repo, login=login, password=pwd, mulcnx=False)
- except AuthenticationError as ex:
- print(ex)
- except (KeyboardInterrupt, EOFError):
- print()
- sys.exit(0)
- else:
- break
- cnx.load_appobjects()
- repo = cnx._repo
- mih = ServerMigrationHelper(None, repo=repo, cnx=cnx, verbosity=0,
- # hack so it don't try to load fs schema
- schema=1)
- return mih, lambda: cnx.close()
-
- def run(self, args):
- appuri = args.pop(0)
- if self.config.repo_uri:
- warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
- DeprecationWarning)
- if urlparse(self.config.repo_uri).scheme == 'inmemory':
- appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)
-
- from cubicweb.utils import parse_repo_uri
- protocol, hostport, appid = parse_repo_uri(appuri)
- if protocol == 'inmemory':
- mih, shutdown_callback = self._handle_inmemory(appid)
- else:
- mih, shutdown_callback = self._handle_networked(appuri)
- try:
- with mih.cnx:
- with mih.cnx.security_enabled(False, False):
- if args:
- # use cmdline parser to access left/right attributes only
- # remember that usage requires instance appid as first argument
- scripts, args = self.cmdline_parser.largs[1:], self.cmdline_parser.rargs
- for script in scripts:
- mih.cmd_process_script(script, scriptargs=args)
- mih.commit()
- else:
- mih.interactive_shell()
- finally:
- shutdown_callback()
-
-
-class RecompileInstanceCatalogsCommand(InstanceCommand):
- """Recompile i18n catalogs for instances.
-
- <instance>...
- identifiers of the instances to consider. If no instance is
- given, recompile for all registered instances.
- """
- name = 'i18ninstance'
-
- @staticmethod
- def i18ninstance_instance(appid):
- """recompile instance's messages catalogs"""
- config = cwcfg.config_for(appid)
- config.quick_start = True # notify this is not a regular start
- repo = config.repository()
- if config._cubes is None:
- # web only config
- config.init_cubes(repo.get_cubes())
- errors = config.i18ncompile()
- if errors:
- print('\n'.join(errors))
-
-
-class ListInstancesCommand(Command):
- """list available instances, useful for bash completion."""
- name = 'listinstances'
- hidden = True
-
- def run(self, args):
- """run the command with its specific arguments"""
- regdir = cwcfg.instances_dir()
- for appid in sorted(listdir(regdir)):
- print(appid)
-
-
-class ListCubesCommand(Command):
- """list available componants, useful for bash completion."""
- name = 'listcubes'
- hidden = True
-
- def run(self, args):
- """run the command with its specific arguments"""
- for cube in cwcfg.available_cubes():
- print(cube)
-
-class ConfigureInstanceCommand(InstanceCommand):
- """Configure instance.
-
- <instance>...
- identifier of the instance to configure.
- """
- name = 'configure'
- actionverb = 'configured'
-
- options = merge_options(InstanceCommand.options +
- (('param',
- {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
- 'default': None,
- 'help': 'set <key> to <value> in configuration file.',
- }),
- ))
-
- def configure_instance(self, appid):
- if self.config.param is not None:
- appcfg = cwcfg.config_for(appid)
- for key, value in self.config.param.items():
- try:
- appcfg.global_set_option(key, value)
- except KeyError:
- raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name))
- appcfg.save()
-
-
-# WSGI #########
-
-WSGI_CHOICES = {}
-from cubicweb.wsgi import server as stdlib_server
-WSGI_CHOICES['stdlib'] = stdlib_server
-try:
- from cubicweb.wsgi import wz
-except ImportError:
- pass
-else:
- WSGI_CHOICES['werkzeug'] = wz
-try:
- from cubicweb.wsgi import tnd
-except ImportError:
- pass
-else:
- WSGI_CHOICES['tornado'] = tnd
-
-
-def wsgichoices():
- return tuple(WSGI_CHOICES)
-
-
-class WSGIStartHandler(InstanceCommand):
- """Start an interactive wsgi server """
- name = 'wsgi'
- actionverb = 'started'
- arguments = '<instance>'
-
- @property
- def options(self):
- return (
- ("debug",
- {'short': 'D', 'action': 'store_true',
- 'default': False,
- 'help': 'start server in debug mode.'}),
- ('method',
- {'short': 'm',
- 'type': 'choice',
- 'metavar': '<method>',
- 'default': 'stdlib',
- 'choices': wsgichoices(),
- 'help': 'wsgi utility/method'}),
- ('loglevel',
- {'short': 'l',
- 'type': 'choice',
- 'metavar': '<log level>',
- 'default': None,
- 'choices': ('debug', 'info', 'warning', 'error'),
- 'help': 'debug if -D is set, error otherwise',
- }),
- )
-
- def wsgi_instance(self, appid):
- config = cwcfg.config_for(appid, debugmode=self['debug'])
- init_cmdline_log_threshold(config, self['loglevel'])
- assert config.name == 'all-in-one'
- meth = self['method']
- server = WSGI_CHOICES[meth]
- return server.run(config)
-
-
-
-for cmdcls in (ListCommand,
- CreateInstanceCommand, DeleteInstanceCommand,
- StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
- WSGIStartHandler,
- ReloadConfigurationCommand, StatusCommand,
- UpgradeInstanceCommand,
- ListVersionsInstanceCommand,
- ShellCommand,
- RecompileInstanceCatalogsCommand,
- ListInstancesCommand, ListCubesCommand,
- ConfigureInstanceCommand,
- ):
- CWCTL.register(cmdcls)
-
-
-
-def run(args):
- """command line tool"""
- import os
- filterwarnings('default', category=DeprecationWarning)
- cwcfg.load_cwctl_plugins()
- try:
- CWCTL.run(args)
- except ConfigurationError as err:
- print('ERROR: ', err)
- sys.exit(1)
- except ExecutionError as err:
- print(err)
- sys.exit(2)
-
-if __name__ == '__main__':
- run(sys.argv[1:])