# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. """ Modules migration handling. """ import imp import logging import os from os.path import join as opj import odoo import odoo.release as release import odoo.tools as tools from odoo.tools.parse_version import parse_version _logger = logging.getLogger(__name__) class MigrationManager(object): """ This class manage the migration of modules Migrations files must be python files containing a "migrate(cr, installed_version)" function. Theses files must respect a directory tree structure: A 'migrations' folder which containt a folder by version. Version can be 'module' version or 'server.module' version (in this case, the files will only be processed by this version of the server). Python file names must start by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation Example: `-- migrations |-- 1.0 | |-- pre-update_table_x.py | |-- pre-update_table_y.py | |-- post-clean-data.py | `-- README.txt # not processed |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server | |-- pre-delete_table_z.py | `-- post-clean-data.py `-- foo.py # not processed This similar structure is generated by the maintenance module with the migrations files get by the maintenance contract """ def __init__(self, cr, graph): self.cr = cr self.graph = graph self.migrations = {} self._get_files() def _get_files(self): """ import addons.base.maintenance.utils as maintenance_utils maintenance_utils.update_migrations_files(self.cr) #""" for pkg in self.graph: self.migrations[pkg.name] = {} if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'): continue get_module_filetree = odoo.modules.module.get_module_filetree self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {} self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {} def migrate_module(self, pkg, stage): assert stage in ('pre', 'post') stageformat = { 'pre': '[>%s]', 'post': '[%s>]', } if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade') or pkg.state == 'to install': return def convert_version(version): if version.count('.') >= 2: return version # the version number already containt the server version return "%s.%s" % (release.major_version, version) def _get_migration_versions(pkg): def __get_dir(tree): return [d for d in tree if tree[d] is not None] versions = list(set( __get_dir(self.migrations[pkg.name]['module']) + __get_dir(self.migrations[pkg.name]['maintenance']) )) versions.sort(key=lambda k: parse_version(convert_version(k))) return versions def _get_migration_files(pkg, version, stage): """ return a list of tuple (module, file) """ m = self.migrations[pkg.name] lst = [] mapping = { 'module': opj(pkg.name, 'migrations'), 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name), } for x in mapping.keys(): if version in m[x]: for f in m[x][version]: if m[x][version][f] is not None: continue if not f.startswith(stage + '-'): continue lst.append(opj(mapping[x], version, f)) lst.sort() return lst parsed_installed_version = parse_version(pkg.installed_version or '') current_version = parse_version(convert_version(pkg.data['version'])) versions = _get_migration_versions(pkg) for version in versions: if parsed_installed_version < parse_version(convert_version(version)) <= current_version: strfmt = {'addon': pkg.name, 'stage': stage, 'version': stageformat[stage] % version, } for pyfile in _get_migration_files(pkg, version, stage): name, ext = os.path.splitext(os.path.basename(pyfile)) if ext.lower() != '.py': continue mod = fp = fp2 = None try: fp, fname = tools.file_open(pyfile, pathinfo=True) if not isinstance(fp, file): # imp.load_source need a real file object, so we create # one from the file-like object we get from file_open fp2 = os.tmpfile() fp2.write(fp.read()) fp2.seek(0) try: mod = imp.load_source(name, fname, fp2 or fp) _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % dict(strfmt, name=mod.__name__)) migrate = mod.migrate except ImportError: _logger.exception('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % dict(strfmt, file=pyfile)) raise except AttributeError: _logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt) else: migrate(self.cr, pkg.installed_version) finally: if fp: fp.close() if fp2: fp2.close() if mod: del mod