machine: implement install validation
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
7004722499
commit
6e56815b42
|
@ -15,27 +15,43 @@ import asyncio
|
|||
import platform
|
||||
import socket
|
||||
import ipaddress
|
||||
import time
|
||||
import shutil
|
||||
import distro
|
||||
import tempfile
|
||||
import getpass
|
||||
from confighelper import FileSourceWrapper
|
||||
from utils import MOONRAKER_PATH
|
||||
|
||||
# Annotation imports
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple
|
||||
Tuple,
|
||||
Union,
|
||||
cast
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from confighelper import ConfigHelper
|
||||
from websockets import WebRequest
|
||||
from app import MoonrakerApp
|
||||
from .shell_command import ShellCommandFactory as SCMDComp
|
||||
from .database import MoonrakerDatabase
|
||||
from .file_manager.file_manager import FileManager
|
||||
from .authorization import Authorization
|
||||
from .announcements import Announcements
|
||||
from .proc_stats import ProcStats
|
||||
from .dbus_manager import DbusManager
|
||||
from dbus_next.aio import ProxyInterface
|
||||
from dbus_next import Variant
|
||||
SudoReturn = Union[Awaitable[Tuple[str, bool]], Tuple[str, bool]]
|
||||
SudoCallback = Callable[[], SudoReturn]
|
||||
|
||||
ALLOWED_SERVICES = [
|
||||
"moonraker", "klipper", "webcamd", "MoonCord",
|
||||
|
@ -96,6 +112,8 @@ class Machine:
|
|||
raise config.error(f"Invalid Provider: {self.provider_type}")
|
||||
self.sys_provider: BaseProvider = pclass(config)
|
||||
logging.info(f"Using System Provider: {self.provider_type}")
|
||||
self.validator = InstallValidator(config)
|
||||
self.sudo_requests: List[Tuple[SudoCallback, str]] = []
|
||||
|
||||
self.server.register_endpoint(
|
||||
"/machine/reboot", ['POST'], self._handle_machine_request)
|
||||
|
@ -114,12 +132,13 @@ class Machine:
|
|||
"/machine/system_info", ['GET'],
|
||||
self._handle_sysinfo_request)
|
||||
self.server.register_endpoint(
|
||||
"/machine/sudo", ["GET"], self._handle_sudo_check)
|
||||
"/machine/sudo/info", ["GET"], self._handle_sudo_info)
|
||||
self.server.register_endpoint(
|
||||
"/machine/sudo/password", ["POST"],
|
||||
self._set_sudo_password)
|
||||
|
||||
self.server.register_notification("machine:service_state_changed")
|
||||
self.server.register_notification("machine:sudo_alert")
|
||||
|
||||
# Register remote methods
|
||||
self.server.register_remote_method(
|
||||
|
@ -152,6 +171,12 @@ class Machine:
|
|||
def public_ip(self) -> str:
|
||||
return self._public_ip
|
||||
|
||||
@property
|
||||
def unit_name(self) -> str:
|
||||
svc_info = self.moonraker_service_info
|
||||
unit_name = svc_info.get("unit_name", "moonraker.service")
|
||||
return unit_name.split(".", 1)[0]
|
||||
|
||||
def get_system_provider(self):
|
||||
return self.sys_provider
|
||||
|
||||
|
@ -189,6 +214,12 @@ class Machine:
|
|||
self.log_service_info(svc_info)
|
||||
self.init_evt.set()
|
||||
|
||||
async def validate_installation(self) -> bool:
|
||||
return await self.validator.perform_validation()
|
||||
|
||||
async def on_exit(self) -> None:
|
||||
await self.validator.remove_announcement()
|
||||
|
||||
async def _handle_machine_request(self, web_request: WebRequest) -> str:
|
||||
ep = web_request.get_endpoint()
|
||||
if self.inside_container:
|
||||
|
@ -210,15 +241,22 @@ class Machine:
|
|||
) -> None:
|
||||
await self.sys_provider.do_service_action(action, service_name)
|
||||
|
||||
def restart_moonraker_service(self):
|
||||
async def wrapper():
|
||||
try:
|
||||
await self.do_service_action("restart", self.unit_name)
|
||||
except Exception:
|
||||
pass
|
||||
self.server.get_event_loop().create_task(wrapper())
|
||||
|
||||
async def _handle_service_request(self, web_request: WebRequest) -> str:
|
||||
name: str = web_request.get('service')
|
||||
action = web_request.get_endpoint().split('/')[-1]
|
||||
if name == "moonraker":
|
||||
if name == self.unit_name:
|
||||
if action != "restart":
|
||||
raise self.server.error(
|
||||
f"Service action '{action}' not available for moonraker")
|
||||
event_loop = self.server.get_event_loop()
|
||||
event_loop.register_callback(self.do_service_action, action, name)
|
||||
self.restart_moonraker_service()
|
||||
elif self.sys_provider.is_service_available(name):
|
||||
await self.do_service_action(action, name)
|
||||
else:
|
||||
|
@ -231,21 +269,73 @@ class Machine:
|
|||
async def _handle_sysinfo_request(self,
|
||||
web_request: WebRequest
|
||||
) -> Dict[str, Any]:
|
||||
return {'system_info': self.system_info}
|
||||
return {"system_info": self.system_info}
|
||||
|
||||
async def _set_sudo_password(self, web_request: WebRequest) -> str:
|
||||
async def _set_sudo_password(
|
||||
self, web_request: WebRequest
|
||||
) -> Dict[str, Any]:
|
||||
self._sudo_password = web_request.get_str("password")
|
||||
if not await self.check_sudo_access():
|
||||
self._sudo_password = None
|
||||
raise self.server.error("Invalid password, sudo access was denied")
|
||||
self.server.send_event("machine:sudo_password_set")
|
||||
return "ok"
|
||||
sudo_responses = ["Sudo password successfully set."]
|
||||
restart: bool = False
|
||||
failed: List[Tuple[SudoCallback, str]] = []
|
||||
failed_msgs: List[str] = []
|
||||
if self.sudo_requests:
|
||||
while self.sudo_requests:
|
||||
cb, msg = self.sudo_requests.pop(0)
|
||||
try:
|
||||
ret = cb()
|
||||
if isinstance(ret, Awaitable):
|
||||
ret = await ret
|
||||
msg, need_restart = ret
|
||||
sudo_responses.append(msg)
|
||||
restart |= need_restart
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
failed.append((cb, msg))
|
||||
failed_msgs.append(str(e))
|
||||
restart = False if len(failed) > 0 else restart
|
||||
self.sudo_requests = failed
|
||||
if not restart and len(sudo_responses) > 1:
|
||||
# at least one successful response and not restarting
|
||||
eventloop = self.server.get_event_loop()
|
||||
eventloop.delay_callback(
|
||||
.05, self.server.send_event,
|
||||
"machine:sudo_alert",
|
||||
{
|
||||
"sudo_requested": self.sudo_requested,
|
||||
"request_messages": self.sudo_request_messages
|
||||
}
|
||||
)
|
||||
if failed_msgs:
|
||||
err_msg = "\n".join(failed_msgs)
|
||||
raise self.server.error(err_msg, 500)
|
||||
if restart:
|
||||
self.restart_moonraker_service()
|
||||
sudo_responses.append(
|
||||
"Moonraker is currently in the process of restarting."
|
||||
)
|
||||
return {
|
||||
"sudo_responses": sudo_responses,
|
||||
"is_restarting": restart
|
||||
}
|
||||
|
||||
async def _handle_sudo_check(
|
||||
async def _handle_sudo_info(
|
||||
self, web_request: WebRequest
|
||||
) -> Dict[str, Any]:
|
||||
check_access = web_request.get("check_access", False)
|
||||
has_sudo: Optional[bool] = None
|
||||
if check_access:
|
||||
has_sudo = await self.check_sudo_access()
|
||||
return {"sudo_access": has_sudo}
|
||||
return {
|
||||
"sudo_access": has_sudo,
|
||||
"linux_user": self.linux_user,
|
||||
"sudo_requested": self.sudo_requested,
|
||||
"request_messages": self.sudo_request_messages
|
||||
}
|
||||
|
||||
def get_system_info(self) -> Dict[str, Any]:
|
||||
return self.system_info
|
||||
|
@ -254,6 +344,30 @@ class Machine:
|
|||
def sudo_password(self) -> Optional[str]:
|
||||
return self._sudo_password
|
||||
|
||||
@property
|
||||
def sudo_requested(self) -> bool:
|
||||
return len(self.sudo_requests) > 0
|
||||
|
||||
@property
|
||||
def linux_user(self) -> str:
|
||||
return getpass.getuser()
|
||||
|
||||
@property
|
||||
def sudo_request_messages(self) -> List[str]:
|
||||
return [req[1] for req in self.sudo_requests]
|
||||
|
||||
def register_sudo_request(
|
||||
self, callback: SudoCallback, message: str
|
||||
) -> None:
|
||||
self.sudo_requests.append((callback, message))
|
||||
self.server.send_event(
|
||||
"machine:sudo_alert",
|
||||
{
|
||||
"sudo_requested": True,
|
||||
"request_messages": self.sudo_request_messages
|
||||
}
|
||||
)
|
||||
|
||||
async def check_sudo_access(self, cmds: List[str] = []) -> bool:
|
||||
if not cmds:
|
||||
cmds = ["systemctl --version", "ls /lost+found"]
|
||||
|
@ -1017,5 +1131,464 @@ class SystemdDbusProvider(BaseProvider):
|
|||
processed[key] = val
|
||||
return processed
|
||||
|
||||
|
||||
# Install validation
|
||||
INSTALL_VERSION = 1
|
||||
SERVICE_VERSION = 1
|
||||
|
||||
SYSTEMD_UNIT = \
|
||||
"""
|
||||
# systemd service file for moonraker
|
||||
[Unit]
|
||||
Description=API Server for Klipper SV%d
|
||||
Requires=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=%s
|
||||
SupplementaryGroups=moonraker-admin
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=%s
|
||||
EnvironmentFile=%s
|
||||
ExecStart=%s $MOONRAKER_ARGS
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
""" # noqa: E122
|
||||
|
||||
ENVIRONMENT = "MOONRAKER_ARGS=\"%s/moonraker/moonraker.py %s\""
|
||||
TEMPLATE_NAME = "password_request.html"
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
class InstallValidator:
|
||||
def __init__(self, config: ConfigHelper) -> None:
|
||||
self.server = config.get_server()
|
||||
self.config = config
|
||||
self.server.load_component(config, "template")
|
||||
self.force_validation = config.getboolean("force_validation", False)
|
||||
self.sc_enabled = config.getboolean("validate_service", True)
|
||||
self.cc_enabled = config.getboolean("validate_config", True)
|
||||
app_args = self.server.get_app_args()
|
||||
self.data_path = pathlib.Path(app_args["data_path"])
|
||||
self._update_backup_path()
|
||||
self.alias = app_args["alias"]
|
||||
self.data_path_valid = True
|
||||
self._sudo_requested = False
|
||||
self.announcement_id = ""
|
||||
|
||||
def _update_backup_path(self):
|
||||
str_time = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
||||
if not hasattr(self, "backup_path"):
|
||||
self.backup_path = self.data_path.joinpath(f"backup/{str_time}")
|
||||
elif not self.backup_path.exists():
|
||||
self.backup_path = self.data_path.joinpath(f"backup/{str_time}")
|
||||
|
||||
async def perform_validation(self) -> bool:
|
||||
db: MoonrakerDatabase = self.server.lookup_component("database")
|
||||
install_ver: int = await db.get_item(
|
||||
"moonraker", "validate_install.install_version", 0
|
||||
)
|
||||
if INSTALL_VERSION <= install_ver and not self.force_validation:
|
||||
logging.debug("Installation version in database up to date")
|
||||
return False
|
||||
need_restart: bool = False
|
||||
has_error: bool = False
|
||||
try:
|
||||
name = "service"
|
||||
need_restart = await self._check_service_file()
|
||||
name = "config"
|
||||
need_restart |= await self._check_configuration()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except ValidationError as ve:
|
||||
has_error = True
|
||||
self.server.add_warning(str(ve))
|
||||
except Exception as e:
|
||||
has_error = True
|
||||
msg = f"Failed to validate {name}: {e}"
|
||||
logging.exception(msg)
|
||||
self.server.add_warning(msg, log=False)
|
||||
else:
|
||||
await db.insert_item(
|
||||
"moonraker", "validate_install.install_version", INSTALL_VERSION
|
||||
)
|
||||
if not has_error and need_restart:
|
||||
machine: Machine = self.server.lookup_component("machine")
|
||||
machine.restart_moonraker_service()
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _check_service_file(self) -> bool:
|
||||
if not self.sc_enabled:
|
||||
return False
|
||||
machine: Machine = self.server.lookup_component("machine")
|
||||
if machine.is_inside_container():
|
||||
raise ValidationError(
|
||||
"Moonraker instance running inside a container, "
|
||||
"cannot validate service file."
|
||||
)
|
||||
if machine.get_provider_type() == "none":
|
||||
raise ValidationError(
|
||||
"No machine provider configured, cannot validate service file."
|
||||
)
|
||||
logging.info("Performing Service Validation...")
|
||||
app_args = self.server.get_app_args()
|
||||
svc_info = machine.get_moonraker_service_info()
|
||||
if not svc_info:
|
||||
raise ValidationError(
|
||||
"Unable to retrieve Moonraker service info. Service file "
|
||||
"must be updated manually."
|
||||
)
|
||||
props: Dict[str, str] = svc_info.get("properties", {})
|
||||
if "FragmentPath" not in props:
|
||||
raise ValidationError(
|
||||
"Unable to locate path to Moonraker's service unit. Service "
|
||||
"file must be must be updated manually."
|
||||
)
|
||||
desc = props.get("Description", "")
|
||||
ver_match = re.match(r"API Server for Klipper SV(\d+)", desc)
|
||||
if ver_match is not None and int(ver_match.group(1)) == SERVICE_VERSION:
|
||||
logging.info("Service file validated and up to date")
|
||||
return False
|
||||
unit: str = svc_info.get("unit_name", "").split(".", 1)[0]
|
||||
if not unit:
|
||||
raise ValidationError(
|
||||
"Unable to retrieve service unit name. Service file "
|
||||
"must be updated manually."
|
||||
)
|
||||
if unit != self.alias and self.alias == "moonraker":
|
||||
# alias differs from unit name, current alias is set to default
|
||||
self.alias = unit
|
||||
if app_args["is_default_data_path"]:
|
||||
# Using default datapath, switch to alias based path to
|
||||
# avoid conflict
|
||||
new_dp = pathlib.Path(f"~/{unit}_data").expanduser().resolve()
|
||||
if new_dp.exists() and not self._check_path_bare(new_dp):
|
||||
raise ValidationError(
|
||||
f"Cannot resolve data path for custom unit '{unit}', "
|
||||
f"data path '{new_dp}' already exists. Service file "
|
||||
"must be updated manually."
|
||||
)
|
||||
# If the current path is bare we can remove it
|
||||
if self._check_path_bare(self.data_path):
|
||||
shutil.rmtree(self.data_path)
|
||||
self.data_path = new_dp
|
||||
if not self.data_path.exists():
|
||||
self.data_path.mkdir()
|
||||
# A non-default datapath requires successful update of the
|
||||
# service
|
||||
self.data_path_valid = False
|
||||
if await machine.check_sudo_access():
|
||||
logging.info("Moonraker has sudo access")
|
||||
else:
|
||||
self._request_sudo_access()
|
||||
raise ValidationError(
|
||||
"Moonraker requires sudo permission to update the system "
|
||||
"service. Please check your notifications for further "
|
||||
"intructions."
|
||||
)
|
||||
self._sudo_requested = False
|
||||
svc_dest = pathlib.Path(props["FragmentPath"])
|
||||
user: str = props["User"]
|
||||
tmp_svc = pathlib.Path(
|
||||
tempfile.gettempdir()
|
||||
).joinpath(f"{unit}-tmp.svc")
|
||||
src_path = pathlib.Path(MOONRAKER_PATH)
|
||||
# Create local environment file
|
||||
env_file = src_path.joinpath(f"{unit}.env")
|
||||
cmd_args = f"-a {unit} -d {self.data_path}"
|
||||
cfg_file = pathlib.Path(app_args["config_file"])
|
||||
fm: FileManager = self.server.lookup_component("file_manager")
|
||||
cfg_path = fm.get_directory("config")
|
||||
log_path = fm.get_directory("logs")
|
||||
if not cfg_path or not cfg_file.parent.samefile(cfg_path):
|
||||
# Configuration file does not exist in config path
|
||||
cmd_args += f" -c {cfg_file}"
|
||||
elif cfg_file.name != "moonraker.conf":
|
||||
cfg_file = self.data_path.joinpath(f"config/{cfg_file.name}")
|
||||
cmd_args += f" -c {cfg_file}"
|
||||
if not app_args["log_file"]:
|
||||
# No log file configured
|
||||
cmd_args += f" -n"
|
||||
else:
|
||||
# Log file does not exist in log path
|
||||
log_file = pathlib.Path(app_args["log_file"])
|
||||
if not log_path or not log_file.parent.samefile(log_path):
|
||||
cmd_args += f" -l {log_file}"
|
||||
elif log_file.name != "moonraker.log":
|
||||
cfg_file = self.data_path.joinpath(f"logs/{log_file.name}")
|
||||
cmd_args += f" -l {log_file}"
|
||||
# backup existing service files
|
||||
self._update_backup_path()
|
||||
svc_bkp_path = self.backup_path.joinpath("service")
|
||||
os.makedirs(str(svc_bkp_path), exist_ok=True)
|
||||
if env_file.exists():
|
||||
env_bkp = svc_bkp_path.joinpath(env_file.name)
|
||||
shutil.copy2(str(env_file), str(env_bkp))
|
||||
service_bkp = svc_bkp_path.joinpath(svc_dest.name)
|
||||
shutil.copy2(str(svc_dest), str(service_bkp))
|
||||
# write temporary service file
|
||||
tmp_svc.write_text(
|
||||
SYSTEMD_UNIT
|
||||
% (SERVICE_VERSION, user, src_path, env_file, sys.executable)
|
||||
)
|
||||
try:
|
||||
await machine.exec_sudo_command(f"cp -f {tmp_svc} {svc_dest}")
|
||||
await machine.exec_sudo_command(f"systemctl daemon-reload")
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
logging.exception("Failed to update moonraker service unit")
|
||||
raise ValidationError(
|
||||
f"Failed to update service unit file '{svc_dest}'. Update must "
|
||||
f"be performed manually."
|
||||
) from None
|
||||
finally:
|
||||
tmp_svc.unlink()
|
||||
# write new environment
|
||||
env_file.write_text(ENVIRONMENT % (src_path, cmd_args))
|
||||
self.data_path_valid = True
|
||||
return True
|
||||
|
||||
def _check_path_bare(self, path: pathlib.Path) -> bool:
|
||||
empty: bool = True
|
||||
for item in path.iterdir():
|
||||
if (
|
||||
item.is_file() or
|
||||
item.is_symlink() or
|
||||
item.name not in ["gcodes", "config", "logs", "certs"]
|
||||
):
|
||||
empty = False
|
||||
break
|
||||
if next(item.iterdir(), None) is not None:
|
||||
empty = False
|
||||
break
|
||||
return empty
|
||||
|
||||
def _link_data_subfolder(
|
||||
self, folder_name: str, source_dir: Union[str, pathlib.Path]
|
||||
) -> None:
|
||||
if isinstance(source_dir, str):
|
||||
source_dir = pathlib.Path(source_dir).expanduser().resolve()
|
||||
if not source_dir.is_dir():
|
||||
raise ValidationError(
|
||||
f"Failed to link subfolder '{folder_name}' to source path "
|
||||
f"'{source_dir}'. The requusted path is not a valid directory."
|
||||
)
|
||||
subfolder = self.data_path.joinpath(folder_name)
|
||||
if subfolder.is_symlink():
|
||||
if not subfolder.samefile(source_dir):
|
||||
raise ValidationError(
|
||||
f"Failed to link subfolder '{folder_name}' to "
|
||||
f"'{source_dir}'. '{folder_name}' already exists and is "
|
||||
f"linked to {subfolder}. This conflict requires "
|
||||
"manual resolution."
|
||||
)
|
||||
return
|
||||
if not subfolder.exists():
|
||||
subfolder.symlink_to(source_dir)
|
||||
return
|
||||
if subfolder.is_dir() and next(subfolder.iterdir(), None) is None:
|
||||
subfolder.rmdir()
|
||||
subfolder.symlink_to(source_dir)
|
||||
return
|
||||
raise ValidationError(
|
||||
f"Failed to link subfolder '{folder_name}' to '{source_dir}'. "
|
||||
f"Folder '{folder_name}' already exists. This conflict requires "
|
||||
"manual resolution."
|
||||
)
|
||||
|
||||
def _link_data_file(
|
||||
self,
|
||||
data_file: Union[str, pathlib.Path],
|
||||
target: Union[str, pathlib.Path]
|
||||
) -> None:
|
||||
if isinstance(data_file, str):
|
||||
data_file = pathlib.Path(data_file)
|
||||
if isinstance(target, str):
|
||||
target = pathlib.Path(target)
|
||||
target = target.expanduser().resolve()
|
||||
if not target.is_file():
|
||||
raise ValidationError(
|
||||
f"Failed to link data file {data_file.name}. Target "
|
||||
f"{target} is not a valid file."
|
||||
)
|
||||
if data_file.is_symlink():
|
||||
if not data_file.samefile(target):
|
||||
raise ValidationError(
|
||||
f"Failed to link data file {data_file.name}. Link "
|
||||
f"to {data_file.resolve()} already exists. This conflict "
|
||||
"must be resolved manually."
|
||||
)
|
||||
return
|
||||
if not data_file.exists():
|
||||
data_file.symlink_to(target)
|
||||
return
|
||||
raise ValidationError(
|
||||
f"Failed to link data file {data_file.name}. File already exists. "
|
||||
f"This conflict must be resolved manually."
|
||||
)
|
||||
|
||||
async def _check_configuration(self) -> bool:
|
||||
if not self.cc_enabled or not self.data_path_valid:
|
||||
return False
|
||||
db: MoonrakerDatabase = self.server.lookup_component("database")
|
||||
cfg_source = cast(FileSourceWrapper, self.config.get_source())
|
||||
cfg_source.backup_source()
|
||||
try:
|
||||
# write current configuration to backup path
|
||||
self._update_backup_path()
|
||||
cfg_bkp_path = self.backup_path.joinpath("config")
|
||||
os.makedirs(str(cfg_bkp_path), exist_ok=True)
|
||||
await cfg_source.write_config(cfg_bkp_path)
|
||||
# Create symbolic links for configured folders
|
||||
server_cfg = self.config["server"]
|
||||
fm_cfg = self.config["file_manager"]
|
||||
db_cfg = self.config["database"]
|
||||
cfg_path = fm_cfg.get("config_path", None)
|
||||
if cfg_path is None:
|
||||
cfg_path = server_cfg.get("config_path", None)
|
||||
if cfg_path is not None:
|
||||
self._link_data_subfolder("config", cfg_path)
|
||||
cfg_source.remove_option("server", "config_path")
|
||||
cfg_source.remove_option("file_manager", "config_path")
|
||||
|
||||
log_path = fm_cfg.get("log_path", None)
|
||||
if log_path is None:
|
||||
log_path = server_cfg.get("log_path", None)
|
||||
if log_path is not None:
|
||||
self._link_data_subfolder("logs", log_path)
|
||||
cfg_source.remove_option("server", "log_path")
|
||||
cfg_source.remove_option("file_manager", "log_path")
|
||||
|
||||
gc_path: Optional[str] = await db.get_item(
|
||||
"moonraker", "file_manager.gcode_path", None
|
||||
)
|
||||
if gc_path is not None:
|
||||
self._link_data_subfolder("gcodes", gc_path)
|
||||
db.delete_item("moonraker", "file_manager.gcode_path")
|
||||
|
||||
db_path = db_cfg.get("database_path", None)
|
||||
default_db = pathlib.Path("~/.moonraker_database").expanduser()
|
||||
if db_path is None and default_db.exists():
|
||||
self._link_data_subfolder("database", default_db)
|
||||
elif db_path is not None:
|
||||
self._link_data_subfolder("database", db_path)
|
||||
cfg_source.remove_option("database", "database_path")
|
||||
|
||||
# Link individual files
|
||||
secrets_path = self.config["secrets"].get("secrets_path", None)
|
||||
if secrets_path is not None:
|
||||
secrets_dest = self.data_path.joinpath(f"{self.alias}.secrets")
|
||||
self._link_data_file(secrets_dest, secrets_path)
|
||||
cfg_source.remove_option("secrets", "secrets_path")
|
||||
certs_path = self.data_path.joinpath("certs")
|
||||
if not certs_path.exists():
|
||||
certs_path.mkdir()
|
||||
ssl_cert = server_cfg.get("ssl_certificate_path", None)
|
||||
if ssl_cert is not None:
|
||||
cert_dest = certs_path.joinpath(f"{self.alias}.cert")
|
||||
self._link_data_file(cert_dest, ssl_cert)
|
||||
cfg_source.remove_option("server", "ssl_certificate_path")
|
||||
ssl_key = server_cfg.get("ssl_key_path", None)
|
||||
if ssl_key is not None:
|
||||
key_dest = certs_path.joinpath(f"{self.alias}.key")
|
||||
self._link_data_file(key_dest, ssl_key)
|
||||
cfg_source.remove_option("server", "ssl_key_path")
|
||||
except Exception:
|
||||
cfg_source.cancel()
|
||||
raise
|
||||
finally:
|
||||
self.cc_enabled = False
|
||||
return await cfg_source.save()
|
||||
|
||||
def _request_sudo_access(self) -> None:
|
||||
if self._sudo_requested:
|
||||
return
|
||||
self._sudo_requested = True
|
||||
auth: Optional[Authorization]
|
||||
auth = self.server.lookup_component("authorizaton", None)
|
||||
if auth is not None:
|
||||
# Bypass authentication requirements
|
||||
auth.register_permited_path("/machine/sudo/password")
|
||||
machine: Machine = self.server.lookup_component("machine")
|
||||
machine.register_sudo_request(
|
||||
self._on_password_received,
|
||||
"Root access required to update Moonraker's systemd service."
|
||||
)
|
||||
if not machine.public_ip:
|
||||
async def wrapper(pub_ip):
|
||||
if not pub_ip:
|
||||
return
|
||||
await self.remove_announcement()
|
||||
self._announce_sudo_request()
|
||||
self.server.register_event_handler(
|
||||
"machine:public_ip_changed", wrapper
|
||||
)
|
||||
self._announce_sudo_request()
|
||||
|
||||
def _announce_sudo_request(self) -> None:
|
||||
machine: Machine = self.server.lookup_component("machine")
|
||||
host_info = self.server.get_host_info()
|
||||
host_addr: str = host_info["address"]
|
||||
if host_addr.lower() not in ["all", "0.0.0.0", "::"]:
|
||||
address = host_addr
|
||||
else:
|
||||
address = machine.public_ip
|
||||
if not address:
|
||||
address = f"{host_info['hostname']}.local"
|
||||
elif ":" in address:
|
||||
# ipv6 address
|
||||
address = f"[{address}]"
|
||||
app: MoonrakerApp = self.server.lookup_component("application")
|
||||
scheme = "https" if app.https_enabled() else "http"
|
||||
host_info = self.server.get_host_info()
|
||||
port = host_info["port"]
|
||||
url = f"{scheme}://{address}:{port}/"
|
||||
ancmp: Announcements = self.server.lookup_component("announcements")
|
||||
entry = ancmp.add_internal_announcement(
|
||||
"Sudo Password Required",
|
||||
"Moonraker requires sudo access to finish updating. "
|
||||
"Please click on the attached link and follow the "
|
||||
"instructions.",
|
||||
url, "high", "machine"
|
||||
)
|
||||
self.announcement_id = entry.get("entry_id", "")
|
||||
|
||||
async def remove_announcement(self):
|
||||
if not self.announcement_id:
|
||||
return
|
||||
ancmp: Announcements = self.server.lookup_component("announcements")
|
||||
# remove stale announcement
|
||||
try:
|
||||
await ancmp.remove_announcement(self.announcement_id)
|
||||
except self.server.error:
|
||||
pass
|
||||
self.announcement_id = ""
|
||||
|
||||
async def _on_password_received(self) -> Tuple[str, bool]:
|
||||
try:
|
||||
name = "Service"
|
||||
await self._check_service_file()
|
||||
name = "Config"
|
||||
await self._check_configuration()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise self.server.error(
|
||||
f"{name} validation failed with error:\n{e}", 500
|
||||
) from e
|
||||
await self.remove_announcement()
|
||||
db: MoonrakerDatabase = self.server.lookup_component("database")
|
||||
await db.insert_item(
|
||||
"moonraker", "validate_install.install_version", INSTALL_VERSION
|
||||
)
|
||||
return "System update complete.", True
|
||||
|
||||
def load_component(config: ConfigHelper) -> Machine:
|
||||
return Machine(config)
|
||||
|
|
Loading…
Reference in New Issue