[FIX] export the method check_connectivity
[odoo/odoo.git] / bin / service / web_services.py
index e469719..4dfdeec 100644 (file)
@@ -1,34 +1,33 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
 ##############################################################################
-#
-#    OpenERP, Open Source Management Solution  
-#    Copyright (C) 2004-2008 Tiny SPRL (<http://tiny.be>). All Rights Reserved
-#    $Id$
+#    
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
 #
 #    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.
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
 #
 #    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.
+#    GNU Affero General Public License for more details.
 #
-#    You should have received a copy of the GNU General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.     
 #
 ##############################################################################
 
-import base64 
+import base64
 import logging
 import os
 import security
-import string
 import thread
 import threading
 import time
-
+import sys
+import platform
 from tools.translate import _
 import addons
 import ir
@@ -37,29 +36,49 @@ import pooler
 import release
 import sql_db
 import tools
+import locale
+from cStringIO import StringIO
 
 logging.basicConfig()
 
-class db(netsvc.Service):
+class db(netsvc.ExportService):
     def __init__(self, name="db"):
-        netsvc.Service.__init__(self, name)
+        netsvc.ExportService.__init__(self, name)
         self.joinGroup("web-services")
-        self.exportMethod(self.create)
-        self.exportMethod(self.get_progress)
-        self.exportMethod(self.drop)
-        self.exportMethod(self.dump)
-        self.exportMethod(self.restore)
-        self.exportMethod(self.list)
-        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()
 
-    def create(self, password, db_name, demo, lang, user_password='admin'):
-        security.check_super(password)
+        self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
+
+    def dispatch(self, method, auth, params):
+        if method in [ 'create', 'get_progress', 'drop', 'dump', 
+            'restore', 'rename', 
+            'change_admin_password', 'migrate_databases' ]:
+            passwd = params[0]
+            params = params[1:]
+            security.check_super(passwd)
+        elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
+            # params = params
+            # No security check for these methods
+            pass
+        else:
+            raise KeyError("Method not found: %s" % method)
+        fn = getattr(self, 'exp_'+method)
+        return fn(*params)
+    
+    def new_dispatch(self,method,auth,params):
+        pass
+    def _create_empty_database(self, name):
+        db = sql_db.db_connect('template1')
+        cr = db.cursor()
+        try:
+            cr.autocommit(True) # avoid transaction block
+            cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % name)
+        finally:
+            cr.close()
+
+    def exp_create(self, db_name, demo, lang, user_password='admin'):
         self.id_protect.acquire()
         self.id += 1
         id = self.id
@@ -67,17 +86,13 @@ class db(netsvc.Service):
 
         self.actions[id] = {'clean': False}
 
-        db = sql_db.db_connect('template1', serialize=1)
-        cr = db.cursor()
-        cr.autocommit(True)
-        time.sleep(0.2)
-        cr.execute('CREATE DATABASE ' + db_name + ' ENCODING \'unicode\'')
-        cr.close()
+        self._create_empty_database(db_name)
+
         class DBInitialize(object):
             def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
+                cr = None
                 try:
                     serv.actions[id]['progress'] = 0
-                    clean = False
                     cr = sql_db.db_connect(db_name).cursor()
                     tools.init_db(cr)
                     cr.commit()
@@ -105,7 +120,6 @@ class db(netsvc.Service):
                 except Exception, e:
                     serv.actions[id]['clean'] = False
                     serv.actions[id]['exception'] = e
-                    from StringIO import StringIO
                     import traceback
                     e_str = StringIO()
                     traceback.print_exc(file=e_str)
@@ -124,8 +138,7 @@ class db(netsvc.Service):
         self.actions[id]['thread'] = create_thread
         return id
 
-    def get_progress(self, password, id):
-        security.check_super(password)
+    def exp_get_progress(self, id):
         if self.actions[id]['thread'].isAlive():
 #           return addons.init_progress[db_name]
             return (min(self.actions[id].get('progress', 0),0.95), [])
@@ -133,24 +146,23 @@ class db(netsvc.Service):
             clean = self.actions[id]['clean']
             if clean:
                 users = self.actions[id]['users']
-                del self.actions[id]
+                self.actions.pop(id)
                 return (1.0, users)
             else:
                 e = self.actions[id]['exception']
-                del self.actions[id]
+                self.actions.pop(id)
                 raise Exception, e
 
-    def drop(self, password, db_name):
-        security.check_super(password)
+    def exp_drop(self, db_name):
         sql_db.close_db(db_name)
         logger = netsvc.Logger()
 
-        db = sql_db.db_connect('template1', serialize=1)
+        db = sql_db.db_connect('template1')
         cr = db.cursor()
-        cr.autocommit(True)
+        cr.autocommit(True) # avoid transaction block
         try:
             try:
-                cr.execute('DROP DATABASE ' + db_name)
+                cr.execute('DROP DATABASE "%s"' % db_name)
             except Exception, e:
                 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
                         'DROP DB: %s failed:\n%s' % (db_name, e))
@@ -162,17 +174,27 @@ class db(netsvc.Service):
             cr.close()
         return True
 
-    def dump(self, password, db_name):
-        security.check_super(password)
+    def _set_pg_psw_env_var(self):
+        if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
+            os.environ['PGPASSWORD'] = tools.config['db_password']
+            self._pg_psw_env_var_is_set = True
+
+    def _unset_pg_psw_env_var(self):
+        if os.name == 'nt' and self._pg_psw_env_var_is_set:
+            os.environ['PGPASSWORD'] = ''
+
+    def exp_dump(self, db_name):
         logger = netsvc.Logger()
 
-        cmd = ['pg_dump', '--format=c']
+        self._set_pg_psw_env_var()
+
+        cmd = ['pg_dump', '--format=c', '--no-owner']
         if tools.config['db_user']:
             cmd.append('--username=' + tools.config['db_user'])
         if tools.config['db_host']:
             cmd.append('--host=' + tools.config['db_host'])
         if tools.config['db_port']:
-            cmd.append('--port=' + tools.config['db_port'])
+            cmd.append('--port=' + str(tools.config['db_port']))
         cmd.append(db_name)
 
         stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
@@ -185,30 +207,30 @@ class db(netsvc.Service):
             raise Exception, "Couldn't dump database"
         logger.notifyChannel("web-services", netsvc.LOG_INFO,
                 'DUMP DB: %s' % (db_name))
+
+        self._unset_pg_psw_env_var()
+
         return base64.encodestring(data)
 
-    def restore(self, password, db_name, data):
-        security.check_super(password)
+    def exp_restore(self, db_name, data):
         logger = netsvc.Logger()
 
+        self._set_pg_psw_env_var()
+
         if self.db_exist(db_name):
             logger.notifyChannel("web-services", netsvc.LOG_WARNING,
                     'RESTORE DB: %s already exists' % (db_name,))
             raise Exception, "Database already exists"
 
-        db = sql_db.db_connect('template1', serialize=1)
-        cr = db.cursor()
-        cr.autocommit(True)
-        cr.execute('CREATE DATABASE ' + db_name + ' ENCODING \'unicode\'')
-        cr.close()
+        self._create_empty_database(db_name)
 
-        cmd = ['pg_restore']
+        cmd = ['pg_restore', '--no-owner']
         if tools.config['db_user']:
             cmd.append('--username=' + tools.config['db_user'])
         if tools.config['db_host']:
             cmd.append('--host=' + tools.config['db_host'])
         if tools.config['db_port']:
-            cmd.append('--port=' + tools.config['db_port'])
+            cmd.append('--port=' + str(tools.config['db_port']))
         cmd.append('--dbname=' + db_name)
         args2 = tuple(cmd)
 
@@ -228,122 +250,173 @@ class db(netsvc.Service):
             raise Exception, "Couldn't restore database"
         logger.notifyChannel("web-services", netsvc.LOG_INFO,
                 'RESTORE DB: %s' % (db_name))
+
+        self._unset_pg_psw_env_var()
+
         return True
 
-    def db_exist(self, db_name):
-        try:
-            db = sql_db.db_connect(db_name)
-            return True
-        except:
-            return False
+    def exp_rename(self, old_name, new_name):
+        sql_db.close_db(old_name)
+        logger = netsvc.Logger()
 
-    def list(self):
         db = sql_db.db_connect('template1')
+        cr = db.cursor()
         try:
-            cr = db.cursor()
-            db_user = tools.config["db_user"]
-            if not db_user and os.name == 'posix':
-                import pwd
-                db_user = pwd.getpwuid(os.getuid())[0]
-            if not db_user:
-                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 str(res[0])
-            if 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,))
+            try:
+                cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
+            except Exception, e:
+                logger.notifyChannel("web-services", netsvc.LOG_ERROR,
+                        'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
+                raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
             else:
-                cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres')")
-            res = [str(name) for (name,) in cr.fetchall()]
+                fs = os.path.join(tools.config['root_path'], 'filestore')
+                if os.path.exists(os.path.join(fs, old_name)):
+                    os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
+
+                logger.notifyChannel("web-services", netsvc.LOG_INFO,
+                    'RENAME DB: %s -> %s' % (old_name, new_name))
+        finally:
+            cr.close()
+        return True
+
+    def exp_db_exist(self, db_name):
+        ## Not True: in fact, check if connection to database is possible. The database may exists
+        return bool(sql_db.db_connect(db_name))
+
+    def exp_list(self):
+        if not tools.config['list_db']:
+            raise Exception('AccessDenied')
+
+        db = sql_db.db_connect('template1')
+        cr = db.cursor()
+        try:
+            try:
+                db_user = tools.config["db_user"]
+                if not db_user and os.name == 'posix':
+                    import pwd
+                    db_user = pwd.getpwuid(os.getuid())[0]
+                if not db_user:
+                    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 str(res[0])
+                if 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') order by datname", (db_user,))
+                else:
+                    cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
+                res = [str(name) for (name,) in cr.fetchall()]
+            except:
+                res = []
+        finally:
             cr.close()
-        except:
-            res = []
         res.sort()
         return res
 
-    def change_admin_password(self, old_password, new_password):
-        security.check_super(old_password)
+    def exp_change_admin_password(self, new_password):
         tools.config['admin_passwd'] = new_password
         tools.config.save()
         return True
 
-    def list_lang(self):
+    def exp_list_lang(self):
         return tools.scan_languages()
 
-    def server_version(self):
+    def exp_server_version(self):
         """ Return the version of the server
             Used by the client to verify the compatibility with its own version
         """
         return release.version
 
-    def migrate_databases(self, password, databases):
-        security.check_super(password)
+    def exp_migrate_databases(self,databases):
+
+        from osv.orm import except_orm
+        from osv.osv import except_osv
+
         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)
+                pooler.restart_pool(db, force_demo=False, update_module=True)
+            except except_orm, inst:
+                self.abortResponse(1, inst.name, 'warning', inst.value)
+            except except_osv, inst:
+                self.abortResponse(1, inst.name, inst.exc_type, inst.value)
+            except Exception:
+                import traceback
+                tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
+                l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
                 raise
+        return True
 db()
 
-class MigrationException(Exception): pass
+class _ObjectService(netsvc.ExportService):
+     "A common base class for those who have fn(db, uid, password,...) "
 
-class common(netsvc.Service):
-    def __init__(self,name="common"):
-        netsvc.Service.__init__(self,name)
-        self.joinGroup("web-services")
-        self.exportMethod(self.ir_get)
-        self.exportMethod(self.ir_set)
-        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)
+     def common_dispatch(self, method, auth, params):
+        (db, uid, passwd ) = params[0:3]
+        params = params[3:]
+        security.check(db,uid,passwd)
         cr = pooler.get_db(db).cursor()
-        res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
+        fn = getattr(self, 'exp_'+method)
+        res = fn(cr, uid, *params)
         cr.commit()
         cr.close()
         return res
 
-    def ir_del(self, db, uid, password, id):
-        security.check(db, uid, password)
-        cr = pooler.get_db(db).cursor()
+class common(_ObjectService):
+    def __init__(self,name="common"):
+        _ObjectService.__init__(self,name)
+        self.joinGroup("web-services")
+
+    def dispatch(self, method, auth, params):
+        logger = netsvc.Logger()
+        if method in [ 'ir_set','ir_del', 'ir_get' ]:
+            return self.common_dispatch(method,auth,params)
+        if method == 'login':
+            # At this old dispatcher, we do NOT update the auth proxy
+            res = security.login(params[0], params[1], params[2])
+            msg = res and 'successful login' or 'bad login or password'
+            # TODO log the client ip address..
+            logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
+            return res or False
+        elif method == 'logout':
+            if auth:
+                auth.logout(params[1])
+            logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
+            return True
+        elif method in ['about', 'timezone_get', 'get_server_environment',
+                        'login_message','get_stats', 'check_connectivity']:
+            pass
+        elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel']:
+            passwd = params[0]
+            params = params[1:]
+            security.check_super(passwd)
+        else:
+            raise Exception("Method not found: %s" % method)
+        
+        fn = getattr(self, 'exp_'+method)
+        return fn(*params)
+
+
+    def new_dispatch(self,method,auth,params):
+        pass
+
+    def exp_ir_set(self, cr, uid, keys, args, name, value, replace=True, isobject=False):
+        res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
+        return res
+
+    def exp_ir_del(self, cr, uid, id):
         res = ir.ir_del(cr,uid, id)
-        cr.commit()
-        cr.close()
         return res
 
-    def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None):
+    def exp_ir_get(self, cr, uid, keys, args=None, meta=None, context=None):
         if not args:
             args=[]
         if not context:
             context={}
-        security.check(db, uid, password)
-        cr = pooler.get_db(db).cursor()
         res = ir.ir_get(cr,uid, keys, args, meta, context)
-        cr.commit()
-        cr.close()
         return res
 
-    def login(self, db, login, password):
-        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.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):
+    def exp_about(self, extended=False):
         """Return information about the OpenERP Server.
 
         @param extended: if True then return version info
@@ -363,75 +436,161 @@ GNU Public Licence.
             return info, release.version
         return info
 
-    def timezone_get(self, db, login, password):
-        return time.tzname[0]
+    def exp_timezone_get(self, db, login, password):
+        #timezone detection is safe in multithread, so lazy init is ok here
+        if (not tools.config['timezone']):
+            tools.config['timezone'] = tools.misc.detect_server_timezone()
+        return tools.config['timezone']
+
+
+    def exp_get_available_updates(self, contract_id, contract_password):
+        import tools.maintenance as tm
+        try:
+            rc = tm.remote_contract(contract_id, contract_password)
+            if not rc.id:
+                raise tm.RemoteContractException('This contract does not exist or is not active')
+
+            return rc.get_available_updates(rc.id, addons.get_modules_with_version())
+
+        except tm.RemoteContractException, e:
+            self.abortResponse(1, 'Migration Error', 'warning', str(e))
 
 
-    def get_migration_scripts(self, password, contract_id, contract_password):
-        security.check_super(password)
+    def exp_get_migration_scripts(self, contract_id, contract_password):
         l = netsvc.Logger()
-        try: 
-            from tools.maintenance import remote_contract
-            rc = remote_contract(contract_id, contract_password)
+        import tools.maintenance as tm
+        try:
+            rc = tm.remote_contract(contract_id, contract_password)
             if not rc.id:
-                raise MigrationException('This contract does not exist or is not active') 
+                raise tm.RemoteContractException('This contract does not exist or is not active')
             if rc.status != 'full':
-                raise MigrationException('Can not get updates for a partial contract')
+                raise tm.RemoteContractException('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
+            zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version())
+
+            from shutil import rmtree, copytree, copy
+
+            backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
+            if zips and not os.path.isdir(backup_directory):
+                l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
+                                store the old modules: %s' % (backup_directory,))
+                os.makedirs(backup_directory)
+
             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))
+                        copytree(mp, os.path.join(backup_directory, module))
+                        if os.path.islink(mp):
+                            os.unlink(mp)
+                        else:
+                            rmtree(mp)
                     else:
+                        copy(mp + 'zip', backup_directory)
                         os.unlink(mp + '.zip')
 
-                mp = os.path.join(tools.config['addons_path'], module + '.zip')
+                try:
+                    try:
+                        base64_decoded = base64.decodestring(zips[module])
+                    except:
+                        l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
+                        raise
+
+                    zip_contents = StringIO(base64_decoded)
+                    zip_contents.seek(0)
+                    try:
+                        try:
+                            tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
+                        except:
+                            l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
+                            rmtree(module)
+                            raise
+                    finally:
+                        zip_contents.close()
+                except:
+                    l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
+                    nmp = os.path.join(backup_directory, module)
+                    if os.path.isdir(nmp):
+                        copytree(nmp, tools.config['addons_path'])
+                    else:
+                        copy(nmp+'.zip', tools.config['addons_path'])
+                    raise
 
-                zip = open(mp, 'w')
-                zip.write(base64.decodestring(zips[module]))
-                zip.close()
-        except MigrationException, e:
+            return True
+        except tm.RemoteContractException, 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
+
+    def exp_get_server_environment(self):
+        os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
+        if not os_lang:
+            os_lang = 'NOT SET'
+        environment = '\nEnvironment Information : \n' \
+                     'System : %s\n' \
+                     'OS Name : %s\n' \
+                     %(platform.platform(), platform.os.name)
+        if os.name == 'posix':
+          if platform.system() == 'Linux':
+             lsbinfo = os.popen('lsb_release -a').read()
+             environment += '%s'%(lsbinfo)
+          else:
+             environment += 'Your System is not lsb compliant\n'
+        environment += 'Operating System Release : %s\n' \
+                    'Operating System Version : %s\n' \
+                    'Operating System Architecture : %s\n' \
+                    'Operating System Locale : %s\n'\
+                    'Python Version : %s\n'\
+                    'OpenERP-Server Version : %s'\
+                    %(platform.release(), platform.version(), platform.architecture()[0],
+                      os_lang, platform.python_version(),release.version)
+        return environment
+
+    def exp_login_message(self):
+        return tools.config.get('login_message', False)
+
+    def exp_set_loglevel(self,loglevel):
+        l = netsvc.Logger()
+        l.set_loglevel(int(loglevel))
+        return True
+
+    def exp_get_stats(self):
+        import threading
+        res = "OpenERP server: %d threads\n" % threading.active_count()
+        res += netsvc.Server.allStats()
+        return res
+
+    def exp_check_connectivity(self):
+        return bool(sql_db.db_connect('template1'))
+
 common()
 
-class objects_proxy(netsvc.Service):
+class objects_proxy(netsvc.ExportService):
     def __init__(self, name="object"):
-        netsvc.Service.__init__(self,name)
+        netsvc.ExportService.__init__(self,name)
         self.joinGroup('web-services')
-        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")
-        res = service.execute(db, uid, object, method, *args)
+    def dispatch(self, method, auth, params):
+        (db, uid, passwd ) = params[0:3]
+        params = params[3:]
+        if method not in ['execute','exec_workflow','obj_list']:
+            raise KeyError("Method not supported %s" % method)
+        security.check(db,uid,passwd)
+        ls = netsvc.LocalService('object_proxy')
+        fn = getattr(ls, method)
+        res = fn(db, uid, *params)
         return res
 
-    def obj_list(self, db, uid, passwd):
-        security.check(db, uid, passwd)
-        service = netsvc.LocalService("object_proxy")
-        res = service.obj_list()
-        return res
+    
+    def new_dispatch(self,method,auth,params):
+        pass
+
 objects_proxy()
 
 
@@ -446,26 +605,36 @@ objects_proxy()
 # Wizard datas: {}
 # TODO: change local request to OSE request/reply pattern
 #
-class wizard(netsvc.Service):
+class wizard(netsvc.ExportService):
     def __init__(self, name='wizard'):
-        netsvc.Service.__init__(self,name)
+        netsvc.ExportService.__init__(self,name)
         self.joinGroup('web-services')
-        self.exportMethod(self.execute)
-        self.exportMethod(self.create)
         self.id = 0
         self.wiz_datas = {}
         self.wiz_name = {}
         self.wiz_uid = {}
 
+    def dispatch(self, method, auth, params):
+        (db, uid, passwd ) = params[0:3]
+        params = params[3:]
+        if method not in ['execute','create']:
+            raise KeyError("Method not supported %s" % method)
+        security.check(db,uid,passwd)
+        fn = getattr(self, 'exp_'+method)
+        res = fn(db, uid, *params)
+        return res
+    
+    def new_dispatch(self,method,auth,params):
+        pass
+
     def _execute(self, db, uid, wiz_id, datas, action, context):
         self.wiz_datas[wiz_id].update(datas)
         wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
         return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
 
-    def create(self, db, uid, passwd, wiz_name, datas=None):
+    def exp_create(self, db, uid, wiz_name, datas=None):
         if not datas:
             datas={}
-        security.check(db, uid, passwd)
 #FIXME: this is not thread-safe
         self.id += 1
         self.wiz_datas[self.id] = {}
@@ -473,10 +642,9 @@ class wizard(netsvc.Service):
         self.wiz_uid[self.id] = uid
         return self.id
 
-    def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None):
+    def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
         if not context:
             context={}
-        security.check(db, uid, passwd)
 
         if wiz_id in self.wiz_uid:
             if self.wiz_uid[wiz_id] == uid:
@@ -493,22 +661,40 @@ wizard()
 # Report state:
 #     False -> True
 #
-class report_spool(netsvc.Service):
+
+class ExceptionWithTraceback(Exception):
+    def __init__(self, msg, tb):
+        self.message = msg
+        self.traceback = tb
+        self.args = (msg, tb)
+
+class report_spool(netsvc.ExportService):
     def __init__(self, name='report'):
-        netsvc.Service.__init__(self, name)
+        netsvc.ExportService.__init__(self, name)
         self.joinGroup('web-services')
-        self.exportMethod(self.report)
-        self.exportMethod(self.report_get)
         self._reports = {}
         self.id = 0
         self.id_protect = threading.Semaphore()
 
-    def report(self, db, uid, passwd, object, ids, datas=None, context=None):
+    def dispatch(self, method, auth, params):
+        (db, uid, passwd ) = params[0:3]
+        params = params[3:]
+        if method not in ['report','report_get']:
+            raise KeyError("Method not supported %s" % method)
+        security.check(db,uid,passwd)
+        fn = getattr(self, 'exp_' + method)
+        res = fn(db, uid, *params)
+        return res
+
+    
+    def new_dispatch(self,method,auth,params):
+        pass
+
+    def exp_report(self, db, uid, object, ids, datas=None, context=None):
         if not datas:
             datas={}
         if not context:
             context={}
-        security.check(db, uid, passwd)
 
         self.id_protect.acquire()
         self.id += 1
@@ -519,22 +705,27 @@ class report_spool(netsvc.Service):
 
         def go(id, uid, ids, datas, context):
             cr = pooler.get_db(db).cursor()
+            import traceback
+            import sys
             try:
                 obj = netsvc.LocalService('report.'+object)
                 (result, format) = obj.create(cr, uid, ids, datas, context)
+                if not result:
+                    tb = sys.exc_info()
+                    self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb)
                 self._reports[id]['result'] = result
                 self._reports[id]['format'] = format
                 self._reports[id]['state'] = True
             except Exception, exception:
-                import traceback
-                import sys
-                tb_s = reduce(lambda x, y: x+y, traceback.format_exception(
-                    sys.exc_type, sys.exc_value, sys.exc_traceback))
+                
+                tb = sys.exc_info()
+                tb_s = "".join(traceback.format_exception(*tb))
                 logger = netsvc.Logger()
                 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
                         'Exception: %s\n%s' % (str(exception), tb_s))
-                self._reports[id]['exception'] = exception
+                self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
                 self._reports[id]['state'] = True
+            cr.commit()
             cr.close()
             return True
 
@@ -563,8 +754,7 @@ class report_spool(netsvc.Service):
             del self._reports[report_id]
         return res
 
-    def report_get(self, db, uid, passwd, report_id):
-        security.check(db, uid, passwd)
+    def exp_report_get(self, db, uid, report_id):
 
         if report_id in self._reports:
             if self._reports[report_id]['uid'] == uid: