cubicweb/server/serverctl.py
changeset 11126 9bacaf91afbc
parent 11057 0b59724cb3f2
child 11190 9b3a4ceff06a
equal deleted inserted replaced
11125:e717da3dc164 11126:9bacaf91afbc
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    24 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
    24 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
    25 # completion). So import locally in command helpers.
    25 # completion). So import locally in command helpers.
    26 import sys
    26 import sys
    27 import os
    27 import os
    28 from contextlib import contextmanager
    28 from contextlib import contextmanager
    29 import logging
       
    30 import subprocess
       
    31 
    29 
    32 from six import string_types
    30 from six import string_types
    33 from six.moves import input
    31 from six.moves import input
    34 
    32 
    35 from logilab.common import nullobject
       
    36 from logilab.common.configuration import Configuration, merge_options
    33 from logilab.common.configuration import Configuration, merge_options
    37 from logilab.common.shellutils import ASK, generate_password
    34 from logilab.common.shellutils import ASK, generate_password
    38 
    35 
    39 from logilab.database import get_db_helper, get_connection
    36 from logilab.database import get_db_helper, get_connection
    40 
    37 
    43 from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
    40 from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
    44 from cubicweb.server import SOURCE_TYPES
    41 from cubicweb.server import SOURCE_TYPES
    45 from cubicweb.server.serverconfig import (
    42 from cubicweb.server.serverconfig import (
    46     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
    43     USER_OPTIONS, ServerConfiguration, SourceConfiguration,
    47     ask_source_config, generate_source_config)
    44     ask_source_config, generate_source_config)
       
    45 
    48 
    46 
    49 # utility functions ###########################################################
    47 # utility functions ###########################################################
    50 
    48 
    51 def source_cnx(source, dbname=None, special_privs=False, interactive=True):
    49 def source_cnx(source, dbname=None, special_privs=False, interactive=True):
    52     """open and return a connection to the system database defined in the
    50     """open and return a connection to the system database defined in the
   100         from logilab.database import _SimpleConnectionWrapper
    98         from logilab.database import _SimpleConnectionWrapper
   101         cnx = _SimpleConnectionWrapper(cnx)
    99         cnx = _SimpleConnectionWrapper(cnx)
   102         cnx.logged_user = user
   100         cnx.logged_user = user
   103     return cnx
   101     return cnx
   104 
   102 
       
   103 
   105 def system_source_cnx(source, dbms_system_base=False,
   104 def system_source_cnx(source, dbms_system_base=False,
   106                       special_privs='CREATE/DROP DATABASE', interactive=True):
   105                       special_privs='CREATE/DROP DATABASE', interactive=True):
   107     """shortcut to get a connextion to the instance system database
   106     """shortcut to get a connextion to the instance system database
   108     defined in the given config. If <dbms_system_base> is True,
   107     defined in the given config. If <dbms_system_base> is True,
   109     connect to the dbms system database instead (for task such as
   108     connect to the dbms system database instead (for task such as
   114         return source_cnx(source, system_db, special_privs=special_privs,
   113         return source_cnx(source, system_db, special_privs=special_privs,
   115                           interactive=interactive)
   114                           interactive=interactive)
   116     return source_cnx(source, special_privs=special_privs,
   115     return source_cnx(source, special_privs=special_privs,
   117                       interactive=interactive)
   116                       interactive=interactive)
   118 
   117 
       
   118 
   119 def _db_sys_cnx(source, special_privs, interactive=True):
   119 def _db_sys_cnx(source, special_privs, interactive=True):
   120     """return a connection on the RDMS system table (to create/drop a user or a
   120     """return a connection on the RDMS system table (to create/drop a user or a
   121     database)
   121     database)
   122     """
   122     """
   123     import logilab.common as lgp
   123     import logilab.common as lgp
   124     lgp.USE_MX_DATETIME = False
   124     lgp.USE_MX_DATETIME = False
   125     driver = source['db-driver']
       
   126     helper = get_db_helper(driver)
       
   127     # connect on the dbms system base to create our base
   125     # connect on the dbms system base to create our base
   128     cnx = system_source_cnx(source, True, special_privs=special_privs,
   126     cnx = system_source_cnx(source, True, special_privs=special_privs,
   129                             interactive=interactive)
   127                             interactive=interactive)
   130     # disable autocommit (isolation_level(1)) because DROP and
   128     # disable autocommit (isolation_level(1)) because DROP and
   131     # CREATE DATABASE can't be executed in a transaction
   129     # CREATE DATABASE can't be executed in a transaction
   132     set_isolation_level = getattr(cnx, 'set_isolation_level', None)
   130     set_isolation_level = getattr(cnx, 'set_isolation_level', None)
   133     if set_isolation_level is not None:
   131     if set_isolation_level is not None:
   134         # set_isolation_level() is psycopg specific
   132         # set_isolation_level() is psycopg specific
   135         set_isolation_level(0)
   133         set_isolation_level(0)
   136     return cnx
   134     return cnx
       
   135 
   137 
   136 
   138 def repo_cnx(config):
   137 def repo_cnx(config):
   139     """return a in-memory repository and a repoapi connection to it"""
   138     """return a in-memory repository and a repoapi connection to it"""
   140     from cubicweb import repoapi
   139     from cubicweb import repoapi
   141     from cubicweb.server.utils import manager_userpasswd
   140     from cubicweb.server.utils import manager_userpasswd
   168         """
   167         """
   169         config = self.config
   168         config = self.config
   170         if not automatic:
   169         if not automatic:
   171             print(underline_title('Configuring the repository'))
   170             print(underline_title('Configuring the repository'))
   172             config.input_config('email', inputlevel)
   171             config.input_config('email', inputlevel)
   173             print('\n'+underline_title('Configuring the sources'))
   172             print('\n' + underline_title('Configuring the sources'))
   174         sourcesfile = config.sources_file()
       
   175         # hack to make Method('default_instance_id') usable in db option defs
   173         # hack to make Method('default_instance_id') usable in db option defs
   176         # (in native.py)
   174         # (in native.py)
   177         sconfig = SourceConfiguration(config,
   175         sconfig = SourceConfiguration(config,
   178                                       options=SOURCE_TYPES['native'].options)
   176                                       options=SOURCE_TYPES['native'].options)
   179         if not automatic:
   177         if not automatic:
   247             helper = get_db_helper(source['db-driver'])
   245             helper = get_db_helper(source['db-driver'])
   248             helper.drop_schema(cursor, db_namespace)
   246             helper.drop_schema(cursor, db_namespace)
   249             print('-> database schema %s dropped' % db_namespace)
   247             print('-> database schema %s dropped' % db_namespace)
   250 
   248 
   251     def _drop_database(self, source):
   249     def _drop_database(self, source):
   252         dbname = source['db-name']
       
   253         if source['db-driver'] == 'sqlite':
   250         if source['db-driver'] == 'sqlite':
   254             print('deleting database file %(db-name)s' % source)
   251             print('deleting database file %(db-name)s' % source)
   255             os.unlink(source['db-name'])
   252             os.unlink(source['db-name'])
   256             print('-> database %(db-name)s dropped.' % source)
   253             print('-> database %(db-name)s dropped.' % source)
   257         else:
   254         else:
   258             helper = get_db_helper(source['db-driver'])
       
   259             with db_sys_transaction(source, privilege='DROP DATABASE') as cursor:
   255             with db_sys_transaction(source, privilege='DROP DATABASE') as cursor:
   260                 print('dropping database %(db-name)s' % source)
   256                 print('dropping database %(db-name)s' % source)
   261                 cursor.execute('DROP DATABASE "%(db-name)s"' % source)
   257                 cursor.execute('DROP DATABASE "%(db-name)s"' % source)
   262                 print('-> database %(db-name)s dropped.' % source)
   258                 print('-> database %(db-name)s dropped.' % source)
   263 
   259 
   324     name = 'db-create'
   320     name = 'db-create'
   325     arguments = '<instance>'
   321     arguments = '<instance>'
   326     min_args = max_args = 1
   322     min_args = max_args = 1
   327     options = (
   323     options = (
   328         ('automatic',
   324         ('automatic',
   329          {'short': 'a', 'action' : 'store_true',
   325          {'short': 'a', 'action': 'store_true',
   330           'default': False,
   326           'default': False,
   331           'help': 'automatic mode: never ask and use default answer to every '
   327           'help': 'automatic mode: never ask and use default answer to every '
   332           'question. this may require that your login match a database super '
   328           'question. this may require that your login match a database super '
   333           'user (allowed to create database & all).',
   329           'user (allowed to create database & all).',
   334           }),
   330           }),
   335         ('config-level',
   331         ('config-level',
   336          {'short': 'l', 'type' : 'int', 'metavar': '<level>',
   332          {'short': 'l', 'type': 'int', 'metavar': '<level>',
   337           'default': 0,
   333           'default': 0,
   338           'help': 'configuration level (0..2): 0 will ask for essential '
   334           'help': 'configuration level (0..2): 0 will ask for essential '
   339           'configuration parameters only while 2 will ask for all parameters',
   335           'configuration parameters only while 2 will ask for all parameters',
   340           }),
   336           }),
   341         ('create-db',
   337         ('create-db',
   342          {'short': 'c', 'type': 'yn', 'metavar': '<y or n>',
   338          {'short': 'c', 'type': 'yn', 'metavar': '<y or n>',
   343           'default': True,
   339           'default': True,
   344           'help': 'create the database (yes by default)'
   340           'help': 'create the database (yes by default)'
   345           }),
   341           }),
   346         )
   342     )
   347 
   343 
   348     def run(self, args):
   344     def run(self, args):
   349         """run the command with its specific arguments"""
   345         """run the command with its specific arguments"""
   350         check_options_consistency(self.config)
   346         check_options_consistency(self.config)
   351         automatic = self.get('automatic')
   347         automatic = self.get('automatic')
   355         dbname = source['db-name']
   351         dbname = source['db-name']
   356         driver = source['db-driver']
   352         driver = source['db-driver']
   357         helper = get_db_helper(driver)
   353         helper = get_db_helper(driver)
   358         if driver == 'sqlite':
   354         if driver == 'sqlite':
   359             if os.path.exists(dbname) and (
   355             if os.path.exists(dbname) and (
   360                 automatic or
   356                     automatic or
   361                 ASK.confirm('Database %s already exists. Drop it?' % dbname)):
   357                     ASK.confirm('Database %s already exists. Drop it?' % dbname)):
   362                 os.unlink(dbname)
   358                 os.unlink(dbname)
   363         elif self.config.create_db:
   359         elif self.config.create_db:
   364             print('\n'+underline_title('Creating the system database'))
   360             print('\n' + underline_title('Creating the system database'))
   365             # connect on the dbms system base to create our base
   361             # connect on the dbms system base to create our base
   366             dbcnx = _db_sys_cnx(source, 'CREATE/DROP DATABASE and / or USER',
   362             dbcnx = _db_sys_cnx(source, 'CREATE/DROP DATABASE and / or USER',
   367                                 interactive=not automatic)
   363                                 interactive=not automatic)
   368             cursor = dbcnx.cursor()
   364             cursor = dbcnx.cursor()
   369             try:
   365             try:
   370                 if helper.users_support:
   366                 if helper.users_support:
   371                     user = source['db-user']
   367                     user = source['db-user']
   372                     if not helper.user_exists(cursor, user) and (automatic or \
   368                     if not helper.user_exists(cursor, user) and (
   373                            ASK.confirm('Create db user %s ?' % user, default_is_yes=False)):
   369                             automatic or
       
   370                             ASK.confirm('Create db user %s ?' % user, default_is_yes=False)):
   374                         helper.create_user(source['db-user'], source.get('db-password'))
   371                         helper.create_user(source['db-user'], source.get('db-password'))
   375                         print('-> user %s created.' % user)
   372                         print('-> user %s created.' % user)
   376                 if dbname in helper.list_databases(cursor):
   373                 if dbname in helper.list_databases(cursor):
   377                     if automatic or ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
   374                     if automatic or ASK.confirm('Database %s already exists -- '
       
   375                                                 'do you want to drop it ?' % dbname):
   378                         cursor.execute('DROP DATABASE "%s"' % dbname)
   376                         cursor.execute('DROP DATABASE "%s"' % dbname)
   379                     else:
   377                     else:
   380                         print('you may want to run "cubicweb-ctl db-init '
   378                         print('you may want to run "cubicweb-ctl db-init '
   381                               '--drop %s" manually to continue.' % config.appid)
   379                               '--drop %s" manually to continue.' % config.appid)
   382                         return
   380                         return
   403                 if automatic or ASK.confirm('Create language %s ?' % extlang):
   401                 if automatic or ASK.confirm('Create language %s ?' % extlang):
   404                     try:
   402                     try:
   405                         helper.create_language(cursor, extlang)
   403                         helper.create_language(cursor, extlang)
   406                     except Exception as exc:
   404                     except Exception as exc:
   407                         print('-> ERROR:', exc)
   405                         print('-> ERROR:', exc)
   408                         print('-> could not create language %s, some stored procedures might be unusable' % extlang)
   406                         print('-> could not create language %s, '
       
   407                               'some stored procedures might be unusable' % extlang)
   409                         cnx.rollback()
   408                         cnx.rollback()
   410                     else:
   409                     else:
   411                         cnx.commit()
   410                         cnx.commit()
   412         print('-> database for instance %s created and necessary extensions installed.' % appid)
   411         print('-> database for instance %s created and necessary extensions installed.' % appid)
   413         print()
   412         print()
   434     name = 'db-init'
   433     name = 'db-init'
   435     arguments = '<instance>'
   434     arguments = '<instance>'
   436     min_args = max_args = 1
   435     min_args = max_args = 1
   437     options = (
   436     options = (
   438         ('automatic',
   437         ('automatic',
   439          {'short': 'a', 'action' : 'store_true',
   438          {'short': 'a', 'action': 'store_true',
   440           'default': False,
   439           'default': False,
   441           'help': 'automatic mode: never ask and use default answer to every '
   440           'help': 'automatic mode: never ask and use default answer to every '
   442           'question.',
   441           'question.',
   443           }),
   442           }),
   444         ('config-level',
   443         ('config-level',
   450          {'short': 'd', 'action': 'store_true',
   449          {'short': 'd', 'action': 'store_true',
   451           'default': False,
   450           'default': False,
   452           'help': 'insert drop statements to remove previously existant '
   451           'help': 'insert drop statements to remove previously existant '
   453           'tables, indexes... (no by default)'
   452           'tables, indexes... (no by default)'
   454           }),
   453           }),
   455         )
   454     )
   456 
   455 
   457     def run(self, args):
   456     def run(self, args):
   458         check_options_consistency(self.config)
   457         check_options_consistency(self.config)
   459         print('\n'+underline_title('Initializing the system database'))
   458         print('\n' + underline_title('Initializing the system database'))
   460         from cubicweb.server import init_repository
   459         from cubicweb.server import init_repository
   461         appid = args[0]
   460         appid = args[0]
   462         config = ServerConfiguration.config_for(appid)
   461         config = ServerConfiguration.config_for(appid)
   463         try:
   462         try:
   464             system = config.system_source_config
   463             system = config.system_source_config
   469                 host=system.get('db-host'), port=system.get('db-port'),
   468                 host=system.get('db-host'), port=system.get('db-port'),
   470                 user=system.get('db-user') or '', password=system.get('db-password') or '',
   469                 user=system.get('db-user') or '', password=system.get('db-password') or '',
   471                 schema=system.get('db-namespace'), **extra)
   470                 schema=system.get('db-namespace'), **extra)
   472         except Exception as ex:
   471         except Exception as ex:
   473             raise ConfigurationError(
   472             raise ConfigurationError(
   474                 'You seem to have provided wrong connection information in '\
   473                 'You seem to have provided wrong connection information in '
   475                 'the %s file. Resolve this first (error: %s).'
   474                 'the %s file. Resolve this first (error: %s).'
   476                 % (config.sources_file(), str(ex).strip()))
   475                 % (config.sources_file(), str(ex).strip()))
   477         init_repository(config, drop=self.config.drop)
   476         init_repository(config, drop=self.config.drop)
   478         if not self.config.automatic:
   477         if not self.config.automatic:
   479             while ASK.confirm('Enter another source ?', default_is_yes=False):
   478             while ASK.confirm('Enter another source ?', default_is_yes=False):
   493     options = (
   492     options = (
   494         ('config-level',
   493         ('config-level',
   495          {'short': 'l', 'type': 'int', 'default': 1,
   494          {'short': 'l', 'type': 'int', 'default': 1,
   496           'help': 'level threshold for questions asked when configuring another source'
   495           'help': 'level threshold for questions asked when configuring another source'
   497           }),
   496           }),
   498         )
   497     )
   499 
   498 
   500     def run(self, args):
   499     def run(self, args):
   501         appid = args[0]
   500         appid = args[0]
   502         config = ServerConfiguration.config_for(appid)
   501         config = ServerConfiguration.config_for(appid)
   503         repo, cnx = repo_cnx(config)
   502         repo, cnx = repo_cnx(config)
   506             with cnx:
   505             with cnx:
   507                 used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
   506                 used = set(n for n, in cnx.execute('Any SN WHERE S is CWSource, S name SN'))
   508                 cubes = repo.get_cubes()
   507                 cubes = repo.get_cubes()
   509                 while True:
   508                 while True:
   510                     type = input('source type (%s): '
   509                     type = input('source type (%s): '
   511                                         % ', '.join(sorted(SOURCE_TYPES)))
   510                                  % ', '.join(sorted(SOURCE_TYPES)))
   512                     if type not in SOURCE_TYPES:
   511                     if type not in SOURCE_TYPES:
   513                         print('-> unknown source type, use one of the available types.')
   512                         print('-> unknown source type, use one of the available types.')
   514                         continue
   513                         continue
   515                     sourcemodule = SOURCE_TYPES[type].module
   514                     sourcemodule = SOURCE_TYPES[type].module
   516                     if not sourcemodule.startswith('cubicweb.'):
   515                     if not sourcemodule.startswith('cubicweb.'):
   517                         # module names look like cubes.mycube.themodule
   516                         # module names look like cubes.mycube.themodule
   518                         sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
   517                         sourcecube = SOURCE_TYPES[type].module.split('.', 2)[1]
   519                         # if the source adapter is coming from an external component,
   518                         # if the source adapter is coming from an external component,
   520                         # ensure it's specified in used cubes
   519                         # ensure it's specified in used cubes
   521                         if not sourcecube in cubes:
   520                         if sourcecube not in cubes:
   522                             print ('-> this source type require the %s cube which is '
   521                             print ('-> this source type require the %s cube which is '
   523                                    'not used by the instance.')
   522                                    'not used by the instance.')
   524                             continue
   523                             continue
   525                     break
   524                     break
   526                 while True:
   525                 while True:
   527                     parser = input('parser type (%s): '
   526                     parser = input('parser type (%s): '
   528                                         % ', '.join(sorted(repo.vreg['parsers'])))
   527                                    % ', '.join(sorted(repo.vreg['parsers'])))
   529                     if parser in repo.vreg['parsers']:
   528                     if parser in repo.vreg['parsers']:
   530                         break
   529                         break
   531                     print('-> unknown parser identifier, use one of the available types.')
   530                     print('-> unknown parser identifier, use one of the available types.')
   532                 while True:
   531                 while True:
   533                     sourceuri = input('source identifier (a unique name used to '
   532                     sourceuri = input('source identifier (a unique name used to '
   534                                           'tell sources apart): ').strip()
   533                                       'tell sources apart): ').strip()
   535                     if not sourceuri:
   534                     if not sourceuri:
   536                         print('-> mandatory.')
   535                         print('-> mandatory.')
   537                     else:
   536                     else:
   538                         sourceuri = unicode(sourceuri, sys.stdin.encoding)
   537                         sourceuri = unicode(sourceuri, sys.stdin.encoding)
   539                         if sourceuri in used:
   538                         if sourceuri in used:
   563     name = 'db-grant-user'
   562     name = 'db-grant-user'
   564     arguments = '<instance> <user>'
   563     arguments = '<instance> <user>'
   565     min_args = max_args = 2
   564     min_args = max_args = 2
   566     options = (
   565     options = (
   567         ('set-owner',
   566         ('set-owner',
   568          {'short': 'o', 'type' : 'yn', 'metavar' : '<yes or no>',
   567          {'short': 'o', 'type': 'yn', 'metavar': '<yes or no>',
   569           'default' : False,
   568           'default': False,
   570           'help': 'Set the user as tables owner if yes (no by default).'}
   569           'help': 'Set the user as tables owner if yes (no by default).'}
   571          ),
   570          ),
   572         )
   571     )
       
   572 
   573     def run(self, args):
   573     def run(self, args):
   574         """run the command with its specific arguments"""
   574         """run the command with its specific arguments"""
   575         from cubicweb.server.sqlutils import sqlexec, sqlgrants
   575         from cubicweb.server.sqlutils import sqlexec, sqlgrants
   576         appid, user = args
   576         appid, user = args
   577         config = ServerConfiguration.config_for(appid)
   577         config = ServerConfiguration.config_for(appid)
   602     name = 'reset-admin-pwd'
   602     name = 'reset-admin-pwd'
   603     arguments = '<instance>'
   603     arguments = '<instance>'
   604     min_args = max_args = 1
   604     min_args = max_args = 1
   605     options = (
   605     options = (
   606         ('password',
   606         ('password',
   607          {'short': 'p', 'type' : 'string', 'metavar' : '<new-password>',
   607          {'short': 'p', 'type': 'string', 'metavar': '<new-password>',
   608           'default' : None,
   608           'default': None,
   609           'help': 'Use this password instead of prompt for one.\n'
   609           'help': 'Use this password instead of prompt for one.\n'
   610                   '/!\ THIS IS AN INSECURE PRACTICE /!\ \n'
   610                   '/!\ THIS IS AN INSECURE PRACTICE /!\ \n'
   611                   'the password will appear in shell history'}
   611                   'the password will appear in shell history'}
   612          ),
   612          ),
   613         )
   613     )
   614 
   614 
   615     def run(self, args):
   615     def run(self, args):
   616         """run the command with its specific arguments"""
   616         """run the command with its specific arguments"""
   617         from cubicweb.server.utils import crypt_password, manager_userpasswd
   617         from cubicweb.server.utils import crypt_password, manager_userpasswd
   618         appid = args[0]
   618         appid = args[0]
   659             cnx.commit()
   659             cnx.commit()
   660             print('-> password reset, sources file regenerated.')
   660             print('-> password reset, sources file regenerated.')
   661         cnx.close()
   661         cnx.close()
   662 
   662 
   663 
   663 
   664 
       
   665 def _remote_dump(host, appid, output, sudo=False):
   664 def _remote_dump(host, appid, output, sudo=False):
   666     # XXX generate unique/portable file name
   665     # XXX generate unique/portable file name
   667     from datetime import date
   666     from datetime import date
   668     filename = '%s-%s.tgz' % (appid, date.today().strftime('%Y-%m-%d'))
   667     filename = '%s-%s.tgz' % (appid, date.today().strftime('%Y-%m-%d'))
   669     dmpcmd = 'cubicweb-ctl db-dump -o /tmp/%s %s' % (filename, appid)
   668     dmpcmd = 'cubicweb-ctl db-dump -o /tmp/%s %s' % (filename, appid)
   680     if os.system(cmd):
   679     if os.system(cmd):
   681         raise ExecutionError('Error while retrieving the dump at /tmp/%s' % filename)
   680         raise ExecutionError('Error while retrieving the dump at /tmp/%s' % filename)
   682     rmcmd = 'ssh -t %s "rm -f /tmp/%s"' % (host, filename)
   681     rmcmd = 'ssh -t %s "rm -f /tmp/%s"' % (host, filename)
   683     print(rmcmd)
   682     print(rmcmd)
   684     if os.system(rmcmd) and not ASK.confirm(
   683     if os.system(rmcmd) and not ASK.confirm(
   685         'An error occurred while deleting remote dump at /tmp/%s. '
   684             'An error occurred while deleting remote dump at /tmp/%s. '
   686         'Continue anyway?' % filename):
   685             'Continue anyway?' % filename):
   687         raise ExecutionError('Error while deleting remote dump at /tmp/%s' % filename)
   686         raise ExecutionError('Error while deleting remote dump at /tmp/%s' % filename)
   688 
   687 
   689 
   688 
   690 def _local_dump(appid, output, format='native'):
   689 def _local_dump(appid, output, format='native'):
   691     config = ServerConfiguration.config_for(appid)
   690     config = ServerConfiguration.config_for(appid)
   692     config.quick_start = True
   691     config.quick_start = True
   693     mih = config.migration_handler(verbosity=1)
   692     mih = config.migration_handler(verbosity=1)
   694     mih.backup_database(output, askconfirm=False, format=format)
   693     mih.backup_database(output, askconfirm=False, format=format)
   695     mih.shutdown()
   694     mih.shutdown()
   696 
   695 
       
   696 
   697 def _local_restore(appid, backupfile, drop, format='native'):
   697 def _local_restore(appid, backupfile, drop, format='native'):
   698     config = ServerConfiguration.config_for(appid)
   698     config = ServerConfiguration.config_for(appid)
   699     config.verbosity = 1 # else we won't be asked for confirmation on problems
   699     config.verbosity = 1  # else we won't be asked for confirmation on problems
   700     config.quick_start = True
   700     config.quick_start = True
   701     mih = config.migration_handler(connect=False, verbosity=1)
   701     mih = config.migration_handler(connect=False, verbosity=1)
   702     mih.restore_database(backupfile, drop, askconfirm=False, format=format)
   702     mih.restore_database(backupfile, drop, askconfirm=False, format=format)
   703     repo = mih.repo
   703     repo = mih.repo
   704     # version of the database
   704     # version of the database
   724         print("** 'cubicweb-ctl upgrade %s'" % config.appid)
   724         print("** 'cubicweb-ctl upgrade %s'" % config.appid)
   725         return
   725         return
   726     # * database version = installed software, database version = instance fs version
   726     # * database version = installed software, database version = instance fs version
   727     #   ok!
   727     #   ok!
   728 
   728 
       
   729 
   729 def instance_status(config, cubicwebapplversion, vcconf):
   730 def instance_status(config, cubicwebapplversion, vcconf):
   730     cubicwebversion = config.cubicweb_version()
   731     cubicwebversion = config.cubicweb_version()
   731     if cubicwebapplversion > cubicwebversion:
   732     if cubicwebapplversion > cubicwebversion:
   732         return 'needsoftupgrade'
   733         return 'needsoftupgrade'
   733     if cubicwebapplversion < cubicwebversion:
   734     if cubicwebapplversion < cubicwebversion:
   734         return 'needapplupgrade'
   735         return 'needapplupgrade'
   735     for cube in config.cubes():
   736     for cube in config.cubes():
   736         try:
   737         try:
   737             softversion = config.cube_version(cube)
   738             softversion = config.cube_version(cube)
   738         except ConfigurationError:
   739         except ConfigurationError:
   739             print('-> Error: no cube version information for %s, please check that the cube is installed.' % cube)
   740             print('-> Error: no cube version information for %s, '
       
   741                   'please check that the cube is installed.' % cube)
   740             continue
   742             continue
   741         try:
   743         try:
   742             applversion = vcconf[cube]
   744             applversion = vcconf[cube]
   743         except KeyError:
   745         except KeyError:
   744             print('-> Error: no cube version information for %s in version configuration.' % cube)
   746             print('-> Error: no cube version information for %s in version configuration.' % cube)
   762     name = 'db-dump'
   764     name = 'db-dump'
   763     arguments = '<instance>'
   765     arguments = '<instance>'
   764     min_args = max_args = 1
   766     min_args = max_args = 1
   765     options = (
   767     options = (
   766         ('output',
   768         ('output',
   767          {'short': 'o', 'type' : 'string', 'metavar' : '<file>',
   769          {'short': 'o', 'type': 'string', 'metavar': '<file>',
   768           'default' : None,
   770           'default': None,
   769           'help': 'Specify the backup file where the backup will be stored.'}
   771           'help': 'Specify the backup file where the backup will be stored.'}
   770          ),
   772          ),
   771         ('sudo',
   773         ('sudo',
   772          {'short': 's', 'action' : 'store_true',
   774          {'short': 's', 'action': 'store_true',
   773           'default' : False,
   775           'default': False,
   774           'help': 'Use sudo on the remote host.'}
   776           'help': 'Use sudo on the remote host.'}
   775          ),
   777          ),
   776         ('format',
   778         ('format',
   777          {'short': 'f', 'default': 'native', 'type': 'choice',
   779          {'short': 'f', 'default': 'native', 'type': 'choice',
   778           'choices': ('native', 'portable'),
   780           'choices': ('native', 'portable'),
   779           'help': '"native" format uses db backend utilities to dump the database. '
   781           'help': '"native" format uses db backend utilities to dump the database. '
   780                   '"portable" format uses a database independent format'}
   782                   '"portable" format uses a database independent format'}
   781          ),
   783          ),
   782         )
   784     )
   783 
   785 
   784     def run(self, args):
   786     def run(self, args):
   785         appid = args[0]
   787         appid = args[0]
   786         if ':' in appid:
   788         if ':' in appid:
   787             host, appid = appid.split(':')
   789             host, appid = appid.split(':')
   788             _remote_dump(host, appid, self.config.output, self.config.sudo)
   790             _remote_dump(host, appid, self.config.output, self.config.sudo)
   789         else:
   791         else:
   790             _local_dump(appid, self.config.output, format=self.config.format)
   792             _local_dump(appid, self.config.output, format=self.config.format)
   791 
   793 
   792 
   794 
   793 
       
   794 
       
   795 class DBRestoreCommand(Command):
   795 class DBRestoreCommand(Command):
   796     """Restore the system database of an instance.
   796     """Restore the system database of an instance.
   797 
   797 
   798     <instance>
   798     <instance>
   799       the identifier of the instance to restore
   799       the identifier of the instance to restore
   802     arguments = '<instance> <backupfile>'
   802     arguments = '<instance> <backupfile>'
   803     min_args = max_args = 2
   803     min_args = max_args = 2
   804 
   804 
   805     options = (
   805     options = (
   806         ('no-drop',
   806         ('no-drop',
   807          {'short': 'n', 'action' : 'store_true', 'default' : False,
   807          {'short': 'n', 'action': 'store_true', 'default': False,
   808           'help': 'for some reason the database doesn\'t exist and so '
   808           'help': 'for some reason the database doesn\'t exist and so '
   809           'should not be dropped.'}
   809           'should not be dropped.'}
   810          ),
   810          ),
   811         ('format',
   811         ('format',
   812          {'short': 'f', 'default': 'native', 'type': 'choice',
   812          {'short': 'f', 'default': 'native', 'type': 'choice',
   813           'choices': ('native', 'portable'),
   813           'choices': ('native', 'portable'),
   814           'help': 'the format used when dumping the database'}),
   814           'help': 'the format used when dumping the database'}),
   815         )
   815     )
   816 
   816 
   817     def run(self, args):
   817     def run(self, args):
   818         appid, backupfile = args
   818         appid, backupfile = args
   819         if self.config.format == 'portable':
   819         if self.config.format == 'portable':
   820             # we need to ensure a DB exist before restoring from portable format
   820             # we need to ensure a DB exist before restoring from portable format
   849     name = 'db-copy'
   849     name = 'db-copy'
   850     arguments = '<src-instance> <dest-instance>'
   850     arguments = '<src-instance> <dest-instance>'
   851     min_args = max_args = 2
   851     min_args = max_args = 2
   852     options = (
   852     options = (
   853         ('no-drop',
   853         ('no-drop',
   854          {'short': 'n', 'action' : 'store_true',
   854          {'short': 'n', 'action': 'store_true',
   855           'default' : False,
   855           'default': False,
   856           'help': 'For some reason the database doesn\'t exist and so '
   856           'help': 'For some reason the database doesn\'t exist and so '
   857           'should not be dropped.'}
   857           'should not be dropped.'}
   858          ),
   858          ),
   859         ('keep-dump',
   859         ('keep-dump',
   860          {'short': 'k', 'action' : 'store_true',
   860          {'short': 'k', 'action': 'store_true',
   861           'default' : False,
   861           'default': False,
   862           'help': 'Specify that the dump file should not be automatically removed.'}
   862           'help': 'Specify that the dump file should not be automatically removed.'}
   863          ),
   863          ),
   864         ('sudo',
   864         ('sudo',
   865          {'short': 's', 'action' : 'store_true',
   865          {'short': 's', 'action': 'store_true',
   866           'default' : False,
   866           'default': False,
   867           'help': 'Use sudo on the remote host.'}
   867           'help': 'Use sudo on the remote host.'}
   868          ),
   868          ),
   869         ('format',
   869         ('format',
   870          {'short': 'f', 'default': 'native', 'type': 'choice',
   870          {'short': 'f', 'default': 'native', 'type': 'choice',
   871           'choices': ('native', 'portable'),
   871           'choices': ('native', 'portable'),
   872           'help': '"native" format uses db backend utilities to dump the database. '
   872           'help': '"native" format uses db backend utilities to dump the database. '
   873                   '"portable" format uses a database independent format'}
   873                   '"portable" format uses a database independent format'}
   874          ),
   874          ),
   875         )
   875     )
   876 
   876 
   877     def run(self, args):
   877     def run(self, args):
   878         import tempfile
   878         import tempfile
   879         srcappid, destappid = args
   879         srcappid, destappid = args
   880         fd, output = tempfile.mkstemp()
   880         fd, output = tempfile.mkstemp()
   901     name = 'db-check'
   901     name = 'db-check'
   902     arguments = '<instance>'
   902     arguments = '<instance>'
   903     min_args = max_args = 1
   903     min_args = max_args = 1
   904     options = (
   904     options = (
   905         ('checks',
   905         ('checks',
   906          {'short': 'c', 'type' : 'csv', 'metavar' : '<check list>',
   906          {'short': 'c', 'type': 'csv', 'metavar': '<check list>',
   907           'default' : ('entities', 'relations',
   907           'default': ('entities', 'relations',
   908                        'mandatory_relations', 'mandatory_attributes',
   908                       'mandatory_relations', 'mandatory_attributes',
   909                        'metadata', 'schema', 'text_index'),
   909                       'metadata', 'schema', 'text_index'),
   910           'help': 'Comma separated list of check to run. By default run all \
   910           'help': 'Comma separated list of check to run. By default run all \
   911 checks, i.e. entities, relations, mandatory_relations, mandatory_attributes, \
   911 checks, i.e. entities, relations, mandatory_relations, mandatory_attributes, \
   912 metadata, text_index and schema.'}
   912 metadata, text_index and schema.'}
   913          ),
   913          ),
   914 
   914 
   915         ('autofix',
   915         ('autofix',
   916          {'short': 'a', 'type' : 'yn', 'metavar' : '<yes or no>',
   916          {'short': 'a', 'type': 'yn', 'metavar': '<yes or no>',
   917           'default' : False,
   917           'default': False,
   918           'help': 'Automatically correct integrity problems if this option \
   918           'help': 'Automatically correct integrity problems if this option \
   919 is set to "y" or "yes", else only display them'}
   919 is set to "y" or "yes", else only display them'}
   920          ),
   920          ),
   921         ('reindex',
   921         ('reindex',
   922          {'short': 'r', 'type' : 'yn', 'metavar' : '<yes or no>',
   922          {'short': 'r', 'type': 'yn', 'metavar': '<yes or no>',
   923           'default' : False,
   923           'default': False,
   924           'help': 're-indexes the database for full text search if this \
   924           'help': 're-indexes the database for full text search if this \
   925 option is set to "y" or "yes" (may be long for large database).'}
   925 option is set to "y" or "yes" (may be long for large database).'}
   926          ),
   926          ),
   927         ('force',
   927         ('force',
   928          {'short': 'f', 'action' : 'store_true',
   928          {'short': 'f', 'action': 'store_true',
   929           'default' : False,
   929           'default': False,
   930           'help': 'don\'t check instance is up to date.'}
   930           'help': 'don\'t check instance is up to date.'}
   931          ),
   931          ),
   932 
   932     )
   933         )
       
   934 
   933 
   935     def run(self, args):
   934     def run(self, args):
   936         from cubicweb.server.checkintegrity import check
   935         from cubicweb.server.checkintegrity import check
   937         appid = args[0]
   936         appid = args[0]
   938         config = ServerConfiguration.config_for(appid)
   937         config = ServerConfiguration.config_for(appid)
   979     """
   978     """
   980     name = 'source-sync'
   979     name = 'source-sync'
   981     arguments = '<instance> <source>'
   980     arguments = '<instance> <source>'
   982     min_args = max_args = 2
   981     min_args = max_args = 2
   983     options = (
   982     options = (
   984             ('loglevel',
   983         ('loglevel',
   985              {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
   984          {'short': 'l', 'type': 'choice', 'metavar': '<log level>',
   986               'default': 'info', 'choices': ('debug', 'info', 'warning', 'error'),
   985           'default': 'info', 'choices': ('debug', 'info', 'warning', 'error')},
   987              }),
   986          ),
   988     )
   987     )
   989 
   988 
   990     def run(self, args):
   989     def run(self, args):
   991         from cubicweb import repoapi
   990         from cubicweb import repoapi
   992         from cubicweb.cwctl import init_cmdline_log_threshold
   991         from cubicweb.cwctl import init_cmdline_log_threshold
  1008         for key, val in stats.items():
  1007         for key, val in stats.items():
  1009             if val:
  1008             if val:
  1010                 print(key, ':', val)
  1009                 print(key, ':', val)
  1011 
  1010 
  1012 
  1011 
  1013 
       
  1014 def permissionshandler(relation, perms):
  1012 def permissionshandler(relation, perms):
  1015     from yams.schema import RelationDefinitionSchema
       
  1016     from yams.buildobjs import DEFAULT_ATTRPERMS
  1013     from yams.buildobjs import DEFAULT_ATTRPERMS
  1017     from cubicweb.schema import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS,
  1014     from cubicweb.schema import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS,
  1018                                  PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
  1015                                  PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
  1019     defaultrelperms = (DEFAULT_ATTRPERMS, PUB_SYSTEM_REL_PERMS,
  1016     defaultrelperms = (DEFAULT_ATTRPERMS, PUB_SYSTEM_REL_PERMS,
  1020                        PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
  1017                        PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS)
  1061 
  1058 
  1062 # extend configure command to set options in sources config file ###############
  1059 # extend configure command to set options in sources config file ###############
  1063 
  1060 
  1064 db_options = (
  1061 db_options = (
  1065     ('db',
  1062     ('db',
  1066      {'short': 'd', 'type' : 'named', 'metavar' : '[section1.]key1:value1,[section2.]key2:value2',
  1063      {'short': 'd', 'type': 'named', 'metavar': '[section1.]key1:value1,[section2.]key2:value2',
  1067       'default': None,
  1064       'default': None,
  1068       'help': '''set <key> in <section> to <value> in "source" configuration file. If <section> is not specified, it defaults to "system".
  1065       'help': '''set <key> in <section> to <value> in "source" configuration file. If
       
  1066 <section> is not specified, it defaults to "system".
  1069 
  1067 
  1070 Beware that changing admin.login or admin.password using this command
  1068 Beware that changing admin.login or admin.password using this command
  1071 will NOT update the database with new admin credentials.  Use the
  1069 will NOT update the database with new admin credentials.  Use the
  1072 reset-admin-pwd command instead.
  1070 reset-admin-pwd command instead.
  1073 ''',
  1071 ''',
  1074       }),
  1072       }),
  1075     )
  1073 )
  1076 
  1074 
  1077 ConfigureInstanceCommand.options = merge_options(
  1075 ConfigureInstanceCommand.options = merge_options(
  1078         ConfigureInstanceCommand.options + db_options)
  1076     ConfigureInstanceCommand.options + db_options)
  1079 
  1077 
  1080 configure_instance = ConfigureInstanceCommand.configure_instance
  1078 configure_instance = ConfigureInstanceCommand.configure_instance
       
  1079 
       
  1080 
  1081 def configure_instance2(self, appid):
  1081 def configure_instance2(self, appid):
  1082     configure_instance(self, appid)
  1082     configure_instance(self, appid)
  1083     if self.config.db is not None:
  1083     if self.config.db is not None:
  1084         appcfg = ServerConfiguration.config_for(appid)
  1084         appcfg = ServerConfiguration.config_for(appid)
  1085         srccfg = appcfg.read_sources_file()
  1085         srccfg = appcfg.read_sources_file()
  1089             else:
  1089             else:
  1090                 section = 'system'
  1090                 section = 'system'
  1091             try:
  1091             try:
  1092                 srccfg[section][key] = value
  1092                 srccfg[section][key] = value
  1093             except KeyError:
  1093             except KeyError:
  1094                 raise ConfigurationError('unknown configuration key "%s" in section "%s" for source' % (key, section))
  1094                 raise ConfigurationError('unknown configuration key "%s" in section "%s" for source'
       
  1095                                          % (key, section))
  1095         admcfg = Configuration(options=USER_OPTIONS)
  1096         admcfg = Configuration(options=USER_OPTIONS)
  1096         admcfg['login'] = srccfg['admin']['login']
  1097         admcfg['login'] = srccfg['admin']['login']
  1097         admcfg['password'] = srccfg['admin']['password']
  1098         admcfg['password'] = srccfg['admin']['password']
  1098         srccfg['admin'] = admcfg
  1099         srccfg['admin'] = admcfg
  1099         appcfg.write_sources_file(srccfg)
  1100         appcfg.write_sources_file(srccfg)
       
  1101 
  1100 ConfigureInstanceCommand.configure_instance = configure_instance2
  1102 ConfigureInstanceCommand.configure_instance = configure_instance2