# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
import json
import os
import shutil
import sys
import time
from typing import ClassVar, NoReturn
import psutil
from clcommon import FormattedException
from clcommon.utils import (
ExternalProgramFailed,
get_cl_version,
get_package_db_errors,
is_ubuntu,
run_command,
)
from clwizard.config import NoSuchModule
from .config import (
Config,
acquire_config_access,
)
from .constants import (
CRASH_LOG_PATH,
FILE_MARKER_PATH,
MAIN_LOG_PATH,
ModuleStatus,
WizardStatus,
)
from .exceptions import CancelModuleException, InstallationFailedException
from .modules import ALL_MODULES, get_supported_modules, run_installation
from .parser import parse_cloudlinux_wizard_opts
from .utils import (
is_background_process_running,
run_background,
setup_logger,
)
class CloudlinuxWizard:
"""Main class for working with Wizard that exposes high level logic."""
# states in which we can remove the module from queue
CANCELLABLE_MODULE_STATUSES: ClassVar[list[str]] = [
ModuleStatus.PENDING,
ModuleStatus.FAILED,
ModuleStatus.CANCELLED,
]
# modules states in which wizard modules can be considered as done
DONE_MODULES_STATUSES: ClassVar[list[str]] = [
ModuleStatus.INSTALLED,
ModuleStatus.CANCELLED,
ModuleStatus.AUTO_SKIPPED,
]
def __init__(self):
self._opts = None
self._supported_modules = get_supported_modules()
self.log = setup_logger("wizard.main", MAIN_LOG_PATH)
def run(self, argv):
"""
CL Wizard main function.
:param argv: command line arguments for wizard
:return: None
"""
self._opts = parse_cloudlinux_wizard_opts(argv)
try:
if self._opts.subparser == "install":
self._validate_system()
if self.is_installation_finished() and not self._opts.force:
self._print_result_and_exit(
result="Installation already finished",
exit_code=1,
)
if self._opts.no_async:
# In async mode, run_background_installation() spawns a new process with --no-async,
# which will execute this branch. So we call _prepare_for_installation() here.
self._prepare_for_installation()
run_installation()
else:
self.run_background_installation(options=self._opts.json_data)
elif self._opts.subparser == "status":
self._validate_system()
if self._opts.initial:
self._get_initial_status()
else:
self._get_modules_statuses()
elif self._opts.subparser == "cancel":
self._cancel_module_installation(self._opts.module)
elif self._opts.subparser == "finish":
self.create_completion_marker()
else:
raise NotImplementedError
if (self._opts.subparser in ["install", "cancel"] and self.is_all_modules_installed()) or (
self._opts.subparser == "finish" and not self.is_all_modules_installed()
):
# Called only once if:
# -- in case of an install: -all modules were installed successfully
# -a module failed during installation,
# but was installed after resuming
# -- in case of cancelling: -a module failed during installation,
# but was canceled by the user and as a result,
# all modules in a 'done' status
# -- in case of finish: -only if user closed the wizard while a module
# had a status other than installed, cancelled or skipped
self.run_collecting_statistics()
self.run_cagefs_force_update()
self._print_result_and_exit()
except FormattedException as err:
self.log.exception(
"Got an error while running cloudlinux-wizard",
exc_info=err,
)
self._print_result_and_exit(
result=err.message,
context=err.context,
details=err.details,
exit_code=1,
)
except InstallationFailedException:
self._print_result_and_exit(
result="Module installation failed, see the log for more information",
exit_code=1,
)
except Exception as err:
# Log the full traceback server-side only; do not return it in the
# response. The wizard's JSON is consumed by lvemanager's root-admin
# dispatcher (the per-user CLI does not expose the wizard), so this is
# API hygiene, not a cross-privilege leak — but raw tracebacks still
# should not be handed back to callers.
self.log.exception("Unknown error in cloudlinux-wizard", exc_info=err)
self._print_result_and_exit(
result="Unknown error occured, please, try again or contact CloudLinux support if it persists.",
)
@staticmethod
def is_installation_finished():
# type: () -> bool
return os.path.isfile(FILE_MARKER_PATH)
def create_completion_marker(self):
# type: () -> None
try:
os.mknod(FILE_MARKER_PATH)
self.log.info("Wizard execution complete")
except OSError as err:
self.log.warning(
"Wizard 'finish' command called more than once, error: '%s'",
str(err),
)
self._print_result_and_exit(
result="Wizard 'finish' command called more than once",
exit_code=1,
)
def run_background_installation(self, options: dict | None = None) -> None:
cmd = sys.argv[:]
# Resolve the respawn executable so the child's Popen/execvp never does
# a PATH lookup, WITHOUT introducing a cwd-relative exec:
# - found on PATH (or an executable path given directly) -> pin absolute;
# - a relative path containing a separator -> abspath (behaviour-
# preserving: execvp would resolve such a path relative to cwd too);
# - a bare (slashless) name not on PATH -> leave it unchanged so execvp
# PATH-searches it (which never includes cwd). Do NOT abspath a bare
# name, which would turn it into