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 ##############################################################################
32 from tools.translate import _
43 class db(netsvc.Service):
44 def __init__(self, name="db"):
45 netsvc.Service.__init__(self, name)
46 self.joinGroup("web-services")
47 self.exportMethod(self.create)
48 self.exportMethod(self.get_progress)
49 self.exportMethod(self.drop)
50 self.exportMethod(self.dump)
51 self.exportMethod(self.restore)
52 self.exportMethod(self.rename)
53 self.exportMethod(self.list)
54 self.exportMethod(self.list_lang)
55 self.exportMethod(self.change_admin_password)
56 self.exportMethod(self.server_version)
57 self.exportMethod(self.migrate_databases)
60 self.id_protect = threading.Semaphore()
62 self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var
64 def create(self, password, db_name, demo, lang, user_password='admin'):
65 security.check_super(password)
66 self.id_protect.acquire()
69 self.id_protect.release()
71 self.actions[id] = {'clean': False}
73 db = sql_db.db_connect('template1')
76 cr.autocommit(True) # avoid transaction block
77 cr.execute('CREATE DATABASE "%s" ENCODING \'unicode\'' % db_name)
82 class DBInitialize(object):
83 def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
86 serv.actions[id]['progress'] = 0
88 cr = sql_db.db_connect(db_name).cursor()
93 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
94 update_module=True)[1]
96 cr = sql_db.db_connect(db_name).cursor()
99 modobj = pool.get('ir.module.module')
100 mids = modobj.search(cr, 1, [('state', '=', 'installed')])
101 modobj.update_translations(cr, 1, mids, lang)
103 cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
104 user_password, lang, 'admin'))
105 cr.execute('SELECT login, password, name ' \
108 serv.actions[id]['users'] = cr.dictfetchall()
109 serv.actions[id]['clean'] = True
113 serv.actions[id]['clean'] = False
114 serv.actions[id]['exception'] = e
115 from cStringIO import StringIO
118 traceback.print_exc(file=e_str)
119 traceback_str = e_str.getvalue()
121 netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
122 serv.actions[id]['traceback'] = traceback_str
125 logger = netsvc.Logger()
126 logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
128 create_thread = threading.Thread(target=dbi,
129 args=(self, id, db_name, demo, lang, user_password))
130 create_thread.start()
131 self.actions[id]['thread'] = create_thread
134 def get_progress(self, password, id):
135 security.check_super(password)
136 if self.actions[id]['thread'].isAlive():
137 # return addons.init_progress[db_name]
138 return (min(self.actions[id].get('progress', 0),0.95), [])
140 clean = self.actions[id]['clean']
142 users = self.actions[id]['users']
146 e = self.actions[id]['exception']
150 def drop(self, password, db_name):
151 security.check_super(password)
152 sql_db.close_db(db_name)
153 logger = netsvc.Logger()
155 db = sql_db.db_connect('template1')
157 cr.autocommit(True) # avoid transaction block
160 cr.execute('DROP DATABASE "%s"' % db_name)
162 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
163 'DROP DB: %s failed:\n%s' % (db_name, e))
164 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
166 logger.notifyChannel("web-services", netsvc.LOG_INFO,
167 'DROP DB: %s' % (db_name))
172 def _set_pg_psw_env_var(self):
173 if os.name == 'nt' and not os.environ.get('PGPASSWORD', ''):
174 os.environ['PGPASSWORD'] = tools.config['db_password']
175 self._pg_psw_env_var_is_set = True
177 def _unset_pg_psw_env_var(self):
178 if os.name == 'nt' and self._pg_psw_env_var_is_set:
179 os.environ['PGPASSWORD'] = ''
181 def dump(self, password, db_name):
182 security.check_super(password)
183 logger = netsvc.Logger()
185 self._set_pg_psw_env_var()
187 cmd = ['pg_dump', '--format=c', '--no-owner']
188 if tools.config['db_user']:
189 cmd.append('--username=' + tools.config['db_user'])
190 if tools.config['db_host']:
191 cmd.append('--host=' + tools.config['db_host'])
192 if tools.config['db_port']:
193 cmd.append('--port=' + str(tools.config['db_port']))
196 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
201 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
202 'DUMP DB: %s failed\n%s' % (db_name, data))
203 raise Exception, "Couldn't dump database"
204 logger.notifyChannel("web-services", netsvc.LOG_INFO,
205 'DUMP DB: %s' % (db_name))
207 self._unset_pg_psw_env_var()
209 return base64.encodestring(data)
211 def restore(self, password, db_name, data):
212 security.check_super(password)
213 logger = netsvc.Logger()
215 self._set_pg_psw_env_var()
217 if self.db_exist(db_name):
218 logger.notifyChannel("web-services", netsvc.LOG_WARNING,
219 'RESTORE DB: %s already exists' % (db_name,))
220 raise Exception, "Database already exists"
222 db = sql_db.db_connect('template1')
224 cr.autocommit(True) # avoid transaction block
226 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % db_name)
231 cmd = ['pg_restore', '--no-owner']
232 if tools.config['db_user']:
233 cmd.append('--username=' + tools.config['db_user'])
234 if tools.config['db_host']:
235 cmd.append('--host=' + tools.config['db_host'])
236 if tools.config['db_port']:
237 cmd.append('--port=' + str(tools.config['db_port']))
238 cmd.append('--dbname=' + db_name)
241 buf=base64.decodestring(data)
243 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
244 file(tmpfile, 'wb').write(buf)
246 args2.append(' ' + tmpfile)
248 stdin, stdout = tools.exec_pg_command_pipe(*args2)
249 if not os.name == "nt":
250 stdin.write(base64.decodestring(data))
254 raise Exception, "Couldn't restore database"
255 logger.notifyChannel("web-services", netsvc.LOG_INFO,
256 'RESTORE DB: %s' % (db_name))
258 self._unset_pg_psw_env_var()
262 def rename(self, password, old_name, new_name):
263 security.check_super(password)
264 sql_db.close_db(old_name)
265 logger = netsvc.Logger()
267 db = sql_db.db_connect('template1')
271 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
273 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
274 'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
275 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
277 fs = os.path.join(tools.config['root_path'], 'filestore')
278 if os.path.exists(os.path.join(fs, old_name)):
279 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
281 logger.notifyChannel("web-services", netsvc.LOG_INFO,
282 'RENAME DB: %s -> %s' % (old_name, new_name))
287 def db_exist(self, db_name):
289 db = sql_db.db_connect(db_name)
295 db = sql_db.db_connect('template1')
298 list_db = tools.config["list_db"]
299 if list_db == 'False':
302 db_user = tools.config["db_user"]
303 if not db_user and os.name == 'posix':
305 db_user = pwd.getpwuid(os.getuid())[0]
307 cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
309 db_user = res and str(res[0])
311 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,))
313 cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
314 res = [str(name) for (name,) in cr.fetchall()]
322 def change_admin_password(self, old_password, new_password):
323 security.check_super(old_password)
324 tools.config['admin_passwd'] = new_password
329 return tools.scan_languages()
331 def server_version(self):
332 """ Return the version of the server
333 Used by the client to verify the compatibility with its own version
335 return release.version
337 def migrate_databases(self, password, databases):
339 from osv.orm import except_orm
340 from osv.osv import except_osv
342 security.check_super(password)
346 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
347 tools.config['update']['base'] = True
348 pooler.restart_pool(db, force_demo=False, update_module=True)
349 except except_orm, inst:
350 self.abortResponse(1, inst.name, 'warning', inst.value)
351 except except_osv, inst:
352 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
355 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
356 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
361 class common(netsvc.Service):
362 def __init__(self,name="common"):
363 netsvc.Service.__init__(self,name)
364 self.joinGroup("web-services")
365 self.exportMethod(self.ir_get)
366 self.exportMethod(self.ir_set)
367 self.exportMethod(self.ir_del)
368 self.exportMethod(self.about)
369 self.exportMethod(self.login)
370 self.exportMethod(self.logout)
371 self.exportMethod(self.timezone_get)
372 self.exportMethod(self.get_available_updates)
373 self.exportMethod(self.get_migration_scripts)
374 self.exportMethod(self.get_server_environment)
375 self.exportMethod(self.login_message)
377 def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False):
378 security.check(db, uid, password)
379 cr = pooler.get_db(db).cursor()
380 res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
385 def ir_del(self, db, uid, password, id):
386 security.check(db, uid, password)
387 cr = pooler.get_db(db).cursor()
388 res = ir.ir_del(cr,uid, id)
393 def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None):
398 security.check(db, uid, password)
399 cr = pooler.get_db(db).cursor()
400 res = ir.ir_get(cr,uid, keys, args, meta, context)
405 def login(self, db, login, password):
406 res = security.login(db, login, password)
407 logger = netsvc.Logger()
408 msg = res and 'successful login' or 'bad login or password'
409 logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower()))
412 def logout(self, db, login, password):
413 logger = netsvc.Logger()
414 logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
417 def about(self, extended=False):
418 """Return information about the OpenERP Server.
420 @param extended: if True then return version info
421 @return string if extended is False else tuple
426 OpenERP is an ERP+CRM program for small and medium businesses.
428 The whole source code is distributed under the terms of the
431 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
434 return info, release.version
437 def timezone_get(self, db, login, password):
438 return time.tzname[0]
441 def get_available_updates(self, password, contract_id, contract_password):
442 security.check_super(password)
443 import tools.maintenance as tm
445 rc = tm.remote_contract(contract_id, contract_password)
447 raise tm.RemoteContractException('This contract does not exist or is not active')
449 return rc.get_available_updates(rc.id, addons.get_modules_with_version())
451 except tm.RemoteContractException, e:
452 self.abortResponse(1, 'Migration Error', 'warning', str(e))
455 def get_migration_scripts(self, password, contract_id, contract_password):
456 security.check_super(password)
458 import tools.maintenance as tm
460 rc = tm.remote_contract(contract_id, contract_password)
462 raise tm.RemoteContractException('This contract does not exist or is not active')
463 if rc.status != 'full':
464 raise tm.RemoteContractException('Can not get updates for a partial contract')
466 l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
468 zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version())
470 from shutil import rmtree, copytree, copy
472 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
473 if zips and not os.path.isdir(backup_directory):
474 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
475 store the old modules: %s' % (backup_directory,))
476 os.makedirs(backup_directory)
479 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
480 mp = addons.get_module_path(module)
482 if os.path.isdir(mp):
483 copytree(mp, os.path.join(backup_directory, module))
484 if os.path.islink(mp):
489 copy(mp + 'zip', backup_directory)
490 os.unlink(mp + '.zip')
494 base64_decoded = base64.decodestring(zips[module])
496 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
499 zip_contents = cStringIO.StringIO(base64_decoded)
503 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
505 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
511 l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
512 nmp = os.path.join(backup_directory, module)
513 if os.path.isdir(nmp):
514 copytree(nmp, tools.config['addons_path'])
516 copy(nmp+'.zip', tools.config['addons_path'])
520 except tm.RemoteContractException, e:
521 self.abortResponse(1, 'Migration Error', 'warning', str(e))
524 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
525 l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
528 def get_server_environment(self):
530 rev_id = os.popen('bzr revision-info').read()
532 rev_id = 'Exception: %s\n' % (tools.ustr(e))
534 os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] )
537 environment = '\nEnvironment Information : \n' \
540 %(platform.platform(), platform.os.name)
541 if os.name == 'posix':
542 if platform.system() == 'Linux':
543 lsbinfo = os.popen('lsb_release -a').read()
544 environment += '%s'%(lsbinfo)
546 environment += 'Your System is not lsb compliant\n'
547 environment += 'Operating System Release : %s\n' \
548 'Operating System Version : %s\n' \
549 'Operating System Architecture : %s\n' \
550 'Operating System Locale : %s\n'\
551 'Python Version : %s\n'\
552 'OpenERP-Server Version : %s\n'\
553 'Last revision No. & ID : %s'\
554 %(platform.release(), platform.version(), platform.architecture()[0],
555 os_lang, platform.python_version(),release.version,rev_id)
559 def login_message(self):
560 return tools.config.get('login_message', False)
564 class objects_proxy(netsvc.Service):
565 def __init__(self, name="object"):
566 netsvc.Service.__init__(self,name)
567 self.joinGroup('web-services')
568 self.exportMethod(self.execute)
569 self.exportMethod(self.exec_workflow)
570 self.exportMethod(self.obj_list)
572 def exec_workflow(self, db, uid, passwd, object, method, id):
573 security.check(db, uid, passwd)
574 service = netsvc.LocalService("object_proxy")
575 res = service.exec_workflow(db, uid, object, method, id)
578 def execute(self, db, uid, passwd, object, method, *args):
579 security.check(db, uid, passwd)
580 service = netsvc.LocalService("object_proxy")
581 res = service.execute(db, uid, object, method, *args)
584 def obj_list(self, db, uid, passwd):
585 security.check(db, uid, passwd)
586 service = netsvc.LocalService("object_proxy")
587 res = service.obj_list()
594 # - None = end of wizard
596 # Wizard Type: 'form'
601 # TODO: change local request to OSE request/reply pattern
603 class wizard(netsvc.Service):
604 def __init__(self, name='wizard'):
605 netsvc.Service.__init__(self,name)
606 self.joinGroup('web-services')
607 self.exportMethod(self.execute)
608 self.exportMethod(self.create)
614 def _execute(self, db, uid, wiz_id, datas, action, context):
615 self.wiz_datas[wiz_id].update(datas)
616 wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
617 return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
619 def create(self, db, uid, passwd, wiz_name, datas=None):
622 security.check(db, uid, passwd)
623 #FIXME: this is not thread-safe
625 self.wiz_datas[self.id] = {}
626 self.wiz_name[self.id] = wiz_name
627 self.wiz_uid[self.id] = uid
630 def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None):
633 security.check(db, uid, passwd)
635 if wiz_id in self.wiz_uid:
636 if self.wiz_uid[wiz_id] == uid:
637 return self._execute(db, uid, wiz_id, datas, action, context)
639 raise Exception, 'AccessDenied'
641 raise Exception, 'WizardNotFound'
645 # TODO: set a maximum report number per user to avoid DOS attacks
651 class ExceptionWithTraceback(Exception):
652 def __init__(self, msg, tb):
655 self.args = (msg, tb)
657 class report_spool(netsvc.Service):
658 def __init__(self, name='report'):
659 netsvc.Service.__init__(self, name)
660 self.joinGroup('web-services')
661 self.exportMethod(self.report)
662 self.exportMethod(self.report_get)
665 self.id_protect = threading.Semaphore()
667 def report(self, db, uid, passwd, object, ids, datas=None, context=None):
672 security.check(db, uid, passwd)
674 self.id_protect.acquire()
677 self.id_protect.release()
679 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
681 def go(id, uid, ids, datas, context):
682 cr = pooler.get_db(db).cursor()
684 obj = netsvc.LocalService('report.'+object)
685 (result, format) = obj.create(cr, uid, ids, datas, context)
686 self._reports[id]['result'] = result
687 self._reports[id]['format'] = format
688 self._reports[id]['state'] = True
689 except Exception, exception:
693 tb_s = "".join(traceback.format_exception(*tb))
694 logger = netsvc.Logger()
695 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
696 'Exception: %s\n%s' % (str(exception), tb_s))
697 self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
698 self._reports[id]['state'] = True
703 thread.start_new_thread(go, (id, uid, ids, datas, context))
706 def _check_report(self, report_id):
707 result = self._reports[report_id]
708 if result['exception']:
709 raise result['exception']
710 res = {'state': result['state']}
712 if tools.config['reportgz']:
714 res2 = zlib.compress(result['result'])
717 #CHECKME: why is this needed???
718 if isinstance(result['result'], unicode):
719 res2 = result['result'].encode('latin1', 'replace')
721 res2 = result['result']
723 res['result'] = base64.encodestring(res2)
724 res['format'] = result['format']
725 del self._reports[report_id]
728 def report_get(self, db, uid, passwd, report_id):
729 security.check(db, uid, passwd)
731 if report_id in self._reports:
732 if self._reports[report_id]['uid'] == uid:
733 return self._check_report(report_id)
735 raise Exception, 'AccessDenied'
737 raise Exception, 'ReportNotFound'
742 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: