author | Denis Laxalde <denis.laxalde@logilab.fr> |
Mon, 19 Jun 2017 18:15:28 +0200 | |
changeset 12188 | fea018b2e056 |
parent 12139 | b5be819872bb |
child 12253 | 9165f4b7426b |
permissions | -rw-r--r-- |
# copyright 2003-2016 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 # *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, 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 logilab.common.decorators import clear_cache from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage 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 def available_cube_names(cwcfg): """Return a list of available cube names, with 'cubicweb_' prefix dropped. """ def drop_prefix(cube): prefix = 'cubicweb_' if cube.startswith(prefix): cube = cube[len(prefix):] return cube return [drop_prefix(cube) for cube in cwcfg.available_cubes()] 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 run(self, args): """run the <command>_method on each argument (a list of instance identifiers) """ if not args: args = list_instances(cwcfg.instances_dir()) 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'): cwcfg.load_available_configs() 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()) cube_names = available_cube_names(cwcfg) namesize = max(len(x) for x in cube_names) 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 cube_names: 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', 'pyramid'), '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(available_cube_names(cwcfg))) 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', 0) <= 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} pyramid {appid}'\n" "- '{ctl} wsgi {appid}'\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 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 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.'}), ('no-config-update', {'short': 'C', 'action': 'store_true', 'default': False, 'help': 'do NOT update config file if set.'}), ('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) clear_cache(config, 'instance_md5_version') else: print('-> no data migration needed for instance %s.' % appid) # rewrite main configuration file if not self.config.no_config_update: 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 _get_mih(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 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) mih, shutdown_callback = self._get_mih(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 = {} try: from cubicweb.wsgi import server as stdlib_server except ImportError: pass else: 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) if 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) CWCTL.register(WSGIStartHandler) for cmdcls in (ListCommand, CreateInstanceCommand, DeleteInstanceCommand, StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand, 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:])