diff --git a/.buckconfig b/.buckconfig index b0ed754..ff14ae5 100644 --- a/.buckconfig +++ b/.buckconfig @@ -1,6 +1,11 @@ # Documentation: https://buckbuild.com/concept/buckconfig.html -[python] - interpreter = python2.7 +[parser] + python_interpreter = /usr/bin/python2 + [python#py3] - interpreter = /usr/bin/python3.6 \ No newline at end of file + interpreter = /usr/bin/python3 + +[repositories] + ubuntu18to20 = . + dist-upgrader = ./dist-upgrader diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96e0612..998de0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,18 +4,23 @@ on: [push] jobs: build: - runs-on: ubuntu-20.04 # Not latest, because python3.6 is not available on latest - # https://github.com/actions/setup-python/issues/544 + # Not latest, because python3.6 is not available on latest + # (https://github.com/actions/setup-python/issues/544), + # and SandakovMM/build-with-buck@v2 action requires it + runs-on: ubuntu-20.04 steps: - name: Checkout repository and submodules - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: recursive + # We use tags to determine version, so fetch them + fetch-depth: 0 + fetch-tags: true - name: Prepare artifact store run: mkdir -p ./buck-out/gen - name: Build ubuntu18to20 id: build - uses: SandakovMM/build-with-buck@v2 + uses: SandakovMM/build-with-buck@v3 with: command: build target: :ubuntu18to20 diff --git a/.gitmodules b/.gitmodules index 53b9f64..7946a51 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "common"] - path = common - url = https://github.com/plesk/distro-conversion-base +[submodule "dist-upgrader"] + path = dist-upgrader + url = https://github.com/plesk/dist-upgrader.git diff --git a/BUCK b/BUCK index 3ad5ebe..b6ccb52 100644 --- a/BUCK +++ b/BUCK @@ -1,44 +1,23 @@ # Copyright 1999-2023. Plesk International GmbH. All rights reserved. # vim:ft=python: -PRODUCT_VERSION = '1.0.0' - -genrule( - name = 'version', - out = 'version.json', - bash = r"""echo "{\"version\": \"%s\", \"revision\": \"`git rev-parse HEAD`\"}" > $OUT""" % (PRODUCT_VERSION), -) - -python_library( - name = 'actions.lib', - srcs = glob(['./actions/*.py']), -) - -python_library( - name = 'ubuntu18to20.lib', - srcs = glob(['main.py', 'messages.py']), - deps = [ - '//common:common.lib', - ':actions.lib', - ], - resources = [ - ':version', - ], -) +include_defs('//product.defs.py') python_binary( - name = 'ubuntu18to20-script', + name = 'ubuntu18to20.pex', platform = 'py3', - main_module = 'main', + build_args = ['--python-shebang', '/usr/bin/env python3'], + main_module = 'ubuntu18to20.main', deps = [ - ':ubuntu18to20.lib', + 'dist-upgrader//pleskdistup:lib', + '//ubuntu18to20:lib', ], ) genrule( name = 'ubuntu18to20', - srcs = [':ubuntu18to20-script'], + srcs = [':ubuntu18to20.pex'], out = 'ubuntu18to20', - cmd = 'cp $(location :ubuntu18to20-script) $OUT && chmod +x $OUT', + cmd = 'cp $(location :ubuntu18to20.pex) $OUT && chmod +x $OUT', ) diff --git a/README.md b/README.md index d6a76bf..e092d8e 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,49 @@ -# The tool to distupgrade an Ubuntu 18 server with Plesk to to Ubuntu 20 - -Ubuntu 18 to Ubuntu 20 distupgrade tool +# The tool to dist-upgrade servers with Plesk from Ubuntu 18 to 20 ## Introduction -This script is the official tool for distupgrade an Ubuntu 18 server with Plesk to Ubuntu 20. The script is based on the official ubuntu distupgrade tool. The script includes additional repository and configuration support provided by Plesk. +This utility is the official tool to dist-upgrade servers with Plesk from Ubuntu 18 to 20. + +The utility uses [Plesk dist-upgrader](https://github.com/plesk/dist-upgrader). ## Preparation -To avoid downtime and data loss, make sure you have read and understood the following information before using the script: +To avoid downtime and data loss, make sure you have read and understood the following information before using the utility: 1. **Upgrade Plesk to the last version.** 2. **Create a full server backup.** Before the upgrade, make a full server backup (which includes a full backup of all the databases). -3. Notify the customers about upcoming downtime. Expected downtime is between 25 and 35 minutes. +3. Notify the customers about upcoming downtime. Expected downtime is 20-30 minutes. 4. **Remote management module must be installed on the server**. 5. We strongly recommend that you **create a snapshot you can use as a recovery point** in case the conversion process fails. 6. Read the [Known issues](#known-issues) section below for the list of known issues. ## Timing -The conversion process should run between 25 and 35 minutes. **Plesk services, hosted websites, and emails will be unavailable during the entirety of the conversion process**. +The conversion process should run between 20 and 30 minutes. **Plesk services, hosted websites, and e-mails will be unavailable during the entirety of the conversion process**. ## Known issues ### Blockers -Do not use the script if any of the following is true: -- **You are running an OS other than Ubuntu 18**. The script was designed to convert Ubuntu 18 servers only. Please, don't use it for other distributions. -- **PHP 7.1 and earlier are not supported** in Ubuntu 20, and will not receive any updates after the conversion. These PHP versions are deprecated and may have security vulnerabilities. So we force to remove this versions before the conversion. -- **Distupgrade inside containers (like Virtuozzo containers, Docker Containers, etc) are not supported**. +Do not use the utility if any of the following is true: +- **Your system is in a container (like Virtuozzo containers, Docker Containers, etc).** ## Requirements - Last Plesk version. -- Ubuntu 18 -- grub is installed - At least 5 GB of free disk space. - At least 2 GB of RAM. -## Using the script -To retrieve the latest available version of the tool, please navigate to the "Releases" section. Once there, locate the most recent version of the tool and download the zip archive. The zip archive will contain the ubuntu18to20 tool binary. +## Conversion phases +The conversion process consists of two phases: +1. The "convert" phase contains preparation and upgrading actions. +2. The "finish" phase is the last phase containing all finishing actions. + +During each phase a conversion plan consisting of stages, which in turn consist of actions, is executed. You can see the general stages in the `--help` output and the detailed plan in the `--show-plan` output. + +## Using the utility +To retrieve the latest available version of the tool, please navigate to the "Releases" section. Once there, locate the most recent version of the tool and download the attached archive. -To prepare the latest version of the tool for use from a command line, please run the following commands: +To prepare the latest version of the tool for use, please run the following commands: ```shell -> wget https://github.com/plesk/ubuntu18to20/releases/download/v1.0.0/ubuntu18to20-1.0.0.zip -> unzip ubuntu18to20-1.0.0.zip +> unzip ubuntu18to20.zip > chmod 755 ubuntu18to20 ``` -To monitor the conversion process, we recommend using the ['screen' utility](https://www.gnu.org/software/screen/) to run the script in the background. To do so, run the following command: +To monitor the conversion process, we recommend using the ['screen' utility](https://www.gnu.org/software/screen/) to run the utility in the background. To do so, run the following command: ```shell > screen -S ubuntu18to20 > ./ubuntu18to20 @@ -51,11 +53,11 @@ If you lose your SSH connection to the server, you can reconnect to the screen s > screen -r ubuntu18to20 ``` - You can also call ubuntu18to20 in the background: ```shell > ./ubuntu18to20 & ``` + And monitor its status with the '--status' or '--monitor' flags: ```shell > ./ubuntu18to20 --status @@ -63,76 +65,61 @@ And monitor its status with the '--status' or '--monitor' flags: ... live monitor session ... ``` -After reboot to the Ubuntu 20, you can check the status of the conversion process by running the following command: +The conversion process requires 3 reboots. It will be resumed automatically after reboot by the `plesk-dist-upgrader` systemd service. In addition to `--status` and `--monitor`, you can check the status of the conversion process by running the following command: ```shell -> python3.8 ./ubuntu18to20 --status -> python3.8 ./ubuntu18to20 --monitor +> systemctl status plesk-dist-upgrader ... live monitor session ... ``` -Running ubuntu18to20 without any arguments initiates the conversion process. The script performs preliminary checks, and if any issues are detected, it provides descriptions of the problems along with guidance on how to resolve them. -Following the preliminary checks, the tool proceeds with the distupgrade process, which is divided into two stages: the distupgrade stage, lasting approximately 20 minutes, and the finishing stage. The distupgrade stage involves the actual upgrade process, after which the server reboots. -Upon reboot, the finishing stage commences, typically taking about 5 minutes to complete. This stage triggers a second reboot at its conclusion. -Upon your next SSH login, you will encounter the following message: +Running dist-upgrader without any arguments initiates the conversion process. The utility performs preliminary checks, and if any issues are detected, it provides descriptions of the problems along with guidance on how to resolve them. +Following the preliminary checks, the tool proceeds with the dist-upgrade process, which is divided into multiple stages. Some stages end with a reboot. You can check the list of stages and steps by `./ubuntu18to20 --show-plan`. +When dist-upgrade is finished, you will see the following login message: ``` =============================================================================== -Message from the Plesk ubuntu18to20 tool: +Message from the Plesk dist-upgrader tool: The server has been upgraded to Ubuntu 20. You can remove this message from the /etc/motd file. =============================================================================== ``` -### Conversion stage options -The conversion process consists of two stage options: "start", and "finish". To run stages individually, use the "--start", and "--finish" flags, or the "-s" flag with name of the stage you want to run. -1. The "start" stage start distupgrade process. -2. The "finish" stage must be called on the first boot of Ubuntu 20. You can rerun this stage if something goes wrong during the first boot to ensure that the problem is fixed and Plesk is ready to use. - -### Other arguments +## Logs +If something goes wrong, read the logs to identify the problem. +The dist-upgrader writes its log to the `/var/log/plesk/ubuntu18to20.log` file, as well as to stdout. +After the first reboot, the process is resumed by the `plesk-dist-upgrader` service, so its output is available in system logs (see `systemctl status plesk-dist-upgrader` and `journalctl -u plesk-dist-upgrader`). -### Logs -If something goes wrong, read the logs to identify the problem. You can also read the logs to check the status of the finish stage during the first boot. -The ubuntu18to20 writes its log to the '/var/log/plesk/ubuntu18to20.log' file, as well as to stdout. - -### Revert -If the script fails during the the "start" stage before the distupgrade performs, you can use the ubuntu18to20 script with the '-r' or '--revert' flags to restore Plesk to normal operation. The ubuntu18to20 will undo some of the changes it made and restart Plesk services. Once you have resolved the root cause of the failure, you can attempt the conversion again. +## Revert +If the utility fails during the the "convert" stage before actual dist-upgrade of packages, you can use the dist-upgrader utility with the `-r` or `--revert` options to restore Plesk to normal operation. The dist-upgrader will undo some of the changes it made and restart Plesk services. Once you have resolved the root cause of the failure, you can attempt the conversion again. Note: -- You cannot use revert to undo the changes after the distupgrade take it's place, because packages provided by Ubuntu 20 already installed. +- You cannot use revert to undo the changes after the dist-upgrade of packages, because packages provided by the new OS version are already installed. +- `--revert` mode is not perfect, it can fail or be unable to restore the initial state of the system. So, the importance of creating full server backup or snapshot before starting dist-upgrade can't be stressed enough. -### Check the status of the conversion process and monitor its progress -To check the status of the conversion process, use the '--status' flag. You can see the current stage of the conversion process, the elapsed time, and the estimated time until finish. +### Checking the status of the conversion process and monitoring its progress +To check the status of the conversion process, use the `--status` option. You can see the current stage of the conversion process, the elapsed time, and the estimated time until finish. ```shell > ./ubuntu18to20 --status ``` -To monitor the progress of the conversion process in real time, use the '--monitor' flag. +To monitor the progress of the conversion process in real time, use the `--monitor` option. ```shell > ./ubuntu18to20 --monitor ( stage 3 / action re-installing plesk components ) 02:26 / 06:18 ``` -After the first reboot you should call the script directly by python3.8 interpreter: -```shell -> python3.8 ./ubuntu18to20 --status -> python3.8 ./ubuntu18to20 --monitor -... live monitor session ... -``` - ## Issue handling -### ubuntu18to20 finish fails on the first boot -If something goes wrong during the finish stage, you will be informed on the next SSH login with this message: +If for some reason the process has failed, inspect the log. By default, it's put to `/var/log/plesk/ubuntu18to20.log`. If the process was interrupted before the first reboot, you can restart it with the `--resume` option. If the problem has happened after the first reboot, you can restart the process by running `systemctl restart plesk-dist-upgrader`. + +If something goes wrong, you will be informed on the next login with this message: ``` =============================================================================== -Message from Plesk ubuntu18to20 tool: -Something went wrong during the final stage of Ubuntu 18 to Ubuntu 20 conversion +Message from Plesk dist-upgrader tool: +Something went wrong during dist-upgrade by ubuntu18to20. See the /var/log/plesk/ubuntu18to20.log file for more information. You can remove this message from the /etc/motd file. =============================================================================== ``` -You can read the ubuntu18to20 log to troubleshoot the issue. If the ubuntu18to20 finish stage fails for any reason, once you have resolved the root cause of the failure, you can retry by running 'python3.8 ubuntu18to20 -s finish'. ### Send feedback -If you got any error, please [create an issue on github](https://github.com/plesk/ubuntu18to20/issues). To do generate feedback archive by calling the tool with '-f' or '--prepare-feedback' flags. +If you got any error, please [create an issue on github](https://github.com/plesk/ubuntu18to20/issues). Describe your problem and attach the feedback archive or at least the log to the issue. The feedback archive can be created by calling the tool with the `--prepare-feedback` option: ```shell > ./ubuntu18to20 --prepare-feedback ``` -Describe your problem and attach the feedback archive to the issue. \ No newline at end of file diff --git a/actions/__init__.py b/actions/__init__.py deleted file mode 100644 index e8b6967..0000000 --- a/actions/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -from .common_checks import * -from .common import * -from .distupgrade import * -from .extensions import * -from .mariadb import * -from .packages import * -from .spamassassin import * -from .systemd import * \ No newline at end of file diff --git a/actions/common.py b/actions/common.py deleted file mode 100644 index 7a66cd7..0000000 --- a/actions/common.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -import os -import shutil -import sys - -from common import action, files, motd, plesk - - -class MoveOldBindConfigToNamed(action.ActiveAction): - def __init__(self): - self.name = "move old bind configuration to named" - self.old_bind_config_path = "/etc/default/bind9" - self.dst_config_path = "/etc/default/named" - - def _is_required(self) -> bool: - return os.path.exists(self.old_bind_config_path) - - def _prepare_action(self): - pass - - def _post_action(self): - shutil.move(self.old_bind_config_path, self.dst_config_path) - - def _revert_action(self): - pass - - -class AddFinishSshLoginMessage(action.ActiveAction): - def __init__(self): - self.name = "add finish ssh login message" - self.finish_message = """ -The server has been upgraded to Ubuntu 20. -""" - - def _prepare_action(self) -> None: - pass - - def _post_action(self) -> None: - motd.add_finish_ssh_login_message(self.finish_message) - motd.publish_finish_ssh_login_message() - - def _revert_action(self) -> None: - pass - - -class AddInProgressSshLoginMessage(action.ActiveAction): - def __init__(self): - self.name = "add in progress ssh login message" - path_to_script = os.path.abspath(sys.argv[0]) - self.in_progress_message = f""" -=============================================================================== -Message from the Plesk ubuntu18to20 tool: -The server is being converted to Ubuntu 20. Please wait. -To see the current conversion status, run the '{path_to_script} --status' command. -To monitor the conversion progress in real time, run the '{path_to_script} --monitor' command. -=============================================================================== -""" - - def _prepare_action(self) -> None: - motd.add_inprogress_ssh_login_message(self.in_progress_message) - - def _post_action(self) -> None: - pass - - def _revert_action(self) -> None: - motd.restore_ssh_login_message() - - -class DisablePleskSshBanner(action.ActiveAction): - def __init__(self): - self.name = "disable plesk ssh banner" - self.banner_command_path = "/root/.plesk_banner" - - def _prepare_action(self) -> None: - if os.path.exists(self.banner_command_path): - files.backup_file(self.banner_command_path) - os.unlink(self.banner_command_path) - - def _post_action(self) -> None: - files.restore_file_from_backup(self.banner_command_path) - - def _revert_action(self) -> None: - files.restore_file_from_backup(self.banner_command_path) - - -class HandleConversionStatus(action.ActiveAction): - def __init__(self): - self.name = "prepare and send conversion status" - - def _prepare_action(self) -> None: - plesk.prepare_conversion_flag() - - def _post_action(self) -> None: - plesk.send_conversion_status(True) - - def _revert_action(self) -> None: - plesk.remove_conversion_flag() - - -class CleanApparmorCacheConfig(action.ActiveAction): - def __init__(self): - self.name = "clean apparmor cache configuration" - self.possible_locations = ["/etc/apparmor/cache", "/etc/apparmor.d/cache"] - - def _is_required(self) -> bool: - return len([location for location in self.possible_locations if os.path.exists(location)]) > 0 - - def _prepare_action(self): - for location in self.possible_locations: - if os.path.exists(location): - shutil.move(location, location + ".backup") - - def _post_action(self): - for location in self.possible_locations: - location = location + ".backup" - if os.path.exists(location): - shutil.rmtree(location) - - def _revert_action(self): - for location in self.possible_locations: - if os.path.exists(location): - shutil.move(location + ".backup", location) - - def estimate_prepare_time(self): - return 1 - - def estimate_post_time(self): - return 1 - - def estimate_revert_time(self): - return 1 diff --git a/actions/common_checks.py b/actions/common_checks.py deleted file mode 100644 index 800d3dd..0000000 --- a/actions/common_checks.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -import os -import subprocess - -from common import action, dist, log, plesk, packages, version - - -class PleskInstallerNotInProgress(action.CheckAction): - def __init__(self): - self.name = "checking if Plesk installer is in progress" - self.description = """The conversion process cannot continue because Plesk Installer is working. -\tPlease wait until it finishes or call 'plesk installer stop' to abort it. -""" - - def _do_check(self) -> bool: - installer_status = subprocess.check_output(["/usr/sbin/plesk", "installer", "--query-status", "--enable-xml-output"], - universal_newlines=True) - if "query_ok" in installer_status: - return True - return False - - -class DistroIsUbuntu18(action.CheckAction): - def __init__(self): - self.name = "checking if distro is Ubuntu18" - self.description = "You are running a distributive other than Ubuntu 18. The tool supports only Ubuntu 18" - - def _do_check(self) -> bool: - return dist.get_distro() == dist.Distro.ubuntu18 - - -class PleskVersionIsActual(action.CheckAction): - def __init__(self): - self.name = "checking if Plesk version is actual" - self.description = "Only Plesk Obsidian 18.0.43 or later is supported. Update Plesk to version 18.0.43 or later and try again." - - def _do_check(self) -> bool: - try: - major, _, iter, _ = plesk.get_plesk_version() - return int(major) >= 18 and int(iter) >= 43 - except Exception as ex: - log.warn("Checking plesk version is failed with error: {}".format(ex)) - - return False - - -class CheckOutdatedPHP(action.CheckAction): - def __init__(self, first_allowed: str): - self.name = "checking outdated PHP" - self.first_allowed = version.PHPVersion(first_allowed) - self.description = "Outdated PHP versions were detected: '{}'. To proceed with the conversion:" - self.fix_domains_step = """Switch the following domains to {} or later: -\t- {} - -\tYou can do so by running the following command: -\t> plesk bin domain -u [domain] -php_handler_id plesk-php80-fastcgi -""" - self.remove_php_step = """Remove outdated PHP packages via Plesk Installer. You can do it by calling the following command: -\tplesk installer remove --components {} -""" - - def _do_check(self) -> bool: - known_php_versions = [ - version.PHPVersion("PHP 5.2"), - version.PHPVersion("PHP 5.3"), - version.PHPVersion("PHP 5.4"), - version.PHPVersion("PHP 5.5"), - version.PHPVersion("PHP 5.6"), - version.PHPVersion("PHP 7.0"), - version.PHPVersion("PHP 7.1"), - ] - outdated_php_versions = [php for php in known_php_versions if php < self.first_allowed] - outdated_php_packages = {f"plesk-php{php.major}{php.minor}": str(php) for php in outdated_php_versions} - - installed_pkgs = packages.filter_installed_packages(outdated_php_packages.keys()) - if len(installed_pkgs) == 0: - return True - - php_hanlers = {"'{}-fastcgi'", "'{}-fpm'", "'{}-fpm-dedicated'"} - outdated_php_handlers = [] - for installed in installed_pkgs: - outdated_php_handlers += [handler.format(installed) for handler in php_hanlers] - - try: - looking_for_domains_sql_request = """ - SELECT d.name FROM domains d JOIN hosting h ON d.id = h.dom_id WHERE h.php_handler_id in ({}); - """.format(", ".join(outdated_php_handlers)) - outdated_php_domains = subprocess.check_output(["/usr/sbin/plesk", "db", looking_for_domains_sql_request], - universal_newlines=True) - outdated_php_domains = [domain[2:-2] for domain in outdated_php_domains.splitlines() - if domain.startswith("|") and not domain.startswith("| name ")] - outdated_php_domains = "\n\t- ".join(outdated_php_domains) - except Exception: - outdated_php_domains = "Unable to get domains list. Please check it manually." - - self.description = self.description.format(", ".join([outdated_php_packages[installed] for installed in installed_pkgs])) - if outdated_php_domains: - self.description += "\n\t1. " + self.fix_domains_step.format(self.first_allowed, outdated_php_domains) + "\n\t2. " - else: - self.description += "\n\t" - - self.description += self.remove_php_step.format(" ".join(outdated_php_packages[installed].replace(" ", "") for installed in installed_pkgs).lower()) - - return False - - -class CheckIsInContainer(action.CheckAction): - def __init__(self): - self.name = "checking if the system not in a container" - self.description = "The system is running in a container-like environment ({}). The conversion is not supported for such systems." - - def _is_docker(self) -> bool: - return os.path.exists("/.dockerenv") - - def _is_podman(self) -> bool: - return os.path.exists("/run/.containerenv") - - def _is_vz_like(self) -> bool: - return os.path.exists("/proc/vz") - - def _do_check(self) -> bool: - if self._is_docker(): - self.description = self.description.format("Docker container") - return False - elif self._is_podman(): - self.description = self.description.format("Podman container") - return False - elif self._is_vz_like(): - self.description = self.description.format("Virtuozzo container") - return False - - return True - - -class PleskWatchdogIsntInstalled(action.CheckAction): - def __init__(self): - self.name = "checking if Plesk watchdog extension is not installed" - self.description = """The Plesk Watchdog extension is installed. Unfortunately the extension is unsupported on Ubuntu 20 and later. -\tPlease remove the extension be calling: plesk installer remove --components watchdog -""" - - def _do_check(self) -> bool: - return not packages.is_package_installed("psa-watchdog") - - -class DPKGIsLocked(action.CheckAction): - def __init__(self): - self.name = "checking if dpkg is locked" - self.description = """It looks like some other process is using dpkg. Please wait until it finishes and try again.""" - - def _do_check(self) -> bool: - return subprocess.run(["/bin/fuser", "/var/lib/apt/lists/lock"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0 diff --git a/actions/distupgrade.py b/actions/distupgrade.py deleted file mode 100644 index 59fb87b..0000000 --- a/actions/distupgrade.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -import os -import subprocess - -from common import action, dist, dpkg, files, log, packages, util - - -class InstallUbuntuUpdateManager(action.ActiveAction): - def __init__(self): - self.name = "installing ubuntu update manager" - - def _prepare_action(self): - packages.install_packages(["update-manager-core"]) - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 10 - - -class SetupUbuntu20Repositories(action.ActiveAction): - def __init__(self): - self.name = "setting up ubuntu 20 repositories" - self.plesk_sourcelist_path = "/etc/apt/sources.list.d/plesk.list" - - def _prepare_action(self): - files.replace_string("/etc/apt/sources.list", "bionic", "focal") - - for root, _, file in os.walk("/etc/apt/sources.list.d/"): - for file in file: - if file.endswith(".list"): - files.replace_string(os.path.join(root, file), "bionic", "focal") - - files.backup_file(self.plesk_sourcelist_path) - files.replace_string(self.plesk_sourcelist_path, "extras", "all") - - packages.update_package_list() - - def _post_action(self): - files.restore_file_from_backup(self.plesk_sourcelist_path) - - def _revert_action(self): - files.restore_file_from_backup(self.plesk_sourcelist_path) - files.replace_string("/etc/apt/sources.list", "focal", "bionic") - - for root, _, file in os.walk("/etc/apt/sources.list.d/"): - for file in file: - if file.endswith(".list"): - files.replace_string(os.path.join(root, file), "focal", "bionic") - - packages.update_package_list() - - def estimate_prepare_time(self): - return 20 - - def estimate_revert_time(self): - return 20 - - -class InstallNextKernelVersion(action.ActiveAction): - def __init__(self): - self.name = "installing kernel from next distro version" - - def _prepare_action(self): - packages.install_packages(["linux-generic"]) - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 2 * 60 + 30 - - -class InstallUdev(action.ActiveAction): - def __init__(self): - self.name = "installing udev" - - def _prepare_action(self): - try: - packages.install_packages(["udev"]) - except Exception: - udevd_service_path = "/lib/systemd/system/systemd-udevd.service" - if os.path.exists(udevd_service_path): - files.replace_string(udevd_service_path, - "ExecReload=udevadm control --reload --timeout 0", - "ExecReload=/bin/udevadm control --reload --timeout 0") - - dpkg.restore_installation() - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 2 * 60 + 30 - - -class RemoveLXD(action.ActiveAction): - def __init__(self): - self.name = "remove lxd" - - def _is_required(self) -> bool: - return packages.is_package_installed("lxd") - - def _prepare_action(self): - packages.remove_packages(["lxd", "lxd-client"]) - - def _post_action(self): - packages.install_packages(["lxd", "lxd-client"]) - - def _revert_action(self): - packages.install_packages(["lxd", "lxd-client"]) - - def estimate_prepare_time(self): - return 30 - - def estimate_post_time(self): - return 30 - - def estimate_revert_time(self): - return 30 - - -class UpgradeGrub(action.ActiveAction): - def __init__(self): - self.name = "upgrade grub from new repositories" - - def _prepare_action(self): - try: - packages.upgrade_packages(["grub-pc"]) - except Exception: - log.warn("grub-pc require configuration, trying to do it automatically") - reconfigure_process = subprocess.Popen("/usr/bin/dpkg --configure grub-pc", - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, universal_newlines=True, - env={"PATH": os.environ["PATH"], "DEBIAN_FRONTEND": "readline"}) - - reconfigure_process.stdin.write("1\n") - stdout, stderr = reconfigure_process.communicate() - - if reconfigure_process.returncode != 0: - log.err("Unable to reconfigure grub-pc package automatically.\nstdout: {}\nstderr: {}".format(stdout, stderr)) - raise Exception("""Unable to reconfigure grub-pc package, plesk perform reconfiguration manually by calling: -1. dpkg --configure grub-pc -2. apt-get install -f""") - - dpkg.restore_installation() - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 5 * 60 - - -class UpgradePackagesFromNewRepositories(action.ActiveAction): - def __init__(self): - self.name = "upgrade packages from new repositories" - - def _prepare_action(self): - packages.upgrade_packages() - packages.autoremove_outdated_packages() - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 5 * 60 - - -class DoDistupgrade(action.ActiveAction): - def __init__(self): - self.name = "make dist-upgrade" - - def _prepare_action(self): - dpkg.do_distupgrade() - - def _post_action(self): - packages.autoremove_outdated_packages() - - def _revert_action(self): - # I believe there is no way to revert dist-upgrade - pass - - def estimate_prepare_time(self): - return 5 * 60 - - def estimate_post_time(self): - return 30 - - -class RepairPleskInstallation(action.ActiveAction): - def __init__(self): - self.name = "repair plesk installation" - - def _prepare_action(self): - pass - - def _post_action(self): - util.logged_check_call(["/usr/sbin/plesk", "repair", "installation", "-y"]) - - def _revert_action(self): - pass - - def estimate_post_time(self): - return 3 * 60 diff --git a/actions/extensions.py b/actions/extensions.py deleted file mode 100644 index d9ab4f3..0000000 --- a/actions/extensions.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -from common import action, systemd - - -class DisableGrafana(action.ActiveAction): - def __init__(self): - self.name = "disabling grafana" - - def _is_required(self) -> bool: - return systemd.is_service_exists("grafana-server.service") - - def _prepare_action(self): - systemd.stop_services(["grafana-server.service"]) - systemd.disable_services(["grafana-server.service"]) - - def _post_action(self): - systemd.enable_services(["grafana-server.service"]) - systemd.start_services(["grafana-server.service"]) - - def _revert_action(self): - systemd.enable_services(["grafana-server.service"]) - systemd.start_services(["grafana-server.service"]) - - def estimate_prepare_time(self): - return 20 - - def estimate_post_time(self): - return 20 - - def estimate_revert_time(self): - return 20 diff --git a/actions/mariadb.py b/actions/mariadb.py deleted file mode 100644 index 655c9f3..0000000 --- a/actions/mariadb.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -import subprocess - -from common import action, dpkg, files, mariadb, packages, systemd - - -MARIADB_VERSION_ON_UBUNTU_20 = mariadb.MariaDBVersion("10.3.38") - - -class AddMysqlConnector(action.ActiveAction): - def __init__(self): - self.name = "install mysql connector" - - def _is_required(self) -> bool: - return mariadb.is_mysql_installed() - - def _prepare_action(self) -> None: - pass - - def _post_action(self) -> None: - subprocess.check_call(["/usr/bin/dnf", "install", "-y", "mariadb-connector-c"]) - - def _revert_action(self) -> None: - pass - - -def get_db_server_config_file(): - return mariadb.get_mysql_config_file_path() if mariadb.is_mysql_installed() else mariadb.get_mariadb_config_file_path() - - -class DisableMariadbInnodbFastShutdown(action.ActiveAction): - def __init__(self): - self.name = "disabling mariadb innodb fast shutdown" - - def _is_required(self) -> bool: - return mariadb.is_mariadb_installed() or mariadb.is_mysql_installed() - - def _prepare_action(self): - target_file = get_db_server_config_file() - files.cnf_set_section_variable(target_file, "mysqld", "innodb_fast_shutdown", "0") - systemd.restart_services(["mariadb", "mysql"]) - - def _post_action(self): - target_file = get_db_server_config_file() - files.cnf_unset_section_variable(target_file, "mysqld", "innodb_fast_shutdown") - systemd.restart_services(["mariadb", "mysql"]) - - def _revert_action(self): - target_file = get_db_server_config_file() - files.cnf_unset_section_variable(target_file, "mysqld", "innodb_fast_shutdown") - systemd.restart_services(["mariadb", "mysql"]) - - def estimate_prepare_time(self): - return 15 - - def estimate_post_time(self): - return 15 - - def estimate_revert_time(self): - return 15 - - -class InstallUbuntu20Mariadb(action.ActiveAction): - def __init__(self): - self.name = "installing mariadb from ubuntu 20 official repository" - - def _is_required(self) -> bool: - return mariadb.is_mariadb_installed() and MARIADB_VERSION_ON_UBUNTU_20 > mariadb.get_installed_mariadb_version() - - def _prepare_action(self): - dpkg.depconfig_parameter_set("libraries/restart-without-asking", "true") - packages.install_packages(["mariadb-server-10.3"], force_package_config=True) - - def _post_action(self): - dpkg.depconfig_parameter_set("libraries/restart-without-asking", "false") - - def _revert_action(self): - dpkg.depconfig_parameter_set("libraries/restart-without-asking", "false") - - def estimate_prepare_time(self): - return 60 - - -class DisableUnsupportedMysqlModes(action.ActiveAction): - def __init__(self): - self.name = "disabling mysql modes unsupported mysql 8.0" - self.deprecated_modes = [ - "ONLY_FULL_GROUP_BY", - "STRICT_TRANS_TABLES", - "NO_ZERO_IN_DATE", - "NO_ZERO_DATE", - "ERROR_FOR_DIVISION_BY_ZERO", - "NO_AUTO_CREATE_USER", - "NO_ENGINE_SUBSTITUTION", - ] - - def _is_required(self) -> bool: - return mariadb.is_mysql_installed() - - def _prepare_action(self): - for config_file in files.find_files_case_insensitive("/etc/mysql", "*.cnf", True): - files.backup_file(config_file) - for mode in self.deprecated_modes: - files.replace_string(config_file, mode + ",", " ") - files.replace_string(config_file, mode, " ") - - def _post_action(self): - for config_file in files.find_files_case_insensitive("/etc/mysql", "*.cnf", True): - files.remove_backup(config_file) - - def _revert_action(self): - for config_file in files.find_files_case_insensitive("/etc/mysql", "*.cnf", True): - files.restore_file_from_backup(config_file) - - def estimate_prepare_time(self): - return 10 - - def estimate_post_time(self): - return 10 - - def estimate_revert_time(self): - return 10 diff --git a/actions/packages.py b/actions/packages.py deleted file mode 100644 index 2cd1585..0000000 --- a/actions/packages.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -from common import action, packages, log, util - -import os - - -class ReinstallSystemd(action.ActiveAction): - def __init__(self): - self.name = "installing systemd from modern repository" - - def _prepare_action(self): - packages.install_packages(["systemd"]) - - def _post_action(self): - pass - - def _revert_action(self): - pass - - def estimate_prepare_time(self): - return 30 - - -class RemoveMailComponents(action.ActiveAction): - def __init__(self): - self.name = "removing mail components" - self.removed_components_list_file = "/tmp/ubuntu18to20_removed_mail_components.txt" - - def _prepare_action(self): - mail_components_2_packages = { - "postfix": "postfix", - "dovecot": "plesk-dovecot", - "qmail": "psa-qmail", - "courier": "psa-courier-imap", - "spamassassin": "psa-spamassassin", - "mailman": "psa-mailman", - } - - components_to_remove = [] - for component, package in mail_components_2_packages.items(): - if packages.is_package_installed(package): - components_to_remove.append(component) - - with open(self.removed_components_list_file, "w") as f: - f.write("\n".join(components_to_remove)) - - util.logged_check_call(["/usr/sbin/plesk", "installer", "remove", "--components", " ".join(components_to_remove)]) - - def _post_action(self): - if not os.path.exists(self.removed_components_list_file): - log.warn("File with removed email components list does not exist. The reinstallation is skipped.") - return - - with open(self.removed_components_list_file, "r") as f: - components_to_install = f.read().splitlines() - util.logged_check_call(["/usr/sbin/plesk", "installer", "add", "--components", " ".join(components_to_install)]) - - os.unlink(self.removed_components_list_file) - - def _revert_action(self): - if not os.path.exists(self.removed_components_list_file): - log.warn("File with removed email components list does not exist. The reinstallation is skipped.") - return - - with open(self.removed_components_list_file, "r") as f: - components_to_install = f.read().splitlines() - util.logged_check_call(["/usr/sbin/plesk", "installer", "add", "--components", " ".join(components_to_install)]) - - os.unlink(self.removed_components_list_file) - - def estimate_prepare_time(self): - return 2 * 60 - - def estimate_post_time(self): - return 3 * 60 - - def estimate_revert_time(self): - return 3 * 60 diff --git a/actions/spamassassin.py b/actions/spamassassin.py deleted file mode 100644 index 970029d..0000000 --- a/actions/spamassassin.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -from common import action - -import os -import shutil - - -class RestoreCurrentSpamassasinConfiguration(action.ActiveAction): - def __init__(self): - self.name = "restore current spamassassin configuration after conversion" - self.spamassasin_config_path = "/etc/spamassassin/local.cf" - self.spamassasin_backup_path = "/tmp/spamassasin_local.cf.backup" - - def _is_required(self) -> bool: - return os.path.exists(self.spamassasin_config_path) - - def _prepare_action(self) -> None: - shutil.copy(self.spamassasin_config_path, self.spamassasin_backup_path) - - def _post_action(self): - shutil.copy(self.spamassasin_backup_path, self.spamassasin_config_path) - - def _revert_action(self): - os.unlink(self.spamassasin_backup_path) diff --git a/actions/systemd.py b/actions/systemd.py deleted file mode 100644 index 476ecdb..0000000 --- a/actions/systemd.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. -import os - -from common import action, systemd - - -class AddUpgradeSystemdService(action.ActiveAction): - - def __init__(self, script_path, options): - self.name = "adding ubuntu18to20 resume service" - - self.script_path = script_path - # ToDo. It's pretty simple to forget to add argument here, so maybe we should find another way - self.options = [ - (" --verbose", options.verbose), - (" --no-reboot", options.no_reboot), - ] - - self.service_name = 'plesk-ubuntu18to20.service' - self.service_file_path = os.path.join('/etc/systemd/system', self.service_name) - self.service_content = ''' -[Unit] -Description=First boot service for upgrade process from Ubuntu 18 to Ubuntu 20. -After=network.target network-online.target - -[Service] -Type=simple -# want to run it once per boot time -RemainAfterExit=yes -# Using python 3.8 since it's the default version in Ubuntu 20 -# our pex should be fine with it -ExecStart=/usr/bin/python3.8 {script_path} -s finish {arguments} - -[Install] -WantedBy=multi-user.target -''' - - def _prepare_action(self): - arguments = "" - for argument, enabled in self.options: - if enabled: - arguments += argument - - systemd.add_systemd_service(self.service_name, - self.service_content.format(script_path=self.script_path, arguments=arguments)) - - def _post_action(self): - systemd.remove_systemd_service(self.service_name) - - def _revert_action(self): - systemd.remove_systemd_service(self.service_name) diff --git a/buck.defs.py b/buck.defs.py new file mode 100644 index 0000000..a275338 --- /dev/null +++ b/buck.defs.py @@ -0,0 +1,33 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. +# vim:ft=python: + +import os.path + +with allow_unsafe_import(): + import subprocess + + +def get_full_base_path(): + path = get_base_path() + cell = get_cell_name() + if cell: + path = os.path.join(cell, path) + return path + + +def get_git_revision(path=None): + if not path: + path = get_full_base_path() + return subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=path, universal_newlines=True).strip() + + +def get_git_revision_description(dirty=True, path=None): + cmd = ['git', 'describe', '--match', 'v[0-9]*'] + if dirty is True: + cmd.append('--dirty') + if not path: + path = get_full_base_path() + try: + return subprocess.check_output(cmd, cwd=path, universal_newlines=True).strip() + except Exception: + return get_git_revision(path=path) diff --git a/build/Dockerfile b/build/Dockerfile deleted file mode 100644 index 580cb9d..0000000 --- a/build/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# The Dockerfile was brought from the repository build-with-buck -# https://github.com/SandakovMM/build-with-buck -# But little changed. Because GitHub actions mount sourcecode inside -# the action, but we should do the same inside the container manually. -# So to build you shoud just call docker like this: -# > docker run -v ./:/target [container-name] build :ubuntu18to20 -FROM openjdk:8 - -MAINTAINER mmsandakov@gmail.com - -# Prepare environment -RUN apt-get update && apt-get install -y git ant build-essential gcc python python-dev python3-distutils -RUN useradd -m -s /bin/false buck -RUN mkdir /buck && chown buck /buck -USER buck - -# Build buck -# We could just clone because I don't expect buck to be updated in this repository -# ToDo: move to buck2 -RUN git clone https://github.com/facebook/buck.git /buck/ -RUN cd /buck && ant - -# We shoul return to root because github actions expects it -USER root -RUN ln -sf /buck/bin/buck /usr/bin/ ; mkdir /target -WORKDIR /target - -ENTRYPOINT ["/usr/bin/buck"] \ No newline at end of file diff --git a/common b/common deleted file mode 160000 index cc3aed0..0000000 --- a/common +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cc3aed07c5d504ce72f343b2b3ca93baa7e0ccd1 diff --git a/dist-upgrader b/dist-upgrader new file mode 160000 index 0000000..9422040 --- /dev/null +++ b/dist-upgrader @@ -0,0 +1 @@ +Subproject commit 942204050b388fb92cafa7c13b67b3d0229665f4 diff --git a/main.py b/main.py deleted file mode 100644 index 6251926..0000000 --- a/main.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/python3 -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. - -import actions - -from datetime import datetime -import json -import logging -import os -import pkg_resources -import sys -import subprocess -import threading -import typing -import time - -from enum import Flag, auto -from optparse import OptionParser, OptionValueError, SUPPRESS_HELP - -import messages -from common import action, dist, feedback, files, log, motd, plesk, systemd, writers - - -DEFAULT_LOG_FILE = "/var/log/plesk/ubuntu18to20.log" - - -def get_version() -> str: - with pkg_resources.resource_stream(__name__, "version.json") as f: - return json.load(f)["version"] - - -def get_revision(short: bool = True) -> str: - with pkg_resources.resource_stream(__name__, "version.json") as f: - revision = json.load(f)["revision"] - if short: - revision = revision[:8] - return revision - - -def prepare_feedback() -> None: - feedback_archive: str = "ubuntu18to20_feedback.zip" - - def get_installed_packages_list(): - packages_file = "installed_packages.txt" - with open(packages_file, "w") as pkgs_file: - try: - pkgs_info = subprocess.check_output(["/usr/bin/apt", "list", "--installed"], universal_newlines=True).splitlines() - for line in pkgs_info: - pkgs_file.write(line + "\n") - except subprocess.CalledProcessError: - pkgs_file.write("Getting installed packages from dpkg failed\n") - - return packages_file - - def get_plesk_version(): - plesk_version_file = "plesk_version.txt" - with open(plesk_version_file, "w") as version_file: - for lines in plesk.get_plesk_full_version(): - version_file.write(lines + "\n") - - return plesk_version_file - - ubuntu_feedback = feedback.Feedback("ubuntu18to20", get_version() + "-" + get_revision(), - [ - DEFAULT_LOG_FILE, - action.ActiveFlow.PATH_TO_ACTIONS_DATA, - ], - [ - get_installed_packages_list, - get_plesk_version, - ]) - ubuntu_feedback.save_archive(feedback_archive) - - print(messages.FEEDBACK_IS_READY_MESSAGE.format(feedback_archive_path=feedback_archive)) - - -class Stages(Flag): - convert = auto() - finish = auto() - revert = auto() - # Todo. The tst stage for debugging purpose only, don't forget to remove it - test = auto() - - -def convert_string_to_stage(option, opt_str, value, parser): - if value == "start" or value == "convert": - parser.values.stage = Stages.convert - return - elif value == "finish": - parser.values.stage = Stages.finish - return - elif value == "revert": - parser.values.stage = Stages.revert - return - elif value == "test": - parser.values.stage = Stages.test - return - - raise OptionValueError("Unknown stage: {}".format(value)) - - -def get_check_actions(options: typing.Any, stage_flag: Stages) -> typing.List[action.CheckAction]: - if Stages.finish in stage_flag: - return [] - - return [ - actions.PleskVersionIsActual(), - actions.PleskInstallerNotInProgress(), - actions.CheckOutdatedPHP("7.1"), - actions.PleskWatchdogIsntInstalled(), - actions.DPKGIsLocked(), - actions.CheckIsInContainer(), - ] - - -def is_required_conditions_satisfied(options: typing.Any, stage_flag: Stages) -> bool: - checks = get_check_actions(options, stage_flag) - - try: - with action.CheckFlow(checks) as check_flow, writers.StdoutWriter() as writer: - writer.write("Do preparation checks...") - check_flow.validate_actions() - failed_checks = check_flow.make_checks() - writer.write("\r") - for check in failed_checks: - writer.write(check) - log.err(check) - - if failed_checks: - return False - return True - except Exception as ex: - log.err("{}".format(ex)) - return False - - -def construct_actions(options: typing.Any, stage_flag: Stages) -> typing.Dict[int, typing.List[action.ActiveAction]]: - return { - 1: [ - actions.HandleConversionStatus(), - actions.AddFinishSshLoginMessage(), - actions.AddInProgressSshLoginMessage(), - actions.DisablePleskSshBanner(), - actions.RepairPleskInstallation(), - actions.DisableMariadbInnodbFastShutdown(), - actions.DisableUnsupportedMysqlModes(), - actions.InstallUbuntuUpdateManager(), - actions.CleanApparmorCacheConfig(), - actions.RestoreCurrentSpamassasinConfiguration(), - actions.DisableGrafana(), - actions.AddUpgradeSystemdService(os.path.abspath(sys.argv[0]), options), - actions.MoveOldBindConfigToNamed(), - actions.RemoveMailComponents(), - actions.SetupUbuntu20Repositories(), - ], - 2: [ - actions.InstallNextKernelVersion(), - actions.InstallUbuntu20Mariadb(), - actions.InstallUdev(), - actions.ReinstallSystemd(), - actions.RemoveLXD(), - actions.UpgradeGrub(), - ], - 3: [ - actions.UpgradePackagesFromNewRepositories(), - ], - 4: [ - actions.DoDistupgrade(), - ], - } - - -def get_flow(stage_flag: Stages, actions_map: typing.Dict[int, typing.List[action.ActiveAction]]) -> action.ActiveFlow: - if Stages.finish in stage_flag: - return action.FinishActionsFlow(actions_map) - elif Stages.revert in stage_flag: - return action.RevertActionsFlow(actions_map) - else: - return action.PrepareActionsFlow(actions_map) - - -def start_flow(flow: action.ActiveFlow) -> None: - with writers.FileWriter(STATUS_FILE_PATH) as status_writer, writers.StdoutWriter() as stdout_writer: - progressbar = action.FlowProgressbar(flow, [stdout_writer, status_writer]) - progress = threading.Thread(target=progressbar.display) - executor = threading.Thread(target=flow.pass_actions) - - progress.start() - executor.start() - - executor.join() - progress.join() - - -STATUS_FILE_PATH = "/tmp/ubuntu18to20.status" - - -def show_status() -> None: - if not os.path.exists(STATUS_FILE_PATH): - print("Conversion process is not running.") - return - - print("Conversion process in progress:") - status = files.get_last_lines(STATUS_FILE_PATH, 1) - print(status[0]) - - -def monitor_status() -> None: - if not os.path.exists(STATUS_FILE_PATH): - print("Conversion process is not running.") - return - - with open(STATUS_FILE_PATH, "r") as status: - status.readlines() - while os.path.exists(STATUS_FILE_PATH): - line = status.readline().rstrip() - sys.stdout.write("\r" + line) - sys.stdout.flush() - time.sleep(1) - - -def show_fail_motd() -> None: - motd.add_finish_ssh_login_message(f""" -Something went wrong during the final stage of Ubuntu 18 to Ubuntu 20 distupgrade -See the {DEFAULT_LOG_FILE} file for more information. -""") - motd.publish_finish_ssh_login_message() - - -def handle_error(error: str) -> None: - sys.stdout.write("\n{}\n".format(error)) - sys.stdout.write(messages.FAIL_MESSAGE_HEAD.format(DEFAULT_LOG_FILE)) - - error_message = f"ubuntu18to20 (version {get_version()}-{get_revision()}) process has been failed. Error: {error}.\n\n" - for line in files.get_last_lines(DEFAULT_LOG_FILE, 100): - sys.stdout.write(line) - error_message += line - - sys.stdout.write(messages.FAIL_MESSAGE_TAIL.format(DEFAULT_LOG_FILE)) - - plesk.send_error_report(error_message) - plesk.send_conversion_status(True) - - log.err(f"ubuntu18to20 process has been failed. Error: {error}") - show_fail_motd() - - -def do_convert(options: typing.Any) -> None: - if not is_required_conditions_satisfied(options, options.stage): - log.err("Please fix noted problems before proceed the conversion") - return 1 - - actions_map = construct_actions(options, options.stage) - - with get_flow(options.stage, actions_map) as flow: - flow.validate_actions() - start_flow(flow) - if flow.is_failed(): - handle_error(flow.get_error()) - return 1 - - if not options.no_reboot and (Stages.convert in options.stage or Stages.finish in options.stage): - log.info("Going to reboot the system") - if Stages.convert in options.stage: - sys.stdout.write(messages.CONVERT_RESTART_MESSAGE.format(time=datetime.now().strftime("%H:%M:%S"), - script_path=os.path.abspath(sys.argv[0]))) - elif Stages.finish in options.stage: - sys.stdout.write(messages.FINISH_RESTART_MESSAGE) - - systemd.do_reboot() - - if Stages.revert in options.stage: - sys.stdout.write(messages.REVET_FINISHED_MESSAGE) - - -HELP_MESSAGE = f"""ubuntu18to20 [options] - - -Use this script to distupgrade an Ubuntu 18 server with Plesk to Ubuntu 20. The process consists of two stages: - - -- Preparation (about 5 minutes) - The OS is prepared for the conversion. -- Distupgrade (about 15 minutes) - The distupgrade takes place. -- Finalization (about 5 minutes) - The server is returned to normal operation. - - - -The script writes a log to the {DEFAULT_LOG_FILE} file. If there are any issues, you can find more information in the log file. -For assistance, submit an issue here https://github.com/plesk/ubuntu18to20/issues and attach this log file. - - -ubuntu18to20 version is {get_version()}-{get_revision()}. -""" - - -def main(): - opts = OptionParser(usage=HELP_MESSAGE) - opts.set_default("stage", Stages.convert) - opts.add_option("--start", action="store_const", dest="stage", const=Stages.convert, - help="Start the conversion stage. This calls the Leapp utility to convert the system " - "and reboot into the temporary OS distribution.") - opts.add_option("-r", "--revert", action="store_const", dest="stage", const=Stages.revert, - help="Revert all changes made by the ubuntu18to20 tool. This option can only take effect " - "if the server has not yet been rebooted into the temporary OS distribution.") - opts.add_option("--finish", action="store_const", dest="stage", const=Stages.finish, - help="Start the finalization stage. This returns Plesk to normal operation. " - "Can be run again if the conversion process failed to finish successfully earlier.") - opts.add_option("-t", "--test", action="store_const", dest="stage", const=Stages.test, help=SUPPRESS_HELP) - opts.add_option("--retry", action="store_true", dest="retry", default=False, - help="Retry the most recently started stage. This option can only take effect " - "during the preparation stage.") - opts.add_option("--status", action="store_true", dest="status", default=False, - help="Show the current status of the conversion process.") - opts.add_option("--monitor", action="store_true", dest="monitor", default=False, - help="Monitor the status of the conversion process in real time.") - opts.add_option("-s", "--stage", action="callback", callback=convert_string_to_stage, type="string", - help="Start one of the conversion process' stages. Allowed values: 'start', 'revert', and 'finish'.") - opts.add_option("-v", "--version", action="store_true", dest="version", default=False, - help="Show the version of the ubuntu18to20 utility.") - opts.add_option("-f", "--prepare-feedback", action="store_true", dest="prepare_feedback", default=False, - help="Prepare feedback archive that should be sent to the developers for further failure investigation.") - opts.add_option("--verbose", action="store_true", dest="verbose", default=False, help="Write verbose logs") - opts.add_option("--no-reboot", action="store_true", dest="no_reboot", default=False, help=SUPPRESS_HELP) - - options, _ = opts.parse_args(args=sys.argv[1:]) - - log.init_logger([DEFAULT_LOG_FILE], [], - loglevel=logging.DEBUG if options.verbose else logging.INFO) - - if options.version: - print(get_version() + "-" + get_revision()) - return 0 - - if options.prepare_feedback: - prepare_feedback() - return 0 - - if dist.get_distro() in [dist.Distro.unsupported, dist.Distro.unknown]: - print(messages.NOT_SUPPORTED_ERROR) - log.err(messages.NOT_SUPPORTED_ERROR) - return 1 - - if options.status: - show_status() - return 0 - - if options.monitor: - monitor_status() - return 0 - - return do_convert(options) - - -if __name__ == "__main__": - main() diff --git a/messages.py b/messages.py deleted file mode 100644 index 90a337b..0000000 --- a/messages.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 1999 - 2023. Plesk International GmbH. All rights reserved. - -CONVERT_RESTART_MESSAGE = """ -\033[92m************************************************************************************** -The distupgrade almost done. The server will now be rebooted in a several seconds. -The finishing stage will takes place after the reboot. Finishing process takes about 5 minutes. -Current server time: {time}. -To monitor the disupgrade status use one of the following commands: - python3.8 {script_path} --status -or - python3.8 {script_path} --monitor -**************************************************************************************\033[0m -""" - -FINISH_RESTART_MESSAGE = """ -\033[92m************************************************************************************** -The distupgrade process has finished. The server will now reboot now. -**************************************************************************************\033[0m -""" - -REVET_FINISHED_MESSAGE = """ -\033[92m************************************************************************************** -All changes have been reverted. Plesk should now return to normal operation. -**************************************************************************************\033[0m -""" - -FAIL_MESSAGE_HEAD = """ -\033[91m************************************************************************************** -The conversion process has failed. Here are the last 100 lines of the {} file: -**************************************************************************************\033[0m -""" - -FAIL_MESSAGE_TAIL = """ -\033[91m************************************************************************************** -The conversion process has failed. See the {} file for more information. -The last 100 lines of the file are shown above. -For assistance, call 'ubuntu18to20 --prepare-feedback' and follow the instructions. -**************************************************************************************\033[0m -""" - -TIME_EXCEEDED_MESSAGE = """ -\033[91m************************************************************************************** -The conversion process is taking too long. It may be stuck. Please verify if the process is -still running by checking if logfile /var/log/plesk/ubuntu18to20.log continues to update. -It is safe to interrupt the process with Ctrl+C and restart it from the same stage. -**************************************************************************************\033[0m -""" - -FEEDBACK_IS_READY_MESSAGE = """ -\033[92m************************************************************************************** -The feedback archive is ready. You can find it here: {feedback_archive_path} -For further assistance, create an issue in our GitHub repository - https://github.com/plesk/ubuntu18to20/issues. -Please attach the feedback archive to the created issue and provide as much information about the problem as you can. -**************************************************************************************\033[0m -""" - -NOT_SUPPORTED_ERROR = "The distribution is not supported yet, please contact Plesk support for further assistance. Supported conversions are: Ubuntu 18 to Ubuntu 20" \ No newline at end of file diff --git a/product.defs.py b/product.defs.py new file mode 100644 index 0000000..fc1cdd0 --- /dev/null +++ b/product.defs.py @@ -0,0 +1,17 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. +# vim:ft=python: + +include_defs('//buck.defs.py') + + +# Function is necessary, because Buck won't allow +# get_git_revision_description() to be called at the top level of an +# included file due to get_base_path() call inside (so, you can't just +# do REVISION = get_git_revision_description()) +def get_ub18to20_revision(): + return get_git_revision_description() + + +def get_ub18to20_version(): + rev = get_ub18to20_revision() + return rev.lstrip('v').split('-', 1)[0] if '-' in rev else '' diff --git a/ubuntu18to20/BUCK b/ubuntu18to20/BUCK new file mode 100644 index 0000000..2437f96 --- /dev/null +++ b/ubuntu18to20/BUCK @@ -0,0 +1,28 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. +# vim:ft=python: + +include_defs('//product.defs.py') + + +genrule( + name = 'config', + srcs = ['config.py'], + out = 'config.py', + bash = '''\ + UB18TO20_VERSION='{version}' + UB18TO20_REVISION='{revision}' + sed -e "s/@@UB18TO20_VERSION@@/$UB18TO20_VERSION/g; s/@@UB18TO20_REVISION@@/$UB18TO20_REVISION/g" "$SRCS" >"$OUT" + '''.format( + version=get_ub18to20_version(), + revision=get_ub18to20_revision(), + ), +) + +python_library( + name = 'lib', + srcs = glob( + ['**/*.py'], + exclude = ['config.py'], + ) + [':config'], + visibility = ['PUBLIC'], +) diff --git a/ubuntu18to20/__init__.py b/ubuntu18to20/__init__.py new file mode 100644 index 0000000..99c26a6 --- /dev/null +++ b/ubuntu18to20/__init__.py @@ -0,0 +1 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. diff --git a/ubuntu18to20/config.py b/ubuntu18to20/config.py new file mode 100644 index 0000000..fd04de1 --- /dev/null +++ b/ubuntu18to20/config.py @@ -0,0 +1,4 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. + +version = "@@UB18TO20_VERSION@@" +revision = "@@UB18TO20_REVISION@@" diff --git a/ubuntu18to20/main.py b/ubuntu18to20/main.py new file mode 100644 index 0000000..2c437ed --- /dev/null +++ b/ubuntu18to20/main.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. + +import sys + +import pleskdistup.main +import pleskdistup.registry + +import ubuntu18to20.upgrader + +if __name__ == "__main__": + pleskdistup.registry.register_upgrader(ubuntu18to20.upgrader.Ubuntu18to20Factory()) + sys.exit(pleskdistup.main.main()) diff --git a/ubuntu18to20/upgrader.py b/ubuntu18to20/upgrader.py new file mode 100644 index 0000000..4e6ea6f --- /dev/null +++ b/ubuntu18to20/upgrader.py @@ -0,0 +1,186 @@ +# Copyright 1999-2023. Plesk International GmbH. All rights reserved. + +import argparse +import os +import typing + +from pleskdistup import actions +from pleskdistup.common import action, feedback +from pleskdistup.phase import Phase +from pleskdistup.upgrader import DistUpgrader, DistUpgraderFactory, PathType, SystemDescription + +import ubuntu18to20.config + + +class Ubuntu18to20Upgrader(DistUpgrader): + _os_from_name = "Ubuntu" + _os_from_version = "18" + _os_to_name = "Ubuntu" + _os_to_version = "20" + + def __init__(self): + super().__init__() + + def __repr__(self) -> str: + attrs = ", ".join(f"{k}={getattr(self, k)!r}" for k in ( + "_os_from_name", "_os_from_version", + "_os_to_name", "_os_to_version", + )) + return f"{self.__class__.__name__}({attrs})" + + def __str__(self) -> str: + return f"{self.__class__.__name__}" + + @classmethod + def supports( + cls, + from_system: typing.Optional[SystemDescription] = None, + to_system: typing.Optional[SystemDescription] = None + ) -> bool: + def matching_system(system: SystemDescription, os_name: str, os_version: str) -> bool: + return ( + (system.os_name is None or system.os_name == os_name) + and (system.os_version is None or system.os_version == os_version) + ) + + return ( + (from_system is None or matching_system(from_system, cls._os_from_name, cls._os_from_version)) + and (to_system is None or matching_system(to_system, cls._os_to_name, cls._os_to_version)) + ) + + @property + def upgrader_name(self) -> str: + return "Plesk::Ubuntu18to20Upgrader" + + @property + def upgrader_version(self) -> str: + return ubuntu18to20.config.revision + + @property + def issues_url(self) -> str: + return "https://github.com/plesk/ubuntu18to20/issues" + + def prepare_feedback( + self, + feed: feedback.Feedback, + ) -> feedback.Feedback: + feed.collect_actions += [ + feedback.collect_installed_packages_apt, + feedback.collect_installed_packages_dpkg, + feedback.collect_apt_policy, + feedback.collect_plesk_version, + ] + return feed + + def construct_actions( + self, + upgrader_bin_path: PathType, + options: typing.Any, + phase: Phase + ) -> typing.Dict[str, typing.List[action.ActiveAction]]: + new_os = f"{self._os_to_name} {self._os_to_version}" + return { + "Prepare": [ + actions.HandleConversionStatus(options.status_flag_path, options.completion_flag_path), + actions.AddFinishSshLoginMessage(new_os), # Executed at the finish phase only + actions.AddInProgressSshLoginMessage(new_os), + actions.DisablePleskSshBanner(), + actions.RepairPleskInstallation(), # Executed at the finish phase only + actions.DisableMariadbInnodbFastShutdown(), + actions.DisableUnsupportedMysqlModes(), + actions.InstallUbuntuUpdateManager(), + actions.CleanApparmorCacheConfig(), + actions.RestoreCurrentSpamassasinConfiguration(options.state_dir), + actions.DisableGrafana(), + actions.AddUpgradeSystemdService(os.path.abspath(upgrader_bin_path), options), + actions.MoveOldBindConfigToNamed(), + actions.RemoveMailComponents(options.state_dir), + actions.SetupUbuntu20Repositories(), + ], + "Pre-install packages": [ + actions.InstallNextKernelVersion(), + actions.InstallUbuntu20Mariadb(), + actions.InstallUdev(), + actions.ReinstallSystemd(), + actions.RemoveLXD(), + actions.UpgradeGrub(), + ], + "Reboot": [ + actions.Reboot(), + ], + "Upgrade packages": [ + actions.UpgradePackagesFromNewRepositories(), + ], + "Dist-upgrade": [ + actions.DoDistupgrade(), + ], + "Finishing actions": [ + actions.Reboot(prepare_next_phase=Phase.FINISH, name="reboot and perform finishing actions"), + actions.Reboot(prepare_reboot=None, post_reboot=action.RebootType.AFTER_LAST_STAGE, name="final reboot"), + ], + } + + def get_check_actions(self, options: typing.Any, phase: Phase) -> typing.List[action.CheckAction]: + if phase is Phase.FINISH: + return [] + + return [ + actions.AssertMinPleskVersion("18.0.43"), + actions.AssertPleskInstallerNotInProgress(), + actions.AssertMinPhpVersion("7.1"), + actions.AssertPleskWatchdogNotInstalled(), + actions.AssertDpkgNotLocked(), + actions.AssertNotInContainer(), + ] + + def parse_args(self, args: typing.Sequence[str]) -> None: + DESC_MESSAGE = f"""Use this upgrader to dist-upgrade an {self._os_from_name} {self._os_from_version} server with Plesk to {self._os_to_name} {self._os_to_version}. The process consists of the following general stages: + +-- Preparation (about 5 minutes) - The OS is prepared for the conversion. +-- Conversion (about 15 minutes) - Plesk and system dist-upgrade is performed. +-- Finalization (about 5 minutes) - The server is returned to normal operation. + +The system will be rebooted after each of the stages, so reboot times +should be added to get the total time estimate. + +To see the detailed plan, run the utility with the --show-plan option. + +For assistance, submit an issue here {self.issues_url} +and attach the feedback archive generated with --prepare-feedback or at least the log file. +""" + parser = argparse.ArgumentParser( + usage=argparse.SUPPRESS, + description=DESC_MESSAGE, + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, + ) + parser.add_argument( + "-h", "--help", action="help", default=argparse.SUPPRESS, + help=argparse.SUPPRESS, + ) + parser.parse_args(args) + + +class Ubuntu18to20Factory(DistUpgraderFactory): + def __init__(self): + super().__init__() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(upgrader_name={self.upgrader_name})" + + def __str__(self) -> str: + return f"{self.__class__.__name__} (creates {self.upgrader_name})" + + def supports( + self, + from_system: typing.Optional[SystemDescription] = None, + to_system: typing.Optional[SystemDescription] = None + ) -> bool: + return Ubuntu18to20Upgrader.supports(from_system, to_system) + + @property + def upgrader_name(self) -> str: + return "Plesk::Ubuntu18to20Upgrader" + + def create_upgrader(self, *args, **kwargs) -> DistUpgrader: + return Ubuntu18to20Upgrader(*args, **kwargs)