1 # -*- encoding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
33 from tools.translate import _
44 class db(netsvc.Service):
45 def __init__(self, name="db"):
46 netsvc.Service.__init__(self, name)
47 self.joinGroup("web-services")
48 self.exportMethod(self.create)
49 self.exportMethod(self.get_progress)
50 self.exportMethod(self.drop)
51 self.exportMethod(self.dump)
52 self.exportMethod(self.restore)
53 self.exportMethod(self.rename)
54 self.exportMethod(self.list)
55 self.exportMethod(self.list_lang)
56 self.exportMethod(self.change_admin_password)
57 self.exportMethod(self.server_version)
58 self.exportMethod(self.migrate_databases)
61 self.id_protect = threading.Semaphore()
63 def create(self, password, db_name, demo, lang, user_password='admin'):
64 security.check_super(password)
65 self.id_protect.acquire()
68 self.id_protect.release()
70 self.actions[id] = {'clean': False}
72 db = sql_db.db_connect('template1')
73 cr = db.serialized_cursor()
75 cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
76 cr.execute('CREATE DATABASE "%s" ENCODING \'unicode\'' % db_name)
79 sql_db.close_db('template1')
80 class DBInitialize(object):
81 def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
84 serv.actions[id]['progress'] = 0
86 cr = sql_db.db_connect(db_name).cursor()
91 pool = pooler.restart_pool(db_name, demo, serv.actions[id],
92 update_module=True)[1]
94 cr = sql_db.db_connect(db_name).cursor()
97 modobj = pool.get('ir.module.module')
98 mids = modobj.search(cr, 1, [('state', '=', 'installed')])
99 modobj.update_translations(cr, 1, mids, lang)
101 cr.execute('UPDATE res_users SET password=%s, context_lang=%s, active=True WHERE login=%s', (
102 user_password, lang, 'admin'))
103 cr.execute('SELECT login, password, name ' \
106 serv.actions[id]['users'] = cr.dictfetchall()
107 serv.actions[id]['clean'] = True
111 serv.actions[id]['clean'] = False
112 serv.actions[id]['exception'] = e
113 from cStringIO import StringIO
116 traceback.print_exc(file=e_str)
117 traceback_str = e_str.getvalue()
119 netsvc.Logger().notifyChannel('web-services', netsvc.LOG_ERROR, 'CREATE DATABASE\n%s' % (traceback_str))
120 serv.actions[id]['traceback'] = traceback_str
123 logger = netsvc.Logger()
124 logger.notifyChannel("web-services", netsvc.LOG_INFO, 'CREATE DATABASE: %s' % (db_name.lower()))
126 create_thread = threading.Thread(target=dbi,
127 args=(self, id, db_name, demo, lang, user_password))
128 create_thread.start()
129 self.actions[id]['thread'] = create_thread
132 def get_progress(self, password, id):
133 security.check_super(password)
134 if self.actions[id]['thread'].isAlive():
135 # return addons.init_progress[db_name]
136 return (min(self.actions[id].get('progress', 0),0.95), [])
138 clean = self.actions[id]['clean']
140 users = self.actions[id]['users']
144 e = self.actions[id]['exception']
148 def drop(self, password, db_name):
149 security.check_super(password)
150 sql_db.close_db(db_name)
151 logger = netsvc.Logger()
153 db = sql_db.db_connect('template1')
154 cr = db.serialized_cursor()
155 cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
158 cr.execute('DROP DATABASE "%s"' % db_name)
160 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
161 'DROP DB: %s failed:\n%s' % (db_name, e))
162 raise Exception("Couldn't drop database %s: %s" % (db_name, e))
164 logger.notifyChannel("web-services", netsvc.LOG_INFO,
165 'DROP DB: %s' % (db_name))
169 sql_db.close_db('template1')
172 def dump(self, password, db_name):
173 security.check_super(password)
174 logger = netsvc.Logger()
176 cmd = ['pg_dump', '--format=c', '--no-owner']
177 if tools.config['db_user']:
178 cmd.append('--username=' + tools.config['db_user'])
179 if tools.config['db_host']:
180 cmd.append('--host=' + tools.config['db_host'])
181 if tools.config['db_port']:
182 cmd.append('--port=' + tools.config['db_port'])
185 stdin, stdout = tools.exec_pg_command_pipe(*tuple(cmd))
190 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
191 'DUMP DB: %s failed\n%s' % (db_name, data))
192 raise Exception, "Couldn't dump database"
193 logger.notifyChannel("web-services", netsvc.LOG_INFO,
194 'DUMP DB: %s' % (db_name))
195 return base64.encodestring(data)
197 def restore(self, password, db_name, data):
198 security.check_super(password)
199 logger = netsvc.Logger()
201 if self.db_exist(db_name):
202 logger.notifyChannel("web-services", netsvc.LOG_WARNING,
203 'RESTORE DB: %s already exists' % (db_name,))
204 raise Exception, "Database already exists"
206 db = sql_db.db_connect('template1')
207 cr = db.serialized_cursor()
208 cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
210 cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % db_name)
213 sql_db.close_db('template1')
215 cmd = ['pg_restore', '--no-owner']
216 if tools.config['db_user']:
217 cmd.append('--username=' + tools.config['db_user'])
218 if tools.config['db_host']:
219 cmd.append('--host=' + tools.config['db_host'])
220 if tools.config['db_port']:
221 cmd.append('--port=' + tools.config['db_port'])
222 cmd.append('--dbname=' + db_name)
225 buf=base64.decodestring(data)
227 tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
228 file(tmpfile, 'wb').write(buf)
230 args2.append(' ' + tmpfile)
232 stdin, stdout = tools.exec_pg_command_pipe(*args2)
233 if not os.name == "nt":
234 stdin.write(base64.decodestring(data))
238 raise Exception, "Couldn't restore database"
239 logger.notifyChannel("web-services", netsvc.LOG_INFO,
240 'RESTORE DB: %s' % (db_name))
243 def rename(self, password, old_name, new_name):
244 security.check_super(password)
245 sql_db.close_db(old_name)
246 logger = netsvc.Logger()
248 db = sql_db.db_connect('template1')
249 cr = db.serialized_cursor()
252 cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
254 logger.notifyChannel("web-services", netsvc.LOG_ERROR,
255 'RENAME DB: %s -> %s failed:\n%s' % (old_name, new_name, e))
256 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
258 fs = os.path.join(tools.config['root_path'], 'filestore')
259 if os.path.exists(os.path.join(fs, old_name)):
260 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
262 logger.notifyChannel("web-services", netsvc.LOG_INFO,
263 'RENAME DB: %s -> %s' % (old_name, new_name))
267 sql_db.close_db('template1')
270 def db_exist(self, db_name):
272 db = sql_db.db_connect(db_name)
278 db = sql_db.db_connect('template1')
282 db_user = tools.config["db_user"]
283 if not db_user and os.name == 'posix':
285 db_user = pwd.getpwuid(os.getuid())[0]
287 cr.execute("select decode(usename, 'escape') from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (tools.config["db_name"],))
289 db_user = res and str(res[0])
291 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,))
293 cr.execute("select decode(datname, 'escape') from pg_database where datname not in('template0', 'template1','postgres') order by datname")
294 res = [str(name) for (name,) in cr.fetchall()]
299 sql_db.close_db('template1')
303 def change_admin_password(self, old_password, new_password):
304 security.check_super(old_password)
305 tools.config['admin_passwd'] = new_password
310 return tools.scan_languages()
312 def server_version(self):
313 """ Return the version of the server
314 Used by the client to verify the compatibility with its own version
316 return release.version
318 def migrate_databases(self, password, databases):
320 from osv.orm import except_orm
321 from osv.osv import except_osv
323 security.check_super(password)
327 l.notifyChannel('migration', netsvc.LOG_INFO, 'migrate database %s' % (db,))
328 tools.config['update']['base'] = True
329 pooler.restart_pool(db, force_demo=False, update_module=True)
330 except except_orm, inst:
331 self.abortResponse(1, inst.name, 'warning', inst.value)
332 except except_osv, inst:
333 self.abortResponse(1, inst.name, inst.exc_type, inst.value)
336 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
337 l.notifyChannel('web-services', netsvc.LOG_ERROR, tb_s)
342 class common(netsvc.Service):
343 def __init__(self,name="common"):
344 netsvc.Service.__init__(self,name)
345 self.joinGroup("web-services")
346 self.exportMethod(self.ir_get)
347 self.exportMethod(self.ir_set)
348 self.exportMethod(self.ir_del)
349 self.exportMethod(self.about)
350 self.exportMethod(self.login)
351 self.exportMethod(self.logout)
352 self.exportMethod(self.timezone_get)
353 self.exportMethod(self.get_available_updates)
354 self.exportMethod(self.get_migration_scripts)
355 self.exportMethod(self.get_environment)
357 def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False):
358 security.check(db, uid, password)
359 cr = pooler.get_db(db).cursor()
360 res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject)
365 def ir_del(self, db, uid, password, id):
366 security.check(db, uid, password)
367 cr = pooler.get_db(db).cursor()
368 res = ir.ir_del(cr,uid, id)
373 def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None):
378 security.check(db, uid, password)
379 cr = pooler.get_db(db).cursor()
380 res = ir.ir_get(cr,uid, keys, args, meta, context)
385 def login(self, db, login, password):
386 res = security.login(db, login, password)
387 logger = netsvc.Logger()
388 msg = res and 'successful login' or 'bad login or password'
389 logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower()))
392 def logout(self, db, login, password):
393 logger = netsvc.Logger()
394 logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db))
397 def about(self, extended=False):
398 """Return information about the OpenERP Server.
400 @param extended: if True then return version info
401 @return string if extended is False else tuple
406 OpenERP is an ERP+CRM program for small and medium businesses.
408 The whole source code is distributed under the terms of the
411 (c) 2003-TODAY, Fabien Pinckaers - Tiny sprl''')
414 return info, release.version
417 def timezone_get(self, db, login, password):
418 return time.tzname[0]
421 def get_available_updates(self, password, contract_id, contract_password):
422 security.check_super(password)
423 import tools.maintenance as tm
425 rc = tm.remote_contract(contract_id, contract_password)
427 raise tm.RemoteContractException('This contract does not exist or is not active')
429 return rc.get_available_updates(rc.id, addons.get_modules_with_version())
431 except tm.RemoteContractException, e:
432 self.abortResponse(1, 'Migration Error', 'warning', str(e))
435 def get_migration_scripts(self, password, contract_id, contract_password):
436 security.check_super(password)
438 import 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')
443 if rc.status != 'full':
444 raise tm.RemoteContractException('Can not get updates for a partial contract')
446 l.notifyChannel('migration', netsvc.LOG_INFO, 'starting migration with contract %s' % (rc.name,))
448 zips = rc.retrieve_updates(rc.id, addons.get_modules_with_version())
450 from shutil import rmtree, copytree, copy
452 backup_directory = os.path.join(tools.config['root_path'], 'backup', time.strftime('%Y-%m-%d-%H-%M'))
453 if zips and not os.path.isdir(backup_directory):
454 l.notifyChannel('migration', netsvc.LOG_INFO, 'create a new backup directory to \
455 store the old modules: %s' % (backup_directory,))
456 os.makedirs(backup_directory)
459 l.notifyChannel('migration', netsvc.LOG_INFO, 'upgrade module %s' % (module,))
460 mp = addons.get_module_path(module)
462 if os.path.isdir(mp):
463 copytree(mp, os.path.join(backup_directory, module))
464 if os.path.islink(mp):
469 copy(mp + 'zip', backup_directory)
470 os.unlink(mp + '.zip')
474 base64_decoded = base64.decodestring(zips[module])
476 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to read the module %s' % (module,))
479 zip_contents = cStringIO.StringIO(base64_decoded)
483 tools.extract_zip_file(zip_contents, tools.config['addons_path'] )
485 l.notifyChannel('migration', netsvc.LOG_ERROR, 'unable to extract the module %s' % (module, ))
491 l.notifyChannel('migration', netsvc.LOG_ERROR, 'restore the previous version of the module %s' % (module, ))
492 nmp = os.path.join(backup_directory, module)
493 if os.path.isdir(nmp):
494 copytree(nmp, tools.config['addons_path'])
496 copy(nmp+'.zip', tools.config['addons_path'])
500 except tm.RemoteContractException, e:
501 self.abortResponse(1, 'Migration Error', 'warning', str(e))
504 tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
505 l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s)
508 def get_environment(self,lang=False):
510 if '.bzr' in os.listdir((os.getcwd()[0:-3])):
511 fp = open(os.path.join(os.getcwd()[0:-3],'.bzr/branch/last-revision'))
515 rev_no = 'Bazaar Not Installed !'
517 rev_no = 'Bazaar Not Installed !'
519 lang = os.environ.get('LANG', '').split('.')[0]
520 environment = 'Environment_Information : \n' \
521 'Operating System : %s\n' \
523 'Operating System Version : %s\n' \
524 'Python Version : %s\n'\
525 'OpenERP-Server Version : %s\n'\
526 'OpenERP-Server Last Revision ID : %s'\
527 'Locale : %s'%(os.name,sys.platform,str(sys.version.split('\n')[1]),str(sys.version[0:5]),release.version,rev_no,lang)
531 class objects_proxy(netsvc.Service):
532 def __init__(self, name="object"):
533 netsvc.Service.__init__(self,name)
534 self.joinGroup('web-services')
535 self.exportMethod(self.execute)
536 self.exportMethod(self.exec_workflow)
537 self.exportMethod(self.obj_list)
539 def exec_workflow(self, db, uid, passwd, object, method, id):
540 security.check(db, uid, passwd)
541 service = netsvc.LocalService("object_proxy")
542 res = service.exec_workflow(db, uid, object, method, id)
545 def execute(self, db, uid, passwd, object, method, *args):
546 security.check(db, uid, passwd)
547 service = netsvc.LocalService("object_proxy")
548 res = service.execute(db, uid, object, method, *args)
551 def obj_list(self, db, uid, passwd):
552 security.check(db, uid, passwd)
553 service = netsvc.LocalService("object_proxy")
554 res = service.obj_list()
561 # - None = end of wizard
563 # Wizard Type: 'form'
568 # TODO: change local request to OSE request/reply pattern
570 class wizard(netsvc.Service):
571 def __init__(self, name='wizard'):
572 netsvc.Service.__init__(self,name)
573 self.joinGroup('web-services')
574 self.exportMethod(self.execute)
575 self.exportMethod(self.create)
581 def _execute(self, db, uid, wiz_id, datas, action, context):
582 self.wiz_datas[wiz_id].update(datas)
583 wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
584 return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
586 def create(self, db, uid, passwd, wiz_name, datas=None):
589 security.check(db, uid, passwd)
590 #FIXME: this is not thread-safe
592 self.wiz_datas[self.id] = {}
593 self.wiz_name[self.id] = wiz_name
594 self.wiz_uid[self.id] = uid
597 def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None):
600 security.check(db, uid, passwd)
602 if wiz_id in self.wiz_uid:
603 if self.wiz_uid[wiz_id] == uid:
604 return self._execute(db, uid, wiz_id, datas, action, context)
606 raise Exception, 'AccessDenied'
608 raise Exception, 'WizardNotFound'
612 # TODO: set a maximum report number per user to avoid DOS attacks
618 class ExceptionWithTraceback(Exception):
619 def __init__(self, msg, tb):
622 self.args = (msg, tb)
624 class report_spool(netsvc.Service):
625 def __init__(self, name='report'):
626 netsvc.Service.__init__(self, name)
627 self.joinGroup('web-services')
628 self.exportMethod(self.report)
629 self.exportMethod(self.report_get)
632 self.id_protect = threading.Semaphore()
634 def report(self, db, uid, passwd, object, ids, datas=None, context=None):
639 security.check(db, uid, passwd)
641 self.id_protect.acquire()
644 self.id_protect.release()
646 self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None}
648 def go(id, uid, ids, datas, context):
649 cr = pooler.get_db(db).cursor()
651 obj = netsvc.LocalService('report.'+object)
652 (result, format) = obj.create(cr, uid, ids, datas, context)
653 self._reports[id]['result'] = result
654 self._reports[id]['format'] = format
655 self._reports[id]['state'] = True
656 except Exception, exception:
660 tb_s = "".join(traceback.format_exception(*tb))
661 logger = netsvc.Logger()
662 logger.notifyChannel('web-services', netsvc.LOG_ERROR,common().get_environment(context.get('lang',False)))
663 logger.notifyChannel('web-services', netsvc.LOG_ERROR,
664 'Exception: %s\n%s' % (str(exception), tb_s))
665 self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb)
666 self._reports[id]['state'] = True
671 thread.start_new_thread(go, (id, uid, ids, datas, context))
674 def _check_report(self, report_id):
675 result = self._reports[report_id]
676 if result['exception']:
677 raise result['exception']
678 res = {'state': result['state']}
680 if tools.config['reportgz']:
682 res2 = zlib.compress(result['result'])
685 #CHECKME: why is this needed???
686 if isinstance(result['result'], unicode):
687 res2 = result['result'].encode('latin1', 'replace')
689 res2 = result['result']
691 res['result'] = base64.encodestring(res2)
692 res['format'] = result['format']
693 del self._reports[report_id]
696 def report_get(self, db, uid, passwd, report_id):
697 security.check(db, uid, passwd)
699 if report_id in self._reports:
700 if self._reports[report_id]['uid'] == uid:
701 return self._check_report(report_id)
703 raise Exception, 'AccessDenied'
705 raise Exception, 'ReportNotFound'
710 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: