[IMP] new exported method to migrate databases remotely
[odoo/odoo.git] / bin / service / web_services.py
index 0b7c849..e469719 100644 (file)
@@ -1,46 +1,42 @@
 # -*- encoding: utf-8 -*-
 ##############################################################################
 #
-# Copyright (c) 2004-2008 Tiny SPRL (http://tiny.be) All Rights Reserved.
+#    OpenERP, Open Source Management Solution  
+#    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
+#    $Id$
 #
-# $Id$
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
 #
-# WARNING: This program as such is intended to be used by professional
-# programmers who take the whole responsability of assessing all potential
-# consequences resulting from its eventual inadequacies and bugs
-# End users who are looking for a ready-to-use solution with commercial
-# garantees and support are strongly adviced to contract a Free Software
-# Service Company
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
 #
-# This program is Free Software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-###############################################################################
-
-import base64, os, string
+##############################################################################
 
-import netsvc
-import pooler, security, ir, tools
+import base64 
 import logging
-
-import threading, thread
-
+import os
+import security
+import string
+import thread
+import threading
 import time
-import base64
-import addons
 
-import sql_db
 from tools.translate import _
+import addons
+import ir
+import netsvc
+import pooler
+import release
+import sql_db
+import tools
 
 logging.basicConfig()
 
@@ -57,6 +53,7 @@ class db(netsvc.Service):
         self.exportMethod(self.list_lang)
         self.exportMethod(self.change_admin_password)
         self.exportMethod(self.server_version)
+        self.exportMethod(self.migrate_databases)
         self.actions = {}
         self.id = 0
         self.id_protect = threading.Semaphore()
@@ -71,8 +68,9 @@ class db(netsvc.Service):
         self.actions[id] = {'clean': False}
 
         db = sql_db.db_connect('template1', serialize=1)
-        db.truedb.autocommit()
         cr = db.cursor()
+        cr.autocommit(True)
+        time.sleep(0.2)
         cr.execute('CREATE DATABASE ' + db_name + ' ENCODING \'unicode\'')
         cr.close()
         class DBInitialize(object):
@@ -85,19 +83,24 @@ class db(netsvc.Service):
                     cr.commit()
                     cr.close()
                     cr = None
-                    pool = pooler.get_pool(db_name, demo,serv.actions[id],
-                            update_module=True)
-                    if lang and lang != 'en_US':
-                        filename = tools.config["root_path"] + "/i18n/" + lang + ".csv"
-                        tools.trans_load(db_name, filename, lang)
-                    serv.actions[id]['clean'] = True
+                    pool = pooler.restart_pool(db_name, demo, serv.actions[id],
+                            update_module=True)[1]
+
                     cr = sql_db.db_connect(db_name).cursor()
-                    cr.execute('UPDATE res_users SET password=%s, active=True WHERE login=%s', (
-                        user_password, 'admin'))
-                    cr.execute('select login, password, name ' \
-                            'from res_users ' \
-                            'order by login')
+
+                    if lang:
+                        modobj = pool.get('ir.module.module')
+                        mids = modobj.search(cr, 1, [('state', '=', 'installed')])
+                        modobj.update_translations(cr, 1, mids, lang)
+
+                    cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
+                        user_password, lang, 'admin'))
+                    cr.execute('SELECT login, password, name ' \
+                               '  FROM res_users ' \
+                               ' ORDER BY login')
                     serv.actions[id]['users'] = cr.dictfetchall()
+                    serv.actions[id]['clean'] = True
+                    cr.commit()
                     cr.close()
                 except Exception, e:
                     serv.actions[id]['clean'] = False
@@ -108,13 +111,12 @@ class db(netsvc.Service):
                     traceback.print_exc(file=e_str)
                     traceback_str = e_str.getvalue()
                     e_str.close()
-                    print traceback_str
+                    netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
                     serv.actions[id]['traceback'] = traceback_str
                     if cr:
                         cr.close()
         logger = netsvc.Logger()
-        logger.notifyChannel("web-services", netsvc.LOG_INFO,
-                'CREATE DB: %s' % (db_name))
+        logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
         dbi = DBInitialize()
         create_thread = threading.Thread(target=dbi,
                 args=(self, id, db_name, demo, lang, user_password))
@@ -140,19 +142,19 @@ class db(netsvc.Service):
 
     def drop(self, password, db_name):
         security.check_super(password)
-        pooler.close_db(db_name)
+        sql_db.close_db(db_name)
         logger = netsvc.Logger()
 
         db = sql_db.db_connect('template1', serialize=1)
-        db.truedb.autocommit()
         cr = db.cursor()
+        cr.autocommit(True)
         try:
             try:
                 cr.execute('DROP DATABASE ' + db_name)
-            except:
-                logger.notifyChannel("web-service", netsvc.LOG_ERROR,
-                    'DROP DB: %s failed' % (db_name,))
-                raise
+            except Exception, e:
+                logger.notifyChannel("web-services", netsvc.LOG_ERROR,
+                        'DROP DB: %s failed:\n%s' % (db_name, e))
+                raise Exception("Couldn't drop database %s: %s" % (db_name, e))
             else:
                 logger.notifyChannel("web-services", netsvc.LOG_INFO,
                     'DROP DB: %s' % (db_name))
@@ -164,11 +166,6 @@ class db(netsvc.Service):
         security.check_super(password)
         logger = netsvc.Logger()
 
-        if tools.config['db_password']:
-            logger.notifyChannel("web-service", netsvc.LOG_ERROR,
-                    'DUMP DB: %s doesn\'t work with password' % (db_name,))
-            raise Exception, "Couldn't dump database with password"
-
         cmd = ['pg_dump', '--format=c']
         if tools.config['db_user']:
             cmd.append('--username=' + tools.config['db_user'])
@@ -183,7 +180,7 @@ class db(netsvc.Service):
         data = stdout.read()
         res = stdout.close()
         if res:
-            logger.notifyChannel("web-service", netsvc.LOG_ERROR,
+            logger.notifyChannel("web-services", netsvc.LOG_ERROR,
                     'DUMP DB: %s failed\n%s' % (db_name, data))
             raise Exception, "Couldn't dump database"
         logger.notifyChannel("web-services", netsvc.LOG_INFO,
@@ -195,18 +192,13 @@ class db(netsvc.Service):
         logger = netsvc.Logger()
 
         if self.db_exist(db_name):
-            logger.notifyChannel("web-service", netsvc.LOG_WARNING,
+            logger.notifyChannel("web-services", netsvc.LOG_WARNING,
                     'RESTORE DB: %s already exists' % (db_name,))
             raise Exception, "Database already exists"
 
-        if tools.config['db_password']:
-            logger.notifyChannel("web-service", netsvc.LOG_ERROR,
-                    'RESTORE DB: %s doesn\'t work with password' % (db_name,))
-            raise Exception, "Couldn't restore database with password"
-
         db = sql_db.db_connect('template1', serialize=1)
-        db.truedb.autocommit()
         cr = db.cursor()
+        cr.autocommit(True)
         cr.execute('CREATE DATABASE ' + db_name + ' ENCODING \'unicode\'')
         cr.close()
 
@@ -241,7 +233,6 @@ class db(netsvc.Service):
     def db_exist(self, db_name):
         try:
             db = sql_db.db_connect(db_name)
-            db.truedb.close()
             return True
         except:
             return False
@@ -255,18 +246,18 @@ class db(netsvc.Service):
                 import pwd
                 db_user = pwd.getpwuid(os.getuid())[0]
             if not db_user:
-                cr.execute("select usename from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
+                cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
                 res = cr.fetchone()
-                db_user = res and res[0]
+                db_user = res and str(res[0])
             if db_user:
-                cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in ('template0', 'template1', 'postgres')", (db_user,))
+                cr.execute("select decode(datname, 'escape') from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in ('template0', 'template1', 'postgres')", (db_user,))
             else:
-                cr.execute("select datname from pg_database where datname not in('template0', 'template1','postgres')")
-            res = [name for (name,) in cr.fetchall()]
+                cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres')")
+            res = [str(name) for (name,) in cr.fetchall()]
             cr.close()
         except:
             res = []
-        db.truedb.close()
+        res.sort()
         return res
 
     def change_admin_password(self, old_password, new_password):
@@ -274,24 +265,31 @@ class db(netsvc.Service):
         tools.config['admin_passwd'] = new_password
         tools.config.save()
         return True
-    
+
     def list_lang(self):
         return tools.scan_languages()
-        import glob
-        file_list = glob.glob(os.path.join(tools.config['root_path'], 'i18n', '*.csv'))
-        def lang_tuple(fname):
-            lang_dict=tools.get_languages()
-            lang = os.path.basename(fname).split(".")[0]
-            return (lang, lang_dict.get(lang, lang))
-        return [lang_tuple(fname) for fname in file_list]
 
     def server_version(self):
         """ Return the version of the server
             Used by the client to verify the compatibility with its own version
         """
-        return tinyerp_version
+        return release.version
+
+    def migrate_databases(self, password, databases):
+        security.check_super(password)
+        l = netsvc.Logger()
+        for db in databases:
+            try:
+                l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
+                tools.config['update']['base'] = True
+                pooler.restart_pool(db, force_demo=False, update_module=True) 
+            except Exception, e:
+                tools.debug(e)
+                raise
 db()
 
+class MigrationException(Exception): pass
+
 class common(netsvc.Service):
     def __init__(self,name="common"):
         netsvc.Service.__init__(self,name)
@@ -301,7 +299,9 @@ class common(netsvc.Service):
         self.exportMethod(self.ir_del)
         self.exportMethod(self.about)
         self.exportMethod(self.login)
+        self.exportMethod(self.logout)
         self.exportMethod(self.timezone_get)
+        self.exportMethod(self.get_migration_scripts)
 
     def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False):
         security.check(db, uid, password)
@@ -335,9 +335,14 @@ class common(netsvc.Service):
         res = security.login(db, login, password)
         logger = netsvc.Logger()
         msg = res and 'successful login' or 'bad login or password'
-        logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db))
+        logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower()))
         return res or False
 
+    def logout(self, db, login, password):
+        logger = netsvc.Logger()
+        logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
+        return True
+
     def about(self, extended=False):
         """Return information about the OpenERP Server.
 
@@ -355,11 +360,51 @@ GNU Public Licence.
 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
 
         if extended:
-            return info, tinyerp_version
+            return info, release.version
         return info
 
     def timezone_get(self, db, login, password):
         return time.tzname[0]
+
+
+    def get_migration_scripts(self, password, contract_id, contract_password):
+        security.check_super(password)
+        l = netsvc.Logger()
+        try: 
+            from tools.maintenance import remote_contract
+            rc = remote_contract(contract_id, contract_password)
+            if not rc.id:
+                raise MigrationException('This contract does not exist or is not active') 
+            if rc.status != 'full':
+                raise MigrationException('Can not get updates for a partial contract')
+
+            l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
+
+            zips = rc.retrieve_updates(rc.id)
+            
+            from shutil import rmtree
+            for module in zips:
+                l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
+                mp = addons.get_module_path(module)
+                if mp:
+                    if os.path.isdir(mp):
+                        rmtree(os.path.realpath(mp))
+                    else:
+                        os.unlink(mp + '.zip')
+
+                mp = os.path.join(tools.config['addons_path'], module + '.zip')
+
+                zip = open(mp, 'w')
+                zip.write(base64.decodestring(zips[module]))
+                zip.close()
+        except MigrationException, e:
+            self.abortResponse(1, 'Migration Error', 'warning', str(e))
+        except Exception, e:
+            import traceback
+            tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
+            l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
+            raise
 common()
 
 class objects_proxy(netsvc.Service):
@@ -369,13 +414,13 @@ class objects_proxy(netsvc.Service):
         self.exportMethod(self.execute)
         self.exportMethod(self.exec_workflow)
         self.exportMethod(self.obj_list)
-        
+
     def exec_workflow(self, db, uid, passwd, object, method, id):
         security.check(db, uid, passwd)
         service = netsvc.LocalService("object_proxy")
         res = service.exec_workflow(db, uid, object, method, id)
         return res
-        
+
     def execute(self, db, uid, passwd, object, method, *args):
         security.check(db, uid, passwd)
         service = netsvc.LocalService("object_proxy")
@@ -464,7 +509,7 @@ class report_spool(netsvc.Service):
         if not context:
             context={}
         security.check(db, uid, passwd)
-        
+
         self.id_protect.acquire()
         self.id += 1
         id = self.id
@@ -473,11 +518,10 @@ class report_spool(netsvc.Service):
         self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
 
         def go(id, uid, ids, datas, context):
+            cr = pooler.get_db(db).cursor()
             try:
-                cr = pooler.get_db(db).cursor()
                 obj = netsvc.LocalService('report.'+object)
                 (result, format) = obj.create(cr, uid, ids, datas, context)
-                cr.close()
                 self._reports[id]['result'] = result
                 self._reports[id]['format'] = format
                 self._reports[id]['state'] = True
@@ -487,10 +531,11 @@ class report_spool(netsvc.Service):
                 tb_s = reduce(lambda x, y: x+y, traceback.format_exception(
                     sys.exc_type, sys.exc_value, sys.exc_traceback))
                 logger = netsvc.Logger()
-                logger.notifyChannel('web-service', netsvc.LOG_ERROR,
+                logger.notifyChannel('web-services', netsvc.LOG_ERROR,
                         'Exception: %s\n%s' % (str(exception), tb_s))
                 self._reports[id]['exception'] = exception
                 self._reports[id]['state'] = True
+            cr.close()
             return True
 
         thread.start_new_thread(go, (id, uid, ids, datas, context))