FacetItem now takes req as first parameter of __init__, THIS IS BACKWARD INCOMPATIBLE
"""cubicweb-ctl commands and command handlers specific to the server.serverconfig:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importosfromlogilab.common.configurationimportREQUIRED,Configuration,ini_format_sectionfromcubicwebimportAuthenticationError,ExecutionError,ConfigurationErrorfromcubicweb.toolsutilsimportCommand,CommandHandler,pop_arg,cmd_run, \register_commands,confirm,restrict_perms_to_userfromcubicweb.server.serverconfigimportServerConfiguration# utility functions ###########################################################defsource_cnx(source,dbname=None,special_privs=False,verbose=True):"""open and return a connection to the system database defined in the given server.serverconfig """fromgetpassimportgetpassfromlogilab.common.dbimportget_connectiondbhost=source['db-host']ifdbnameisNone:dbname=source['db-name']driver=source['db-driver']print'**** connecting to %s database %s@%s'%(driver,dbname,dbhost),ifnotverboseor(notspecial_privsandsource.get('db-user')):user=source['db-user']print'as',userifsource.get('db-password'):password=source['db-password']else:password=getpass('password: ')else:printifspecial_privs:print'WARNING'print'the user will need the following special access rights on the database:'printspecial_privsprintdefault_user=source.get('db-user',os.environ.get('USER',''))user=raw_input('user (%r by default): '%default_user)user=userordefault_userifuser==source.get('db-user')andsource.get('db-password'):password=source['db-password']else:password=getpass('password: ')returnget_connection(driver,dbhost,dbname,user,password=password,port=source.get('db-port'))defsystem_source_cnx(source,dbms_system_base=False,special_privs='CREATE/DROP DATABASE',verbose=True):"""shortcut to get a connextion to the application system database defined in the given config. If <dbms_system_base> is True, connect to the dbms system database instead (for task such as create/drop the application database) """ifdbms_system_base:fromlogilab.common.adbhimportget_adv_func_helpersystem_db=get_adv_func_helper(source['db-driver']).system_database()returnsource_cnx(source,system_db,special_privs=special_privs,verbose=verbose)returnsource_cnx(source,special_privs=special_privs,verbose=verbose)def_db_sys_cnx(source,what,db=None,user=None,verbose=True):"""return a connection on the RDMS system table (to create/drop a user or a database """fromlogilab.common.adbhimportget_adv_func_helperspecial_privs=''driver=source['db-driver']helper=get_adv_func_helper(driver)ifuserisnotNoneandhelper.users_support:special_privs+='%s USER'%whatifdbisnotNone:special_privs+=' %s DATABASE'%what# connect on the dbms system base to create our basecnx=system_source_cnx(source,True,special_privs=special_privs,verbose=verbose)# disable autocommit (isolation_level(1)) because DROP and# CREATE DATABASE can't be executed in a transactiontry:cnx.set_isolation_level(0)exceptAttributeError:# set_isolation_level() is psycopg specificpassreturncnxdefgenerate_sources_file(sourcesfile,sourcescfg,keys=None):"""serialize repository'sources configuration into a INI like file the `keys` parameter may be used to sort sections """fromcubicweb.server.sourcesimportSOURCE_TYPESifkeysisNone:keys=sourcescfg.keys()else:forkeyinsourcescfg:ifnotkeyinkeys:keys.append(key)stream=open(sourcesfile,'w')foruriinkeys:sconfig=sourcescfg[uri]ifisinstance(sconfig,dict):# get a Configuration object_sconfig=Configuration(options=SOURCE_TYPES[sconfig['adapter']].options)forattr,valinsconfig.items():ifattr=='uri':continueifattr=='adapter':_sconfig.adapter=valelse:_sconfig.set_option(attr,val)sconfig=_sconfigoptsbysect=list(sconfig.options_by_section())assertlen(optsbysect)==1ini_format_section(stream,uri,optsbysect[0][1])ifhasattr(sconfig,'adapter'):print>>streamprint>>stream,'# adapter for this source (YOU SHOULD NOT CHANGE THIS)'print>>stream,'adapter=%s'%sconfig.adapterprint>>streamdefrepo_cnx(config):"""return a in-memory repository and a db api connection it"""fromcubicweb.dbapiimportin_memory_cnxfromcubicweb.server.utilsimportmanager_userpasswdtry:login=config.sources()['admin']['login']pwd=config.sources()['admin']['password']exceptKeyError:login,pwd=manager_userpasswd()whileTrue:try:returnin_memory_cnx(config,login,pwd)exceptAuthenticationError:print'wrong user/password'login,pwd=manager_userpasswd()# repository specific command handlers ########################################classRepositoryCreateHandler(CommandHandler):cmdname='create'cfgname='repository'defbootstrap(self,cubes,inputlevel=0):"""create an application by copying files from the given cube and by asking information necessary to build required configuration files """fromcubicweb.server.sourcesimportSOURCE_TYPESconfig=self.configprint'application\'s repository configuration'print'-'*72config.input_config('email',inputlevel)ifconfig.pyro_enabled():config.input_config('pyro-server',inputlevel)printprint'repository sources configuration'print'-'*72sourcesfile=config.sources_file()sconfig=Configuration(options=SOURCE_TYPES['native'].options)sconfig.adapter='native'sconfig.input_config(inputlevel=inputlevel)sourcescfg={'system':sconfig}whileraw_input('enter another source [y/N]: ').strip().lower()=='y':sourcetype=raw_input('source type (%s): '%', '.join(SOURCE_TYPES.keys()))sconfig=Configuration(options=SOURCE_TYPES[sourcetype].options)sconfig.adapter=sourcetypesourceuri=raw_input('source uri: ').strip()assertnotsourceuriinsourcescfgsconfig.input_config(inputlevel=inputlevel)sourcescfg[sourceuri]=sconfig# module names look like cubes.mycube.themodulesourcecube=SOURCE_TYPES[sourcetype].module.split('.',2)[1]# if the source adapter is coming from an external component, ensure# it's specified in used cubesifsourcecube!='cubicweb'andnotsourcecubeincubes:cubes.append(sourcecube)sconfig=Configuration(options=USER_OPTIONS)sconfig.input_config(inputlevel=inputlevel)sourcescfg['admin']=sconfiggenerate_sources_file(sourcesfile,sourcescfg,['admin','system'])restrict_perms_to_user(sourcesfile)# remember selected cubes for later initialization of the databaseconfig.write_bootstrap_cubes_file(cubes)defpostcreate(self):ifconfirm('do you want to create repository\'s system database?'):verbosity=(self.config.mode=='installed')and'y'or'n'cmd_run('db-create',self.config.appid,'--verbose=%s'%verbosity)else:print'nevermind, you can do it later using the db-create command'USER_OPTIONS=(('login',{'type':'string','default':REQUIRED,'help':"cubicweb manager account's login "'(this user will be created)','inputlevel':0,}),('password',{'type':'password','help':"cubicweb manager account's password",'inputlevel':0,}),)classRepositoryDeleteHandler(CommandHandler):cmdname='delete'cfgname='repository'defcleanup(self):"""remove application's configuration and database"""fromlogilab.common.adbhimportget_adv_func_helpersource=self.config.sources()['system']dbname=source['db-name']helper=get_adv_func_helper(source['db-driver'])ifconfirm('delete database %s ?'%dbname):user=source['db-user']orNonecnx=_db_sys_cnx(source,'DROP DATABASE',user=user)cursor=cnx.cursor()try:cursor.execute('DROP DATABASE %s'%dbname)print'database %s dropped'%dbname# XXX should check we are not connected as userifuserandhelper.users_supportand \confirm('delete user %s ?'%user,default_is_yes=False):cursor.execute('DROP USER %s'%user)print'user %s dropped'%usercnx.commit()except:cnx.rollback()raiseclassRepositoryStartHandler(CommandHandler):cmdname='start'cfgname='repository'defstart_command(self,ctlconf,debug):command=['cubicweb-ctl start-repository ']ifdebug:command.append('--debug')command.append(self.config.appid)return' '.join(command)classRepositoryStopHandler(CommandHandler):cmdname='stop'cfgname='repository'defpoststop(self):"""if pyro is enabled, ensure the repository is correctly unregistered """ifself.config.pyro_enabled():fromcubicweb.server.repositoryimportpyro_unregisterpyro_unregister(self.config)# repository specific commands ################################################classCreateApplicationDBCommand(Command):"""Create the system database of an application (run after 'create'). You will be prompted for a login / password to use to connect to the system database. The given user should have almost all rights on the database (ie a super user on the dbms allowed to create database, users, languages...). <application> the identifier of the application to initialize. """name='db-create'arguments='<application>'options=(("create-db",{'short':'c','type':"yn",'metavar':'<y or n>','default':True,'help':'create the database (yes by default)'}),("verbose",{'short':'v','type':'yn','metavar':'<verbose>','default':'n','help':'verbose mode: will ask all possible configuration questions',}),)defrun(self,args):"""run the command with its specific arguments"""fromlogilab.common.adbhimportget_adv_func_helperfromindexerimportget_indexerverbose=self.get('verbose')appid=pop_arg(args,msg="No application specified !")config=ServerConfiguration.config_for(appid)create_db=self.config.create_dbsource=config.sources()['system']driver=source['db-driver']helper=get_adv_func_helper(driver)ifcreate_db:# connect on the dbms system base to create our basedbcnx=_db_sys_cnx(source,'CREATE DATABASE and / or USER',verbose=verbose)cursor=dbcnx.cursor()try:ifhelper.users_support:user=source['db-user']ifnothelper.user_exists(cursor,user)and \confirm('create db user %s ?'%user,default_is_yes=False):helper.create_user(source['db-user'],source['db-password'])print'user %s created'%userdbname=source['db-name']ifdbnameinhelper.list_databases(cursor):ifconfirm('DB %s already exists -- do you want to drop it ?'%dbname):cursor.execute('DROP DATABASE %s'%dbname)else:returnifdbcnx.logged_user!=source['db-user']:helper.create_database(cursor,dbname,source['db-user'],source['db-encoding'])else:helper.create_database(cursor,dbname,encoding=source['db-encoding'])dbcnx.commit()print'database %s created'%source['db-name']except:dbcnx.rollback()raisecnx=system_source_cnx(source,special_privs='LANGUAGE C',verbose=verbose)cursor=cnx.cursor()indexer=get_indexer(driver)indexer.init_extensions(cursor)# postgres specific stuff ifdriver=='postgres':# install plpythonu/plpgsql language if not installed by the cubeforextlangin('plpythonu','plpgsql'):helper.create_language(cursor,extlang)cursor.close()cnx.commit()print'database for application %s created and necessary extensions installed'%appidprintifconfirm('do you want to initialize the system database?'):cmd_run('db-init',config.appid)else:print'nevermind, you can do it later using the db-init command'classInitApplicationCommand(Command):"""Initialize the system database of an application (run after 'db-create'). You will be prompted for a login / password to use to connect to the system database. The given user should have the create tables, and grant permissions. <application> the identifier of the application to initialize. """name='db-init'arguments='<application>'options=(("drop",{'short':'d','action':'store_true','default':False,'help':'insert drop statements to remove previously existant \tables, indexes... (no by default)'}),)defrun(self,args):fromcubicweb.serverimportinit_repositoryappid=pop_arg(args,msg="No application specified !")config=ServerConfiguration.config_for(appid)init_repository(config,drop=self.config.drop)classGrantUserOnApplicationCommand(Command):"""Grant a database user on a repository system database. <application> the identifier of the application <user> the database's user requiring grant access """name='db-grant-user'arguments='<application> <user>'options=(("set-owner",{'short':'o','type':"yn",'metavar':'<yes or no>','default':False,'help':'Set the user as tables owner if yes (no by default).'}),)defrun(self,args):"""run the command with its specific arguments"""fromcubicweb.server.sqlutilsimportsqlexec,sqlgrantsappid=pop_arg(args,1,msg="No application specified !")user=pop_arg(args,msg="No user specified !")config=ServerConfiguration.config_for(appid)source=config.sources()['system']set_owner=self.config.set_ownercnx=system_source_cnx(source,special_privs='GRANT')cursor=cnx.cursor()schema=config.load_schema()try:sqlexec(sqlgrants(schema,source['db-driver'],user,set_owner=set_owner),cursor)exceptException,ex:cnx.rollback()importtracebacktraceback.print_exc()print'An error occured:',exelse:cnx.commit()print'grants given to %s on application %s'%(appid,user)classStartRepositoryCommand(Command):"""Start an CubicWeb RQL server for a given application. The server will be accessible through pyro <application> the identifier of the application to initialize. """name='start-repository'arguments='<application>'options=(("debug",{'short':'D','action':'store_true','help':'start server in debug mode.'}),)defrun(self,args):fromcubicweb.server.serverimportRepositoryServerappid=pop_arg(args,msg="No application specified !")config=ServerConfiguration.config_for(appid)debug=self.config.debug# create the serverserver=RepositoryServer(config,debug)# go ! (don't daemonize in debug mode)ifnotdebugandserver.daemonize(config['pid-file'])==-1:returnuid=config['uid']ifuidisnotNone:try:uid=int(uid)exceptValueError:frompwdimportgetpwnamuid=getpwnam(uid).pw_uidos.setuid(uid)server.install_sig_handlers()server.connect(config['host'],0)server.run()def_remote_dump(host,appid,output,sudo=False):dmpcmd='cubicweb-ctl db-dump -o /tmp/%s.dump %s'%(appid,appid)ifsudo:dmpcmd='sudo %s'%(dmpcmd)dmpcmd='ssh -t %s "%s"'%(host,dmpcmd)printdmpcmdifos.system(dmpcmd):raiseExecutionError('Error while dumping the database')ifoutputisNone:frommx.DateTimeimporttodaydate=today().strftime('%Y-%m-%d')output='%s-%s.dump'%(appid,date)cmd='scp %s:/tmp/%s.dump %s'%(host,appid,output)printcmdifos.system(cmd):raiseExecutionError('Error while retrieving the dump')rmcmd='ssh -t %s "rm -f /tmp/%s.dump"'%(host,appid)printrmcmdifos.system(rmcmd)andnotconfirm('an error occured while deleting remote dump. Continue anyway?'):raiseExecutionError('Error while deleting remote dump')def_local_dump(appid,output):config=ServerConfiguration.config_for(appid)# schema=1 to avoid unnecessary schema loadingmih=config.migration_handler(connect=False,schema=1)mih.backup_database(output,askconfirm=False)def_local_restore(appid,backupfile,drop):config=ServerConfiguration.config_for(appid)# schema=1 to avoid unnecessary schema loadingmih=config.migration_handler(connect=False,schema=1)mih.restore_database(backupfile,drop)repo=mih.repo_connect()# version of the databasedbversions=repo.get_versions()mih.shutdown()ifnotdbversions:print"bad or missing version information in the database, don't upgrade file system"return# version of installed softwareeversion=dbversions['cubicweb']status=application_status(config,eversion,dbversions)# * database version > installed softwareifstatus=='needsoftupgrade':print"database is using some earlier version than installed software!"print"please upgrade your software and then upgrade the instance"print"using command 'cubicweb-ctl upgrade %s'"%config.appidreturn# * database version < installed software, an upgrade will be necessary# anyway, just rewrite vc.conf and warn user he has to upgradeifstatus=='needapplupgrade':print"database is using some older version than installed software."print"You'll have to upgrade the instance using command"print"'cubicweb-ctl upgrade %s'"%config.appidreturn# * database version = installed software, database version = instance fs version# ok!defapplication_status(config,cubicwebapplversion,vcconf):cubicwebversion=config.cubicweb_version()ifcubicwebapplversion>cubicwebversion:return'needsoftupgrade'ifcubicwebapplversion<cubicwebversion:return'needapplupgrade'forcubeinconfig.cubes():try:softversion=config.cube_version(cube)exceptConfigurationError:print"no cube version information for %s, is the cube installed?"%cubecontinuetry:applversion=vcconf[cube]exceptKeyError:print"no cube version information for %s in version configuration"%cubecontinueifsoftversion==applversion:continueifsoftversion>applversion:return'needsoftupgrade'elifsoftversion<applversion:return'needapplupgrade'returnNoneclassDBDumpCommand(Command):"""Backup the system database of an application. <application> the identifier of the application to backup format [[user@]host:]appname """name='db-dump'arguments='<application>'options=(("output",{'short':'o','type':"string",'metavar':'<file>','default':None,'help':'Specify the backup file where the backup will be stored.'}),('sudo',{'short':'s','action':'store_true','default':False,'help':'Use sudo on the remote host.'}),)defrun(self,args):appid=pop_arg(args,1,msg="No application specified !")if':'inappid:host,appid=appid.split(':')_remote_dump(host,appid,self.config.output,self.config.sudo)else:_local_dump(appid,self.config.output)classDBRestoreCommand(Command):"""Restore the system database of an application. <application> the identifier of the application to restore """name='db-restore'arguments='<application> <backupfile>'options=(("no-drop",{'short':'n','action':'store_true','default':False,'help':'for some reason the database doesn\'t exist and so ''should not be dropped.'}),)defrun(self,args):appid=pop_arg(args,1,msg="No application specified !")backupfile=pop_arg(args,msg="No backup file specified !")_local_restore(appid,backupfile,notself.config.no_drop)classDBCopyCommand(Command):"""Copy the system database of an application (backup and restore). <src-application> the identifier of the application to backup format [[user@]host:]appname <dest-application> the identifier of the application to restore """name='db-copy'arguments='<src-application> <dest-application>'options=(("no-drop",{'short':'n','action':'store_true','default':False,'help':'For some reason the database doesn\'t exist and so ''should not be dropped.'}),("keep-dump",{'short':'k','action':'store_true','default':False,'help':'Specify that the dump file should not be automatically removed.'}),('sudo',{'short':'s','action':'store_true','default':False,'help':'Use sudo on the remote host.'}),)defrun(self,args):importtempfilesrcappid=pop_arg(args,1,msg="No source application specified !")destappid=pop_arg(args,msg="No destination application specified !")output=tempfile.mktemp()if':'insrcappid:host,srcappid=srcappid.split(':')_remote_dump(host,srcappid,output,self.config.sudo)else:_local_dump(srcappid,output)_local_restore(destappid,output,notself.config.no_drop)ifself.config.keep_dump:print'you can get the dump file at',outputelse:os.remove(output)classCheckRepositoryCommand(Command):"""Check integrity of the system database of an application. <application> the identifier of the application to check """name='db-check'arguments='<application>'options=(("checks",{'short':'c','type':"csv",'metavar':'<check list>','default':('entities','relations','metadata','schema','text_index'),'help':'Comma separated list of check to run. By default run all \checks, i.e. entities, relations, text_index and metadata.'}),("autofix",{'short':'a','type':"yn",'metavar':'<yes or no>','default':False,'help':'Automatically correct integrity problems if this option \is set to "y" or "yes", else only display them'}),("reindex",{'short':'r','type':"yn",'metavar':'<yes or no>','default':False,'help':'re-indexes the database for full text search if this \option is set to "y" or "yes" (may be long for large database).'}),)defrun(self,args):fromcubicweb.server.checkintegrityimportcheckappid=pop_arg(args,1,msg="No application specified !")config=ServerConfiguration.config_for(appid)repo,cnx=repo_cnx(config)check(repo,cnx,self.config.checks,self.config.reindex,self.config.autofix)classRebuildFTICommand(Command):"""Rebuild the full-text index of the system database of an application. <application> the identifier of the application to rebuild """name='db-rebuild-fti'arguments='<application>'options=()defrun(self,args):fromcubicweb.server.checkintegrityimportreindex_entitiesappid=pop_arg(args,1,msg="No application specified !")config=ServerConfiguration.config_for(appid)repo,cnx=repo_cnx(config)session=repo._get_session(cnx.sessionid,setpool=True)reindex_entities(repo.schema,session)cnx.commit()classSynchronizeApplicationSchemaCommand(Command):"""Synchronize persistent schema with cube schema. Will synchronize common stuff between the cube schema and the actual persistent schema, but will not add/remove any entity or relation. <application> the identifier of the application to synchronize. """name='schema-sync'arguments='<application>'defrun(self,args):appid=pop_arg(args,msg="No application specified !")config=ServerConfiguration.config_for(appid)mih=config.migration_handler()mih.cmd_synchronize_schema()register_commands((CreateApplicationDBCommand,InitApplicationCommand,GrantUserOnApplicationCommand,StartRepositoryCommand,DBDumpCommand,DBRestoreCommand,DBCopyCommand,CheckRepositoryCommand,RebuildFTICommand,SynchronizeApplicationSchemaCommand,))