"""%%prog %s [options] %s
CubicWeb main instances controller.
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
%s"""
import sys
from os import remove, listdir, system, kill, getpgid, pathsep
from os.path import exists, join, isfile, isdir
from logilab.common.clcommands import register_commands, pop_arg
from logilab.common.shellutils import ASK
from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage, underline_title
from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CONFIGURATIONS
from cubicweb.toolsutils import Command, main_run, rm, create_dir
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:
break
nbtry += 1
sleep(waittime)
else:
raise ExecutionError('can\'t kill process %s' % pid)
def list_instances(regdir):
return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir)))
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
"""
regdir = cwcfg.registry_dir()
_allinstances = list_instances(regdir)
if isfile(join(regdir, 'startorder')):
allinstances = []
for line in file(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):
for appid in args:
if askconfirm:
print '*'*72
if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
continue
self.run_arg(appid)
def run_arg(self, appid):
cmdmeth = getattr(self, '%s_instance' % self.name)
try:
cmdmeth(appid)
except (KeyboardInterrupt, SystemExit):
print >> sys.stderr, '%s aborted' % self.name
sys.exit(2) # specific error code
except (ExecutionError, ConfigurationError), ex:
print >> sys.stderr, 'instance %s not %s: %s' % (
appid, self.actionverb, ex)
except Exception, ex:
import traceback
traceback.print_exc()
print >> sys.stderr, 'instance %s not %s: %s' % (
appid, self.actionverb, ex)
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
"""
name = 'list'
options = (
('verbose',
{'short': 'v', 'action' : 'store_true',
'help': "display more information."}),
)
def run(self, args):
"""run the command with its specific arguments"""
if args:
raise BadCommandUsage('Too much arguments')
print 'CubicWeb version:', cwcfg.cubicweb_version()
print 'Detected mode:', cwcfg.mode
print
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
try:
cubesdir = pathsep.join(cwcfg.cubes_search_path())
namesize = max(len(x) for x in cwcfg.available_cubes())
except ConfigurationError, 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():
if cube in ('CVS', '.svn', 'shared', '.hg'):
continue
try:
tinfo = cwcfg.cube_pkginfo(cube)
tversion = tinfo.version
except ConfigurationError:
tinfo = None
tversion = '[missing cube information]'
print '* %s %s' % (cube.ljust(namesize), tversion)
if self.config.verbose:
shortdesc = tinfo and (getattr(tinfo, 'short_desc', '')
or tinfo.__doc__)
if shortdesc:
print ' '+ ' \n'.join(shortdesc.splitlines())
modes = detect_available_modes(cwcfg.cube_dir(cube))
print ' available modes: %s' % ', '.join(modes)
print
try:
regdir = cwcfg.registry_dir()
except ConfigurationError, 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, exc:
print ' (BROKEN instance, %s)' % exc
continue
else:
print 'No instance available in %s' % regdir
print
class CreateInstanceCommand(Command):
"""Create an instance from a cube. This is an 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,eemail')
<instance>
an identifier for the instance to create
"""
name = 'create'
arguments = '<cube> <instance>'
options = (
("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', 'twisted'),
'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.',
}
),
)
def run(self, args):
"""run the command with its specific arguments"""
from logilab.common.textutils import splitstrip
configname = self.config.config
cubes = splitstrip(pop_arg(args, 1))
appid = pop_arg(args)
# get the configuration and helper
cwcfg.creating = True
config = cwcfg.config_for(appid, configname)
config.set_language = False
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, ex:
print ex
print '\navailable cubes:',
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)
# load site_cubicweb from the cubes dir (if any)
config.load_site_cubicweb()
# cubicweb-ctl configuration
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.config_level)
# write down configuration
config.save()
print '-> generated %s' % config.main_config_file()
# handle i18n files structure
# in the first cube given
print '-> preparing i18n catalogs'
from cubicweb.common 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 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 %r.\n' % config.apphome
helper.postcreate()
class DeleteInstanceCommand(Command):
"""Delete an instance. Will remove instance's files and
unregister it.
"""
name = 'delete'
arguments = '<instance>'
options = ()
def run(self, args):
"""run the command with its specific arguments"""
appid = pop_arg(args, msg="No instance specified !")
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, 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(InstanceCommand):
"""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',
}),
)
def start_instance(self, appid):
"""start the instance's server"""
# use get() since start may be used from other commands (eg upgrade)
# without all options defined
debug = self.get('debug')
force = self.get('force')
loglevel = self.get('loglevel')
config = cwcfg.config_for(appid)
if loglevel is not None:
loglevel = 'LOG_%s' % loglevel.upper()
config.global_set_option('log-threshold', loglevel)
config.init_log(loglevel, debug=debug, force=True)
if self.get('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 force:
msg = "%s seems to be running. Remove %s by hand if necessary or use \
the --force option."
raise ExecutionError(msg % (appid, pidf))
helper.start_command(config, debug)
return 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):
print >> sys.stderr, "%s doesn't exist." % pidf
return
import signal
pid = int(open(pidf).read().strip())
try:
kill(pid, signal.SIGTERM)
except:
print >> sys.stderr, "process %s seems already dead." % pid
else:
try:
wait_process_end(pid)
except ExecutionError, ex:
print >> sys.stderr, ex
print >> sys.stderr, 'trying SIGKILL'
try:
kill(pid, signal.SIGKILL)
except:
# 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,
StopInstanceCommand):
"""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.registry_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
stopped = []
for appid in args:
if askconfirm:
print '*'*72
if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
continue
self.stop_instance(appid)
stopped.append(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):
self.stop_instance(appid)
if self.start_instance(appid):
print 'instance %s %s' % (appid, self.actionverb)
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"""
for mode in cwcfg.possible_configurations(appid):
config = cwcfg.config_for(appid, mode)
print '[%s-%s]' % (appid, mode),
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"
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
continue
print "running with pid %s" % (pid)
class UpgradeInstanceCommand(InstanceCommandFork,
StartInstanceCommand,
StopInstanceCommand):
"""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-componant-version',
{'short': 't', 'type' : 'csv', 'metavar': 'cube1=X.Y.Z,cube2=X.Y.Z',
'default': None,
'help': 'force migration from the indicated version for the specified cube.'}),
('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 ordered_instances(self):
# need this since mro return StopInstanceCommand implementation
return InstanceCommand.ordered_instances(self)
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)
config.repairing = True # notice we're not starting the server
config.verbosity = self.config.verbosity
try:
config.set_sources_mode(self.config.ext_sources or ('migration',))
except AttributeError:
# not a server config
pass
# get instance and installed versions for the server and the componants
mih = config.migration_handler()
repo = mih.repo_connect()
vcconf = repo.get_versions()
if self.config.force_componant_version:
packversions = {}
for vdef in self.config.force_componant_version:
componant, version = vdef.split('=')
packversions[componant] = Version(version)
vcconf.update(packversions)
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))
if not self.config.fs_only and not toupgrade:
print '-> no software migration needed for instance %s.' % appid
return
for cube, fromversion, toversion in toupgrade:
print '-> migration needed from %s to %s for %s' % (fromversion, toversion, cube)
# only stop once we're sure we have something to do
if not (cwcfg.mode == 'dev' or self.config.nostartstop):
self.stop_instance(appid)
# run cubicweb/componants migration scripts
mih.migrate(vcconf, reversed(toupgrade), self.config)
# rewrite main configuration file
mih.rewrite_configuration()
# handle i18n upgrade:
# * install new languages
# * recompile catalogs
# in the first componant given
from cubicweb.common 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
mih.shutdown()
print
print '-> instance migrated.'
if not (cwcfg.mode == 'dev' or self.config.nostartstop):
self.start_instance(appid)
print
class ShellCommand(Command):
"""Run an interactive migration shell. 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.
<instance>
the identifier of the instance to connect.
"""
name = 'shell'
arguments = '<instance> [batch command file]'
options = (
('system-only',
{'short': 'S', 'action' : 'store_true',
'default': False,
'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.'}),
('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 'all' given, \
will connect to all defined sources. If 'migration' is given, appropriate \
sources for migration will be automatically selected.",
}),
('force',
{'short': 'f', 'action' : 'store_true',
'default' : False,
'help': 'don\'t check instance is up to date.'}
),
)
def run(self, args):
appid = pop_arg(args, 99, msg="No instance specified !")
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()
if args:
for arg in args:
mih.process_script(arg)
else:
mih.interactive_shell()
mih.shutdown()
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)
try:
config.bootstrap_cubes()
except IOError, ex:
import errno
if ex.errno != errno.ENOENT:
raise
# bootstrap_cubes files doesn't exist
# notify this is not a regular start
config.repairing = True
# create an in-memory repository, will call config.init_cubes()
config.repository()
except AttributeError:
# web only config
config.init_cubes(config.repository().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.registry_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
register_commands((ListCommand,
CreateInstanceCommand,
DeleteInstanceCommand,
StartInstanceCommand,
StopInstanceCommand,
RestartInstanceCommand,
ReloadConfigurationCommand,
StatusCommand,
UpgradeInstanceCommand,
ShellCommand,
RecompileInstanceCatalogsCommand,
ListInstancesCommand, ListCubesCommand,
))
def run(args):
"""command line tool"""
cwcfg.load_cwctl_plugins()
main_run(args, __doc__)
if __name__ == '__main__':
run(sys.argv[1:])