gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
9.8KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. """ Modules migration handling. """
  4. import glob
  5. import importlib.util
  6. import inspect
  7. import itertools
  8. import logging
  9. import os
  10. import re
  11. from collections import defaultdict
  12. from os.path import join as opj
  13. import odoo.release as release
  14. import odoo.upgrade
  15. from odoo.tools.parse_version import parse_version
  16. from odoo.tools.misc import file_path
  17. _logger = logging.getLogger(__name__)
  18. VERSION_RE = re.compile(
  19. r"""^
  20. # Optional prefix with Odoo version
  21. ((
  22. 6\.1|
  23. # "x.0" version, with x >= 6.
  24. [6-9]\.0|
  25. # multi digits "x.0" versions
  26. [1-9]\d+\.0|
  27. # x.saas~y, where x >= 7 and x <= 10
  28. (7|8|9|10)\.saas~[1-9]\d*|
  29. # saas~x.y, where x >= 11 and y between 1 and 9
  30. # FIXME handle version >= saas~100 (expected in year 2106)
  31. saas~(1[1-9]|[2-9]\d+)\.[1-9]
  32. )\.)?
  33. # After Odoo version we allow precisely 2 or 3 parts
  34. # note this will also allow 0.0.0 which has a special meaning
  35. \d+\.\d+(\.\d+)?
  36. $""",
  37. re.VERBOSE | re.ASCII,
  38. )
  39. def load_script(path, module_name):
  40. full_path = file_path(path) if not os.path.isabs(path) else path
  41. spec = importlib.util.spec_from_file_location(module_name, full_path)
  42. module = importlib.util.module_from_spec(spec)
  43. spec.loader.exec_module(module)
  44. return module
  45. class MigrationManager(object):
  46. """ Manages the migration of modules.
  47. Migrations files must be python files containing a ``migrate(cr, installed_version)``
  48. function. These files must respect a directory tree structure: A 'migrations' folder
  49. which contains a folder by version. Version can be 'module' version or 'server.module'
  50. version (in this case, the files will only be processed by this version of the server).
  51. Python file names must start by ``pre-`` or ``post-`` and will be executed, respectively,
  52. before and after the module initialisation. ``end-`` scripts are run after all modules have
  53. been updated.
  54. A special folder named ``0.0.0`` can contain scripts that will be run on any version change.
  55. In `pre` stage, ``0.0.0`` scripts are run first, while in ``post`` and ``end``, they are run last.
  56. Example::
  57. <moduledir>
  58. `-- migrations
  59. |-- 1.0
  60. | |-- pre-update_table_x.py
  61. | |-- pre-update_table_y.py
  62. | |-- post-create_plop_records.py
  63. | |-- end-cleanup.py
  64. | `-- README.txt # not processed
  65. |-- 9.0.1.1 # processed only on a 9.0 server
  66. | |-- pre-delete_table_z.py
  67. | `-- post-clean-data.py
  68. |-- 0.0.0
  69. | `-- end-invariants.py # processed on all version update
  70. `-- foo.py # not processed
  71. """
  72. def __init__(self, cr, graph):
  73. self.cr = cr
  74. self.graph = graph
  75. self.migrations = defaultdict(dict)
  76. self._get_files()
  77. def _get_files(self):
  78. def _get_upgrade_path(pkg):
  79. for path in odoo.upgrade.__path__:
  80. upgrade_path = opj(path, pkg)
  81. if os.path.exists(upgrade_path):
  82. yield upgrade_path
  83. def _verify_upgrade_version(path, version):
  84. full_path = opj(path, version)
  85. if not os.path.isdir(full_path):
  86. return False
  87. if version == "tests":
  88. return False
  89. if not VERSION_RE.match(version):
  90. _logger.warning("Invalid version for upgrade script %r", full_path)
  91. return False
  92. return True
  93. def get_scripts(path):
  94. if not path:
  95. return {}
  96. return {
  97. version: glob.glob(opj(path, version, '*.py'))
  98. for version in os.listdir(path)
  99. if _verify_upgrade_version(path, version)
  100. }
  101. def check_path(path):
  102. try:
  103. return file_path(path)
  104. except FileNotFoundError:
  105. return False
  106. for pkg in self.graph:
  107. if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade' or
  108. getattr(pkg, 'load_state', None) == 'to upgrade'):
  109. continue
  110. self.migrations[pkg.name] = {
  111. 'module': get_scripts(check_path(pkg.name + '/migrations')),
  112. 'module_upgrades': get_scripts(check_path(pkg.name + '/upgrades')),
  113. }
  114. scripts = defaultdict(list)
  115. for p in _get_upgrade_path(pkg.name):
  116. for v, s in get_scripts(p).items():
  117. scripts[v].extend(s)
  118. self.migrations[pkg.name]["upgrade"] = scripts
  119. def migrate_module(self, pkg, stage):
  120. assert stage in ('pre', 'post', 'end')
  121. stageformat = {
  122. 'pre': '[>%s]',
  123. 'post': '[%s>]',
  124. 'end': '[$%s]',
  125. }
  126. state = pkg.state if stage in ('pre', 'post') else getattr(pkg, 'load_state', None)
  127. if not (hasattr(pkg, 'update') or state == 'to upgrade') or state == 'to install':
  128. return
  129. def convert_version(version):
  130. if version == "0.0.0":
  131. return version
  132. if version.count(".") > 2:
  133. return version # the version number already contains the server version, see VERSION_RE for details
  134. return "%s.%s" % (release.major_version, version)
  135. def _get_migration_versions(pkg, stage):
  136. versions = sorted({
  137. ver
  138. for lv in self.migrations[pkg.name].values()
  139. for ver, lf in lv.items()
  140. if lf
  141. }, key=lambda k: parse_version(convert_version(k)))
  142. if "0.0.0" in versions:
  143. # reorder versions
  144. versions.remove("0.0.0")
  145. if stage == "pre":
  146. versions.insert(0, "0.0.0")
  147. else:
  148. versions.append("0.0.0")
  149. return versions
  150. def _get_migration_files(pkg, version, stage):
  151. """ return a list of migration script files
  152. """
  153. m = self.migrations[pkg.name]
  154. return sorted(
  155. (
  156. f
  157. for k in m
  158. for f in m[k].get(version, [])
  159. if os.path.basename(f).startswith(f"{stage}-")
  160. ),
  161. key=os.path.basename,
  162. )
  163. installed_version = getattr(pkg, 'load_version', pkg.installed_version) or ''
  164. parsed_installed_version = parse_version(installed_version)
  165. current_version = parse_version(convert_version(pkg.data['version']))
  166. def compare(version):
  167. if version == "0.0.0" and parsed_installed_version < current_version:
  168. return True
  169. full_version = convert_version(version)
  170. majorless_version = (version != full_version)
  171. if majorless_version:
  172. # We should not re-execute major-less scripts when upgrading to new Odoo version
  173. # a module in `9.0.2.0` should not re-execute a `2.0` script when upgrading to `10.0.2.0`.
  174. # In which case we must compare just the module version
  175. return parsed_installed_version[2:] < parse_version(full_version)[2:] <= current_version[2:]
  176. return parsed_installed_version < parse_version(full_version) <= current_version
  177. versions = _get_migration_versions(pkg, stage)
  178. for version in versions:
  179. if compare(version):
  180. strfmt = {'addon': pkg.name,
  181. 'stage': stage,
  182. 'version': stageformat[stage] % version,
  183. }
  184. for pyfile in _get_migration_files(pkg, version, stage):
  185. name, ext = os.path.splitext(os.path.basename(pyfile))
  186. if ext.lower() != '.py':
  187. continue
  188. try:
  189. mod = load_script(pyfile, name)
  190. except ImportError as e:
  191. raise ImportError('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % dict(strfmt, file=pyfile)) from e
  192. if not hasattr(mod, 'migrate'):
  193. raise AttributeError(
  194. 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function, not found in %(file)s' % dict(
  195. strfmt,
  196. file=pyfile,
  197. ))
  198. try:
  199. sig = inspect.signature(mod.migrate)
  200. except TypeError as e:
  201. raise TypeError("module %(addon)s: `migrate` needs to be a function, got %(migrate)r" % dict(strfmt, migrate=mod.migrate)) from e
  202. if not (
  203. tuple(sig.parameters.keys()) in VALID_MIGRATE_PARAMS
  204. and all(p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) for p in sig.parameters.values())
  205. ):
  206. raise TypeError("module %(addon)s: `migrate`'s signature should be `(cr, version)`, %(func)s is %(sig)s" % dict(strfmt, func=mod.migrate, sig=sig))
  207. _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % dict(strfmt, name=mod.__name__)) # noqa: G002
  208. mod.migrate(self.cr, installed_version)
  209. VALID_MIGRATE_PARAMS = list(itertools.product(
  210. ['cr', '_cr'],
  211. ['version', '_version'],
  212. ))
上海开阖软件有限公司 沪ICP备12045867号-1