tryton-upgrades/upgrade

421 lines
15 KiB
Plaintext
Raw Normal View History

2017-01-19 01:59:38 +01:00
#!/usr/bin/env python
import os
2017-01-20 01:01:29 +01:00
import sys
2017-01-19 01:59:38 +01:00
import yaml
import argparse
from itertools import chain
2017-01-19 01:59:38 +01:00
import psycopg2
import subprocess
2018-09-28 01:09:27 +02:00
import logging
import time
from enum import Enum
2017-01-19 01:59:38 +01:00
from blessings import Terminal
2017-01-21 01:30:25 +01:00
import trytond
2018-09-28 01:09:27 +02:00
from trytond.config import config as CONFIG, parse_uri
2017-01-21 01:30:25 +01:00
trytond_version = '.'.join(trytond.__version__.split('.')[:2])
2018-09-28 01:09:27 +02:00
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
2017-01-21 01:30:25 +01:00
class Step(Enum):
BEFORE = 'before'
UNINSTALL = 'uninstall'
FIRST_UPDATE = 'first-update'
AFTER = 'after'
SECOND_UPDATE = 'second-update'
2017-01-19 01:59:38 +01:00
t = Terminal()
2017-01-20 01:01:29 +01:00
def get_url():
if config_file:
CONFIG.update_etc(config_file)
url = parse_uri(CONFIG.get('database', 'uri'))
else:
url = parse_uri(os.environ.get('TRYTOND_DATABASE__URI', ''))
2017-01-20 01:01:29 +01:00
return url
2017-01-19 01:59:38 +01:00
def run(*args):
2021-06-22 15:37:16 +02:00
logger.info('RUNNING: %s' % ' '.join(args))
2017-01-20 01:01:29 +01:00
process = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, bufsize=1, text=True, shell=False)
2017-03-22 18:58:49 +01:00
out, err = process.communicate()
summary = set()
in_traceback = False
for line in chain(out.split('\n'), err.split('\n')):
2017-01-20 01:01:29 +01:00
line = line.strip()
if 'ERROR' in line:
s = line[line.index('ERROR'):]
summary.add(s)
2017-01-20 01:01:29 +01:00
line = t.red(line)
elif 'WARNING' in line:
s = line[line.index('WARNING'):]
summary.add(s)
2017-01-20 01:01:29 +01:00
line = t.yellow(line)
elif 'Traceback' in line or in_traceback:
in_traceback = True
line = t.red(line)
if line.startswith('Exception'):
in_traceback = False
2018-09-28 01:09:27 +02:00
logger.info(line)
2017-03-22 18:58:49 +01:00
2017-01-19 01:59:38 +01:00
process.stdout.close()
2017-04-19 09:54:53 +02:00
process.stderr.close()
2017-01-19 01:59:38 +01:00
process.wait()
2017-03-22 18:58:49 +01:00
2017-01-20 01:01:29 +01:00
if summary:
summary = sorted(list(summary))
total = len(summary)
if cmd.show_ignored:
ignored = 0
else:
ignores = config['ignore']
summary = [x for x in summary if x.strip() not in ignores]
ignored = total - len(summary)
logger.error('WARNING AND ERROR SUMMARY (%d + %d ignored):' % (
len(summary), ignored))
logger.error(t.yellow('\n'.join(summary)))
2017-01-20 01:01:29 +01:00
return process.returncode
2017-01-19 01:59:38 +01:00
def execute(query, *args, **kwargs):
if not args:
args = kwargs
cursor.execute(query, args)
def run_trytond(to_install=None):
to_run = ['trytond/bin/trytond-admin', '-v']
if config_file:
to_run += ['-c', config_file]
2017-01-19 01:59:38 +01:00
if to_install:
2021-07-12 21:56:44 +02:00
to_run += ['-u'] + to_install
2017-01-19 01:59:38 +01:00
to_run.append('--all')
to_run.append('--activate-dependencies')
2017-01-19 01:59:38 +01:00
to_run.append('-d')
to_run.append(database_name)
2017-01-20 01:01:29 +01:00
returncode = run(*to_run)
if returncode:
2018-09-28 01:09:27 +02:00
logger.error('Trytond update failed. Upgrade aborted.')
2017-01-20 01:01:29 +01:00
return returncode
2017-01-19 01:59:38 +01:00
def table_exists(table):
execute('SELECT count(*) FROM information_schema.tables '
'WHERE table_name=%s', table)
return bool(cursor.fetchone()[0])
def field_exists(field):
table, field = field.split('.')
execute('SELECT count(*) FROM information_schema.columns '
'WHERE table_name=%s AND column_name=%s', table, field)
return bool(cursor.fetchone()[0])
def where_exists(query):
execute(query)
return bool(cursor.fetchone()[0])
class Upgrade:
def __init__(self, connection):
self.connection = connection
self.steps = None
self.before = None
self.after = None
self.to_install = None
self.to_uninstall = None
self.database_name = None
self.config_file = None
def uninstall_modules(self):
module_table = None
for table in ('ir_module_module', 'ir_module'):
if table_exists(table):
module_table = table
break
for module in self.to_uninstall:
logger.info('Module: %s' % module)
execute('DELETE FROM ' + module_table + '_dependency WHERE '
'module IN (SELECT id FROM ' + module_table + ' WHERE name=%s)',
module)
execute('DELETE FROM ' + module_table + ' WHERE name=%s', module)
execute('SELECT model, db_id FROM ir_model_data WHERE module=%s',
module)
for model, db_id in cursor.fetchall():
logger.info('DELETING %s %s' % (model, db_id))
if model == 'res.user':
continue
execute('DELETE FROM "' + model.replace('.', '_')
+ '" WHERE id=%s', db_id)
execute('DELETE FROM ir_model_data WHERE module=%s', module)
if table_exists('babi_report'):
execute('DELETE from babi_filter_parameter where filter in'
' (SELECT id FROM babi_filter WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s)))' %
module_table)
execute('DELETE FROM babi_filter WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
execute('DELETE from babi_order where report in'
' (SELECT id FROM babi_report WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s)))' %
module_table)
execute('DELETE from babi_measure where report in'
' (SELECT id FROM babi_report WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s)))' %
module_table)
execute('DELETE from babi_dimension where expression in'
' (SELECT id FROM babi_expression WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s)))' %
module_table)
execute('DELETE FROM babi_expression WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
execute('DELETE FROM babi_report WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
2018-02-27 12:34:09 +01:00
if table_exists('mass_editing'):
execute('DELETE FROM mass_editing WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
execute('DELETE FROM ir_trigger WHERE model IN (SELECT '
'id FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
execute('DELETE FROM ir_action_act_window WHERE res_model IN (SELECT '
'model FROM ir_model WHERE module NOT IN (SELECT name FROM %s))' %
module_table)
execute('DELETE FROM ir_action_wizard WHERE model in (SELECT model FROM '
'ir_model WHERE module NOT IN (SELECT name FROM %s))' % module_table)
execute('DELETE FROM ir_model WHERE module NOT IN (SELECT name FROM '
'%s)' % module_table)
execute('DELETE FROM ir_model_field WHERE module NOT IN (SELECT name FROM '
'%s)' % module_table)
execute('DELETE FROM ir_ui_view WHERE module NOT IN (SELECT name FROM '
'%s)' % module_table)
def process_actions(self, actions):
if not actions:
return
for action in actions:
if isinstance(action, dict):
tables = action.get('tables', '')
fields = action.get('fields', '')
version = action.get('version', trytond_version)
query = action.get('query')
script = action.get('script')
where = action.get('where')
# Check version
if not (float(version) <= float(trytond_version) and float(version) > float(from_version)):
continue
# Check tables
found = True
tables = tables.split()
for table in tables:
if not table_exists(table):
logger.info("TABLE '%s' NOT FOUND" % table)
found = False
break
if not found:
continue
# Check fields
found = True
fields = fields.split()
for field in fields:
if not field_exists(field):
logger.info("FIELD '%s' NOT FOUND" % field)
found = False
break
if not found:
continue
# Check where
if where and not where_exists(where):
logger.info("WHERE '%s' NOT FOUND" % where)
continue
2017-03-15 09:10:53 +01:00
else:
query = action
script = None
if query:
logger.info(query)
query = query.replace('%', '%%')
execute(query)
if script:
if os.path.isfile(script):
# We must commit before executing the script so the script
# is not locked by our transaction
logger.info(t.green('Executing: %s' % script))
2019-07-09 00:18:18 +02:00
self.connection.commit()
res = run(script, self.database_name, self.config_file or
'')
2019-07-09 00:18:18 +02:00
self.cursor = self.connection.cursor()
if res:
logger.error(t.red('Script "%s" returned the following '
'error code: %d. Upgrade aborted.') % (script,
res))
return 1
else:
logger.warning("Not found script: %s" % script)
logger.error(t.red('Script "%s" not found. Upgrade '
'aborted.') % script)
return 1
def run(self):
for step in self.steps:
2019-07-09 00:18:18 +02:00
self.cursor = self.connection.cursor()
logger.info(t.green('Executing step %s...') % step.value)
if step == Step.BEFORE:
res = self.process_actions(self.before)
self.connection.commit()
elif step == Step.UNINSTALL:
res = self.uninstall_modules()
self.connection.commit()
elif step == Step.FIRST_UPDATE:
res = run_trytond(self.to_install)
elif step == Step.AFTER:
res = self.process_actions(self.after)
self.connection.commit()
elif step == Step.SECOND_UPDATE:
res = run_trytond()
if res:
return res
if not self.steps:
logger.error(t.red('No steps executed. Invalid from/until steps'))
return 1
logger.info(t.green('Finished executing steps: %s'
% ', '.join([x.value for x in steps_to_run])))
2017-01-19 01:59:38 +01:00
2017-01-23 22:57:04 +01:00
parser = argparse.ArgumentParser(description='Upgrade a Tryton database to the '
'version of the trytond library available.')
2017-01-20 01:01:29 +01:00
parser.add_argument('database', nargs=1, help='PostgreSQL database to upgrade')
2017-01-23 22:57:04 +01:00
parser.add_argument('from_version', nargs=1, help='Tryton version of the '
'database to be migrated')
parser.add_argument('-c', '--config', default=None,
help='path to the trytond configuration file')
parser.add_argument('--show-ignored', dest='show_ignored', default=False,
help='Show warnings that would otherwise be ignored')
parser.add_argument('--override', dest='override', default='upgrade.yml',
help='Search on the given filename for values to override default '
'configuration. Default: upgrade.yml')
steps = ', '.join([x.value for x in Step])
parser.add_argument('--until', default=None, help='Run the upgrade process '
'only until the given step. Possible steps include: %s' % steps)
parser.add_argument('--from', dest='from_', default=None, help='Run the '
'upgrade process from the given step. Possible steps include: %s' % steps)
2017-01-19 01:59:38 +01:00
cmd = parser.parse_args()
2017-01-19 01:59:38 +01:00
# Compute from steps_to_run
from_ = cmd.from_
steps_to_run = []
for step in Step:
if not from_ or step.value == from_ or steps_to_run:
steps_to_run.append(step)
if cmd.until and step.value == cmd.until:
break
database_name, = cmd.database
from_version, = cmd.from_version
if not cmd.config:
instance = os.path.basename(os.path.realpath(os.path.join(
os.path.dirname(os.path.realpath(__file__)), '..')))
paths = (
'/etc/trytond/%s.conf' % instance,
os.environ.get('TRYTOND_CONFIG'),
)
for config_file in paths:
2019-02-01 23:08:20 +01:00
if not config_file:
continue
2018-09-28 01:09:27 +02:00
logger.info('Checking %s...' % config_file)
if os.path.exists(config_file):
break
if config_file:
logger.info("Configuration file: %s" % config_file)
else:
logger.info("No configuration file found.")
else:
config_file = cmd.config
2018-02-06 17:21:21 +01:00
if not os.path.isfile(config_file):
logger.info('File "%s" not found' % config_file)
2018-02-06 17:21:21 +01:00
sys.exit(1)
logger.info('Loading configuration from "%s"' % config_file)
2017-01-19 01:59:38 +01:00
2017-01-20 01:01:29 +01:00
url = get_url()
2017-01-19 01:59:38 +01:00
2021-08-25 00:52:55 +02:00
config = yaml.load(open('upgrades/config.yml', 'r').read(),
Loader=yaml.FullLoader)
2017-01-20 01:01:29 +01:00
config.setdefault('to_uninstall', [])
config.setdefault('to_install', [])
config.setdefault('ignore', [])
config.setdefault('vars', {})
if os.path.exists(cmd.override):
logger.info('Overriding configuration using "%s"' % cmd.override)
override = yaml.load(open(cmd.override, 'r').read(), Loader=yaml.FullLoader)
logger.info
2021-07-07 18:05:48 +02:00
if override:
config['to_install'] += override.get('to_install', [])
config['to_uninstall'] += override.get('to_uninstall', [])
config['before'] = (override.get('before_before', []) +
config.get('before', []) + override.get('before', []))
config['after'] = (override.get('before_after', []) +
config.get('after', []) + override.get('after', []))
config['ignore'] += override.get('ignore', [])
config['vars'].update(override.get('vars', {}))
else:
logger.info('Overriding file "%s" not found.' % cmd.override)
2017-01-20 01:01:29 +01:00
if url.username:
connection = psycopg2.connect(dbname=database_name, host=url.hostname,
port=url.port, user=url.username, password=url.password)
else:
connection = psycopg2.connect(dbname=database_name)
# Ensure variable values are of type str
config['vars'] = {k: str(v) for k, v in config['vars'].items()}
# Set environment variables
os.environ.update(config['vars'])
start = time.time()
2017-01-20 01:01:29 +01:00
cursor = connection.cursor()
upgrade = Upgrade(connection)
upgrade.before = config.get('before', [])
upgrade.after = config.get('after', [])
upgrade.to_install = config.get('to_install', [])
upgrade.to_uninstall = config.get('to_uninstall', [])
upgrade.database_name = database_name
upgrade.config_file = config_file
upgrade.steps = steps_to_run
returncode = upgrade.run()
end = time.time()
logger.info(t.cyan('Elapsed time: %.1fs' % (end - start)))
exit(returncode or 0)