|
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:]) |