[REF] Refactoring according to the review of CHS
[odoo/odoo.git] / openerp / service / db.py
1 # -*- coding: utf-8 -*-
2
3 import base64
4 import contextlib
5 import logging
6 import os
7 import threading
8 import traceback
9
10 import openerp
11 from openerp import SUPERUSER_ID
12 import openerp.release
13 import openerp.sql_db
14 import openerp.tools
15
16 import security
17
18 _logger = logging.getLogger(__name__)
19
20 self_actions = {}
21 self_id = 0
22 self_id_protect = threading.Semaphore()
23
24 # This should be moved to openerp.modules.db, along side initialize().
25 def _initialize_db(id, db_name, demo, lang, user_password):
26     try:
27         cr = None
28         try:
29             self_actions[id]['progress'] = 0
30             cr = openerp.sql_db.db_connect(db_name).cursor()
31             openerp.modules.db.initialize(cr) # TODO this should be removed as it is done by RegistryManager.new().
32             openerp.tools.config['lang'] = lang
33             cr.commit()
34         finally:
35             if cr:
36                 cr.close()
37                 cr = None
38
39         registry = openerp.modules.registry.RegistryManager.new(
40             db_name, demo, self_actions[id], update_module=True)
41
42         try:
43             cr = openerp.sql_db.db_connect(db_name).cursor()
44
45             if lang:
46                 modobj = registry['ir.module.module']
47                 mids = modobj.search(cr, SUPERUSER_ID, [('state', '=', 'installed')])
48                 modobj.update_translations(cr, SUPERUSER_ID, mids, lang)
49
50             # update admin's password and lang
51             values = {'password': user_password, 'lang': lang}
52             registry['res.users'].write(cr, SUPERUSER_ID, [SUPERUSER_ID], values)
53
54             cr.execute('SELECT login, password FROM res_users ORDER BY login')
55             self_actions[id].update(users=cr.dictfetchall(), clean=True)
56             cr.commit()
57         finally:
58             if cr:
59                 cr.close()
60     except Exception, e:
61         self_actions[id].update(clean=False, exception=e)
62         _logger.exception('CREATE DATABASE failed:')
63         self_actions[id]['traceback'] = traceback.format_exc()
64
65 def dispatch(method, params):
66     if method in [ 'create', 'get_progress', 'drop', 'dump',
67         'restore', 'rename',
68         'change_admin_password', 'migrate_databases',
69         'create_database', 'duplicate_database' ]:
70         passwd = params[0]
71         params = params[1:]
72         security.check_super(passwd)
73     elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]:
74         # params = params
75         # No security check for these methods
76         pass
77     else:
78         raise KeyError("Method not found: %s" % method)
79     fn = globals()['exp_' + method]
80     return fn(*params)
81
82 def _create_empty_database(name):
83     db = openerp.sql_db.db_connect('postgres')
84     cr = db.cursor()
85     chosen_template = openerp.tools.config['db_template']
86     cr.execute("""SELECT datname 
87                           FROM pg_database
88                           WHERE datname = %s """,
89                        (name,))
90     if cr.fetchall():
91         raise openerp.exceptions.Warning(" %s database already exists!" % name )
92     try:
93         cr.autocommit(True) # avoid transaction block
94         cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (name, chosen_template))
95     finally:
96         cr.close()
97
98 def exp_create(db_name, demo, lang, user_password='admin'):
99     self_id_protect.acquire()
100     global self_id
101     self_id += 1
102     id = self_id
103     self_id_protect.release()
104
105     self_actions[id] = {'clean': False}
106
107     _create_empty_database(db_name)
108
109     _logger.info('CREATE DATABASE %s', db_name.lower())
110     create_thread = threading.Thread(target=_initialize_db,
111             args=(id, db_name, demo, lang, user_password))
112     create_thread.start()
113     self_actions[id]['thread'] = create_thread
114     return id
115
116 def exp_create_database(db_name, demo, lang, user_password='admin'):
117     """ Similar to exp_create but blocking."""
118     self_id_protect.acquire()
119     global self_id
120     self_id += 1
121     id = self_id
122     self_id_protect.release()
123
124     self_actions[id] = {'clean': False}
125
126     _logger.info('Create database `%s`.', db_name)
127     _create_empty_database(db_name)
128     _initialize_db(id, db_name, demo, lang, user_password)
129     return True
130
131 def exp_duplicate_database(db_original_name, db_name):
132     _logger.info('Duplicate database `%s` to `%s`.', db_original_name, db_name)
133     openerp.sql_db.close_db(db_original_name)
134     db = openerp.sql_db.db_connect('postgres')
135     cr = db.cursor()
136     try:
137         cr.autocommit(True) # avoid transaction block
138         cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "%s" """ % (db_name, db_original_name))
139     finally:
140         cr.close()
141     return True
142
143 def exp_get_progress(id):
144     if self_actions[id]['thread'].isAlive():
145 #       return openerp.modules.init_progress[db_name]
146         return min(self_actions[id].get('progress', 0),0.95), []
147     else:
148         clean = self_actions[id]['clean']
149         if clean:
150             users = self_actions[id]['users']
151             for user in users:
152                 # Remove the None passwords as they can't be marshalled by XML-RPC.
153                 if user['password'] is None:
154                     user['password'] = ''
155             self_actions.pop(id)
156             return 1.0, users
157         else:
158             e = self_actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'.
159             self_actions.pop(id)
160             raise Exception, e
161
162 def exp_drop(db_name):
163     if not exp_db_exist(db_name):
164         return False
165     openerp.modules.registry.RegistryManager.delete(db_name)
166     openerp.sql_db.close_db(db_name)
167
168     db = openerp.sql_db.db_connect('postgres')
169     cr = db.cursor()
170     cr.autocommit(True) # avoid transaction block
171     try:
172         # Try to terminate all other connections that might prevent
173         # dropping the database
174         try:
175
176             # PostgreSQL 9.2 renamed pg_stat_activity.procpid to pid:
177             # http://www.postgresql.org/docs/9.2/static/release-9-2.html#AEN110389
178             pid_col = 'pid' if cr._cnx.server_version >= 90200 else 'procpid'
179
180             cr.execute("""SELECT pg_terminate_backend(%(pid_col)s)
181                           FROM pg_stat_activity
182                           WHERE datname = %%s AND 
183                                 %(pid_col)s != pg_backend_pid()""" % {'pid_col': pid_col},
184                        (db_name,))
185         except Exception:
186             pass
187
188         try:
189             cr.execute('DROP DATABASE "%s"' % db_name)
190         except Exception, e:
191             _logger.error('DROP DB: %s failed:\n%s', db_name, e)
192             raise Exception("Couldn't drop database %s: %s" % (db_name, e))
193         else:
194             _logger.info('DROP DB: %s', db_name)
195     finally:
196         cr.close()
197     return True
198
199 @contextlib.contextmanager
200 def _set_pg_password_in_environment():
201     """ On systems where pg_restore/pg_dump require an explicit
202     password (i.e. when not connecting via unix sockets, and most
203     importantly on Windows), it is necessary to pass the PG user
204     password in the environment or in a special .pgpass file.
205
206     This context management method handles setting
207     :envvar:`PGPASSWORD` if it is not already
208     set, and removing it afterwards.
209
210     See also http://www.postgresql.org/docs/8.4/static/libpq-envars.html
211     
212     .. note:: This is not thread-safe, and should never be enabled for
213          SaaS (giving SaaS users the super-admin password is not a good idea
214          anyway)
215     """
216     if os.environ.get('PGPASSWORD') or not openerp.tools.config['db_password']:
217         yield
218     else:
219         os.environ['PGPASSWORD'] = openerp.tools.config['db_password']
220         try:
221             yield
222         finally:
223             del os.environ['PGPASSWORD']
224
225
226 def exp_dump(db_name):
227     with _set_pg_password_in_environment():
228         cmd = ['pg_dump', '--format=c', '--no-owner']
229         if openerp.tools.config['db_user']:
230             cmd.append('--username=' + openerp.tools.config['db_user'])
231         if openerp.tools.config['db_host']:
232             cmd.append('--host=' + openerp.tools.config['db_host'])
233         if openerp.tools.config['db_port']:
234             cmd.append('--port=' + str(openerp.tools.config['db_port']))
235         cmd.append(db_name)
236
237         stdin, stdout = openerp.tools.exec_pg_command_pipe(*tuple(cmd))
238         stdin.close()
239         data = stdout.read()
240         res = stdout.close()
241
242         if not data or res:
243             _logger.error(
244                     'DUMP DB: %s failed! Please verify the configuration of the database password on the server. '
245                     'You may need to create a .pgpass file for authentication, or specify `db_password` in the '
246                     'server configuration file.\n %s', db_name, data)
247             raise Exception, "Couldn't dump database"
248         _logger.info('DUMP DB successful: %s', db_name)
249
250         return base64.encodestring(data)
251
252 def exp_restore(db_name, data):
253     with _set_pg_password_in_environment():
254         if exp_db_exist(db_name):
255             _logger.warning('RESTORE DB: %s already exists', db_name)
256             raise Exception, "Database already exists"
257
258         _create_empty_database(db_name)
259
260         cmd = ['pg_restore', '--no-owner']
261         if openerp.tools.config['db_user']:
262             cmd.append('--username=' + openerp.tools.config['db_user'])
263         if openerp.tools.config['db_host']:
264             cmd.append('--host=' + openerp.tools.config['db_host'])
265         if openerp.tools.config['db_port']:
266             cmd.append('--port=' + str(openerp.tools.config['db_port']))
267         cmd.append('--dbname=' + db_name)
268         args2 = tuple(cmd)
269
270         buf=base64.decodestring(data)
271         if os.name == "nt":
272             tmpfile = (os.environ['TMP'] or 'C:\\') + os.tmpnam()
273             file(tmpfile, 'wb').write(buf)
274             args2=list(args2)
275             args2.append(tmpfile)
276             args2=tuple(args2)
277         stdin, stdout = openerp.tools.exec_pg_command_pipe(*args2)
278         if not os.name == "nt":
279             stdin.write(base64.decodestring(data))
280         stdin.close()
281         res = stdout.close()
282         if res:
283             raise Exception, "Couldn't restore database"
284         _logger.info('RESTORE DB: %s', db_name)
285
286         return True
287
288 def exp_rename(old_name, new_name):
289     openerp.modules.registry.RegistryManager.delete(old_name)
290     openerp.sql_db.close_db(old_name)
291
292     db = openerp.sql_db.db_connect('postgres')
293     cr = db.cursor()
294     cr.autocommit(True) # avoid transaction block
295     try:
296         try:
297             cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
298         except Exception, e:
299             _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e)
300             raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e))
301         else:
302             fs = os.path.join(openerp.tools.config['root_path'], 'filestore')
303             if os.path.exists(os.path.join(fs, old_name)):
304                 os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name))
305
306             _logger.info('RENAME DB: %s -> %s', old_name, new_name)
307     finally:
308         cr.close()
309     return True
310
311 def exp_db_exist(db_name):
312     ## Not True: in fact, check if connection to database is possible. The database may exists
313     return bool(openerp.sql_db.db_connect(db_name))
314
315 def exp_list(document=False):
316     if not openerp.tools.config['list_db'] and not document:
317         raise openerp.exceptions.AccessDenied()
318     chosen_template = openerp.tools.config['db_template']
319     templates_list = tuple(set(['template0', 'template1', 'postgres', chosen_template]))
320     db = openerp.sql_db.db_connect('postgres')
321     cr = db.cursor()
322     try:
323         try:
324             db_user = openerp.tools.config["db_user"]
325             if not db_user and os.name == 'posix':
326                 import pwd
327                 db_user = pwd.getpwuid(os.getuid())[0]
328             if not db_user:
329                 cr.execute("select usename from pg_user where usesysid=(select datdba from pg_database where datname=%s)", (openerp.tools.config["db_name"],))
330                 res = cr.fetchone()
331                 db_user = res and str(res[0])
332             if db_user:
333                 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))
334             else:
335                 cr.execute("select datname from pg_database where datname not in %s order by datname", (templates_list,))
336             res = [str(name) for (name,) in cr.fetchall()]
337         except Exception:
338             res = []
339     finally:
340         cr.close()
341     res.sort()
342     return res
343
344 def exp_change_admin_password(new_password):
345     openerp.tools.config['admin_passwd'] = new_password
346     openerp.tools.config.save()
347     return True
348
349 def exp_list_lang():
350     return openerp.tools.scan_languages()
351
352 def exp_server_version():
353     """ Return the version of the server
354         Used by the client to verify the compatibility with its own version
355     """
356     return openerp.release.version
357
358 def exp_migrate_databases(databases):
359     for db in databases:
360         _logger.info('migrate database %s', db)
361         openerp.tools.config['update']['base'] = True
362         openerp.modules.registry.RegistryManager.new(db, force_demo=False, update_module=True)
363     return True
364
365 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: