Files
encoach_backend_new_v2/odoo/addons/iot_drivers/tools/upgrade.py
Yamen Ahmad 3e83d8d7d5 feat: add complete EnCoach backend — Odoo 19 + all addons
Includes:
- Odoo 19 framework (odoo/)
- 27 custom EnCoach addons (new_project/custom_addons/)
  - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api
  - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment
  - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media
  - encoach_courseware, encoach_communication, encoach_subscription
  - encoach_notification, encoach_approval, encoach_branding
  - encoach_classroom, encoach_registration, encoach_stats
  - encoach_faq, encoach_ticket, encoach_training, encoach_resources
  - encoach_adaptive_ai, encoach_sis
- 21 OpenEduCat Enterprise modules (new_project/enterprise-19/)
- 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/)
- Configuration: odoo.conf, requirements.txt, scripts
- 200+ REST API endpoints with JWT authentication
- SRS and test documentation

Made-with: Cursor
2026-04-01 17:10:04 +04:00

228 lines
8.8 KiB
Python

"""Module to manage odoo code upgrades using git"""
import logging
import platform
import requests
import subprocess
from odoo.addons.iot_drivers.tools.helpers import (
odoo_restart,
path_file,
require_db,
toggleable,
unlink_file,
)
from odoo.addons.iot_drivers.tools.system import rpi_only, IS_RPI, IS_TEST
_logger = logging.getLogger(__name__)
def git(*args):
"""Run a git command with the given arguments, taking system
into account.
:param args: list of arguments to pass to git
"""
git_executable = 'git' if IS_RPI else path_file('git', 'cmd', 'git.exe')
command = [git_executable, f'--work-tree={path_file("odoo")}', f'--git-dir={path_file("odoo", ".git")}', *args]
p = subprocess.run(command, stdout=subprocess.PIPE, text=True, check=False)
if p.returncode == 0:
return p.stdout.strip()
return None
def pip(*args):
"""Run a pip command with the given arguments, taking system
into account.
:param args: list of arguments to pass to pip
"""
python_executable = [] if IS_RPI else [path_file('python', 'python.exe'), '-m']
command = [*python_executable, 'pip', *args]
if IS_RPI and args[0] == 'install':
command.append('--user')
command.append('--break-system-package')
p = subprocess.run(command, stdout=subprocess.PIPE, check=False)
return p.returncode
def get_db_branch(server_url):
"""Get the current branch of the database.
:param server_url: The URL of the connected Odoo database.
:return: the current branch of the database
"""
try:
response = requests.post(server_url + "/web/webclient/version_info", json={}, timeout=5)
response.raise_for_status()
except requests.exceptions.HTTPError:
_logger.exception('Could not reach configured server to get the Odoo version')
return None
try:
return response.json()['result']['server_serie'].replace('~', '-')
except ValueError:
_logger.exception('Could not load JSON data: Received data is not valid JSON.\nContent:\n%s', response.content)
return None
@rpi_only
def check_version_upgrades(local_branch, db_branch):
"""
Check if the iot is < 19.1 and upgrading to >= saas-19.1
If so and current python version is less than 3.12, run the scripts
located in upgrade_scripts/ to upgrade the python version
:param local_branch: The local git branch (Ex: "19.0" / "17.0-hw-drivers-compatibility-with-trixie-yaso")
:param db_branch: The git branch of the connected Odoo database (Ex: "saas-19.1" / "master" etc.)
"""
try:
# 1. Check if the upgrade script needs to be ran
# Needed if local branch is < 19.1 and db branch is >= 19.1 + python version < 3.12
_logger.info("Checking for version upgrades for local branch %s / db_branch %s", local_branch, db_branch)
version_db = db_branch[-4:] if db_branch != 'master' else db_branch # master is currently always >= 19.1
version_local = local_branch[-4:] if local_branch != 'master' else local_branch
local_python_version = tuple(int(x) for x in platform.python_version_tuple()[:2])
if version_local >= '19.1' or version_db < '19.1' or local_python_version >= (3, 12):
_logger.info("Ignoring unnecessary upgrade for local branch %s / db_branch %s with python version %s", local_branch, db_branch, local_python_version)
return
_logger.warning("Updating to Debian Trixie for >= 19.1")
subprocess.run(
['/home/pi/odoo/addons/iot_drivers/tools/upgrade_scripts/upgrade_trixie/upgrade_trixie.sh'], check=True,
)
except subprocess.CalledProcessError:
_logger.exception("Failed to upgrade to debian Trixie. Check /home/pi/upgrade.log file for more details")
@toggleable
@require_db
def check_git_branch(server_url=None):
"""Check if the local branch is the same as the connected Odoo DB and
checkout to match it if needed.
:param server_url: The URL of the connected Odoo database (provided by decorator).
"""
if IS_TEST:
return
db_branch = get_db_branch(server_url)
if not db_branch:
_logger.warning("Could not get the database branch, skipping git checkout")
return
try:
if not git('ls-remote', 'origin', db_branch):
db_branch = 'master'
local_branch = git('symbolic-ref', '-q', '--short', 'HEAD')
_logger.info("IoT Box git branch: %s / Associated Odoo db's git branch: %s", local_branch, db_branch)
if db_branch != local_branch:
# Repository updates
unlink_file("odoo/.git/shallow.lock") # In case of previous crash/power-off, clean old lockfile
check_version_upgrades(local_branch, db_branch)
checkout(db_branch)
update_requirements()
# System updates
update_packages()
# Miscellaneous updates (version migrations)
misc_migration_updates()
_logger.warning("Update completed, restarting...")
odoo_restart()
except Exception:
_logger.exception('An error occurred while trying to update the code with git')
def _ensure_production_remote():
"""Ensure that the remote repository is the production one
(https://github.com/odoo/odoo.git).
"""
production_remote = "https://github.com/odoo/odoo.git"
if git("remote", "get-url", "origin") != production_remote:
_logger.info("Setting remote repository to production: %s", production_remote)
git("remote", "set-url", "origin", production_remote)
def checkout(branch):
"""Checkout to the given branch of the given git remote.
:param branch: The name of the branch to check out.
"""
_logger.info("Preparing local repository for checkout")
_ensure_production_remote()
_logger.warning("Checking out origin/%s", branch)
if git("fetch", "origin", branch, "--depth=1", "--prune") is None:
_logger.error("Failed to fetch origin/%", branch)
return
if git("reset", "FETCH_HEAD", "--hard") is None:
_logger.error("Failed to reset on FETCH_HEAD")
return
git('branch', '-m', branch) # Rename the current branch to the target branch name
_logger.info("Cleaning the working directory")
git("clean", "-dfx")
def update_requirements():
"""Update the Python requirements of the IoT Box, installing the ones
listed in the requirements.txt file.
"""
requirements_file = path_file('odoo', 'addons', 'iot_box_image', 'configuration', 'requirements.txt')
if not requirements_file.exists():
_logger.info("No requirements file found, not updating.")
return
_logger.info("Updating pip requirements")
pip('install', '-r', requirements_file)
@rpi_only
def update_packages():
"""Update apt packages on the IoT Box, installing the ones listed in
the packages.txt file.
Requires ``writable`` context manager.
"""
packages_file = path_file('odoo', 'addons', 'iot_box_image', 'configuration', 'packages.txt')
if not packages_file.exists():
_logger.info("No packages file found, not updating.")
return
# update and install packages in the foreground
commands = (
"export DEBIAN_FRONTEND=noninteractive && "
"mount -t proc proc /proc && "
"apt-get update && "
f"xargs apt-get -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install < {packages_file}"
)
_logger.warning("Updating apt packages")
if subprocess.run(
f'sudo chroot /root_bypass_ramdisks /bin/bash -c "{commands}"', shell=True, check=False
).returncode != 0:
_logger.error("An error occurred while trying to update the packages")
return
# upgrade and remove packages in the background
background_cmd = 'chroot /root_bypass_ramdisks /bin/bash -c "apt-get upgrade -y && apt-get -y autoremove"'
subprocess.Popen(["sudo", "bash", "-c", background_cmd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@rpi_only
def misc_migration_updates():
"""Run miscellaneous updates after the code update."""
_logger.warning("Running version migration updates")
if path_file('odoo', 'addons', 'point_of_sale').exists():
# TODO: remove this when v18.0 is deprecated (point_of_sale/tools/posbox/ -> iot_box_image/)
ramdisks_service = "/root_bypass_ramdisks/etc/systemd/system/ramdisks.service"
subprocess.run(
['sudo', 'sed', '-i', 's|iot_box_image|point_of_sale/tools/posbox|g', ramdisks_service], check=False
)
if path_file('odoo', 'addons', 'hw_drivers').exists():
# TODO: remove this when v18.4 is deprecated (hw_drivers/,hw_posbox_homepage/ -> iot_drivers/)
subprocess.run(
['sed', '-i', 's|iot_drivers|hw_drivers,hw_posbox_homepage|g', '/home/pi/odoo.conf'], check=False
)