晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。 林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。 见渔人,乃大惊,问所从来。具答之。便要还家,设酒杀鸡作食。村中闻有此人,咸来问讯。自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋。此人一一为具言所闻,皆叹惋。余人各复延至其家,皆出酒食。停数日,辞去。此中人语云:“不足为外人道也。”(间隔 一作:隔绝) 既出,得其船,便扶向路,处处志之。及郡下,诣太守,说如此。太守即遣人随其往,寻向所志,遂迷,不复得路。 南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。
| DIR:/opt/cloudlinux/venv/lib64/python3.11/site-packages/clwizard/ |
| Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/clwizard/wizard.py |
# 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 sys
import time
import traceback
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:
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.",
details=traceback.format_exc(),
)
@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[:]
cmd.append("--no-async")
with acquire_config_access() as config:
# two processes cannot use config at same time
# so we can safely do check for running process here
if is_background_process_running():
self._print_result_and_exit(
result="Unable to start a new installation because a background task is still working",
exit_code=1,
)
# the only case when options are None is the 'resume' case
if options is not None:
config.set_modules(options)
# worker will not be able to acquire reading lock
# and will wait unless we finally close config file
worker_pid = run_background(cmd).pid
config.worker_pid = worker_pid
self._print_result_and_exit(result="success", pid=worker_pid)
def _validate_system(self):
"""
Check whether Wizard supports the current system.
"""
if get_cl_version() is None:
self._print_result_and_exit(
result="Could not identify the CloudLinux version. "
"Restart your system. If you have the same problem again - "
"contact CloudLinux support.",
)
def _prepare_for_installation(self):
"""
Prepare the environment before performing the installation.
This function updates the package lists if run on Ubuntu, expires Yum
cache when running on RHEL-based systems.
"""
if is_ubuntu():
cmd = ["apt-get", "-q", "update"]
try:
out = run_command(cmd)
self.log.info("apt-get update output:\n%s", out)
except ExternalProgramFailed as err:
self.log.exception("Error during apt-get update", exc_info=err)
else:
cmd = ["yum", "-qy", "clean", "expire-cache"]
try:
out = run_command(cmd)
self.log.info("yum clean expire-cache output:\n%s", out)
except ExternalProgramFailed as err:
self.log.exception("Error during yum clean expire-cache", exc_info=err)
def _get_module_log_path(self, module_name):
"""
Get path to module log file.
"""
return self._supported_modules[module_name].LOG_FILE
def _get_modules_statuses(self):
"""
Get information about background worker state.
"""
# we should return modules in order, but config
# does not know about it, let's sort modules here
modules = []
with acquire_config_access() as config:
state = self._get_wizard_state(config)
for name in self._supported_modules:
try:
status = config.get_module_status(name)
status_time = config.get_module_status_time(name)
except NoSuchModule:
continue
module_status = {
"status": status,
"name": name,
"status_time": status_time,
}
if status in [ModuleStatus.FAILED, ModuleStatus.AUTO_SKIPPED]:
module_status["log_file"] = self._get_module_log_path(name)
modules.append(module_status)
if state == WizardStatus.CRASHED:
self._print_result_and_exit(
wizard_status=state,
modules=modules,
crash_log=CRASH_LOG_PATH,
)
self._print_result_and_exit(wizard_status=state, modules=modules)
def _get_initial_status(self):
"""
Get initial modules status that is used by lvemanager to display wizard pages.
"""
error_message = get_package_db_errors()
if error_message:
# package manager DB corrupted
self._print_result_and_exit(result=error_message)
else:
all_modules_set = {str(key) for key in ALL_MODULES}
supported_modules_set = set(self._supported_modules.keys())
unsupported_by_cp = list(all_modules_set - supported_modules_set)
self._print_result_and_exit(
modules={module_name: cls().initial_status() for module_name, cls in self._supported_modules.items()},
unsuppored_by_cp=unsupported_by_cp,
)
def _cancel_module_installation(self, module: str) -> None:
"""
Remove module from queue or print the error if it's not possible.
"""
self.log.info("Trying to cancel the installation of module '%s'", module)
with acquire_config_access() as config:
status = config.get_module_status(module)
if status in self.CANCELLABLE_MODULE_STATUSES:
config.set_module_status(
module_name=module,
new_state=ModuleStatus.CANCELLED,
)
self.log.info("Module '%s' installation successfully canceled", module)
else:
self.log.warning(
"Unable to cancel module '%s' installation, because it is in status '%s'",
module,
status,
)
raise CancelModuleException(module, status)
def run_collecting_statistics(self):
"""
Collect user's statistics.
"""
cmd = ["/usr/sbin/cloudlinux-summary", "--send"]
if not os.environ.get("SYNCHRONOUS_SUMMARY"):
cmd.append("--async")
self.log.info("Collecting statistics...")
try:
out = run_command(cmd)
self.log.info("Statistics collection command output: '%s'", out)
except ExternalProgramFailed as err:
self.log.exception("Error during statistics collection", exc_info=err)
def is_all_modules_installed(self):
# type: () -> bool
"""
Check that all modules were either:
-- installed
-- canceled
-- or auto-skipped
"""
with acquire_config_access() as config:
statuses = list(config.statuses.values())
return all(status in self.DONE_MODULES_STATUSES for status in statuses)
def run_cagefs_force_update(self):
"""
Run cagefsctl --force-update in background.
"""
cagefsctl_bin = "/usr/sbin/cagefsctl"
if not os.path.isfile(cagefsctl_bin):
return
cmd = [cagefsctl_bin, "--force-update", "--wait-lock"]
self.log.info("Starting cagefs force-update in the background: %s", cmd)
cagefsctl_proc = run_background(cmd)
# In Cloudlinux tests environment statistics wait for cagefsctl --force-update terminate
is_test_environment = bool(os.environ.get("CL_TEST_SYSTEM"))
if is_test_environment:
cagefsctl_proc.communicate()
def _get_wizard_state(self, config: Config) -> str:
# worker pid is None only in the case when wizard
# wasn't EVER called, this worker pid will stay
# in config forever, even after wizard is Done
if config.worker_pid is None:
return WizardStatus.IDLE
try:
psutil.Process(config.worker_pid)
except psutil.NoSuchProcess:
# Background process is no longer alive.
# 1. Wizard DONE: all modules are in state "installed", "cancelled" or "auto-skipped".
# 2. Wizard FAILED: one of the modules in state "failed" or "cancelled"
# and no modules are in status "installing"
# 3. Wizard CRASHED: none of the above.
statuses = list(config.statuses.values())
if all(status in self.DONE_MODULES_STATUSES for status in statuses):
return WizardStatus.DONE
# cancel module`s status is acceptable for general wizard status FAILED, DO NOT CHANGE IT PLS (LU-1295)
# An extra check for "installing" status is needed to exclude possible CRASHED wizard status
if any(status in (ModuleStatus.FAILED, ModuleStatus.CANCELLED) for status in statuses) and not any(
status in (ModuleStatus.INSTALLING,) for status in statuses
):
return WizardStatus.FAILED
return WizardStatus.CRASHED
return WizardStatus.IN_PROGRESS
@staticmethod
def _print_result_and_exit(
result: str = "success",
exit_code: int = 0,
**extra,
) -> NoReturn:
"""
Print data in default format for web and exit.
:param dict extra: extra fields for the response, usually we expect 'context' here
"""
message = {"result": result, "timestamp": time.time()}
message.update(extra)
print(json.dumps(message, indent=2, sort_keys=True))
sys.exit(exit_code)
|