1# -*- coding: utf-8 -*- 2import base64 3import json 4import logging 5import os 6import shutil 7import tempfile 8import threading 9import traceback 10from xml.etree import ElementTree as ET 11import zipfile 12 13from psycopg2 import sql 14from pytz import country_timezones 15from functools import wraps 16from contextlib import closing 17from decorator import decorator 18 19import psycopg2 20 21import odoo 22from odoo import SUPERUSER_ID 23from odoo.exceptions import AccessDenied 24import odoo.release 25import odoo.sql_db 26import odoo.tools 27from odoo.sql_db import db_connect 28from odoo.release import version_info 29 30_logger = logging.getLogger(__name__) 31 32class DatabaseExists(Warning): 33 pass 34 35 36def check_db_management_enabled(method): 37 def if_db_mgt_enabled(method, self, *args, **kwargs): 38 if not odoo.tools.config['list_db']: 39 _logger.error('Database management functions blocked, admin disabled database listing') 40 raise AccessDenied() 41 return method(self, *args, **kwargs) 42 return decorator(if_db_mgt_enabled, method) 43 44#---------------------------------------------------------- 45# Master password required 46#---------------------------------------------------------- 47 48def check_super(passwd): 49 if passwd and odoo.tools.config.verify_admin_password(passwd): 50 return True 51 raise odoo.exceptions.AccessDenied() 52 53# This should be moved to odoo.modules.db, along side initialize(). 54def _initialize_db(id, db_name, demo, lang, user_password, login='admin', country_code=None, phone=None): 55 try: 56 db = odoo.sql_db.db_connect(db_name) 57 with closing(db.cursor()) as cr: 58 # TODO this should be removed as it is done by Registry.new(). 59 odoo.modules.db.initialize(cr) 60 odoo.tools.config['load_language'] = lang 61 cr.commit() 62 63 registry = odoo.modules.registry.Registry.new(db_name, demo, None, update_module=True) 64 65 with closing(db.cursor()) as cr: 66 env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 67 68 if lang: 69 modules = env['ir.module.module'].search([('state', '=', 'installed')]) 70 modules._update_translations(lang) 71 72 if country_code: 73 country = env['res.country'].search([('code', 'ilike', country_code)])[0] 74 env['res.company'].browse(1).write({'country_id': country_code and country.id, 'currency_id': country_code and country.currency_id.id}) 75 if len(country_timezones.get(country_code, [])) == 1: 76 users = env['res.users'].search([]) 77 users.write({'tz': country_timezones[country_code][0]}) 78 if phone: 79 env['res.company'].browse(1).write({'phone': phone}) 80 if '@' in login: 81 env['res.company'].browse(1).write({'email': login}) 82 83 # update admin's password and lang and login 84 values = {'password': user_password, 'lang': lang} 85 if login: 86 values['login'] = login 87 emails = odoo.tools.email_split(login) 88 if emails: 89 values['email'] = emails[0] 90 env.ref('base.user_admin').write(values) 91 92 cr.execute('SELECT login, password FROM res_users ORDER BY login') 93 cr.commit() 94 except Exception as e: 95 _logger.exception('CREATE DATABASE failed:') 96 97def _create_empty_database(name): 98 db = odoo.sql_db.db_connect('postgres') 99 with closing(db.cursor()) as cr: 100 chosen_template = odoo.tools.config['db_template'] 101 cr.execute("SELECT datname FROM pg_database WHERE datname = %s", 102 (name,), log_exceptions=False) 103 if cr.fetchall(): 104 raise DatabaseExists("database %r already exists!" % (name,)) 105 else: 106 cr.autocommit(True) # avoid transaction block 107 108 # 'C' collate is only safe with template0, but provides more useful indexes 109 collate = sql.SQL("LC_COLLATE 'C'" if chosen_template == 'template0' else "") 110 cr.execute( 111 sql.SQL("CREATE DATABASE {} ENCODING 'unicode' {} TEMPLATE {}").format( 112 sql.Identifier(name), collate, sql.Identifier(chosen_template) 113 )) 114 115 if odoo.tools.config['unaccent']: 116 try: 117 db = odoo.sql_db.db_connect(name) 118 with closing(db.cursor()) as cr: 119 cr.execute("CREATE EXTENSION IF NOT EXISTS unaccent") 120 cr.commit() 121 except psycopg2.Error: 122 pass 123 124@check_db_management_enabled 125def exp_create_database(db_name, demo, lang, user_password='admin', login='admin', country_code=None, phone=None): 126 """ Similar to exp_create but blocking.""" 127 _logger.info('Create database `%s`.', db_name) 128 _create_empty_database(db_name) 129 _initialize_db(id, db_name, demo, lang, user_password, login, country_code, phone) 130 return True 131 132@check_db_management_enabled 133def exp_duplicate_database(db_original_name, db_name): 134 _logger.info('Duplicate database `%s` to `%s`.', db_original_name, db_name) 135 odoo.sql_db.close_db(db_original_name) 136 db = odoo.sql_db.db_connect('postgres') 137 with closing(db.cursor()) as cr: 138 cr.autocommit(True) # avoid transaction block 139 _drop_conn(cr, db_original_name) 140 cr.execute(sql.SQL("CREATE DATABASE {} ENCODING 'unicode' TEMPLATE {}").format( 141 sql.Identifier(db_name), 142 sql.Identifier(db_original_name) 143 )) 144 145 registry = odoo.modules.registry.Registry.new(db_name) 146 with registry.cursor() as cr: 147 # if it's a copy of a database, force generation of a new dbuuid 148 env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 149 env['ir.config_parameter'].init(force=True) 150 151 from_fs = odoo.tools.config.filestore(db_original_name) 152 to_fs = odoo.tools.config.filestore(db_name) 153 if os.path.exists(from_fs) and not os.path.exists(to_fs): 154 shutil.copytree(from_fs, to_fs) 155 return True 156 157def _drop_conn(cr, db_name): 158 # Try to terminate all other connections that might prevent 159 # dropping the database 160 try: 161 # PostgreSQL 9.2 renamed pg_stat_activity.procpid to pid: 162 # http://www.postgresql.org/docs/9.2/static/release-9-2.html#AEN110389 163 pid_col = 'pid' if cr._cnx.server_version >= 90200 else 'procpid' 164 165 cr.execute("""SELECT pg_terminate_backend(%(pid_col)s) 166 FROM pg_stat_activity 167 WHERE datname = %%s AND 168 %(pid_col)s != pg_backend_pid()""" % {'pid_col': pid_col}, 169 (db_name,)) 170 except Exception: 171 pass 172 173@check_db_management_enabled 174def exp_drop(db_name): 175 if db_name not in list_dbs(True): 176 return False 177 odoo.modules.registry.Registry.delete(db_name) 178 odoo.sql_db.close_db(db_name) 179 180 db = odoo.sql_db.db_connect('postgres') 181 with closing(db.cursor()) as cr: 182 cr.autocommit(True) # avoid transaction block 183 _drop_conn(cr, db_name) 184 185 try: 186 cr.execute(sql.SQL('DROP DATABASE {}').format(sql.Identifier(db_name))) 187 except Exception as e: 188 _logger.info('DROP DB: %s failed:\n%s', db_name, e) 189 raise Exception("Couldn't drop database %s: %s" % (db_name, e)) 190 else: 191 _logger.info('DROP DB: %s', db_name) 192 193 fs = odoo.tools.config.filestore(db_name) 194 if os.path.exists(fs): 195 shutil.rmtree(fs) 196 return True 197 198@check_db_management_enabled 199def exp_dump(db_name, format): 200 with tempfile.TemporaryFile(mode='w+b') as t: 201 dump_db(db_name, t, format) 202 t.seek(0) 203 return base64.b64encode(t.read()).decode() 204 205@check_db_management_enabled 206def dump_db_manifest(cr): 207 pg_version = "%d.%d" % divmod(cr._obj.connection.server_version / 100, 100) 208 cr.execute("SELECT name, latest_version FROM ir_module_module WHERE state = 'installed'") 209 modules = dict(cr.fetchall()) 210 manifest = { 211 'odoo_dump': '1', 212 'db_name': cr.dbname, 213 'version': odoo.release.version, 214 'version_info': odoo.release.version_info, 215 'major_version': odoo.release.major_version, 216 'pg_version': pg_version, 217 'modules': modules, 218 } 219 return manifest 220 221@check_db_management_enabled 222def dump_db(db_name, stream, backup_format='zip'): 223 """Dump database `db` into file-like object `stream` if stream is None 224 return a file object with the dump """ 225 226 _logger.info('DUMP DB: %s format %s', db_name, backup_format) 227 228 cmd = ['pg_dump', '--no-owner'] 229 cmd.append(db_name) 230 231 if backup_format == 'zip': 232 with tempfile.TemporaryDirectory() as dump_dir: 233 filestore = odoo.tools.config.filestore(db_name) 234 if os.path.exists(filestore): 235 shutil.copytree(filestore, os.path.join(dump_dir, 'filestore')) 236 with open(os.path.join(dump_dir, 'manifest.json'), 'w') as fh: 237 db = odoo.sql_db.db_connect(db_name) 238 with db.cursor() as cr: 239 json.dump(dump_db_manifest(cr), fh, indent=4) 240 cmd.insert(-1, '--file=' + os.path.join(dump_dir, 'dump.sql')) 241 odoo.tools.exec_pg_command(*cmd) 242 if stream: 243 odoo.tools.osutil.zip_dir(dump_dir, stream, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql') 244 else: 245 t=tempfile.TemporaryFile() 246 odoo.tools.osutil.zip_dir(dump_dir, t, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql') 247 t.seek(0) 248 return t 249 else: 250 cmd.insert(-1, '--format=c') 251 stdin, stdout = odoo.tools.exec_pg_command_pipe(*cmd) 252 if stream: 253 shutil.copyfileobj(stdout, stream) 254 else: 255 return stdout 256 257@check_db_management_enabled 258def exp_restore(db_name, data, copy=False): 259 def chunks(d, n=8192): 260 for i in range(0, len(d), n): 261 yield d[i:i+n] 262 data_file = tempfile.NamedTemporaryFile(delete=False) 263 try: 264 for chunk in chunks(data): 265 data_file.write(base64.b64decode(chunk)) 266 data_file.close() 267 restore_db(db_name, data_file.name, copy=copy) 268 finally: 269 os.unlink(data_file.name) 270 return True 271 272@check_db_management_enabled 273def restore_db(db, dump_file, copy=False): 274 assert isinstance(db, str) 275 if exp_db_exist(db): 276 _logger.info('RESTORE DB: %s already exists', db) 277 raise Exception("Database already exists") 278 279 _create_empty_database(db) 280 281 filestore_path = None 282 with tempfile.TemporaryDirectory() as dump_dir: 283 if zipfile.is_zipfile(dump_file): 284 # v8 format 285 with zipfile.ZipFile(dump_file, 'r') as z: 286 # only extract known members! 287 filestore = [m for m in z.namelist() if m.startswith('filestore/')] 288 z.extractall(dump_dir, ['dump.sql'] + filestore) 289 290 if filestore: 291 filestore_path = os.path.join(dump_dir, 'filestore') 292 293 pg_cmd = 'psql' 294 pg_args = ['-q', '-f', os.path.join(dump_dir, 'dump.sql')] 295 296 else: 297 # <= 7.0 format (raw pg_dump output) 298 pg_cmd = 'pg_restore' 299 pg_args = ['--no-owner', dump_file] 300 301 args = [] 302 args.append('--dbname=' + db) 303 pg_args = args + pg_args 304 305 if odoo.tools.exec_pg_command(pg_cmd, *pg_args): 306 raise Exception("Couldn't restore database") 307 308 registry = odoo.modules.registry.Registry.new(db) 309 with registry.cursor() as cr: 310 env = odoo.api.Environment(cr, SUPERUSER_ID, {}) 311 if copy: 312 # if it's a copy of a database, force generation of a new dbuuid 313 env['ir.config_parameter'].init(force=True) 314 if filestore_path: 315 filestore_dest = env['ir.attachment']._filestore() 316 shutil.move(filestore_path, filestore_dest) 317 318 _logger.info('RESTORE DB: %s', db) 319 320@check_db_management_enabled 321def exp_rename(old_name, new_name): 322 odoo.modules.registry.Registry.delete(old_name) 323 odoo.sql_db.close_db(old_name) 324 325 db = odoo.sql_db.db_connect('postgres') 326 with closing(db.cursor()) as cr: 327 cr.autocommit(True) # avoid transaction block 328 _drop_conn(cr, old_name) 329 try: 330 cr.execute(sql.SQL('ALTER DATABASE {} RENAME TO {}').format(sql.Identifier(old_name), sql.Identifier(new_name))) 331 _logger.info('RENAME DB: %s -> %s', old_name, new_name) 332 except Exception as e: 333 _logger.info('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e) 334 raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e)) 335 336 old_fs = odoo.tools.config.filestore(old_name) 337 new_fs = odoo.tools.config.filestore(new_name) 338 if os.path.exists(old_fs) and not os.path.exists(new_fs): 339 shutil.move(old_fs, new_fs) 340 return True 341 342@check_db_management_enabled 343def exp_change_admin_password(new_password): 344 odoo.tools.config.set_admin_password(new_password) 345 odoo.tools.config.save() 346 return True 347 348@check_db_management_enabled 349def exp_migrate_databases(databases): 350 for db in databases: 351 _logger.info('migrate database %s', db) 352 odoo.tools.config['update']['base'] = True 353 odoo.modules.registry.Registry.new(db, force_demo=False, update_module=True) 354 return True 355 356#---------------------------------------------------------- 357# No master password required 358#---------------------------------------------------------- 359 360@odoo.tools.mute_logger('odoo.sql_db') 361def exp_db_exist(db_name): 362 ## Not True: in fact, check if connection to database is possible. The database may exists 363 try: 364 db = odoo.sql_db.db_connect(db_name) 365 with db.cursor(): 366 return True 367 except Exception: 368 return False 369 370def list_dbs(force=False): 371 if not odoo.tools.config['list_db'] and not force: 372 raise odoo.exceptions.AccessDenied() 373 374 if not odoo.tools.config['dbfilter'] and odoo.tools.config['db_name']: 375 # In case --db-filter is not provided and --database is passed, Odoo will not 376 # fetch the list of databases available on the postgres server and instead will 377 # use the value of --database as comma seperated list of exposed databases. 378 res = sorted(db.strip() for db in odoo.tools.config['db_name'].split(',')) 379 return res 380 381 chosen_template = odoo.tools.config['db_template'] 382 templates_list = tuple(set(['postgres', chosen_template])) 383 db = odoo.sql_db.db_connect('postgres') 384 with closing(db.cursor()) as cr: 385 try: 386 cr.execute("select datname from pg_database where datdba=(select usesysid from pg_user where usename=current_user) and not datistemplate and datallowconn and datname not in %s order by datname", (templates_list,)) 387 res = [odoo.tools.ustr(name) for (name,) in cr.fetchall()] 388 except Exception: 389 _logger.exception('Listing databases failed:') 390 res = [] 391 return res 392 393def list_db_incompatible(databases): 394 """"Check a list of databases if they are compatible with this version of Odoo 395 396 :param databases: A list of existing Postgresql databases 397 :return: A list of databases that are incompatible 398 """ 399 incompatible_databases = [] 400 server_version = '.'.join(str(v) for v in version_info[:2]) 401 for database_name in databases: 402 with closing(db_connect(database_name).cursor()) as cr: 403 if odoo.tools.table_exists(cr, 'ir_module_module'): 404 cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ('base',)) 405 base_version = cr.fetchone() 406 if not base_version or not base_version[0]: 407 incompatible_databases.append(database_name) 408 else: 409 # e.g. 10.saas~15 410 local_version = '.'.join(base_version[0].split('.')[:2]) 411 if local_version != server_version: 412 incompatible_databases.append(database_name) 413 else: 414 incompatible_databases.append(database_name) 415 for database_name in incompatible_databases: 416 # release connection 417 odoo.sql_db.close_db(database_name) 418 return incompatible_databases 419 420 421def exp_list(document=False): 422 if not odoo.tools.config['list_db']: 423 raise odoo.exceptions.AccessDenied() 424 return list_dbs() 425 426def exp_list_lang(): 427 return odoo.tools.scan_languages() 428 429def exp_list_countries(): 430 list_countries = [] 431 root = ET.parse(os.path.join(odoo.tools.config['root_path'], 'addons/base/data/res_country_data.xml')).getroot() 432 for country in root.find('data').findall('record[@model="res.country"]'): 433 name = country.find('field[@name="name"]').text 434 code = country.find('field[@name="code"]').text 435 list_countries.append([code, name]) 436 return sorted(list_countries, key=lambda c: c[1]) 437 438def exp_server_version(): 439 """ Return the version of the server 440 Used by the client to verify the compatibility with its own version 441 """ 442 return odoo.release.version 443 444#---------------------------------------------------------- 445# db service dispatch 446#---------------------------------------------------------- 447 448def dispatch(method, params): 449 g = globals() 450 exp_method_name = 'exp_' + method 451 if method in ['db_exist', 'list', 'list_lang', 'server_version']: 452 return g[exp_method_name](*params) 453 elif exp_method_name in g: 454 passwd = params[0] 455 params = params[1:] 456 check_super(passwd) 457 return g[exp_method_name](*params) 458 else: 459 raise KeyError("Method not found: %s" % method) 460