1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
31 from tools.translate import _
40 from cStringIO import StringIO
44 class db(netsvc.ExportService):
45 def __init__(self, name="db"):
46 netsvc.ExportService.__init__(self, name)
47 self.joinGroup("web-services")
50 self.id_protect = threading.Semaphore()
52 self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
54 def dispatch(self, method, auth, params):
55 if method in [ 'create', 'get_progress', 'drop', 'dump',
57 'change_admin_password', 'migrate_databases' ]:
60 security.check_super(passwd)
61 elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
63 # No security check for these methods
66 raise KeyError("Method not found: %s" % method)
67 fn = getattr(self, 'exp_'+method)
70 def new_dispatch(self,method,auth,params):
72 def _create_empty_database(self, name):
73 db = sql_db.db_connect('template1')
76 cr.autocommit(True) # avoid transaction block
77 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % name)
81 def exp_create(self, db_name, demo, lang, user_password='admin'):
82 self.id_protect.acquire()
85 self.id_protect.release()
87 self.actions[id] = {'clean': False}
89 self._create_empty_database(db_name)
91 class DBInitialize(object):
92 def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
95 serv.actions[id]['progress'] = 0
96 cr = sql_db.db_connect(db_name).cursor()
101 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
102 update_module=True)[1]
104 cr = sql_db.db_connect(db_name).cursor()
107 modobj = pool.get('ir.module.module')
108 mids = modobj.search(cr, 1, [('state', '=', 'installed')])
109 modobj.update_translations(cr, 1, mids, lang)
111 cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
112 user_password, lang, 'admin'))
113 cr.execute('SELECT login, password, name ' \
116 serv.actions[id]['users'] = cr.dictfetchall()
117 serv.actions[id]['clean'] = True
121 serv.actions[id]['clean'] = False
122 serv.actions[id]['exception'] = e
125 traceback.print_exc(file=e_str)
126 traceback_str = e_str.getvalue()
128 netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
129 serv.actions[id]['traceback'] = traceback_str
132 logger = netsvc.Logger()
133 logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
135 create_thread = threading.Thread(target=dbi,
136 args=(self, id, db_name, demo, lang, user_password))
137 create_thread.start()
138 self.actions[id]['thread'] = create_thread
141 def exp_get_progress(self, id):
142 if self.actions[id]['thread'].isAlive():
143 # return addons.init_progress[db_name]
144 return (min(self.actions[id].get('progress', 0),0.95), [])
146 clean = self.actions[id]['clean']
148 users = self.actions[id]['users']
152 e = self.actions[id]['exception']
156 def exp_drop(self, db_name):
157 sql_db.close_db(db_name)
158 logger = netsvc.Logger()
160 db = sql_db.db_connect('template1')
162 cr.autocommit(True) # avoid transaction block
165 cr.execute('DROP DATABASE "%s"' % db_name)
167 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
168 'DROP DB: %s failed:\n%s' % (db_name, e))
169 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
171 logger.notifyChannel("web-services", netsvc.LOG_INFO,
172 'DROP DB: %s' % (db_name))
177 def _set_pg_psw_env_var(self):
178 if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
179 os.environ['PGPASSWORD'] = tools.config['db_password']
180 self._pg_psw_env_var_is_set = True
182 def _unset_pg_psw_env_var(self):
183 if os.name == 'nt' and self._pg_psw_env_var_is_set:
184 os.environ['PGPASSWORD'] = ''
186 def exp_dump(self, db_name):
187 logger = netsvc.Logger()
189 self._set_pg_psw_env_var()
191 cmd = ['pg_dump', '--format=c', '--no-owner']
192 if tools.config['db_user']:
193 cmd.append('--username=' + tools.config['db_user'])
194 if tools.config['db_host']:
195 cmd.append('--host=' + tools.config['db_host'])
196 if tools.config['db_port']:
197 cmd.append('--port=' + str(tools.config['db_port']))
200 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
205 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
206 'DUMP DB: %s failed\n%s' % (db_name, data))
207 raise Exception, "Couldn't dump database"
208 logger.notifyChannel("web-services", netsvc.LOG_INFO,
209 'DUMP DB: %s' % (db_name))
211 self._unset_pg_psw_env_var()
213 return base64.encodestring(data)
215 def exp_restore(self, db_name, data):
216 logger = netsvc.Logger()
218 self._set_pg_psw_env_var()
220 if self.exp_db_exist(db_name):
221 logger.notifyChannel("web-services", netsvc.LOG_WARNING,
222 'RESTORE DB: %s already exists' % (db_name,))
223 raise Exception, "Database already exists"
225 self._create_empty_database(db_name)
227 cmd = ['pg_restore', '--no-owner']
228 if tools.config['db_user']:
229 cmd.append('--username=' + tools.config['db_user'])
230 if tools.config['db_host']:
231 cmd.append('--host=' + tools.config['db_host'])
232 if tools.config['db_port']:
233 cmd.append('--port=' + str(tools.config['db_port']))
234 cmd.append('--dbname=' + db_name)
237 buf=base64.decodestring(data)
239 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
240 file(tmpfile, 'wb').write(buf)
242 args2.append(' ' + tmpfile)
244 stdin, stdout = tools.exec_pg_command_pipe(*args2)
245 if not os.name == "nt":
246 stdin.write(base64.decodestring(data))
250 raise Exception, "Couldn't restore database"
251 logger.notifyChannel("web-services", netsvc.LOG_INFO,
252 'RESTORE DB: %s' % (db_name))
254 self._unset_pg_psw_env_var()
258 def exp_rename(self, old_name, new_name):
259 sql_db.close_db(old_name)
260 logger = netsvc.Logger()
262 db = sql_db.db_connect('template1')
266 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
268 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
269 'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
270 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
272 fs = os.path.join(tools.config['root_path'], 'filestore')
273 if os.path.exists(os.path.join(fs, old_name)):
274 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
276 logger.notifyChannel("web-services", netsvc.LOG_INFO,
277 'RENAME DB: %s -> %s' % (old_name, new_name))
282 def exp_db_exist(self, db_name):
283 ## Not True: in fact, check if connection to database is possible. The database may exists
284 return bool(sql_db.db_connect(db_name))
287 if not tools.config['list_db']:
288 raise Exception('AccessDenied')
290 db = sql_db.db_connect('template1')
294 db_user = tools.config["db_user"]
295 if not db_user and os.name == 'posix':
297 db_user = pwd.getpwuid(os.getuid())[0]
299 cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
301 db_user = res and str(res[0])
303 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,))
305 cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
306 res = [str(name) for (name,) in cr.fetchall()]
314 def exp_change_admin_password(self, new_password):
315 tools.config['admin_passwd'] = new_password
319 def exp_list_lang(self):
320 return tools.scan_languages()
322 def exp_server_version(self):
323 """ Return the version of the server
324 Used by the client to verify the compatibility with its own version
326 return release.version
328 def exp_migrate_databases(self,databases):
330 from osv.orm import except_orm
331 from osv.osv import except_osv
336 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
337 tools.config['update']['base'] = True
338 pooler.restart_pool(db, force_demo=False, update_module=True)
339 except except_orm, inst:
340 self.abortResponse(1, inst.name, 'warning', inst.value)
341 except except_osv, inst:
342 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
345 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
346 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
351 class _ObjectService(netsvc.ExportService):
352 "A common base class for those who have fn(db, uid, password,...) "
354 def common_dispatch(self, method, auth, params):
355 (db, uid, passwd ) = params[0:3]
357 security.check(db,uid,passwd)
358 cr = pooler.get_db(db).cursor()
359 fn = getattr(self, 'exp_'+method)
360 res = fn(cr, uid, *params)
365 class common(_ObjectService):
366 def __init__(self,name="common"):
367 _ObjectService.__init__(self,name)
368 self.joinGroup("web-services")
370 def dispatch(self, method, auth, params):
371 logger = netsvc.Logger()
372 if method in [ 'ir_set','ir_del', 'ir_get' ]:
373 return self.common_dispatch(method,auth,params)
374 if method == 'login':
375 # At this old dispatcher, we do NOT update the auth proxy
376 res = security.login(params[0], params[1], params[2])
377 msg = res and 'successful login' or 'bad login or password'
378 # TODO log the client ip address..
379 logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
381 elif method == 'logout':
383 auth.logout(params[1])
384 logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
386 elif method in ['about', 'timezone_get', 'get_server_environment',
387 'login_message','get_stats', 'check_connectivity']:
389 elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel']:
392 security.check_super(passwd)
394 raise Exception("Method not found: %s" % method)
396 fn = getattr(self, 'exp_'+method)
400 def new_dispatch(self,method,auth,params):
403 def exp_ir_set(self, cr, uid, keys, args, name, value, replace=True, isobject=False):
404 res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
407 def exp_ir_del(self, cr, uid, id):
408 res = ir.ir_del(cr,uid, id)
411 def exp_ir_get(self, cr, uid, keys, args=None, meta=None, context=None):
416 res = ir.ir_get(cr,uid, keys, args, meta, context)
419 def exp_about(self, extended=False):
420 """Return information about the OpenERP Server.
422 @param extended: if True then return version info
423 @return string if extended is False else tuple
428 OpenERP is an ERP+CRM program for small and medium businesses.
430 The whole source code is distributed under the terms of the
433 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
436 return info, release.version
439 def exp_timezone_get(self, db, login, password):
440 #timezone detection is safe in multithread, so lazy init is ok here
441 if (not tools.config['timezone']):
442 tools.config['timezone'] = tools.misc.detect_server_timezone()
443 return tools.config['timezone']
446 def exp_get_available_updates(self, contract_id, contract_password):
447 import tools.maintenance as tm
449 rc = tm.remote_contract(contract_id, contract_password)
451 raise tm.RemoteContractException('This contract does not exist or is not active')
453 return rc.get_available_updates(rc.id, addons.get_modules_with_version())
455 except tm.RemoteContractException, e:
456 self.abortResponse(1, 'Migration Error', 'warning', str(e))
459 def exp_get_migration_scripts(self, contract_id, contract_password):
461 import tools.maintenance as tm
463 rc = tm.remote_contract(contract_id, contract_password)
465 raise tm.RemoteContractException('This contract does not exist or is not active')
466 if rc.status != 'full':
467 raise tm.RemoteContractException('Can not get updates for a partial contract')
469 l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
471 zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version())
473 from shutil import rmtree, copytree, copy
475 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
476 if zips and not os.path.isdir(backup_directory):
477 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
478 store the old modules: %s' % (backup_directory,))
479 os.makedirs(backup_directory)
482 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
483 mp = addons.get_module_path(module)
485 if os.path.isdir(mp):
486 copytree(mp, os.path.join(backup_directory, module))
487 if os.path.islink(mp):
492 copy(mp + 'zip', backup_directory)
493 os.unlink(mp + '.zip')
497 base64_decoded = base64.decodestring(zips[module])
499 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
502 zip_contents = StringIO(base64_decoded)
506 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
508 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
514 l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
515 nmp = os.path.join(backup_directory, module)
516 if os.path.isdir(nmp):
517 copytree(nmp, tools.config['addons_path'])
519 copy(nmp+'.zip', tools.config['addons_path'])
523 except tm.RemoteContractException, e:
524 self.abortResponse(1, 'Migration Error', 'warning', str(e))
527 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
528 l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
531 def exp_get_server_environment(self):
532 os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
535 environment = '\nEnvironment Information : \n' \
538 %(platform.platform(), platform.os.name)
539 if os.name == 'posix':
540 if platform.system() == 'Linux':
541 lsbinfo = os.popen('lsb_release -a').read()
542 environment += '%s'%(lsbinfo)
544 environment += 'Your System is not lsb compliant\n'
545 environment += 'Operating System Release : %s\n' \
546 'Operating System Version : %s\n' \
547 'Operating System Architecture : %s\n' \
548 'Operating System Locale : %s\n'\
549 'Python Version : %s\n'\
550 'OpenERP-Server Version : %s'\
551 %(platform.release(), platform.version(), platform.architecture()[0],
552 os_lang, platform.python_version(),release.version)
555 def exp_login_message(self):
556 return tools.config.get('login_message', False)
558 def exp_set_loglevel(self,loglevel):
560 l.set_loglevel(int(loglevel))
563 def exp_get_stats(self):
565 res = "OpenERP server: %d threads\n" % threading.active_count()
566 res += netsvc.Server.allStats()
569 def exp_check_connectivity(self):
570 return bool(sql_db.db_connect('template1'))
574 class objects_proxy(netsvc.ExportService):
575 def __init__(self, name="object"):
576 netsvc.ExportService.__init__(self,name)
577 self.joinGroup('web-services')
579 def dispatch(self, method, auth, params):
580 (db, uid, passwd ) = params[0:3]
582 if method not in ['execute','exec_workflow','obj_list']:
583 raise KeyError("Method not supported %s" % method)
584 security.check(db,uid,passwd)
585 ls = netsvc.LocalService('object_proxy')
586 fn = getattr(ls, method)
587 res = fn(db, uid, *params)
591 def new_dispatch(self,method,auth,params):
599 # - None = end of wizard
601 # Wizard Type: 'form'
606 # TODO: change local request to OSE request/reply pattern
608 class wizard(netsvc.ExportService):
609 def __init__(self, name='wizard'):
610 netsvc.ExportService.__init__(self,name)
611 self.joinGroup('web-services')
617 def dispatch(self, method, auth, params):
618 (db, uid, passwd ) = params[0:3]
620 if method not in ['execute','create']:
621 raise KeyError("Method not supported %s" % method)
622 security.check(db,uid,passwd)
623 fn = getattr(self, 'exp_'+method)
624 res = fn(db, uid, *params)
627 def new_dispatch(self,method,auth,params):
630 def _execute(self, db, uid, wiz_id, datas, action, context):
631 self.wiz_datas[wiz_id].update(datas)
632 wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
633 return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
635 def exp_create(self, db, uid, wiz_name, datas=None):
638 #FIXME: this is not thread-safe
640 self.wiz_datas[self.id] = {}
641 self.wiz_name[self.id] = wiz_name
642 self.wiz_uid[self.id] = uid
645 def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
649 if wiz_id in self.wiz_uid:
650 if self.wiz_uid[wiz_id] == uid:
651 return self._execute(db, uid, wiz_id, datas, action, context)
653 raise Exception, 'AccessDenied'
655 raise Exception, 'WizardNotFound'
659 # TODO: set a maximum report number per user to avoid DOS attacks
665 class ExceptionWithTraceback(Exception):
666 def __init__(self, msg, tb):
669 self.args = (msg, tb)
671 class report_spool(netsvc.ExportService):
672 def __init__(self, name='report'):
673 netsvc.ExportService.__init__(self, name)
674 self.joinGroup('web-services')
677 self.id_protect = threading.Semaphore()
679 def dispatch(self, method, auth, params):
680 (db, uid, passwd ) = params[0:3]
682 if method not in ['report','report_get']:
683 raise KeyError("Method not supported %s" % method)
684 security.check(db,uid,passwd)
685 fn = getattr(self, 'exp_' + method)
686 res = fn(db, uid, *params)
690 def new_dispatch(self,method,auth,params):
693 def exp_report(self, db, uid, object, ids, datas=None, context=None):
699 self.id_protect.acquire()
702 self.id_protect.release()
704 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
706 def go(id, uid, ids, datas, context):
707 cr = pooler.get_db(db).cursor()
711 obj = netsvc.LocalService('report.'+object)
712 (result, format) = obj.create(cr, uid, ids, datas, context)
715 self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb)
716 self._reports[id]['result'] = result
717 self._reports[id]['format'] = format
718 self._reports[id]['state'] = True
719 except Exception, exception:
722 tb_s = "".join(traceback.format_exception(*tb))
723 logger = netsvc.Logger()
724 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
725 'Exception: %s\n%s' % (str(exception), tb_s))
726 self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
727 self._reports[id]['state'] = True
732 thread.start_new_thread(go, (id, uid, ids, datas, context))
735 def _check_report(self, report_id):
736 result = self._reports[report_id]
737 if result['exception']:
738 raise result['exception']
739 res = {'state': result['state']}
741 if tools.config['reportgz']:
743 res2 = zlib.compress(result['result'])
746 #CHECKME: why is this needed???
747 if isinstance(result['result'], unicode):
748 res2 = result['result'].encode('latin1', 'replace')
750 res2 = result['result']
752 res['result'] = base64.encodestring(res2)
753 res['format'] = result['format']
754 del self._reports[report_id]
757 def exp_report_get(self, db, uid, report_id):
759 if report_id in self._reports:
760 if self._reports[report_id]['uid'] == uid:
761 return self._check_report(report_id)
763 raise Exception, 'AccessDenied'
765 raise Exception, 'ReportNotFound'
770 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: