From 00c4846d11c894565c12a906028124d595cfb477 Mon Sep 17 00:00:00 2001 From: Eric Callahan Date: Sat, 15 Oct 2022 08:37:45 -0400 Subject: [PATCH] machine: validation fixes Protect the sudo request with a lock. Add retries to the sudo commands. Signed-off-by: Eric Callahan --- moonraker/components/machine.py | 111 ++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/moonraker/components/machine.py b/moonraker/components/machine.py index 987b508..cfa1110 100644 --- a/moonraker/components/machine.py +++ b/moonraker/components/machine.py @@ -86,6 +86,7 @@ class Machine: dist_info['release_info'] = distro.distro_release_info() self.inside_container = False self.moonraker_service_info: Dict[str, Any] = {} + self.sudo_req_lock = asyncio.Lock() self._sudo_password: Optional[str] = None sudo_template = config.gettemplate("sudo_password", None) if sudo_template is not None: @@ -282,50 +283,53 @@ class Machine: 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") - 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." + async with self.sudo_req_lock: + 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" ) + 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 @@ -391,7 +395,7 @@ class Machine: return False return True - async def exec_sudo_command(self, command: str) -> str: + async def exec_sudo_command(self, command: str, tries: int = 1) -> str: proc_input = None full_cmd = f"sudo {command}" if self._sudo_password is not None: @@ -399,7 +403,7 @@ class Machine: full_cmd = f"sudo -S {command}" shell_cmd: SCMDComp = self.server.lookup_component("shell_command") return await shell_cmd.exec_cmd( - full_cmd, proc_input=proc_input, log_complete=False + full_cmd, proc_input=proc_input, log_complete=False, retries=tries ) def _get_sdcard_info(self) -> Dict[str, Any]: @@ -861,7 +865,7 @@ class SystemdCliProvider(BaseProvider): ) prop_args = ",".join(properties) props: str = await self.shell_cmd.exec_cmd( - f"systemctl show -p {prop_args} {unit_name}" + f"systemctl show -p {prop_args} {unit_name}", retries=5 ) raw_props: Dict[str, Any] = {} lines = [p.strip() for p in props.split("\n") if p.strip] @@ -1276,6 +1280,7 @@ class InstallValidator: "must be updated manually." ) if unit != "moonraker": + logging.info(f"Custom service file detected: {unit}") # Not using he default unit name if app_args["is_default_data_path"]: # No datapath set, create a new, unique data path @@ -1290,11 +1295,13 @@ class InstallValidator: 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(): + logging.info(f"New data path created at {self.data_path}") self.data_path.mkdir() # A non-default datapath requires successful update of the # service @@ -1362,8 +1369,13 @@ class InstallValidator: % (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("systemctl daemon-reload") + # write new environment + env_file.write_text(ENVIRONMENT % (src_path, cmd_args)) + await machine.exec_sudo_command( + f"cp -f {tmp_svc} {svc_dest}", tries=5) + await machine.exec_sudo_command( + "systemctl daemon-reload", tries=5 + ) except asyncio.CancelledError: raise except Exception: @@ -1374,9 +1386,8 @@ class InstallValidator: ) from None finally: tmp_svc.unlink() - # write new environment - env_file.write_text(ENVIRONMENT % (src_path, cmd_args)) self.data_path_valid = True + self.sc_enabled = False return True def _check_path_bare(self, path: pathlib.Path) -> bool: