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 ##############################################################################
33 from cStringIO import StringIO
34 from openerp.tools.translate import _
35 import openerp.netsvc as netsvc
36 import openerp.pooler as pooler
37 import openerp.release as release
38 import openerp.sql_db as sql_db
39 import openerp.tools as tools
40 import openerp.modules
41 import openerp.exceptions
42 from openerp.service import http_server
44 #.apidoc title: Exported Service methods
45 #.apidoc module-mods: member-order: bysource
47 """ This python module defines the RPC methods available to remote clients.
49 Each 'Export Service' is a group of 'methods', which in turn are RPC
50 procedures to be called. Each method has its own arguments footprint.
53 _logger = logging.getLogger(__name__)
55 RPC_VERSION_1 = {'server_version': '6.1', 'protocol_version': 1}
57 # This should be moved to openerp.modules.db, along side initialize().
58 def _initialize_db(serv, id, db_name, demo, lang, user_password):
61 serv.actions[id]['progress'] = 0
62 cr = sql_db.db_connect(db_name).cursor()
63 openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by pooler.restart_pool.
64 tools.config['lang'] = lang
68 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
69 update_module=True)[1]
71 cr = sql_db.db_connect(db_name).cursor()
74 modobj = pool.get('ir.module.module')
75 mids = modobj.search(cr, 1, [('state', '=', 'installed')])
76 modobj.update_translations(cr, 1, mids, lang)
78 cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
79 user_password, lang, 'admin'))
80 cr.execute('SELECT login, password, name ' \
83 serv.actions[id].update(users=cr.dictfetchall(), clean=True)
87 serv.actions[id].update(clean=False, exception=e)
88 _logger.exception('CREATE DATABASE failed:')
89 serv.actions[id]['traceback'] = traceback.format_exc()
93 class db(netsvc.ExportService):
94 def __init__(self, name="db"):
95 netsvc.ExportService.__init__(self, name)
98 self.id_protect = threading.Semaphore()
100 self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
102 def dispatch(self, method, params):
103 if method in [ 'create', 'get_progress', 'drop', 'dump',
105 'change_admin_password', 'migrate_databases',
109 security.check_super(passwd)
110 elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
112 # No security check for these methods
115 raise KeyError("Method not found: %s" % method)
116 fn = getattr(self, 'exp_'+method)
119 def _create_empty_database(self, name):
120 db = sql_db.db_connect('template1')
122 chosen_template = tools.config['db_template']
124 cr.autocommit(True) # avoid transaction block
125 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (name, chosen_template))
129 def exp_create(self, db_name, demo, lang, user_password='admin'):
130 self.id_protect.acquire()
133 self.id_protect.release()
135 self.actions[id] = {'clean': False}
137 self._create_empty_database(db_name)
139 _logger.info('CREATE DATABASE %s', db_name.lower())
140 create_thread = threading.Thread(target=_initialize_db,
141 args=(self, id, db_name, demo, lang, user_password))
142 create_thread.start()
143 self.actions[id]['thread'] = create_thread
146 def exp_create_database(self, db_name, demo, lang, user_password='admin'):
147 """ Similar to exp_create but blocking."""
148 self.id_protect.acquire()
151 self.id_protect.release()
153 self.actions[id] = {'clean': False}
155 _logger.info('CREATE DATABASE %s', db_name.lower())
156 self._create_empty_database(db_name)
157 _initialize_db(self, id, db_name, demo, lang, user_password)
160 def exp_get_progress(self, id):
161 if self.actions[id]['thread'].isAlive():
162 # return openerp.modules.init_progress[db_name]
163 return (min(self.actions[id].get('progress', 0),0.95), [])
165 clean = self.actions[id]['clean']
167 users = self.actions[id]['users']
171 e = self.actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'.
175 def exp_drop(self, db_name):
176 if not self.exp_db_exist(db_name):
178 openerp.modules.registry.RegistryManager.delete(db_name)
179 sql_db.close_db(db_name)
181 db = sql_db.db_connect('template1')
183 cr.autocommit(True) # avoid transaction block
185 # Try to terminate all other connections that might prevent
186 # dropping the database
188 cr.execute("""SELECT pg_terminate_backend(procpid)
189 FROM pg_stat_activity
190 WHERE datname = %s AND
191 procpid != pg_backend_pid()""",
197 cr.execute('DROP DATABASE "%s"' % db_name)
199 _logger.error('DROP DB: %s failed:\n%s', db_name, e)
200 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
202 _logger.info('DROP DB: %s', db_name)
208 def _set_pg_psw_env_var(self):
209 # see http://www.postgresql.org/docs/8.4/static/libpq-envars.html
210 # FIXME: This is not thread-safe, and should never be enabled for
211 # SaaS (giving SaaS users the super-admin password is not a good idea
213 if tools.config['db_password'] and not os.environ.get('PGPASSWORD', ''):
214 os.environ['PGPASSWORD'] = tools.config['db_password']
215 self._pg_psw_env_var_is_set = True
217 def _unset_pg_psw_env_var(self):
218 if self._pg_psw_env_var_is_set:
219 os.environ['PGPASSWORD'] = ''
221 def exp_dump(self, db_name):
223 self._set_pg_psw_env_var()
224 cmd = ['pg_dump', '--format=c', '--no-owner']
225 if tools.config['db_user']:
226 cmd.append('--username=' + tools.config['db_user'])
227 if tools.config['db_host']:
228 cmd.append('--host=' + tools.config['db_host'])
229 if tools.config['db_port']:
230 cmd.append('--port=' + str(tools.config['db_port']))
233 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
240 'DUMP DB: %s failed! Please verify the configuration of the database password on the server. '\
241 'It should be provided as a -w <PASSWD> command-line option, or as `db_password` in the '\
242 'server configuration file.\n %s' % (db_name, data))
243 raise Exception, "Couldn't dump database"
244 _logger.info('DUMP DB successful: %s', db_name)
246 return base64.encodestring(data)
248 self._unset_pg_psw_env_var()
250 def exp_restore(self, db_name, data):
252 self._set_pg_psw_env_var()
254 if self.exp_db_exist(db_name):
255 _logger.warning('RESTORE DB: %s already exists' % (db_name,))
256 raise Exception, "Database already exists"
258 self._create_empty_database(db_name)
260 cmd = ['pg_restore', '--no-owner']
261 if tools.config['db_user']:
262 cmd.append('--username=' + tools.config['db_user'])
263 if tools.config['db_host']:
264 cmd.append('--host=' + tools.config['db_host'])
265 if tools.config['db_port']:
266 cmd.append('--port=' + str(tools.config['db_port']))
267 cmd.append('--dbname=' + db_name)
270 buf=base64.decodestring(data)
272 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
273 file(tmpfile, 'wb').write(buf)
275 args2.append(tmpfile)
277 stdin, stdout = tools.exec_pg_command_pipe(*args2)
278 if not os.name == "nt":
279 stdin.write(base64.decodestring(data))
283 raise Exception, "Couldn't restore database"
284 _logger.info('RESTORE DB: %s' % (db_name))
288 self._unset_pg_psw_env_var()
290 def exp_rename(self, old_name, new_name):
291 openerp.modules.registry.RegistryManager.delete(old_name)
292 sql_db.close_db(old_name)
294 db = sql_db.db_connect('template1')
296 cr.autocommit(True) # avoid transaction block
299 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
301 _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e)
302 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
304 fs = os.path.join(tools.config['root_path'], 'filestore')
305 if os.path.exists(os.path.join(fs, old_name)):
306 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
308 _logger.info('RENAME DB: %s -> %s', old_name, new_name)
313 def exp_db_exist(self, db_name):
314 ## Not True: in fact, check if connection to database is possible. The database may exists
315 return bool(sql_db.db_connect(db_name))
317 def exp_list(self, document=False):
318 if not tools.config['list_db'] and not document:
319 raise openerp.exceptions.AccessDenied()
320 chosen_template = tools.config['db_template']
321 templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template]))
322 db = sql_db.db_connect('template1')
326 db_user = tools.config["db_user"]
327 if not db_user and os.name == 'posix':
329 db_user = pwd.getpwuid(os.getuid())[0]
331 cr.execute("select usename from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
333 db_user = res and str(res[0])
335 cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=%s) and datname not in %s order by datname", (db_user, templates_list))
337 cr.execute("select datname from pg_database where datname not in %s order by datname", (templates_list,))
338 res = [str(name) for (name,) in cr.fetchall()]
346 def exp_change_admin_password(self, new_password):
347 tools.config['admin_passwd'] = new_password
351 def exp_list_lang(self):
352 return tools.scan_languages()
354 def exp_server_version(self):
355 """ Return the version of the server
356 Used by the client to verify the compatibility with its own version
358 return release.version
360 def exp_migrate_databases(self,databases):
362 from openerp.osv.orm import except_orm
363 from openerp.osv.osv import except_osv
367 _logger.info('migrate database %s', db)
368 tools.config['update']['base'] = True
369 pooler.restart_pool(db, force_demo=False, update_module=True)
370 except except_orm, inst:
371 netsvc.abort_response(1, inst.name, 'warning', inst.value)
372 except except_osv, inst:
373 netsvc.abort_response(1, inst.name, 'warning', inst.value)
375 _logger.exception('Exception in migrate_databases:')
379 class common(netsvc.ExportService):
381 def __init__(self,name="common"):
382 netsvc.ExportService.__init__(self,name)
384 def dispatch(self, method, params):
385 if method in ['login', 'about', 'timezone_get', 'get_server_environment',
386 'login_message','get_stats', 'check_connectivity',
387 'list_http_services', 'version', 'authenticate']:
389 elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel', 'get_os_time', 'get_sqlcount']:
392 security.check_super(passwd)
394 raise Exception("Method not found: %s" % method)
396 fn = getattr(self, 'exp_'+method)
399 def exp_login(self, db, login, password):
400 # TODO: legacy indirection through 'security', should use directly
401 # the res.users model
402 res = security.login(db, login, password)
403 msg = res and 'successful login' or 'bad login or password'
404 _logger.info("%s from '%s' using database '%s'", msg, login, db.lower())
407 def exp_authenticate(self, db, login, password, user_agent_env):
408 res_users = pooler.get_pool(db).get('res.users')
409 return res_users.authenticate(db, login, password, user_agent_env)
411 def exp_version(self):
414 def exp_about(self, extended=False):
415 """Return information about the OpenERP Server.
417 @param extended: if True then return version info
418 @return string if extended is False else tuple
423 OpenERP is an ERP+CRM program for small and medium businesses.
425 The whole source code is distributed under the terms of the
428 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
431 return info, release.version
434 def exp_timezone_get(self, db, login, password):
435 return tools.misc.get_server_timezone()
437 def exp_get_available_updates(self, contract_id, contract_password):
438 import openerp.tools.maintenance as tm
440 rc = tm.remote_contract(contract_id, contract_password)
442 raise tm.RemoteContractException('This contract does not exist or is not active')
444 return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
446 except tm.RemoteContractException, e:
447 netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
450 def exp_get_migration_scripts(self, contract_id, contract_password):
451 import openerp.tools.maintenance as tm
453 rc = tm.remote_contract(contract_id, contract_password)
455 raise tm.RemoteContractException('This contract does not exist or is not active')
456 if rc.status != 'full':
457 raise tm.RemoteContractException('Can not get updates for a partial contract')
459 _logger.info('starting migration with contract %s', rc.name)
461 zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version())
463 from shutil import rmtree, copytree, copy
465 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
466 if zips and not os.path.isdir(backup_directory):
467 _logger.info('create a new backup directory to \
468 store the old modules: %s', backup_directory)
469 os.makedirs(backup_directory)
472 _logger.info('upgrade module %s', module)
473 mp = openerp.modules.get_module_path(module)
475 if os.path.isdir(mp):
476 copytree(mp, os.path.join(backup_directory, module))
477 if os.path.islink(mp):
482 copy(mp + 'zip', backup_directory)
483 os.unlink(mp + '.zip')
487 base64_decoded = base64.decodestring(zips[module])
489 _logger.error('unable to read the module %s', module)
492 zip_contents = StringIO(base64_decoded)
496 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
498 _logger.error('unable to extract the module %s', module)
504 _logger.error('restore the previous version of the module %s', module)
505 nmp = os.path.join(backup_directory, module)
506 if os.path.isdir(nmp):
507 copytree(nmp, tools.config['addons_path'])
509 copy(nmp+'.zip', tools.config['addons_path'])
513 except tm.RemoteContractException, e:
514 netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
516 _logger.exception('Exception in get_migration_script:')
519 def exp_get_server_environment(self):
520 os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
523 environment = '\nEnvironment Information : \n' \
526 %(platform.platform(), platform.os.name)
527 if os.name == 'posix':
528 if platform.system() == 'Linux':
529 lsbinfo = os.popen('lsb_release -a').read()
530 environment += '%s'%(lsbinfo)
532 environment += 'Your System is not lsb compliant\n'
533 environment += 'Operating System Release : %s\n' \
534 'Operating System Version : %s\n' \
535 'Operating System Architecture : %s\n' \
536 'Operating System Locale : %s\n'\
537 'Python Version : %s\n'\
538 'OpenERP-Server Version : %s'\
539 %(platform.release(), platform.version(), platform.architecture()[0],
540 os_lang, platform.python_version(),release.version)
543 def exp_login_message(self):
544 return tools.config.get('login_message', False)
546 def exp_set_loglevel(self, loglevel, logger=None):
547 # TODO Previously, the level was set on the now deprecated
548 # `openerp.netsvc.Logger` class.
551 def exp_get_stats(self):
552 res = "OpenERP server: %d threads\n" % threading.active_count()
553 res += netsvc.Server.allStats()
556 def exp_list_http_services(self):
557 return http_server.list_http_services()
559 def exp_check_connectivity(self):
560 return bool(sql_db.db_connect('template1'))
562 def exp_get_os_time(self):
565 def exp_get_sqlcount(self):
566 if not logging.getLogger('openerp.sql_db').isEnabledFor(logging.DEBUG):
567 _logger.warning("Counters of SQL will not be reliable unless logger openerp.sql_db is set to level DEBUG or higer.")
568 return sql_db.sql_counter
571 class objects_proxy(netsvc.ExportService):
572 def __init__(self, name="object"):
573 netsvc.ExportService.__init__(self,name)
575 def dispatch(self, method, params):
576 (db, uid, passwd ) = params[0:3]
577 threading.current_thread().uid = uid
579 if method == 'obj_list':
580 raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
581 if method not in ['execute', 'execute_kw', 'exec_workflow']:
582 raise NameError("Method not available %s" % method)
583 security.check(db,uid,passwd)
584 assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
585 fn = getattr(openerp.osv.osv.service, method)
586 res = fn(db, uid, *params)
592 # - None = end of wizard
594 # Wizard Type: 'form'
599 # TODO: change local request to OSE request/reply pattern
601 class wizard(netsvc.ExportService):
602 def __init__(self, name='wizard'):
603 netsvc.ExportService.__init__(self,name)
609 def dispatch(self, method, params):
610 (db, uid, passwd ) = params[0:3]
611 threading.current_thread().uid = uid
613 if method not in ['execute','create']:
614 raise KeyError("Method not supported %s" % method)
615 security.check(db,uid,passwd)
616 fn = getattr(self, 'exp_'+method)
617 res = fn(db, uid, *params)
620 def _execute(self, db, uid, wiz_id, datas, action, context):
621 self.wiz_datas[wiz_id].update(datas)
622 wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
623 return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
625 def exp_create(self, db, uid, wiz_name, datas=None):
628 #FIXME: this is not thread-safe
630 self.wiz_datas[self.id] = {}
631 self.wiz_name[self.id] = wiz_name
632 self.wiz_uid[self.id] = uid
635 def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
639 if wiz_id in self.wiz_uid:
640 if self.wiz_uid[wiz_id] == uid:
641 return self._execute(db, uid, wiz_id, datas, action, context)
643 raise openerp.exceptions.AccessDenied()
645 raise openerp.exceptions.Warning('Wizard not found.')
648 # TODO: set a maximum report number per user to avoid DOS attacks
654 class report_spool(netsvc.ExportService):
655 def __init__(self, name='report'):
656 netsvc.ExportService.__init__(self, name)
659 self.id_protect = threading.Semaphore()
661 def dispatch(self, method, params):
662 (db, uid, passwd ) = params[0:3]
663 threading.current_thread().uid = uid
665 if method not in ['report', 'report_get', 'render_report']:
666 raise KeyError("Method not supported %s" % method)
667 security.check(db,uid,passwd)
668 fn = getattr(self, 'exp_' + method)
669 res = fn(db, uid, *params)
672 def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
678 self.id_protect.acquire()
681 self.id_protect.release()
683 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
685 cr = pooler.get_db(db).cursor()
687 obj = netsvc.LocalService('report.'+object)
688 (result, format) = obj.create(cr, uid, ids, datas, context)
691 self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
692 self._reports[id]['result'] = result
693 self._reports[id]['format'] = format
694 self._reports[id]['state'] = True
695 except Exception, exception:
697 _logger.exception('Exception: %s\n', str(exception))
698 if hasattr(exception, 'name') and hasattr(exception, 'value'):
699 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
702 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
703 self._reports[id]['state'] = True
707 return self._check_report(id)
709 def exp_report(self, db, uid, object, ids, datas=None, context=None):
715 self.id_protect.acquire()
718 self.id_protect.release()
720 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
722 def go(id, uid, ids, datas, context):
723 cr = pooler.get_db(db).cursor()
725 obj = netsvc.LocalService('report.'+object)
726 (result, format) = obj.create(cr, uid, ids, datas, context)
729 self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
730 self._reports[id]['result'] = result
731 self._reports[id]['format'] = format
732 self._reports[id]['state'] = True
733 except Exception, exception:
734 _logger.exception('Exception: %s\n', str(exception))
735 if hasattr(exception, 'name') and hasattr(exception, 'value'):
736 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
739 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
740 self._reports[id]['state'] = True
745 thread.start_new_thread(go, (id, uid, ids, datas, context))
748 def _check_report(self, report_id):
749 result = self._reports[report_id]
750 exc = result['exception']
752 netsvc.abort_response(exc, exc.message, 'warning', exc.traceback)
753 res = {'state': result['state']}
755 if tools.config['reportgz']:
757 res2 = zlib.compress(result['result'])
760 #CHECKME: why is this needed???
761 if isinstance(result['result'], unicode):
762 res2 = result['result'].encode('latin1', 'replace')
764 res2 = result['result']
766 res['result'] = base64.encodestring(res2)
767 res['format'] = result['format']
768 del self._reports[report_id]
771 def exp_report_get(self, db, uid, report_id):
772 if report_id in self._reports:
773 if self._reports[report_id]['uid'] == uid:
774 return self._check_report(report_id)
776 raise Exception, 'AccessDenied'
778 raise Exception, 'ReportNotFound'
781 def start_web_services():
789 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: