mangono-addon-auto_migration 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mangono_addon_auto_migration-2.0.0.dist-info/METADATA +13 -0
- mangono_addon_auto_migration-2.0.0.dist-info/RECORD +12 -0
- mangono_addon_auto_migration-2.0.0.dist-info/WHEEL +5 -0
- mangono_addon_auto_migration-2.0.0.dist-info/entry_points.txt +3 -0
- mangono_addon_auto_migration-2.0.0.dist-info/top_level.txt +1 -0
- odoo/addons/auto_migration/__init__.py +3 -0
- odoo/addons/auto_migration/__manifest__.py +16 -0
- odoo/addons/auto_migration/auto_load.py +4 -0
- odoo/addons/auto_migration/documentation.adoc +159 -0
- odoo/addons/auto_migration/hooks.py +7 -0
- odoo/addons/auto_migration/i18n/fr.po +16 -0
- odoo/addons/auto_migration/mangono_migration.py +415 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mangono-addon-auto_migration
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Project-URL: Homepage, https://mangono.fr
|
|
5
|
+
Author-Email: mangono <opensource+odoo@mangono.fr>
|
|
6
|
+
License-Expression: AGPL-3.0
|
|
7
|
+
Classifier: Framework :: Odoo
|
|
8
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Dist: environ-odoo-config<2,>=0.6.5
|
|
11
|
+
Description-Content-Type: text/plain
|
|
12
|
+
Description: This server wide module provides support to run pre- and post-installation scripts
|
|
13
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
odoo/addons/auto_migration/__init__.py,sha256=XnrwL7vmyhRTYi9YvudtdpYr16kG23j-k59TRAWIQfU,123
|
|
2
|
+
odoo/addons/auto_migration/__manifest__.py,sha256=xLi6VJzYkiJyDSj9yLEnTLEAmWLQqbzXBn_2JOiWe8M,489
|
|
3
|
+
odoo/addons/auto_migration/auto_load.py,sha256=m70OuVD-Is7JA91Tu9wypgghr0ljAt1u3Wx7j-JY1A4,237
|
|
4
|
+
odoo/addons/auto_migration/documentation.adoc,sha256=Hsp3wC5Y4LJTuw01efzzIZKlYrZhL75IQFBd0XFeaoE,6645
|
|
5
|
+
odoo/addons/auto_migration/hooks.py,sha256=i9xOAVzoBtRVyDhs9f6b-wJAboXqwEuT16LIS9345xY,637
|
|
6
|
+
odoo/addons/auto_migration/mangono_migration.py,sha256=BVjtDYW0ohkeVehEDnuHmlAfH-2BNXGFzg4CNJVvnuc,16123
|
|
7
|
+
odoo/addons/auto_migration/i18n/fr.po,sha256=UqeDlp8ksOuoMFCBU9M3xJvqE09s3kKoKpCT2vK9uTA,432
|
|
8
|
+
mangono_addon_auto_migration-2.0.0.dist-info/METADATA,sha256=m7iQ5kAo1-_kb5yhHQ4tb4kwqbVnxSbGZV7a3KEZVr4,522
|
|
9
|
+
mangono_addon_auto_migration-2.0.0.dist-info/WHEEL,sha256=oMFM00-3w15fuhxrOoJBKP3EfQFiV1TFGhhmJn2iH-o,100
|
|
10
|
+
mangono_addon_auto_migration-2.0.0.dist-info/entry_points.txt,sha256=oHx9JvLBZVSkJLv8uLpO0a895NFrUwRB-QzzmvEYSQM,126
|
|
11
|
+
mangono_addon_auto_migration-2.0.0.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
|
|
12
|
+
mangono_addon_auto_migration-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
odoo
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auto_migration",
|
|
3
|
+
"description": "This server wide module provides support to run pre- and post-installation scripts",
|
|
4
|
+
"category": "Extra Tools",
|
|
5
|
+
"depends": ["base"],
|
|
6
|
+
"installable": False,
|
|
7
|
+
"auto_install": False,
|
|
8
|
+
"version": "2.0.0",
|
|
9
|
+
"author": "mangono",
|
|
10
|
+
"license": "AGPL-3",
|
|
11
|
+
"maintainer": "Mangono",
|
|
12
|
+
"website": "https://mangono.fr",
|
|
13
|
+
"data": [],
|
|
14
|
+
"pre_init_hook": "create_migration_channel",
|
|
15
|
+
"post_load": "patch_migration",
|
|
16
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
= AUTO MIGRATION
|
|
2
|
+
|
|
3
|
+
== Purpose
|
|
4
|
+
|
|
5
|
+
Provide service for pre-installation and post-installation scripts (migration scripts)
|
|
6
|
+
|
|
7
|
+
scripts are run upon install or update (-i ou -u)
|
|
8
|
+
Goal is to avoid having to play manually SQL or python script
|
|
9
|
+
|
|
10
|
+
We do not use odoo migration script system because it requires to manage module's version, which is difficult for project with simultaneous dev
|
|
11
|
+
|
|
12
|
+
== Usage
|
|
13
|
+
=== settings
|
|
14
|
+
this module must be declared as server wide module
|
|
15
|
+
|
|
16
|
+
- docker : `--load web,kanban,auto_migration`
|
|
17
|
+
- nestor : using `nestor.cfg` file at the project's root, for example :
|
|
18
|
+
----
|
|
19
|
+
default:
|
|
20
|
+
version: "13.0"
|
|
21
|
+
workers: 3
|
|
22
|
+
options:
|
|
23
|
+
load: auto_migration
|
|
24
|
+
----
|
|
25
|
+
- clever cloud : using `.clever/envs/update.env` file
|
|
26
|
+
----
|
|
27
|
+
LOAD_AUTO_MIGRATION="True"
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
- odoo-cloud
|
|
31
|
+
|
|
32
|
+
This module is auto-loadable and will be loaded if the library is present in your `requirements.txt`
|
|
33
|
+
|
|
34
|
+
You may need to add an explicit load if you want to use it in the install step of the runbot
|
|
35
|
+
----
|
|
36
|
+
variables:
|
|
37
|
+
# ODOO_ENV aare propagated to the odoo service inside the runbot
|
|
38
|
+
LOAD_AUTO_MIGRATION: auto_migration
|
|
39
|
+
----
|
|
40
|
+
|
|
41
|
+
- mise runbot
|
|
42
|
+
|
|
43
|
+
If you need the module loaded in the install step of mise runbot, you need to set `ODOO_ENV_CONFIG_EXCLUDE_EXT: "auto_load_widemodules"` in addition to `LOAD_AUTO_MIGRATION`.
|
|
44
|
+
|
|
45
|
+
`*IMPORTANT*` : *do not install the auto_migration module!* +
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
`auto_migrations` directory must be created at the root of your module's directory (__`mangono_migration` directory is kept for backward compatibility, will be deprecated in future release__)
|
|
49
|
+
|
|
50
|
+
A `auto_migration` table will be created in your database to record the status of the script running and record logs. *There is no graphical interface, and there is no odoo models*.
|
|
51
|
+
You can use SQL request to check script execution, and use usual `_logger.info()` to log script activity.
|
|
52
|
+
|
|
53
|
+
_auto_migration library is available from V12 to V19, with python >= 3.8.19
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
=== pre-installation
|
|
57
|
+
pre-installation script are run before update of the module, and allow to fix data before module update or create column to avoid compute.
|
|
58
|
+
|
|
59
|
+
each pre_installation script for a given ticket must be in a python file called `pre_<ticket_number>.py`, located in a 'migrations/auto' directory in your module. +
|
|
60
|
+
Scripts are played in alphabetical order of the file's names. Inside a file, method are played in declaration order, unless priority parameter is set in the decorator.
|
|
61
|
+
Lowest number is highest priority (1 more priority than 25). +
|
|
62
|
+
|
|
63
|
+
you must import :
|
|
64
|
+
`from odoo.addons.auto_migration.mangono_migration import auto_migrate`
|
|
65
|
+
|
|
66
|
+
the functions must be decorated by `@auto_migrate` and have this signature :
|
|
67
|
+
`def xxxxx(self)` or for backward compatibility `def xxxxx(self, has_run):`
|
|
68
|
+
|
|
69
|
+
self is a factice object (not usual odoo self), but self.env is a real odoo env, `self.env['model']` is able to access odoo models, `self.env.cr` or `self.cr` can be used to perform request +
|
|
70
|
+
self.has_run is a boolean true if the script has already been run (same for has_run parameter if used)
|
|
71
|
+
|
|
72
|
+
EXAMPLE:
|
|
73
|
+
[source,python]
|
|
74
|
+
----
|
|
75
|
+
@migrate_mangono(run_always=False, priority=10, allowed_to_fail=True)
|
|
76
|
+
def pre_t1245(self):
|
|
77
|
+
# the function will always get self.has_run = False,
|
|
78
|
+
# because it will be called only if it has not run yet
|
|
79
|
+
self.env['my.model'].search(my_domain).unlink # with allowed_to_fail, if this fails, the script
|
|
80
|
+
# action will be rollbacked but update goes on
|
|
81
|
+
----
|
|
82
|
+
|
|
83
|
+
SIMPLE EXAMPLE:
|
|
84
|
+
[source,python]
|
|
85
|
+
----
|
|
86
|
+
@migrate_mangono
|
|
87
|
+
def pre_t5432(self):
|
|
88
|
+
self.cr.execute("UPDATE xxxx")
|
|
89
|
+
----
|
|
90
|
+
|
|
91
|
+
if the optional decorator parameter `run_always` is set to True
|
|
92
|
+
the method must use the has_run bool to check if the script has already been run on this base or check if
|
|
93
|
+
there is something to do before doing it. (_default value of run_always is False_)
|
|
94
|
+
|
|
95
|
+
in case of exception the script is rollbacked, it is not written as run in the `auto_migration` table and installation will stop, other scripts will not be executed
|
|
96
|
+
|
|
97
|
+
if the optional decorator parameter `allowed_to_fail` is set to `True`, the script failure will not interrupt installation or update process. The DB is rollbacked to the state at the beginning of the script execution. Failure logs will be reported in table logs and a warning level log is issued in odoo logs. (_default is False_)
|
|
98
|
+
|
|
99
|
+
The optional decorator parameter `priority` can be set to a priority from 1 to 9999 (_default_) to decide the order in which script from the same python file will be played (priority 1 is played first).
|
|
100
|
+
|
|
101
|
+
NOTE: old decorator @migrate_mangono is kept for backaward compatibility
|
|
102
|
+
|
|
103
|
+
=== post-installation
|
|
104
|
+
|
|
105
|
+
post-installation script are run after the module is installed/updated, before module translation are loaded, and before `post_init_hook`
|
|
106
|
+
|
|
107
|
+
python file must be called `post_<Ticket_number>`
|
|
108
|
+
|
|
109
|
+
decorator use is the same
|
|
110
|
+
|
|
111
|
+
IMPORTANT: when a post script fails, all post action are rollbacked, but pre action are committed
|
|
112
|
+
|
|
113
|
+
=== end
|
|
114
|
+
|
|
115
|
+
same as post, with end_XXXX +
|
|
116
|
+
Ces scripts sont joués après l'installation/mise à jour de tous les modules.
|
|
117
|
+
|
|
118
|
+
=== afterbase
|
|
119
|
+
Use to play a script just after base is updated, before any other modules are updated.
|
|
120
|
+
|
|
121
|
+
To allow this, your script must reside in any installed modules (usually project_erp) and the file must be named `afterbase_xxxx.py`. You must run `update base`
|
|
122
|
+
|
|
123
|
+
You cannot use models in this script, everything must be done with SQL
|
|
124
|
+
|
|
125
|
+
=== jobification :
|
|
126
|
+
|
|
127
|
+
you can start job from the script by calling `with_delay` as usual. +
|
|
128
|
+
Note that job will not start during update phase. They will only start when the instance is restarted after the update.
|
|
129
|
+
|
|
130
|
+
=== helper function
|
|
131
|
+
`table_exists_in_db(cr, tablename)` can be used to check if a table exists in database
|
|
132
|
+
|
|
133
|
+
`column_exists_in_db(cr, table_name, column_name)` can be used to check if a column exists in the given table.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
== Script example
|
|
137
|
+
|
|
138
|
+
[source,python]
|
|
139
|
+
----
|
|
140
|
+
|
|
141
|
+
import logging
|
|
142
|
+
from odoo.addons.auto_migration.mangono_migration import auto_migrate
|
|
143
|
+
|
|
144
|
+
_logger = logging.getLogger(__name__)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@auto_migrate(run_always=False, allowed_to_fail=False)
|
|
148
|
+
def update_rh_rights(self, has_run):
|
|
149
|
+
# Le script consiste à :
|
|
150
|
+
# - extraire tous les utilisateurs faisant partie du groupe Admin/Droits d'accès
|
|
151
|
+
# - les faire appartenir au groupe RH/Formation.
|
|
152
|
+
group_admin = self.env.ref('base.group_erp_manager')
|
|
153
|
+
group_training = self.env.ref(
|
|
154
|
+
'xxx_donnees_ref.group_xxx_training_manager')
|
|
155
|
+
users = group_admin.users
|
|
156
|
+
for user in users:
|
|
157
|
+
_logger.info(u"user %s added to RRH/formation group", user)
|
|
158
|
+
user.groups_id = [(4, group_training.id)]
|
|
159
|
+
----
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
def create_migration_channel(cr):
|
|
2
|
+
"""create if necessary the root.migration_job channel for execution of job launched by migration"""
|
|
3
|
+
cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname='queue_job_channel'")
|
|
4
|
+
if cr.fetchall(): # queue_job_channel may not exists if module connector is not installed
|
|
5
|
+
cr.execute("""SELECT COUNT(*) FROM queue_job_channel WHERE NAME = 'root.migration_jobs'""")
|
|
6
|
+
if not cr.fetchall()[0][0]: # the channel doesn(t exist yet, let's create it
|
|
7
|
+
cr.execute("""INSERT INTO queue_job_channel (name, parent_id) VALUES ('root.migration_jobs', 1)""")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Translation of Odoo Server.
|
|
2
|
+
# This file contains the translation of the following modules:
|
|
3
|
+
#
|
|
4
|
+
msgid ""
|
|
5
|
+
msgstr ""
|
|
6
|
+
"Project-Id-Version: Odoo Server 8.0\n"
|
|
7
|
+
"Report-Msgid-Bugs-To: \n"
|
|
8
|
+
"POT-Creation-Date: 2019-03-12 11:13+0000\n"
|
|
9
|
+
"PO-Revision-Date: 2019-03-12 11:13+0000\n"
|
|
10
|
+
"Last-Translator: <>\n"
|
|
11
|
+
"Language-Team: \n"
|
|
12
|
+
"MIME-Version: 1.0\n"
|
|
13
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
14
|
+
"Content-Transfer-Encoding: \n"
|
|
15
|
+
"Plural-Forms: \n"
|
|
16
|
+
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from inspect import getmembers, isfunction
|
|
8
|
+
from operator import attrgetter
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from environ_odoo_config.odoo_utils import get_server_wide_modules
|
|
12
|
+
from typing_extensions import Self
|
|
13
|
+
|
|
14
|
+
import odoo
|
|
15
|
+
|
|
16
|
+
Path = str
|
|
17
|
+
MAJOR = odoo.release.version_info[0]
|
|
18
|
+
MAJOR_STR = odoo.release.serie
|
|
19
|
+
MIGRATION_DIR = [
|
|
20
|
+
"auto_migrations",
|
|
21
|
+
"ndp_migration",
|
|
22
|
+
"mangono_migration", # Name of the directory where the script must be located
|
|
23
|
+
]
|
|
24
|
+
if MAJOR < 19:
|
|
25
|
+
from odoo import (
|
|
26
|
+
SUPERUSER_ID,
|
|
27
|
+
api,
|
|
28
|
+
fields,
|
|
29
|
+
registry, # pylint: disable=ungrouped-imports; pylint: disable=ungrouped-imports
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
from odoo import (
|
|
33
|
+
SUPERUSER_ID,
|
|
34
|
+
api,
|
|
35
|
+
fields,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# from odoo.addons.queue_job.models import with_delay
|
|
39
|
+
from odoo.modules.migration import MigrationManager # pylint: disable=ungrouped-imports
|
|
40
|
+
from odoo.modules.module import (
|
|
41
|
+
get_module_path,
|
|
42
|
+
get_modules, # pylint: disable=ungrouped-imports; pylint: disable=ungrouped-imports
|
|
43
|
+
)
|
|
44
|
+
from odoo.tools import config # pylint: disable=ungrouped-imports
|
|
45
|
+
|
|
46
|
+
_logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_valid_migration_file_for_stage(stage: str, name: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
:param name: filename without path
|
|
52
|
+
:return: True is filename is in the form : stage_XXXX.py (case insensitive)
|
|
53
|
+
"""
|
|
54
|
+
_, ext = os.path.splitext(name)
|
|
55
|
+
return ext.lower() == ".py" and name.lower().startswith((stage + "_").lower())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def table_exists_in_db(cr, tablename: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
:param cr: a cursor object
|
|
61
|
+
:param tablename: name of the table (stock_move)
|
|
62
|
+
:return: True if the table exists in database, False otherwise
|
|
63
|
+
"""
|
|
64
|
+
cr.execute(
|
|
65
|
+
"""
|
|
66
|
+
SELECT EXISTS (
|
|
67
|
+
SELECT FROM pg_catalog.pg_class c
|
|
68
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
69
|
+
WHERE n.nspname = 'public'
|
|
70
|
+
AND c.relname = %s)
|
|
71
|
+
""",
|
|
72
|
+
(tablename,),
|
|
73
|
+
)
|
|
74
|
+
return cr.fetchone()[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def column_exists_in_db(cr, table_name: str, column_name: str) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
:param cr: cursor object
|
|
80
|
+
:param table_name: 'stock_move' for exmaple
|
|
81
|
+
:param column_name: 'location_id' for example
|
|
82
|
+
:return: True if column exists in table, false if columns or table doesn't exist
|
|
83
|
+
"""
|
|
84
|
+
if table_exists_in_db(cr, table_name):
|
|
85
|
+
cr.execute(
|
|
86
|
+
"""
|
|
87
|
+
SELECT EXISTS(SELECT *
|
|
88
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
89
|
+
WHERE TABLE_NAME = %s
|
|
90
|
+
AND COLUMN_NAME = %s);
|
|
91
|
+
""",
|
|
92
|
+
(table_name, column_name),
|
|
93
|
+
)
|
|
94
|
+
return cr.fetchone()[0]
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mangono_migrate_module(self, pkg, stage: str):
|
|
99
|
+
# this function is called from loading.py, self gives access to cr and graph
|
|
100
|
+
_logger.debug("auto_migrate_module stage %s module %s", pkg.name, stage)
|
|
101
|
+
if pkg.name not in ["base", "auto_migration"] and not config["test_enable"]:
|
|
102
|
+
run_migration_script(self, stage, pkg)
|
|
103
|
+
result = MigrationManager.origin_migrate_module(self, pkg, stage)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ruff: noqa: PERF203
|
|
108
|
+
def get_module_filetree(module_path: Path) -> dict[str, Path]:
|
|
109
|
+
"""return a dict of file: absolute_path for every file in migration directory of module_path"""
|
|
110
|
+
res = {}
|
|
111
|
+
for directory in MIGRATION_DIR:
|
|
112
|
+
try:
|
|
113
|
+
res.update(
|
|
114
|
+
{
|
|
115
|
+
f: os.path.join(module_path, directory, f)
|
|
116
|
+
for f in sorted(os.listdir(os.path.join(module_path, directory)))
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
except FileNotFoundError:
|
|
120
|
+
continue
|
|
121
|
+
return res
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def just_after_base(self, stage: str, pkg) -> None:
|
|
125
|
+
"""
|
|
126
|
+
:param stage: the migration stage
|
|
127
|
+
:param pkg: a Package object
|
|
128
|
+
:return: if the package is the first one after base, looks for scripts with stage 'afterbase' in all migration dirs
|
|
129
|
+
During an "update base", this will execute these migration script beginning with afterbase_ ...
|
|
130
|
+
before any other community, common, ... modules are loaded (only base is loaded)
|
|
131
|
+
"""
|
|
132
|
+
if MAJOR < 19:
|
|
133
|
+
list_pkg = list(pkg.graph)
|
|
134
|
+
else:
|
|
135
|
+
list_pkg = list(pkg.module_graph)
|
|
136
|
+
if stage == "pre" and list_pkg[0].name == "base" and list_pkg[1] == pkg: # we are the first package after base
|
|
137
|
+
mods = get_modules()
|
|
138
|
+
for mod in mods:
|
|
139
|
+
self.cr.execute("""select state from ir_module_module where name = %s""", (mod,))
|
|
140
|
+
state = self.cr.fetchone()
|
|
141
|
+
if state and state[0] in [
|
|
142
|
+
"installed",
|
|
143
|
+
"to upgrade",
|
|
144
|
+
]: # we check only installed modules
|
|
145
|
+
module_path = get_module_path(mod)
|
|
146
|
+
file_tree = get_module_filetree(module_path)
|
|
147
|
+
if file_tree:
|
|
148
|
+
run_scripts_of_module(self, file_tree, "afterbase")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def run_migration_script(self, stage: str, pkg) -> None:
|
|
152
|
+
_logger.info("Running migration script %s stage: %s", pkg.name, stage)
|
|
153
|
+
if MAJOR >= 18:
|
|
154
|
+
_registry = odoo.modules.registry.Registry(self.cr.dbname)
|
|
155
|
+
else:
|
|
156
|
+
_registry = registry(self.cr.dbname)
|
|
157
|
+
just_after_base(self, stage, pkg) # really working ??
|
|
158
|
+
module_path = get_module_path(pkg.name)
|
|
159
|
+
file_tree = get_module_filetree(module_path)
|
|
160
|
+
if file_tree:
|
|
161
|
+
if stage == "pre":
|
|
162
|
+
if MAJOR >= 19:
|
|
163
|
+
_registry._setup_models__(self.cr)
|
|
164
|
+
else:
|
|
165
|
+
_registry.setup_models(self.cr)
|
|
166
|
+
_logger.debug("=== found auto migration directory in %s for stage %s", pkg.name, stage)
|
|
167
|
+
run_scripts_of_module(self, file_tree, stage)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def run_scripts_of_module(self, file_tree: dict[str, Path], stage: str) -> None:
|
|
171
|
+
"""run all script in this module matching stage (python file beginning with stage_)"""
|
|
172
|
+
_logger.debug("=== running auto migration module %s", stage)
|
|
173
|
+
for filename, file_path in file_tree.items():
|
|
174
|
+
# python file must be directly under auto_migrations directory
|
|
175
|
+
if _is_valid_migration_file_for_stage(stage, filename):
|
|
176
|
+
file_without_ext = os.path.splitext(filename)[0]
|
|
177
|
+
# load and execute _pre_migrate()
|
|
178
|
+
py_mod = load_module_from_file(file_without_ext, file_path)
|
|
179
|
+
# we recognize the python function to run because the @auto_migrate wrapper adds an
|
|
180
|
+
# 'is_auto_migration' attribute to it, <migrate_mangono> is imported by the import, must not be run
|
|
181
|
+
scripts = [
|
|
182
|
+
f[1]
|
|
183
|
+
for f in getmembers(py_mod, isfunction)
|
|
184
|
+
if f[0] != "migrate_mangono"
|
|
185
|
+
and (hasattr(f[1], "is_mangono_migration") or hasattr(f[1], "is_auto_migration"))
|
|
186
|
+
]
|
|
187
|
+
for method in sorted(scripts, key=attrgetter("priority")):
|
|
188
|
+
module = file_path.split("/")[-3]
|
|
189
|
+
ident = "{module}.{method}".format(module=module or "", method=method.__name__)
|
|
190
|
+
script = MangonoMigration.get_or_create(self.cr, ident)
|
|
191
|
+
script.run_method_if_needed(method)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def patch_migration() -> None:
|
|
195
|
+
if "auto_migration" in get_server_wide_modules(MAJOR_STR) and not config["test_enable"]:
|
|
196
|
+
MigrationManager.origin_migrate_module = (
|
|
197
|
+
hasattr(MigrationManager, "origin_migrate_module")
|
|
198
|
+
and MigrationManager.origin_migrate_module
|
|
199
|
+
or MigrationManager.migrate_module
|
|
200
|
+
)
|
|
201
|
+
MigrationManager.migrate_module = mangono_migrate_module
|
|
202
|
+
if "openupgrade_framework" in get_server_wide_modules(MAJOR_STR) and hasattr(
|
|
203
|
+
MigrationManager.origin_migrate_module, "_original_method"
|
|
204
|
+
):
|
|
205
|
+
# When openupgrade is activated with mangono_migration we need to copy _original_method ref
|
|
206
|
+
mangono_migrate_module._original_method = MigrationManager.origin_migrate_module._original_method # pyright: ignore[reportFunctionMemberAccess]
|
|
207
|
+
MigrationManager._logger = hasattr(MigrationManager, "_logger") and MigrationManager._logger or _logger
|
|
208
|
+
_logger.info("module auto_migration enabled")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class MangonoMigration: # pylint: disable=useless-object-inheritance
|
|
212
|
+
"""
|
|
213
|
+
Provide service for pre-installation and post-installation scripts
|
|
214
|
+
see <documentation.adoc> for detailed documentation
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
_description = "Automation scripting system for pre and post installation scripting"
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
ticket: str,
|
|
222
|
+
cr,
|
|
223
|
+
md5: str | None = None,
|
|
224
|
+
has_run: bool = False,
|
|
225
|
+
create_date=None,
|
|
226
|
+
write_date=None,
|
|
227
|
+
log: str | None = None,
|
|
228
|
+
):
|
|
229
|
+
"""prepare the table for script execution recording if necessary"""
|
|
230
|
+
super().__init__()
|
|
231
|
+
self.ticket = ticket
|
|
232
|
+
self.md5 = md5 or ""
|
|
233
|
+
self.log = log or ""
|
|
234
|
+
self.has_run = has_run
|
|
235
|
+
self.create_date = create_date or fields.Datetime.now()
|
|
236
|
+
self.write_date = write_date or fields.Datetime.now()
|
|
237
|
+
self.cr = cr
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def create_table_if_needed(cls, cr) -> None:
|
|
241
|
+
"""Keep as classmethod because used in get_or_create to create an instance of MangonoMigration"""
|
|
242
|
+
cr.execute("ALTER TABLE IF EXISTS ndp_migration RENAME TO auto_migration")
|
|
243
|
+
cr.execute("ALTER TABLE IF EXISTS mangono_migration RENAME TO auto_migration")
|
|
244
|
+
cr.execute(
|
|
245
|
+
"""CREATE TABLE IF NOT EXISTS auto_migration ( id serial NOT NULL
|
|
246
|
+
CONSTRAINT
|
|
247
|
+
mangono_migration_pkey
|
|
248
|
+
PRIMARY
|
|
249
|
+
KEY,
|
|
250
|
+
create_date
|
|
251
|
+
timestamp,
|
|
252
|
+
write_date
|
|
253
|
+
timestamp,
|
|
254
|
+
ticket
|
|
255
|
+
varchar
|
|
256
|
+
NOT
|
|
257
|
+
NULL
|
|
258
|
+
CONSTRAINT
|
|
259
|
+
mangono_migration_unique_ticket
|
|
260
|
+
UNIQUE,
|
|
261
|
+
md5
|
|
262
|
+
varchar,
|
|
263
|
+
has_run bool,
|
|
264
|
+
log varchar
|
|
265
|
+
);"""
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def set_run(self) -> None:
|
|
269
|
+
"""record the fact that this script has been run"""
|
|
270
|
+
md5 = ""
|
|
271
|
+
self.create_table_if_needed(self.cr)
|
|
272
|
+
date_str = fields.Datetime.to_string(fields.Datetime.now())
|
|
273
|
+
_logger.debug("script %s has run succesfully", self.ticket)
|
|
274
|
+
self.cr.execute(
|
|
275
|
+
"""UPDATE auto_migration SET has_run = TRUE, md5 = %s, write_date = %s, log = %s
|
|
276
|
+
WHERE ticket = %s""",
|
|
277
|
+
(md5, date_str, self.log + "\n" + date_str + " has_run", self.ticket),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def get_or_create(cls, cr, ident: str) -> Self:
|
|
282
|
+
"""return MangonoMigration object from existing record of database, create it in base if it doesn't exist"""
|
|
283
|
+
|
|
284
|
+
def get_value() -> dict[str, Any]:
|
|
285
|
+
# odoo himself use string formating to insert table name into query
|
|
286
|
+
cls.create_table_if_needed(cr)
|
|
287
|
+
cr.execute("""SELECT * FROM auto_migration WHERE ticket = %s""", (ident,))
|
|
288
|
+
return cr.dictfetchone()
|
|
289
|
+
|
|
290
|
+
res = get_value()
|
|
291
|
+
if not res:
|
|
292
|
+
cr.execute(
|
|
293
|
+
"""INSERT INTO auto_migration
|
|
294
|
+
(ticket, has_run, create_date, write_date)
|
|
295
|
+
VALUES (%s, FALSE, %s, %s)""",
|
|
296
|
+
(ident, fields.Datetime.now(), fields.Datetime.now()),
|
|
297
|
+
)
|
|
298
|
+
res = get_value()
|
|
299
|
+
res.pop("id")
|
|
300
|
+
return MangonoMigration(cr=cr, **res)
|
|
301
|
+
|
|
302
|
+
def log_error(self, msg: str) -> None:
|
|
303
|
+
self.create_table_if_needed(self.cr)
|
|
304
|
+
self.cr.execute(
|
|
305
|
+
"""UPDATE auto_migration SET log = %s WHERE ticket = %s""",
|
|
306
|
+
(
|
|
307
|
+
f"{self.log}\n{fields.Datetime.to_string(fields.Datetime.now())} : {msg}",
|
|
308
|
+
self.ticket,
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def run_method_if_needed(self, method: callable) -> None:
|
|
313
|
+
_logger.info("preparing to run %s", method.__name__)
|
|
314
|
+
try:
|
|
315
|
+
if method.run_always or not self.has_run:
|
|
316
|
+
if method.allowed_to_fail:
|
|
317
|
+
with self.cr.savepoint(): # when failed, changes are rollbacked
|
|
318
|
+
_logger.info(
|
|
319
|
+
"auto_migration executing %s allowed to fail",
|
|
320
|
+
method.__name__,
|
|
321
|
+
)
|
|
322
|
+
self._call_method(method)
|
|
323
|
+
else:
|
|
324
|
+
_logger.info(
|
|
325
|
+
"auto_migration executing %s not allowed to fail",
|
|
326
|
+
method.__name__,
|
|
327
|
+
)
|
|
328
|
+
self._call_method(method)
|
|
329
|
+
# pylint: disable=broad-except
|
|
330
|
+
except Exception as err:
|
|
331
|
+
_logger.warning("Error during migration <%s>: %s", self.ticket, err)
|
|
332
|
+
self.log_error(f"{err}")
|
|
333
|
+
if not method.allowed_to_fail:
|
|
334
|
+
# note that the self allowed_to_fail which fails will have an error log, but not set to run
|
|
335
|
+
raise
|
|
336
|
+
else:
|
|
337
|
+
if method.run_always or not self.has_run:
|
|
338
|
+
self.set_run()
|
|
339
|
+
|
|
340
|
+
def _call_method(self, method: callable) -> None:
|
|
341
|
+
parameters = inspect.signature(method).parameters
|
|
342
|
+
if len(parameters) > 2:
|
|
343
|
+
self.log_error(f"{method.__name__} wrong signature (too many arguments)")
|
|
344
|
+
elif len(parameters) == 2 and parameters.get("has_run"):
|
|
345
|
+
# old signature with parameter has_run
|
|
346
|
+
method(ProxySelf(self.cr, self.has_run), self.has_run)
|
|
347
|
+
else: # cas où la méthode ne prend pas has_run en paramètre
|
|
348
|
+
method(ProxySelf(self.cr, self.has_run))
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class ProxySelf: # pylint: disable=useless-object-inheritance
|
|
352
|
+
"""provide a factice object with env attribute to be able to use self.env, self.env.cr in script
|
|
353
|
+
self.has_run is true when the script has already been run (for run_always script)
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
def __init__(self, cr, has_run: bool = False):
|
|
357
|
+
super().__init__()
|
|
358
|
+
env = api.Environment(cr, SUPERUSER_ID, {})
|
|
359
|
+
self.env = env
|
|
360
|
+
self.cr = cr
|
|
361
|
+
self.uid = SUPERUSER_ID
|
|
362
|
+
self.context = self.env.context
|
|
363
|
+
self.has_run = has_run
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def auto_migrate(run_always: bool | callable = False, priority: int = 9999, allowed_to_fail: bool = False) -> callable:
|
|
367
|
+
def _migrate_mangono(method: callable) -> callable:
|
|
368
|
+
"""This decorator decorate the method with the argument declared in the call to auto_migrate()
|
|
369
|
+
and return the decorated method without changing the signature"""
|
|
370
|
+
method.run_always = run_always
|
|
371
|
+
method.priority = priority
|
|
372
|
+
method.allowed_to_fail = allowed_to_fail
|
|
373
|
+
method.is_auto_migration = True
|
|
374
|
+
return method
|
|
375
|
+
|
|
376
|
+
if callable(run_always):
|
|
377
|
+
# use case of @auto_migrate without bracket, in this case the first argument is the method, we need
|
|
378
|
+
# to implement default value for missings arguments
|
|
379
|
+
method = run_always
|
|
380
|
+
run_always = False
|
|
381
|
+
priority = 9999
|
|
382
|
+
allowed_to_fail = False
|
|
383
|
+
return _migrate_mangono(method)
|
|
384
|
+
|
|
385
|
+
return _migrate_mangono
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
migrate_mangono = auto_migrate
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
##############################################################################
|
|
392
|
+
# Copyright (c) 2013-2018, Lawrence Livermore National Security, LLC.
|
|
393
|
+
# Produced at the Lawrence Livermore National Laboratory.
|
|
394
|
+
# Extract from spack
|
|
395
|
+
# This program is free software; you can redistribute it and/or modify
|
|
396
|
+
# it under the terms of the GNU Lesser General Public License (as
|
|
397
|
+
# published by the Free Software Foundation) version 2.1, February 1999.
|
|
398
|
+
##############################################################################
|
|
399
|
+
def load_module_from_file(module_name, module_path):
|
|
400
|
+
"""Loads a python module from the path of the corresponding file.
|
|
401
|
+
Args:
|
|
402
|
+
module_name (str): namespace where the python module will be loaded,
|
|
403
|
+
e.g. ``foo.bar``
|
|
404
|
+
module_path (str): path of the python file containing the module
|
|
405
|
+
Returns:
|
|
406
|
+
A valid module object
|
|
407
|
+
Raises:
|
|
408
|
+
ImportError: when the module can't be loaded
|
|
409
|
+
FileNotFoundError: when module_path doesn't exist
|
|
410
|
+
"""
|
|
411
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
412
|
+
module = importlib.util.module_from_spec(spec)
|
|
413
|
+
spec.loader.exec_module(module)
|
|
414
|
+
|
|
415
|
+
return module
|