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 ##############################################################################
30 from openerp.tools.translate import _
31 import openerp.netsvc as netsvc
32 import openerp.pooler as pooler
33 import openerp.release as release
34 import openerp.sql_db as sql_db
35 import openerp.tools as tools
36 import openerp.modules
39 from cStringIO import StringIO
42 class edi(netsvc.ExportService):
43 def exp_get_edi_document(self, edi_token, db_name):
46 cr = pooler.get_db(db_name).cursor()
48 raise Exception("No database cursor found!")
49 pool = pooler.get_pool(db_name)
50 edi_pool = pool.get('ir.edi.document')
52 res = edi_pool.get_document(cr, 1, edi_token)
57 def exp_import_edi_document(self, db, uid, passwd, edi_document, context=None):
59 cr = pooler.get_db(db).cursor()
60 pool = pooler.get_pool(db)
61 edi_pool = pool.get('ir.edi.document')
64 res = edi_pool.import_edi(cr, uid, edi_document=edi_document, context=context)
69 def exp_import_edi_url(self, db, uid, passwd, edi_url, context=None):
71 cr = pooler.get_db(db).cursor()
72 pool = pooler.get_pool(db)
73 edi_pool = pool.get('ir.edi.document')
75 res = edi_pool.import_edi(cr, uid, edi_url=edi_url, context=context)
81 def __init__(self, name="edi"):
82 netsvc.ExportService.__init__(self, name)
83 self.joinGroup("web-services")
85 def dispatch(self, method, auth, params):
86 if method in ['import_edi_document', 'import_edi_url']:
87 (db, uid, passwd ) = params[0:3]
88 security.check(db, uid, passwd)
89 elif method in ['get_edi_document']:
91 # No security check for these methods
94 raise KeyError("Method not found: %s" % method)
95 fn = getattr(self, 'exp_'+method)
98 class db(netsvc.ExportService):
99 def __init__(self, name="db"):
100 netsvc.ExportService.__init__(self, name)
101 self.joinGroup("web-services")
104 self.id_protect = threading.Semaphore()
106 self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
108 def dispatch(self, method, auth, params):
109 if method in [ 'create', 'get_progress', 'drop', 'dump',
111 'change_admin_password', 'migrate_databases' ]:
114 security.check_super(passwd)
115 elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
117 # No security check for these methods
120 raise KeyError("Method not found: %s" % method)
121 fn = getattr(self, 'exp_'+method)
124 def new_dispatch(self,method,auth,params):
126 def _create_empty_database(self, name):
127 db = sql_db.db_connect('template1')
130 cr.autocommit(True) # avoid transaction block
131 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % name)
135 def exp_create(self, db_name, demo, lang, user_password='admin'):
136 self.id_protect.acquire()
139 self.id_protect.release()
141 self.actions[id] = {'clean': False}
143 self._create_empty_database(db_name)
145 class DBInitialize(object):
146 def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
149 serv.actions[id]['progress'] = 0
150 cr = sql_db.db_connect(db_name).cursor()
151 openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by pooler.restart_pool.
152 tools.config['lang'] = lang
156 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
157 update_module=True)[1]
159 cr = sql_db.db_connect(db_name).cursor()
162 modobj = pool.get('ir.module.module')
163 mids = modobj.search(cr, 1, [('state', '=', 'installed')])
164 modobj.update_translations(cr, 1, mids, lang)
166 cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
167 user_password, lang, 'admin'))
168 cr.execute('SELECT login, password, name ' \
171 serv.actions[id]['users'] = cr.dictfetchall()
172 serv.actions[id]['clean'] = True
176 serv.actions[id]['clean'] = False
177 serv.actions[id]['exception'] = e
180 traceback.print_exc(file=e_str)
181 traceback_str = e_str.getvalue()
183 netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
184 serv.actions[id]['traceback'] = traceback_str
187 logger = netsvc.Logger()
188 logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
190 create_thread = threading.Thread(target=dbi,
191 args=(self, id, db_name, demo, lang, user_password))
192 create_thread.start()
193 self.actions[id]['thread'] = create_thread
196 def exp_get_progress(self, id):
197 if self.actions[id]['thread'].isAlive():
198 # return openerp.modules.init_progress[db_name]
199 return (min(self.actions[id].get('progress', 0),0.95), [])
201 clean = self.actions[id]['clean']
203 users = self.actions[id]['users']
207 e = self.actions[id]['exception']
211 def exp_drop(self, db_name):
212 sql_db.close_db(db_name)
213 openerp.netsvc.Agent.cancel(db_name)
214 logger = netsvc.Logger()
216 db = sql_db.db_connect('template1')
218 cr.autocommit(True) # avoid transaction block
221 cr.execute('DROP DATABASE "%s"' % db_name)
223 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
224 'DROP DB: %s failed:\n%s' % (db_name, e))
225 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
227 logger.notifyChannel("web-services", netsvc.LOG_INFO,
228 'DROP DB: %s' % (db_name))
233 def _set_pg_psw_env_var(self):
234 if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
235 os.environ['PGPASSWORD'] = tools.config['db_password']
236 self._pg_psw_env_var_is_set = True
238 def _unset_pg_psw_env_var(self):
239 if os.name == 'nt' and self._pg_psw_env_var_is_set:
240 os.environ['PGPASSWORD'] = ''
242 def exp_dump(self, db_name):
243 logger = netsvc.Logger()
245 self._set_pg_psw_env_var()
247 cmd = ['pg_dump', '--format=c', '--no-owner']
248 if tools.config['db_user']:
249 cmd.append('--username=' + tools.config['db_user'])
250 if tools.config['db_host']:
251 cmd.append('--host=' + tools.config['db_host'])
252 if tools.config['db_port']:
253 cmd.append('--port=' + str(tools.config['db_port']))
256 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
261 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
262 'DUMP DB: %s failed\n%s' % (db_name, data))
263 raise Exception, "Couldn't dump database"
264 logger.notifyChannel("web-services", netsvc.LOG_INFO,
265 'DUMP DB: %s' % (db_name))
267 self._unset_pg_psw_env_var()
269 return base64.encodestring(data)
271 def exp_restore(self, db_name, data):
272 logger = netsvc.Logger()
274 self._set_pg_psw_env_var()
276 if self.exp_db_exist(db_name):
277 logger.notifyChannel("web-services", netsvc.LOG_WARNING,
278 'RESTORE DB: %s already exists' % (db_name,))
279 raise Exception, "Database already exists"
281 self._create_empty_database(db_name)
283 cmd = ['pg_restore', '--no-owner']
284 if tools.config['db_user']:
285 cmd.append('--username=' + tools.config['db_user'])
286 if tools.config['db_host']:
287 cmd.append('--host=' + tools.config['db_host'])
288 if tools.config['db_port']:
289 cmd.append('--port=' + str(tools.config['db_port']))
290 cmd.append('--dbname=' + db_name)
293 buf=base64.decodestring(data)
295 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
296 file(tmpfile, 'wb').write(buf)
298 args2.append(' ' + tmpfile)
300 stdin, stdout = tools.exec_pg_command_pipe(*args2)
301 if not os.name == "nt":
302 stdin.write(base64.decodestring(data))
306 raise Exception, "Couldn't restore database"
307 logger.notifyChannel("web-services", netsvc.LOG_INFO,
308 'RESTORE DB: %s' % (db_name))
310 self._unset_pg_psw_env_var()
314 def exp_rename(self, old_name, new_name):
315 sql_db.close_db(old_name)
316 openerp.netsvc.Agent.cancel(db_name)
317 logger = netsvc.Logger()
319 db = sql_db.db_connect('template1')
321 cr.autocommit(True) # avoid transaction block
324 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
326 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
327 'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
328 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
330 fs = os.path.join(tools.config['root_path'], 'filestore')
331 if os.path.exists(os.path.join(fs, old_name)):
332 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
334 logger.notifyChannel("web-services", netsvc.LOG_INFO,
335 'RENAME DB: %s -> %s' % (old_name, new_name))
340 def exp_db_exist(self, db_name):
341 ## Not True: in fact, check if connection to database is possible. The database may exists
342 return bool(sql_db.db_connect(db_name))
344 def exp_list(self, document=False):
345 if not tools.config['list_db'] and not document:
346 raise Exception('AccessDenied')
348 db = sql_db.db_connect('template1')
352 db_user = tools.config["db_user"]
353 if not db_user and os.name == 'posix':
355 db_user = pwd.getpwuid(os.getuid())[0]
357 cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
359 db_user = res and str(res[0])
361 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,))
363 cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
364 res = [str(name) for (name,) in cr.fetchall()]
372 def exp_change_admin_password(self, new_password):
373 tools.config['admin_passwd'] = new_password
377 def exp_list_lang(self):
378 return tools.scan_languages()
380 def exp_server_version(self):
381 """ Return the version of the server
382 Used by the client to verify the compatibility with its own version
384 return release.version
386 def exp_migrate_databases(self,databases):
388 from openerp.osv.orm import except_orm
389 from openerp.osv.osv import except_osv
394 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
395 tools.config['update']['base'] = True
396 pooler.restart_pool(db, force_demo=False, update_module=True)
397 except except_orm, inst:
398 self.abortResponse(1, inst.name, 'warning', inst.value)
399 except except_osv, inst:
400 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
403 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
404 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
408 class _ObjectService(netsvc.ExportService):
409 "A common base class for those who have fn(db, uid, password,...) "
411 def common_dispatch(self, method, auth, params):
412 (db, uid, passwd ) = params[0:3]
414 security.check(db,uid,passwd)
415 cr = pooler.get_db(db).cursor()
416 fn = getattr(self, 'exp_'+method)
417 res = fn(cr, uid, *params)
422 class common(_ObjectService):
423 def __init__(self,name="common"):
424 _ObjectService.__init__(self,name)
425 self.joinGroup("web-services")
427 def dispatch(self, method, auth, params):
428 logger = netsvc.Logger()
429 if method == 'login':
430 # At this old dispatcher, we do NOT update the auth proxy
431 res = security.login(params[0], params[1], params[2])
432 msg = res and 'successful login' or 'bad login or password'
433 # TODO log the client ip address..
434 logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower()))
436 elif method == 'logout':
438 auth.logout(params[1])
439 logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
441 elif method in ['about', 'timezone_get', 'get_server_environment',
442 'login_message','get_stats', 'check_connectivity',
443 'list_http_services']:
445 elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel', 'get_os_time', 'get_sqlcount']:
448 security.check_super(passwd)
450 raise Exception("Method not found: %s" % method)
452 fn = getattr(self, 'exp_'+method)
456 def new_dispatch(self,method,auth,params):
459 def exp_about(self, extended=False):
460 """Return information about the OpenERP Server.
462 @param extended: if True then return version info
463 @return string if extended is False else tuple
468 OpenERP is an ERP+CRM program for small and medium businesses.
470 The whole source code is distributed under the terms of the
473 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
476 return info, release.version
479 def exp_timezone_get(self, db, login, password):
480 return tools.misc.get_server_timezone()
482 def exp_get_available_updates(self, contract_id, contract_password):
483 import openerp.tools.maintenance as tm
485 rc = tm.remote_contract(contract_id, contract_password)
487 raise tm.RemoteContractException('This contract does not exist or is not active')
489 return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
491 except tm.RemoteContractException, e:
492 self.abortResponse(1, 'Migration Error', 'warning', str(e))
495 def exp_get_migration_scripts(self, contract_id, contract_password):
497 import openerp.tools.maintenance as tm
499 rc = tm.remote_contract(contract_id, contract_password)
501 raise tm.RemoteContractException('This contract does not exist or is not active')
502 if rc.status != 'full':
503 raise tm.RemoteContractException('Can not get updates for a partial contract')
505 l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
507 zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version())
509 from shutil import rmtree, copytree, copy
511 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
512 if zips and not os.path.isdir(backup_directory):
513 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
514 store the old modules: %s' % (backup_directory,))
515 os.makedirs(backup_directory)
518 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
519 mp = openerp.modules.get_module_path(module)
521 if os.path.isdir(mp):
522 copytree(mp, os.path.join(backup_directory, module))
523 if os.path.islink(mp):
528 copy(mp + 'zip', backup_directory)
529 os.unlink(mp + '.zip')
533 base64_decoded = base64.decodestring(zips[module])
535 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
538 zip_contents = StringIO(base64_decoded)
542 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
544 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
550 l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
551 nmp = os.path.join(backup_directory, module)
552 if os.path.isdir(nmp):
553 copytree(nmp, tools.config['addons_path'])
555 copy(nmp+'.zip', tools.config['addons_path'])
559 except tm.RemoteContractException, e:
560 self.abortResponse(1, 'Migration Error', 'warning', str(e))
563 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
564 l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
567 def exp_get_server_environment(self):
568 os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
571 environment = '\nEnvironment Information : \n' \
574 %(platform.platform(), platform.os.name)
575 if os.name == 'posix':
576 if platform.system() == 'Linux':
577 lsbinfo = os.popen('lsb_release -a').read()
578 environment += '%s'%(lsbinfo)
580 environment += 'Your System is not lsb compliant\n'
581 environment += 'Operating System Release : %s\n' \
582 'Operating System Version : %s\n' \
583 'Operating System Architecture : %s\n' \
584 'Operating System Locale : %s\n'\
585 'Python Version : %s\n'\
586 'OpenERP-Server Version : %s'\
587 %(platform.release(), platform.version(), platform.architecture()[0],
588 os_lang, platform.python_version(),release.version)
591 def exp_login_message(self):
592 return tools.config.get('login_message', False)
594 def exp_set_loglevel(self, loglevel, logger=None):
596 l.set_loglevel(int(loglevel), logger)
599 def exp_get_stats(self):
601 res = "OpenERP server: %d threads\n" % threading.active_count()
602 res += netsvc.Server.allStats()
605 def exp_list_http_services(self):
606 return http_server.list_http_services()
608 def exp_check_connectivity(self):
609 return bool(sql_db.db_connect('template1'))
611 def exp_get_os_time(self):
614 def exp_get_sqlcount(self):
615 logger = logging.getLogger('db.cursor')
616 if not logger.isEnabledFor(logging.DEBUG_SQL):
617 logger.warning("Counters of SQL will not be reliable unless DEBUG_SQL is set at the server's config.")
618 return sql_db.sql_counter
621 class objects_proxy(netsvc.ExportService):
622 def __init__(self, name="object"):
623 netsvc.ExportService.__init__(self,name)
624 self.joinGroup('web-services')
626 def dispatch(self, method, auth, params):
627 (db, uid, passwd ) = params[0:3]
629 if method == 'obj_list':
630 raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
631 if method not in ['execute','exec_workflow']:
632 raise NameError("Method not available %s" % method)
633 security.check(db,uid,passwd)
634 ls = netsvc.LocalService('object_proxy')
635 fn = getattr(ls, method)
636 res = fn(db, uid, *params)
640 def new_dispatch(self,method,auth,params):
646 # - None = end of wizard
648 # Wizard Type: 'form'
653 # TODO: change local request to OSE request/reply pattern
655 class wizard(netsvc.ExportService):
656 def __init__(self, name='wizard'):
657 netsvc.ExportService.__init__(self,name)
658 self.joinGroup('web-services')
664 def dispatch(self, method, auth, params):
665 (db, uid, passwd ) = params[0:3]
667 if method not in ['execute','create']:
668 raise KeyError("Method not supported %s" % method)
669 security.check(db,uid,passwd)
670 fn = getattr(self, 'exp_'+method)
671 res = fn(db, uid, *params)
674 def new_dispatch(self,method,auth,params):
677 def _execute(self, db, uid, wiz_id, datas, action, context):
678 self.wiz_datas[wiz_id].update(datas)
679 wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
680 return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
682 def exp_create(self, db, uid, wiz_name, datas=None):
685 #FIXME: this is not thread-safe
687 self.wiz_datas[self.id] = {}
688 self.wiz_name[self.id] = wiz_name
689 self.wiz_uid[self.id] = uid
692 def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
696 if wiz_id in self.wiz_uid:
697 if self.wiz_uid[wiz_id] == uid:
698 return self._execute(db, uid, wiz_id, datas, action, context)
700 raise Exception, 'AccessDenied'
702 raise Exception, 'WizardNotFound'
705 # TODO: set a maximum report number per user to avoid DOS attacks
711 class ExceptionWithTraceback(Exception):
712 def __init__(self, msg, tb):
715 self.args = (msg, tb)
717 class report_spool(netsvc.ExportService):
718 def __init__(self, name='report'):
719 netsvc.ExportService.__init__(self, name)
720 self.joinGroup('web-services')
723 self.id_protect = threading.Semaphore()
725 def dispatch(self, method, auth, params):
726 (db, uid, passwd ) = params[0:3]
728 if method not in ['report','report_get']:
729 raise KeyError("Method not supported %s" % method)
730 security.check(db,uid,passwd)
731 fn = getattr(self, 'exp_' + method)
732 res = fn(db, uid, *params)
736 def new_dispatch(self,method,auth,params):
739 def exp_report(self, db, uid, object, ids, datas=None, context=None):
745 self.id_protect.acquire()
748 self.id_protect.release()
750 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
752 def go(id, uid, ids, datas, context):
753 cr = pooler.get_db(db).cursor()
757 obj = netsvc.LocalService('report.'+object)
758 (result, format) = obj.create(cr, uid, ids, datas, context)
761 self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb)
762 self._reports[id]['result'] = result
763 self._reports[id]['format'] = format
764 self._reports[id]['state'] = True
765 except Exception, exception:
768 tb_s = "".join(traceback.format_exception(*tb))
769 logger = netsvc.Logger()
770 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
771 'Exception: %s\n%s' % (str(exception), tb_s))
772 if hasattr(exception, 'name') and hasattr(exception, 'value'):
773 self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value))
775 self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
776 self._reports[id]['state'] = True
781 thread.start_new_thread(go, (id, uid, ids, datas, context))
784 def _check_report(self, report_id):
785 result = self._reports[report_id]
786 exc = result['exception']
788 self.abortResponse(exc, exc.message, 'warning', exc.traceback)
789 res = {'state': result['state']}
791 if tools.config['reportgz']:
793 res2 = zlib.compress(result['result'])
796 #CHECKME: why is this needed???
797 if isinstance(result['result'], unicode):
798 res2 = result['result'].encode('latin1', 'replace')
800 res2 = result['result']
802 res['result'] = base64.encodestring(res2)
803 res['format'] = result['format']
804 del self._reports[report_id]
807 def exp_report_get(self, db, uid, report_id):
808 if report_id in self._reports:
809 if self._reports[report_id]['uid'] == uid:
810 return self._check_report(report_id)
812 raise Exception, 'AccessDenied'
814 raise Exception, 'ReportNotFound'
817 def start_web_services():
826 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: