From 383fd6f5d403206328db26d90dfb8de9c976e2f1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Jan 2024 20:59:40 +0100 Subject: [PATCH 01/10] First draft for migrate_to_bookworm --- src/migrations/0027_migrate_to_bookworm.py | 427 +++++++++++++++++++++ src/tools.py | 27 +- src/utils/system.py | 110 ++++++ 3 files changed, 538 insertions(+), 26 deletions(-) create mode 100644 src/migrations/0027_migrate_to_bookworm.py diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py new file mode 100644 index 000000000..86d2ce49e --- /dev/null +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -0,0 +1,427 @@ +import glob +import os + +from moulinette import m18n +from yunohost.utils.error import YunohostError +from moulinette.utils.process import check_output +from moulinette.utils.filesystem import read_file, write_to_file + +from yunohost.tools import ( + Migration, + tools_update, +) +from yunohost.app import unstable_apps +from yunohost.regenconf import manually_modified_files +from yunohost.utils.system import ( + free_space_in_directory, + get_ynh_package_version, + _list_upgradable_apt_packages, + aptitude_with_progress_bar, +) + +# getActionLogger is not there in bookworm, +# we use this try/except to make it agnostic wether or not we're on 11.x or 12.x +# otherwise this may trigger stupid issues +try: + from moulinette.utils.log import getActionLogger + logger = getActionLogger("yunohost.migration") +except ImportError: + import logging + logger = logging.getLogger("yunohost.migration") + + +N_CURRENT_DEBIAN = 11 +N_CURRENT_YUNOHOST = 11 + +VENV_REQUIREMENTS_SUFFIX = ".requirements_backup_for_bookworm_upgrade.txt" + + +def _get_all_venvs(dir, level=0, maxlevel=3): + """ + Returns the list of all python virtual env directories recursively + + Arguments: + dir - the directory to scan in + maxlevel - the depth of the recursion + level - do not edit this, used as an iterator + """ + if not os.path.exists(dir): + return [] + + result = [] + # Using os functions instead of glob, because glob doesn't support hidden folders, and we need recursion with a fixed depth + for file in os.listdir(dir): + path = os.path.join(dir, file) + if os.path.isdir(path): + activatepath = os.path.join(path, "bin", "activate") + if os.path.isfile(activatepath): + content = read_file(activatepath) + if ("VIRTUAL_ENV" in content) and ("PYTHONHOME" in content): + result.append(path) + continue + if level < maxlevel: + result += _get_all_venvs(path, level=level + 1) + return result + + +def _backup_pip_freeze_for_python_app_venvs(): + """ + Generate a requirements file for all python virtual env located inside /opt/ and /var/www/ + """ + + venvs = _get_all_venvs("/opt/") + _get_all_venvs("/var/www/") + for venv in venvs: + # Generate a requirements file from venv + os.system( + f"{venv}/bin/pip freeze > {venv}{VENV_REQUIREMENTS_SUFFIX} 2>/dev/null" + ) + + +class MyMigration(Migration): + "Upgrade the system to Debian Bookworm and Yunohost 12.x" + + mode = "manual" + + def run(self): + self.check_assertions() + + logger.info(m18n.n("migration_0027_start")) + + # + # Add new apt .deb signing key + # + + new_apt_key = "https://forge.yunohost.org/yunohost_bookworm.asc" + os.system(f'wget --timeout 900 --quiet "{new_apt_key}" --output-document=- | gpg --dearmor >"/usr/share/keyrings/yunohost-bookworm.gpg"') + + # Add Sury key even if extra_php_version.list was already there, + # because some old system may be using an outdated key not valid for Bookworm + # and that'll block the migration + os.system( + 'wget --timeout 900 --quiet "https://packages.sury.org/php/apt.gpg" --output-document=- | gpg --dearmor >"/etc/apt/trusted.gpg.d/extra_php_version.gpg"' + ) + + # + # Patch sources.list + # + + logger.info(m18n.n("migration_0027_patching_sources_list")) + self.patch_apt_sources_list() + + # + # Get requirements of the different venvs from python apps + # + + _backup_pip_freeze_for_python_app_venvs() + + # + # Run apt update + # + + aptitude_with_progress_bar("update") + + # Tell libc6 it's okay to restart system stuff during the upgrade + os.system( + "echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections" + ) + + # Do not restart nginx during the upgrade of nginx-common and nginx-extras ... + # c.f. https://manpages.debian.org/bullseye/init-system-helpers/deb-systemd-invoke.1p.en.html + # and zcat /usr/share/doc/init-system-helpers/README.policy-rc.d.gz + # and the code inside /usr/bin/deb-systemd-invoke to see how it calls /usr/sbin/policy-rc.d ... + # and also invoke-rc.d ... + write_to_file( + "/usr/sbin/policy-rc.d", + '#!/bin/bash\n[[ "$1" =~ "nginx" ]] && exit 101 || exit 0', + ) + os.system("chmod +x /usr/sbin/policy-rc.d") + + # Don't send an email to root about the postgresql migration. It should be handled automatically after. + os.system( + "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" + ) + + # + # Patch yunohost conflicts + # + logger.info(m18n.n("migration_0027_patch_yunohost_conflicts")) + + self.patch_yunohost_conflicts() + + # + # Critical fix for RPI otherwise network is down after rebooting + # https://forum.yunohost.org/t/20652 + # + # FIXME : this is from buster->bullseye, do we still needed it ? + # + #if os.system("systemctl | grep -q dhcpcd") == 0: + # logger.info("Applying fix for DHCPCD ...") + # os.system("mkdir -p /etc/systemd/system/dhcpcd.service.d") + # write_to_file( + # "/etc/systemd/system/dhcpcd.service.d/wait.conf", + # "[Service]\nExecStart=\nExecStart=/usr/sbin/dhcpcd -w", + # ) + + # + # Main upgrade + # + logger.info(m18n.n("migration_0027_main_upgrade")) + + # Mark php, mariadb, metronome and rspamd as "auto" so that they may be uninstalled if they ain't explicitly wanted by app or admins + php_packages = self.get_php_packages() + aptitude_with_progress_bar(f"markauto mariadb-server metronome rspamd {' '.join(php_packages)}") + + # Hold import yunohost packages + apps_packages = self.get_apps_equivs_packages() + aptitude_with_progress_bar(f"hold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") + + aptitude_with_progress_bar("upgrade cron --show-why -y -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") + + # FIXME : find a way to simulate and validate the upgrade first + aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold'") + + if self.debian_major_version() == N_CURRENT_DEBIAN: + raise YunohostError("migration_0027_still_on_buster_after_main_upgrade") + + # Clean the mess + logger.info(m18n.n("migration_0027_cleaning_up")) + os.system( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt autoremove --assume-yes" + ) + os.system("apt clean --assume-yes") + + # + # Stupid hack for stupid dnsmasq not picking up its new init.d script then breaking everything ... + # https://forum.yunohost.org/t/20676 + # + # FIXME : this is from buster->bullseye, do we still needed it ? + # + #if os.path.exists("/etc/init.d/dnsmasq.dpkg-dist"): + # logger.info("Copying new version for /etc/init.d/dnsmasq ...") + # os.system("cp /etc/init.d/dnsmasq.dpkg-dist /etc/init.d/dnsmasq") + + # + # Yunohost upgrade + # + logger.info(m18n.n("migration_0027_yunohost_upgrade")) + + aptitude_with_progress_bar(f"unhold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") + + # FIXME : find a way to simulate and validate the upgrade first + # FIXME : why were libluajit needed in the first place ? + aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin moulinette ssowat libluajit-5.1-2- libluajit-5.1-common- -y -o Dpkg::Options::='--force-confold'") + + #cmd = "LC_ALL=C" + #cmd += " DEBIAN_FRONTEND=noninteractive" + #cmd += " APT_LISTCHANGES_FRONTEND=none" + #cmd += " apt dist-upgrade " + #cmd += " --quiet -o=Dpkg::Use-Pty=0 --fix-broken --dry-run" + #cmd += " | grep -q 'ynh-deps'" + + #logger.info("Simulating upgrade...") + #if os.system(cmd) == 0: + # raise YunohostError( + # "The upgrade cannot be completed, because some app dependencies would need to be removed?", + # raw_msg=True, + # ) + + # FIXME : + #postupgradecmds = "rm -f /usr/sbin/policy-rc.d\n" + #postupgradecmds += "echo 'Restarting nginx...' >&2\n" + #postupgradecmds += "systemctl restart nginx\n" + + def debian_major_version(self): + # The python module "platform" and lsb_release are not reliable because + # on some setup, they may still return Release=9 even after upgrading to + # buster ... (Apparently this is related to OVH overriding some stuff + # with /etc/lsb-release for instance -_-) + # Instead, we rely on /etc/os-release which should be the raw info from + # the distribution... + return int( + check_output( + "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" + ) + ) + + def yunohost_major_version(self): + return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) + + def check_assertions(self): + # Be on bullseye (11.x) and yunohost 11.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be > 12.x but yunohost package + # would still be in 11.x... + if ( + not self.debian_major_version() == N_CURRENT_DEBIAN + and not self.yunohost_major_version() == N_CURRENT_YUNOHOST + ): + try: + # Here we try to find the previous migration log, which should be somewhat recent and be at least 10k (we keep the biggest one) + maybe_previous_migration_log_id = check_output( + "cd /var/log/yunohost/categories/operation && find -name '*migrate*.log' -size +10k -mtime -100 -exec ls -s {} \\; | sort -n | tr './' ' ' | awk '{print $2}' | tail -n 1" + ) + if maybe_previous_migration_log_id: + logger.info( + f"NB: the previous migration log id seems to be {maybe_previous_migration_log_id}. You can share it with the support team with : sudo yunohost log share {maybe_previous_migration_log_id}" + ) + except Exception: + # Yeah it's not that important ... it's to simplify support ... + pass + + raise YunohostError("migration_0027_not_bullseye") + + # Have > 1 Go free space on /var/ ? + if free_space_in_directory("/var/") / (1024**3) < 1.0: + raise YunohostError("migration_0027_not_enough_free_space") + + # Have > 70 MB free space on /var/ ? + if free_space_in_directory("/boot/") / (1024**2) < 70.0: + raise YunohostError( + "/boot/ has less than 70MB available. This will probably trigger a crash during the upgrade because a new kernel needs to be installed. Please look for advice on the forum on how to remove old, unused kernels to free up some space in /boot/.", + raw_msg=True, + ) + + # Check system is up to date + # (but we don't if 'bullseye' is already in the sources.list ... + # which means maybe a previous upgrade crashed and we're re-running it) + if os.path.exists("/etc/apt/sources.list") and " bookworm " not in read_file( + "/etc/apt/sources.list" + ): + tools_update(target="system") + upgradable_system_packages = list(_list_upgradable_apt_packages()) + upgradable_system_packages = [ + package["name"] for package in upgradable_system_packages + ] + upgradable_system_packages = set(upgradable_system_packages) + # Lime2 have hold packages to avoid ethernet instability + # See https://github.com/YunoHost/arm-images/commit/b4ef8c99554fd1a122a306db7abacc4e2f2942df + lime2_hold_packages = set( + [ + "armbian-firmware", + "armbian-bsp-cli-lime2", + "linux-dtb-current-sunxi", + "linux-image-current-sunxi", + "linux-u-boot-lime2-current", + "linux-image-next-sunxi", + ] + ) + if upgradable_system_packages - lime2_hold_packages: + raise YunohostError("migration_0027_system_not_fully_up_to_date") + + @property + def disclaimer(self): + # Avoid having a super long disclaimer + uncessary check if we ain't + # on bullseye / yunohost 11.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be 12.x but yunohost package + # would still be in 11.x... + if ( + not self.debian_major_version() == N_CURRENT_DEBIAN + and not self.yunohost_major_version() == N_CURRENT_YUNOHOST + ): + return None + + # Get list of problematic apps ? I.e. not official or community+working + problematic_apps = unstable_apps() + problematic_apps = "".join(["\n - " + app for app in problematic_apps]) + + # Manually modified files ? (c.f. yunohost service regen-conf) + modified_files = manually_modified_files() + modified_files = "".join(["\n - " + f for f in modified_files]) + + message = m18n.n("migration_0027_general_warning") + + message = ( + "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/?? FIXME ?? \n\n" + + message + + "\n\n" + + "Packages 'metronome' (xmpp server) and 'rspamd' (mail antispam) are now optional dependencies and may get uninstalled during the upgrade. Make sure to explicitly re-install those using 'apt install' after the upgrade if you care about those!" + ) + + if problematic_apps: + message += "\n\n" + m18n.n( + "migration_0027_problematic_apps_warning", + problematic_apps=problematic_apps, + ) + + if modified_files: + message += "\n\n" + m18n.n( + "migration_0027_modified_files", manually_modified_files=modified_files + ) + + return message + + def patch_apt_sources_list(self): + sources_list = glob.glob("/etc/apt/sources.list.d/*.list") + if os.path.exists("/etc/apt/sources.list"): + sources_list.append("/etc/apt/sources.list") + + # This : + # - replace single 'bullseye' occurence by 'bookworm' + # - comments lines containing "backports" + # - replace 'bullseye/updates' by 'bookworm/updates' (or same with -) + # - make sure the yunohost line has the "signed-by" thingy + # Special note about the security suite: + # https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html#security-archive + for f in sources_list: + command = ( + f"sed -i {f} " + "-e 's@ bullseye @ bookworm @g' " + "-e '/backports/ s@^#*@#@' " + "-e 's@ bullseye/updates @ bookworm-security @g' " + "-e 's@ bullseye-@ bookworm-@g' " + "-e 's@deb.*http://forge.yunohost.org@deb [signed-by=/usr/share/keyrings/yunohost-bookworm.gpg] http://forge.yunohost.org@g' " + ) + os.system(command) + + # Stupid OVH has some repo configured which dont work with next debian and break apt ... + os.system("rm -f /etc/apt/sources.list.d/ovh-*.list") + + def get_apps_equivs_packages(self): + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep 'ynh-deps$' || true; }" + ) + + output = check_output(command) + + return output.split("\n") if output else [] + + def get_php_packages(self): + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep '^php' || true; }" + ) + + output = check_output(command) + + return output.split("\n") if output else [] + + def patch_yunohost_conflicts(self): + # + # This is a super dirty hack to remove the conflicts from yunohost's debian/control file + # Those conflicts are there to prevent mistakenly upgrading critical packages + # such as dovecot, postfix, nginx, openssl, etc... usually related to mistakenly + # using backports etc. + # + # The hack consists in savagely removing the conflicts directly in /var/lib/dpkg/status + # + + # We only patch the conflict if we're on yunohost 11.x + if self.yunohost_major_version() != N_CURRENT_YUNOHOST: + return + + conflicts = check_output("dpkg-query -s yunohost | grep '^Conflicts:'").strip() + if conflicts: + # We want to keep conflicting with apache/bind9 tho + new_conflicts = "Conflicts: apache2, bind9" + + command = ( + f"sed -i /var/lib/dpkg/status -e 's@{conflicts}@{new_conflicts}@g'" + ) + logger.debug(f"Running: {command}") + os.system(command) diff --git a/src/tools.py b/src/tools.py index 5b5fc3156..9075e0bfe 100644 --- a/src/tools.py +++ b/src/tools.py @@ -49,6 +49,7 @@ from yunohost.utils.system import ( ynh_packages_version, dpkg_is_broken, dpkg_lock_available, + _apt_log_line_is_relevant, ) from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger @@ -530,32 +531,6 @@ def tools_upgrade(operation_logger, target=None): operation_logger.success() -def _apt_log_line_is_relevant(line): - irrelevants = [ - "service sudo-ldap already provided", - "Reading database ...", - "Preparing to unpack", - "Selecting previously unselected package", - "Created symlink /etc/systemd", - "Replacing config file", - "Creating config file", - "Installing new version of config file", - "Installing new config file as you requested", - ", does not exist on system.", - "unable to delete old directory", - "update-alternatives:", - "Configuration file '/etc", - "==> Modified (by you or by a script) since installation.", - "==> Package distributor has shipped an updated version.", - "==> Keeping old config file as default.", - "is a disabled or a static unit", - " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", - "insserv: warning: current stop runlevel", - "insserv: warning: current start runlevel", - ] - return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) - - @is_unit_operation() def tools_shutdown(operation_logger, force=False): shutdown = force diff --git a/src/utils/system.py b/src/utils/system.py index 6a77e293b..f5d9e646c 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -191,3 +191,113 @@ def _dump_sources_list(): if line.startswith("#") or not line.strip(): continue yield filename.replace("/etc/apt/", "") + ":" + line.strip() + + +def aptitude_with_progress_bar(cmd): + + from moulinette.utils.process import call_async_output + + msg_to_verb = { + "Preparing for removal": "Removing", + "Preparing to configure": "Installing", + "Removing": "Removing", + "Unpacking": "Installing", + "Configuring": "Installing", + "Installing": "Installing", + "Installed": "Installing", + "Preparing": "Installing", + "Done": "Done", + "Failed?": "Failed?", + } + + disable_progress_bar = False + if cmd.startswith("update"): + # the status-fd does stupid stuff for 'aptitude update', percentage is always zero except last iteration + disable_progress_bar = True + + def log_apt_status_to_progress_bar(data): + + if disable_progress_bar: + return + + t, package, percent, msg = data.split(":", 3) + + # We only display the stuff related to download once + if t == "dlstatus": + if log_apt_status_to_progress_bar.download_message_displayed is False: + logger.info("Downloading...") + log_apt_status_to_progress_bar.download_message_displayed = True + return + + if package == "dpkg-exec": + return + if package and log_apt_status_to_progress_bar.previous_package and package == log_apt_status_to_progress_bar.previous_package: + return + + try: + percent = round(float(percent), 1) + except Exception: + return + + verb = "Processing" + for m, v in msg_to_verb.items(): + if msg.startswith(m): + verb = v + + log_apt_status_to_progress_bar.previous_package = package + + width = 20 + done = "#" * int(width * percent / 100) + remain = "." * (width - len(done)) + logger.info(f"[{done}{remain}] > {percent}% {verb} {package}\r") + + log_apt_status_to_progress_bar.previous_package = None + log_apt_status_to_progress_bar.download_message_displayed = False + + def strip_boring_dpkg_reading_database(s): + return re.sub(r'(\(Reading database ... \d*%?|files and directories currently installed.\))', '', s) + + callbacks = ( + lambda l: logger.debug(strip_boring_dpkg_reading_database(l).rstrip() + "\r"), + lambda l: logger.warning(l.rstrip() + "\r"), # ... aptitude has no stderr ? :| if _apt_log_line_is_relevant(l.rstrip()) else logger.debug(l.rstrip() + "\r"), + lambda l: log_apt_status_to_progress_bar(l.rstrip()), + ) + + cmd = ( + f'LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none aptitude {cmd} --quiet=2 -o=Dpkg::Use-Pty=0 -o "APT::Status-Fd=$YNH_STDINFO"' + ) + + logger.debug(f"Running: {cmd}") + + ret = call_async_output(cmd, callbacks, shell=True) + + if log_apt_status_to_progress_bar.previous_package is not None and ret == 0: + log_apt_status_to_progress_bar("done::100:Done") + elif ret != 0: + raise YunohostError(f"Failed to run command 'aptitude {cmd}'", raw_msg=True) + + +def _apt_log_line_is_relevant(line): + irrelevants = [ + "service sudo-ldap already provided", + "Reading database ...", + "Preparing to unpack", + "Selecting previously unselected package", + "Created symlink /etc/systemd", + "Replacing config file", + "Creating config file", + "Installing new version of config file", + "Installing new config file as you requested", + ", does not exist on system.", + "unable to delete old directory", + "update-alternatives:", + "Configuration file '/etc", + "==> Modified (by you or by a script) since installation.", + "==> Package distributor has shipped an updated version.", + "==> Keeping old config file as default.", + "is a disabled or a static unit", + " update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults", + "insserv: warning: current stop runlevel", + "insserv: warning: current start runlevel", + ] + return line.rstrip() and all(i not in line.rstrip() for i in irrelevants) From c6aec680b9555d6d4ef00934afd1ac515294e6e0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 4 Jul 2024 00:16:27 +0200 Subject: [PATCH 02/10] Backport i18n string + code for bookworm migration --- locales/en.json | 14 ++++++++++++++ src/migrations/0027_migrate_to_bookworm.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 760ab3a49..8aeafb936 100644 --- a/locales/en.json +++ b/locales/en.json @@ -591,6 +591,20 @@ "migration_0024_rebuild_python_venv_disclaimer_rebuild": "Rebuilding the virtualenv will be attempted for the following apps (NB: the operation may take some time!): {rebuild_apps}", "migration_0024_rebuild_python_venv_failed": "Failed to rebuild the Python virtualenv for {app}. The app may not work as long as this is not resolved. You should fix the situation by forcing the upgrade of this app using `yunohost app upgrade --force {app}`.", "migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", + "migration_description_0027_migrate_to_bookworm": "Upgrade the system to Debian Bookworm and YunoHost 12", + "migration_0027_cleaning_up": "Cleaning up cache and packages not useful anymore…", + "migration_0027_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", + "migration_0027_main_upgrade": "Starting main upgrade…", + "migration_0027_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", + "migration_0027_not_bullseye": "The current Debian distribution is not Bullseye! If you already ran the Bullseye->Bookworm migration, then this error is symptomatic of the fact that the migration procedure was not 100% succesful (otherwise YunoHost would have flagged it as completed). It is recommended to investigate what happened with the support team, who will need the **full** log of the `migration, which can be found in Tools > Logs in the webadmin.", + "migration_0027_not_enough_free_space": "Free space is pretty low in /var/! You should have at least 1GB free to run this migration.", + "migration_0027_patch_yunohost_conflicts": "Applying patch to workaround conflict issue…", + "migration_0027_patching_sources_list": "Patching the sources.lists…", + "migration_0027_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from the YunoHost app catalog, or are not flagged as 'working'. Consequently, it cannot be guaranteed that they will still work after the upgrade: {problematic_apps}", + "migration_0027_start": "Starting migration to Bullseye", + "migration_0027_still_on_bullseye_after_main_upgrade": "Something went wrong during the main upgrade, the system appears to still be on Debian Bullseye", + "migration_0027_system_not_fully_up_to_date": "Your system is not fully up-to-date. Please perform a regular upgrade before running the migration to Bullseye.", + "migration_0027_yunohost_upgrade": "Starting YunoHost core upgrade…", "migration_description_0021_migrate_to_bullseye": "Upgrade the system to Debian Bullseye and YunoHost 11.x", "migration_description_0022_php73_to_php74_pools": "Migrate php7.3-fpm 'pool' conf files to php7.4", "migration_description_0023_postgresql_11_to_13": "Migrate databases from PostgreSQL 11 to 13", diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 86d2ce49e..9ff497398 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -181,7 +181,7 @@ class MyMigration(Migration): aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold'") if self.debian_major_version() == N_CURRENT_DEBIAN: - raise YunohostError("migration_0027_still_on_buster_after_main_upgrade") + raise YunohostError("migration_0027_still_on_bullseye_after_main_upgrade") # Clean the mess logger.info(m18n.n("migration_0027_cleaning_up")) From 772e772b244377f219b32a4c1825f1e7780fb5b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 4 Jul 2024 19:13:43 +0200 Subject: [PATCH 03/10] bullseye->bookorm: delay the yunohost-api restart such that the migration doesnt appear as failed from the webamin --- locales/en.json | 3 ++- src/migrations/0027_migrate_to_bookworm.py | 11 +++++++++++ src/utils/system.py | 5 +++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 8aeafb936..46d2f03b8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -593,6 +593,7 @@ "migration_0024_rebuild_python_venv_in_progress": "Now attempting to rebuild the Python virtualenv for `{app}`", "migration_description_0027_migrate_to_bookworm": "Upgrade the system to Debian Bookworm and YunoHost 12", "migration_0027_cleaning_up": "Cleaning up cache and packages not useful anymore…", + "migration_0027_delayed_api_restart": "The YunoHost API will automatically be restarted in 15 seconds. It may be unavailable for a few seconds, and then you will have to login again.", "migration_0027_general_warning": "Please note that this migration is a delicate operation. The YunoHost team did its best to review and test it, but the migration might still break parts of the system or its apps.\n\nTherefore, it is recommended to:\n - Perform a backup of any critical data or app. More info on https://yunohost.org/backup;\n - Be patient after launching the migration: Depending on your Internet connection and hardware, it might take up to a few hours for everything to upgrade.", "migration_0027_main_upgrade": "Starting main upgrade…", "migration_0027_modified_files": "Please note that the following files were found to be manually modified and might be overwritten following the upgrade: {manually_modified_files}", @@ -796,4 +797,4 @@ "yunohost_installing": "Installing YunoHost…", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", "yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc." -} \ No newline at end of file +} diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 9ff497398..e3d6007c6 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -1,5 +1,6 @@ import glob import os +import subprocess from moulinette import m18n from yunohost.utils.error import YunohostError @@ -230,6 +231,16 @@ class MyMigration(Migration): #postupgradecmds += "echo 'Restarting nginx...' >&2\n" #postupgradecmds += "systemctl restart nginx\n" + # If running from the webadmin, restart the API after a delay + if Moulinette.interface.type == "api": + logger.warning(m18n.n("migration_0027_delayed_api_restart")) + sleep(5) + # Restart the API after 10 sec (at now doesn't support sub-minute times...) + # We do this so that the API / webadmin still gets the proper HTTP response + cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart yunohost-api"' + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd]) + def debian_major_version(self): # The python module "platform" and lsb_release are not reliable because # on some setup, they may still return Release=9 even after upgrading to diff --git a/src/utils/system.py b/src/utils/system.py index 105aea704..690c8f3c8 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -287,6 +287,11 @@ def aptitude_with_progress_bar(cmd): f'LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none aptitude {cmd} --quiet=2 -o=Dpkg::Use-Pty=0 -o "APT::Status-Fd=$YNH_STDINFO"' ) + # If upgrading yunohost from the API, delay the Yunohost-api restart + # (this should be the last time we need it before bookworm, because on bookworm, yunohost-admin cookies will be persistent upon api restart) + if " yunohost " in cmd and Moulinette.interface.type == "api": + cmd = "YUNOHOST_API_RESTART_WILL_BE_HANDLED_BY_YUNOHOST=yes " + cmd + logger.debug(f"Running: {cmd}") ret = call_async_output(cmd, callbacks, shell=True) From c694ea2cbca91ac4fe126b99e7c9474bc1c03ba2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 4 Jul 2024 19:27:51 +0200 Subject: [PATCH 04/10] bullseye->bookworm: force-regen the nsswitch configuration because for some reason it gets reset? --- src/migrations/0027_migrate_to_bookworm.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index e3d6007c6..898c4e347 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -12,7 +12,7 @@ from yunohost.tools import ( tools_update, ) from yunohost.app import unstable_apps -from yunohost.regenconf import manually_modified_files +from yunohost.regenconf import manually_modified_files, regen_conf from yunohost.utils.system import ( free_space_in_directory, get_ynh_package_version, @@ -181,6 +181,11 @@ class MyMigration(Migration): # FIXME : find a way to simulate and validate the upgrade first aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold'") + # Force regenconf of nsswitch because for some reason + # /etc/nsswitch.conf is reset despite the --force-confold? It's a + # disaster because then admins cannot "sudo" >_> ... + regen_conf(names=["nsswitch"], force=True) + if self.debian_major_version() == N_CURRENT_DEBIAN: raise YunohostError("migration_0027_still_on_bullseye_after_main_upgrade") From f344cb037bb5a569a7791ce10f1e2cc5066b3c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Thu, 4 Jul 2024 21:00:38 +0200 Subject: [PATCH 05/10] Fix missing import of moulinette.Moulinette --- src/migrations/0027_migrate_to_bookworm.py | 2 +- src/utils/system.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 898c4e347..0e0388d8b 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -2,7 +2,7 @@ import glob import os import subprocess -from moulinette import m18n +from moulinette import Moulinette, m18n from yunohost.utils.error import YunohostError from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_file, write_to_file diff --git a/src/utils/system.py b/src/utils/system.py index 690c8f3c8..497f6d8df 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -20,6 +20,7 @@ import re import os import logging +from moulinette import Moulinette from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError From 90d4cd99b9af877e305040399ec420bd0529d87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Thu, 4 Jul 2024 21:26:38 +0200 Subject: [PATCH 06/10] Add missing from time import sleep ; also restart nginx at the end of the migration --- src/migrations/0027_migrate_to_bookworm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 0e0388d8b..57bbecafc 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -1,6 +1,7 @@ import glob import os import subprocess +from time import sleep from moulinette import Moulinette, m18n from yunohost.utils.error import YunohostError @@ -242,7 +243,7 @@ class MyMigration(Migration): sleep(5) # Restart the API after 10 sec (at now doesn't support sub-minute times...) # We do this so that the API / webadmin still gets the proper HTTP response - cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart yunohost-api"' + cmd = 'at -M now >/dev/null 2>&1 <<< "sleep 10; systemctl restart nginx yunohost-api"' # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... subprocess.check_call(["bash", "-c", cmd]) From 2763e04012df8cab95df8794a665ee516508b6f4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jul 2024 00:32:06 +0200 Subject: [PATCH 07/10] bullseye->bookworm: dirty hack to explicitly remove rspamd because it's causing too many issues in dependency resolution idk --- src/migrations/0027_migrate_to_bookworm.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 57bbecafc..835b45ca5 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -177,10 +177,17 @@ class MyMigration(Migration): apps_packages = self.get_apps_equivs_packages() aptitude_with_progress_bar(f"hold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") - aptitude_with_progress_bar("upgrade cron --show-why -y -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") + # Dirty hack to be able to remove rspamd because it's causing too many issues due to libluajit ... + command = ( + f"sed -i /var/lib/dpkg/status -e 's@rspamd, @@g'" + ) + logger.debug(f"Running: {command}") + os.system(command) + + aptitude_with_progress_bar("upgrade cron rspamd- libluajit-5.1-2- --show-why -y -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") # FIXME : find a way to simulate and validate the upgrade first - aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold'") + aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold' <<< 'y\ny\ny'") # Force regenconf of nsswitch because for some reason # /etc/nsswitch.conf is reset despite the --force-confold? It's a @@ -215,8 +222,7 @@ class MyMigration(Migration): aptitude_with_progress_bar(f"unhold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") # FIXME : find a way to simulate and validate the upgrade first - # FIXME : why were libluajit needed in the first place ? - aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin moulinette ssowat libluajit-5.1-2- libluajit-5.1-common- -y -o Dpkg::Options::='--force-confold'") + aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin moulinette ssowat -y -o Dpkg::Options::='--force-confold'") #cmd = "LC_ALL=C" #cmd += " DEBIAN_FRONTEND=noninteractive" From 0f34d7e10f2472a65fca7e8dd377a35973580f23 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jul 2024 16:55:47 +0200 Subject: [PATCH 08/10] bullseye->bookworm: more tweaks for the 'assume yes' in aptitude call, can't use raw bash redirects, gotta use stdin= from subprocess ... and we want only a limited number of 'yes' and not an infinite yes like the -y option does resuling in conflict resolution loops --- src/migrations/0027_migrate_to_bookworm.py | 10 ++++------ src/utils/system.py | 6 +++++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py index 835b45ca5..17b252871 100644 --- a/src/migrations/0027_migrate_to_bookworm.py +++ b/src/migrations/0027_migrate_to_bookworm.py @@ -178,16 +178,14 @@ class MyMigration(Migration): aptitude_with_progress_bar(f"hold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") # Dirty hack to be able to remove rspamd because it's causing too many issues due to libluajit ... - command = ( - f"sed -i /var/lib/dpkg/status -e 's@rspamd, @@g'" - ) + command = "sed -i /var/lib/dpkg/status -e 's@rspamd, @@g'" logger.debug(f"Running: {command}") os.system(command) - aptitude_with_progress_bar("upgrade cron rspamd- libluajit-5.1-2- --show-why -y -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") + aptitude_with_progress_bar("upgrade cron rspamd- libluajit-5.1-2- --show-why -o APT::Force-LoopBreak=1 -o Dpkg::Options::='--force-confold'") # FIXME : find a way to simulate and validate the upgrade first - aptitude_with_progress_bar("full-upgrade --show-why -y -o Dpkg::Options::='--force-confold' <<< 'y\ny\ny'") + aptitude_with_progress_bar("full-upgrade --show-why -o Dpkg::Options::='--force-confold'") # Force regenconf of nsswitch because for some reason # /etc/nsswitch.conf is reset despite the --force-confold? It's a @@ -222,7 +220,7 @@ class MyMigration(Migration): aptitude_with_progress_bar(f"unhold yunohost moulinette ssowat yunohost-admin {' '.join(apps_packages)}") # FIXME : find a way to simulate and validate the upgrade first - aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin moulinette ssowat -y -o Dpkg::Options::='--force-confold'") + aptitude_with_progress_bar("full-upgrade --show-why yunohost yunohost-admin moulinette ssowat -o Dpkg::Options::='--force-confold'") #cmd = "LC_ALL=C" #cmd += " DEBIAN_FRONTEND=noninteractive" diff --git a/src/utils/system.py b/src/utils/system.py index 497f6d8df..9b6de576c 100644 --- a/src/utils/system.py +++ b/src/utils/system.py @@ -284,6 +284,7 @@ def aptitude_with_progress_bar(cmd): lambda l: log_apt_status_to_progress_bar(l.rstrip()), ) + original_cmd = cmd cmd = ( f'LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none aptitude {cmd} --quiet=2 -o=Dpkg::Use-Pty=0 -o "APT::Status-Fd=$YNH_STDINFO"' ) @@ -295,12 +296,15 @@ def aptitude_with_progress_bar(cmd): logger.debug(f"Running: {cmd}") + read, write = os.pipe() + os.write(write, b"y\ny\ny") + os.close(write) ret = call_async_output(cmd, callbacks, shell=True) if log_apt_status_to_progress_bar.previous_package is not None and ret == 0: log_apt_status_to_progress_bar("done::100:Done") elif ret != 0: - raise YunohostError(f"Failed to run command 'aptitude {cmd}'", raw_msg=True) + raise YunohostError(f"Failed to run command 'aptitude {original_cmd}'", raw_msg=True) def _apt_log_line_is_relevant(line): From 26fba087d6614f15147f17855e1f8eef7bda8b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Mon, 8 Jul 2024 22:37:40 +0200 Subject: [PATCH 09/10] Add aptitude to deps for the migration to bookworm --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 8139375e7..31190903e 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon, , python-is-python3 , nginx, nginx-extras (>=1.18) - , apt, apt-transport-https, apt-utils, dirmngr + , apt, apt-transport-https, apt-utils, aptitude, dirmngr , openssh-server, iptables, fail2ban, bind9-dnsutils , openssl, ca-certificates, netcat-openbsd, iproute2 , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd From 49961145caf4a24cd6d9af8100b0c9325184cc21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Mon, 8 Jul 2024 23:18:36 +0200 Subject: [PATCH 10/10] Disable migration to bookworm until it is ready --- ...igrate_to_bookworm.py => 0027_migrate_to_bookworm.py.disabled} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/migrations/{0027_migrate_to_bookworm.py => 0027_migrate_to_bookworm.py.disabled} (100%) diff --git a/src/migrations/0027_migrate_to_bookworm.py b/src/migrations/0027_migrate_to_bookworm.py.disabled similarity index 100% rename from src/migrations/0027_migrate_to_bookworm.py rename to src/migrations/0027_migrate_to_bookworm.py.disabled