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 ##############################################################################
21 from __future__ import with_statement
34 from cStringIO import StringIO
35 from openerp.tools.translate import _
36 import openerp.netsvc as netsvc
37 import openerp.pooler as pooler
38 import openerp.release as release
39 import openerp.sql_db as sql_db
40 import openerp.tools as tools
41 import openerp.modules
42 import openerp.exceptions
43 from openerp.service import http_server
44 from openerp import SUPERUSER_ID
46 #.apidoc title: Exported Service methods
47 #.apidoc module-mods: member-order: bysource
49 """ This python module defines the RPC methods available to remote clients.
51 Each 'Export Service' is a group of 'methods', which in turn are RPC
52 procedures to be called. Each method has its own arguments footprint.
55 _logger = logging.getLogger(__name__)
57 RPC_VERSION_1 = {'server_version': '6.1', 'protocol_version': 1}
59 # This should be moved to openerp.modules.db, along side initialize().
60 def _initialize_db(serv, id, db_name, demo, lang, user_password):
63 serv.actions[id]['progress'] = 0
64 cr = sql_db.db_connect(db_name).cursor()
65 openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by pooler.restart_pool.
66 tools.config['lang'] = lang
70 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
71 update_module=True)[1]
73 cr = sql_db.db_connect(db_name).cursor()
76 modobj = pool.get('ir.module.module')
77 mids = modobj.search(cr, SUPERUSER_ID, [('state', '=', 'installed')])
78 modobj.update_translations(cr, SUPERUSER_ID, mids, lang)
80 cr.execute('UPDATE res_users SET password=%s, lang=%s, active=True WHERE login=%s', (
81 user_password, lang, 'admin'))
82 cr.execute('SELECT login, password ' \
85 serv.actions[id].update(users=cr.dictfetchall(), clean=True)
89 serv.actions[id].update(clean=False, exception=e)
90 _logger.exception('CREATE DATABASE failed:')
91 serv.actions[id]['traceback'] = traceback.format_exc()
95 class db(netsvc.ExportService):
96 def __init__(self, name="db"):
97 netsvc.ExportService.__init__(self, name)
100 self.id_protect = threading.Semaphore()
102 def dispatch(self, method, params):
103 if method in [ 'create', 'get_progress', 'drop', 'dump',
105 'change_admin_password', 'migrate_databases',
106 'create_database', 'duplicate_database' ]:
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('postgres')
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)
156 self._create_empty_database(db_name)
157 _initialize_db(self, id, db_name, demo, lang, user_password)
160 def exp_duplicate_database(self, db_original_name, db_name):
161 _logger.info('Duplicate database `%s` to `%s`.', db_original_name, db_name)
162 sql_db.close_db(db_original_name)
163 db = sql_db.db_connect('postgres')
166 cr.autocommit(True) # avoid transaction block
167 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (db_name, db_original_name))
172 def exp_get_progress(self, id):
173 if self.actions[id]['thread'].isAlive():
174 # return openerp.modules.init_progress[db_name]
175 return min(self.actions[id].get('progress', 0),0.95), []
177 clean = self.actions[id]['clean']
179 users = self.actions[id]['users']
183 e = self.actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'.
187 def exp_drop(self, db_name):
188 if not self.exp_db_exist(db_name):
190 openerp.modules.registry.RegistryManager.delete(db_name)
191 sql_db.close_db(db_name)
193 db = sql_db.db_connect('postgres')
195 cr.autocommit(True) # avoid transaction block
197 # Try to terminate all other connections that might prevent
198 # dropping the database
201 # PostgreSQL 9.2 renamed pg_stat_activity.procpid to pid:
202 # http://www.postgresql.org/docs/9.2/static/release-9-2.html#AEN110389
203 pid_col = 'pid' if cr._cnx.server_version >= 90200 else 'procpid'
205 cr.execute("""SELECT pg_terminate_backend(%(pid_col)s)
206 FROM pg_stat_activity
207 WHERE datname = %%s AND
208 %(pid_col)s != pg_backend_pid()""" % {'pid_col': pid_col},
214 cr.execute('DROP DATABASE "%s"' % db_name)
216 _logger.error('DROP DB: %s failed:\n%s', db_name, e)
217 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
219 _logger.info('DROP DB: %s', db_name)
224 @contextlib.contextmanager
225 def _set_pg_password_in_environment(self):
226 """ On Win32, pg_dump (and pg_restore) require that
227 :envvar:`PGPASSWORD` be set
229 This context management method handles setting
230 :envvar:`PGPASSWORD` iif win32 and the envvar is not already
231 set, and removing it afterwards.
233 if os.name != 'nt' or os.environ.get('PGPASSWORD'):
236 os.environ['PGPASSWORD'] = tools.config['db_password']
240 del os.environ['PGPASSWORD']
243 def exp_dump(self, db_name):
244 logger = logging.getLogger('openerp.service.web_services.db.dump')
245 with self._set_pg_password_in_environment():
246 cmd = ['pg_dump', '--format=c', '--no-owner']
247 if tools.config['db_user']:
248 cmd.append('--username=' + tools.config['db_user'])
249 if tools.config['db_host']:
250 cmd.append('--host=' + tools.config['db_host'])
251 if tools.config['db_port']:
252 cmd.append('--port=' + str(tools.config['db_port']))
255 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
262 'DUMP DB: %s failed! Please verify the configuration of the database password on the server. '
263 'It should be provided as a -w <PASSWD> command-line option, or as `db_password` in the '
264 'server configuration file.\n %s', db_name, data)
265 raise Exception, "Couldn't dump database"
266 logger.info('DUMP DB successful: %s', db_name)
268 return base64.encodestring(data)
270 def exp_restore(self, db_name, data):
271 logger = logging.getLogger('openerp.service.web_services.db.restore')
272 with self._set_pg_password_in_environment():
273 if self.exp_db_exist(db_name):
274 logger.warning('RESTORE DB: %s already exists', db_name)
275 raise Exception, "Database already exists"
277 self._create_empty_database(db_name)
279 cmd = ['pg_restore', '--no-owner']
280 if tools.config['db_user']:
281 cmd.append('--username=' + tools.config['db_user'])
282 if tools.config['db_host']:
283 cmd.append('--host=' + tools.config['db_host'])
284 if tools.config['db_port']:
285 cmd.append('--port=' + str(tools.config['db_port']))
286 cmd.append('--dbname=' + db_name)
289 buf=base64.decodestring(data)
291 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
292 file(tmpfile, 'wb').write(buf)
294 args2.append(tmpfile)
296 stdin, stdout = tools.exec_pg_command_pipe(*args2)
297 if not os.name == "nt":
298 stdin.write(base64.decodestring(data))
302 raise Exception, "Couldn't restore database"
303 logger.info('RESTORE DB: %s', db_name)
307 def exp_rename(self, old_name, new_name):
308 openerp.modules.registry.RegistryManager.delete(old_name)
309 sql_db.close_db(old_name)
311 db = sql_db.db_connect('postgres')
313 cr.autocommit(True) # avoid transaction block
316 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
318 _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e)
319 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
321 fs = os.path.join(tools.config['root_path'], 'filestore')
322 if os.path.exists(os.path.join(fs, old_name)):
323 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
325 _logger.info('RENAME DB: %s -> %s', old_name, new_name)
330 def exp_db_exist(self, db_name):
331 ## Not True: in fact, check if connection to database is possible. The database may exists
332 return bool(sql_db.db_connect(db_name))
334 def exp_list(self, document=False):
335 if not tools.config['list_db'] and not document:
336 raise openerp.exceptions.AccessDenied()
337 chosen_template = tools.config['db_template']
338 templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template]))
339 db = sql_db.db_connect('postgres')
343 db_user = tools.config["db_user"]
344 if not db_user and os.name == 'posix':
346 db_user = pwd.getpwuid(os.getuid())[0]
348 cr.execute("select usename from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
350 db_user = res and str(res[0])
352 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))
354 cr.execute("select datname from pg_database where datname not in %s order by datname", (templates_list,))
355 res = [str(name) for (name,) in cr.fetchall()]
363 def exp_change_admin_password(self, new_password):
364 tools.config['admin_passwd'] = new_password
368 def exp_list_lang(self):
369 return tools.scan_languages()
371 def exp_server_version(self):
372 """ Return the version of the server
373 Used by the client to verify the compatibility with its own version
375 return release.version
377 def exp_migrate_databases(self,databases):
379 from openerp.osv.orm import except_orm
380 from openerp.osv.osv import except_osv
384 _logger.info('migrate database %s', db)
385 tools.config['update']['base'] = True
386 pooler.restart_pool(db, force_demo=False, update_module=True)
387 except except_orm, inst:
388 netsvc.abort_response(1, inst.name, 'warning', inst.value)
389 except except_osv, inst:
390 netsvc.abort_response(1, inst.name, 'warning', inst.value)
392 _logger.exception('Exception in migrate_databases:')
396 class common(netsvc.ExportService):
398 def __init__(self,name="common"):
399 netsvc.ExportService.__init__(self,name)
401 def dispatch(self, method, params):
402 if method in ['login', 'about', 'timezone_get', 'get_server_environment',
403 'login_message','get_stats', 'check_connectivity',
404 'list_http_services', 'version', 'authenticate']:
406 elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel', 'get_os_time', 'get_sqlcount']:
409 security.check_super(passwd)
411 raise Exception("Method not found: %s" % method)
413 fn = getattr(self, 'exp_'+method)
416 def exp_login(self, db, login, password):
417 # TODO: legacy indirection through 'security', should use directly
418 # the res.users model
419 res = security.login(db, login, password)
420 msg = res and 'successful login' or 'bad login or password'
421 _logger.info("%s from '%s' using database '%s'", msg, login, db.lower())
424 def exp_authenticate(self, db, login, password, user_agent_env):
425 res_users = pooler.get_pool(db).get('res.users')
426 return res_users.authenticate(db, login, password, user_agent_env)
428 def exp_version(self):
431 def exp_about(self, extended=False):
432 """Return information about the OpenERP Server.
434 @param extended: if True then return version info
435 @return string if extended is False else tuple
440 OpenERP is an ERP+CRM program for small and medium businesses.
442 The whole source code is distributed under the terms of the
445 (c) 2003-TODAY - OpenERP SA''')
448 return info, release.version
451 def exp_timezone_get(self, db, login, password):
452 return tools.misc.get_server_timezone()
454 def exp_get_available_updates(self, contract_id, contract_password):
455 import openerp.tools.maintenance as tm
457 rc = tm.remote_contract(contract_id, contract_password)
459 raise tm.RemoteContractException('This contract does not exist or is not active')
461 return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version())
463 except tm.RemoteContractException, e:
464 netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
467 def exp_get_migration_scripts(self, contract_id, contract_password):
468 import openerp.tools.maintenance as tm
470 rc = tm.remote_contract(contract_id, contract_password)
472 raise tm.RemoteContractException('This contract does not exist or is not active')
473 if rc.status != 'full':
474 raise tm.RemoteContractException('Can not get updates for a partial contract')
476 _logger.info('starting migration with contract %s', rc.name)
478 zips = rc.retrieve_updates(rc.id, openerp.modules.get_modules_with_version())
480 from shutil import rmtree, copytree, copy
482 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
483 if zips and not os.path.isdir(backup_directory):
484 _logger.info('create a new backup directory to store the old modules: %s', backup_directory)
485 os.makedirs(backup_directory)
488 _logger.info('upgrade module %s', module)
489 mp = openerp.modules.get_module_path(module)
491 if os.path.isdir(mp):
492 copytree(mp, os.path.join(backup_directory, module))
493 if os.path.islink(mp):
498 copy(mp + 'zip', backup_directory)
499 os.unlink(mp + '.zip')
503 base64_decoded = base64.decodestring(zips[module])
505 _logger.error('unable to read the module %s', module)
508 zip_contents = StringIO(base64_decoded)
512 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
514 _logger.error('unable to extract the module %s', module)
520 _logger.error('restore the previous version of the module %s', module)
521 nmp = os.path.join(backup_directory, module)
522 if os.path.isdir(nmp):
523 copytree(nmp, tools.config['addons_path'])
525 copy(nmp+'.zip', tools.config['addons_path'])
529 except tm.RemoteContractException, e:
530 netsvc.abort_response(1, 'Migration Error', 'warning', str(e))
532 _logger.exception('Exception in get_migration_script:')
535 def exp_get_server_environment(self):
536 os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
539 environment = '\nEnvironment Information : \n' \
542 %(platform.platform(), platform.os.name)
543 if os.name == 'posix':
544 if platform.system() == 'Linux':
545 lsbinfo = os.popen('lsb_release -a').read()
546 environment += '%s'% lsbinfo
548 environment += 'Your System is not lsb compliant\n'
549 environment += 'Operating System Release : %s\n' \
550 'Operating System Version : %s\n' \
551 'Operating System Architecture : %s\n' \
552 'Operating System Locale : %s\n'\
553 'Python Version : %s\n'\
554 'OpenERP-Server Version : %s'\
555 %(platform.release(), platform.version(), platform.architecture()[0],
556 os_lang, platform.python_version(),release.version)
559 def exp_login_message(self):
560 return tools.config.get('login_message', False)
562 def exp_set_loglevel(self, loglevel, logger=None):
563 # TODO Previously, the level was set on the now deprecated
564 # `openerp.netsvc.Logger` class.
567 def exp_get_stats(self):
568 res = "OpenERP server: %d threads\n" % threading.active_count()
569 res += netsvc.Server.allStats()
572 def exp_list_http_services(self):
573 return http_server.list_http_services()
575 def exp_check_connectivity(self):
576 return bool(sql_db.db_connect('postgres'))
578 def exp_get_os_time(self):
581 def exp_get_sqlcount(self):
582 if not logging.getLogger('openerp.sql_db').isEnabledFor(logging.DEBUG):
583 _logger.warning("Counters of SQL will not be reliable unless logger openerp.sql_db is set to level DEBUG or higer.")
584 return sql_db.sql_counter
587 class objects_proxy(netsvc.ExportService):
588 def __init__(self, name="object"):
589 netsvc.ExportService.__init__(self,name)
591 def dispatch(self, method, params):
592 (db, uid, passwd ) = params[0:3]
593 threading.current_thread().uid = uid
595 if method == 'obj_list':
596 raise NameError("obj_list has been discontinued via RPC as of 6.0, please query ir.model directly!")
597 if method not in ['execute', 'execute_kw', 'exec_workflow']:
598 raise NameError("Method not available %s" % method)
599 security.check(db,uid,passwd)
600 assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
601 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
602 fn = getattr(openerp.osv.osv.service, method)
603 res = fn(db, uid, *params)
604 openerp.modules.registry.RegistryManager.signal_caches_change(db)
609 # TODO: set a maximum report number per user to avoid DOS attacks
615 class report_spool(netsvc.ExportService):
616 def __init__(self, name='report'):
617 netsvc.ExportService.__init__(self, name)
620 self.id_protect = threading.Semaphore()
622 def dispatch(self, method, params):
623 (db, uid, passwd ) = params[0:3]
624 threading.current_thread().uid = uid
626 if method not in ['report', 'report_get', 'render_report']:
627 raise KeyError("Method not supported %s" % method)
628 security.check(db,uid,passwd)
629 openerp.modules.registry.RegistryManager.check_registry_signaling(db)
630 fn = getattr(self, 'exp_' + method)
631 res = fn(db, uid, *params)
632 openerp.modules.registry.RegistryManager.signal_caches_change(db)
635 def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
641 self.id_protect.acquire()
644 self.id_protect.release()
646 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
648 cr = pooler.get_db(db).cursor()
650 obj = netsvc.LocalService('report.'+object)
651 (result, format) = obj.create(cr, uid, ids, datas, context)
654 self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
655 self._reports[id]['result'] = result
656 self._reports[id]['format'] = format
657 self._reports[id]['state'] = True
658 except Exception, exception:
660 _logger.exception('Exception: %s\n', exception)
661 if hasattr(exception, 'name') and hasattr(exception, 'value'):
662 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value))
665 self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb)
666 self._reports[id]['state'] = True
670 return self._check_report(id)
672 def exp_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 def go(id, uid, ids, datas, context):
686 cr = pooler.get_db(db).cursor()
688 obj = netsvc.LocalService('report.'+object)
689 (result, format) = obj.create(cr, uid, ids, datas, context)
692 self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb)
693 self._reports[id]['result'] = result
694 self._reports[id]['format'] = format
695 self._reports[id]['state'] = True
696 except Exception, exception:
697 _logger.exception('Exception: %s\n', 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
708 thread.start_new_thread(go, (id, uid, ids, datas, context))
711 def _check_report(self, report_id):
712 result = self._reports[report_id]
713 exc = result['exception']
715 netsvc.abort_response(exc, exc.message, 'warning', exc.traceback)
716 res = {'state': result['state']}
718 if tools.config['reportgz']:
720 res2 = zlib.compress(result['result'])
723 #CHECKME: why is this needed???
724 if isinstance(result['result'], unicode):
725 res2 = result['result'].encode('latin1', 'replace')
727 res2 = result['result']
729 res['result'] = base64.encodestring(res2)
730 res['format'] = result['format']
731 del self._reports[report_id]
734 def exp_report_get(self, db, uid, report_id):
735 if report_id in self._reports:
736 if self._reports[report_id]['uid'] == uid:
737 return self._check_report(report_id)
739 raise Exception, 'AccessDenied'
741 raise Exception, 'ReportNotFound'
751 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: