update_manager: add "full" update endpoint
This endpoint will perform a full system update. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
10da7b714f
commit
9133b59dbf
|
@ -88,6 +88,8 @@ class GitDeploy(AppDeploy):
|
||||||
if self.repo.is_current():
|
if self.repo.is_current():
|
||||||
# No need to update
|
# No need to update
|
||||||
return
|
return
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
f"Updating Application {self.name}...")
|
||||||
inst_hash = await self._get_file_hash(self.install_script)
|
inst_hash = await self._get_file_hash(self.install_script)
|
||||||
pyreqs_hash = await self._get_file_hash(self.python_reqs)
|
pyreqs_hash = await self._get_file_hash(self.python_reqs)
|
||||||
npm_hash = await self._get_file_hash(self.npm_pkg_json)
|
npm_hash = await self._get_file_hash(self.npm_pkg_json)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import time
|
||||||
import tempfile
|
import tempfile
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import tornado.gen
|
import tornado.gen
|
||||||
|
import tornado.util
|
||||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
from tornado.locks import Event, Lock
|
from tornado.locks import Event, Lock
|
||||||
|
@ -135,6 +136,7 @@ class UpdateManager:
|
||||||
|
|
||||||
self.cmd_request_lock = Lock()
|
self.cmd_request_lock = Lock()
|
||||||
self.initialized_lock = Event()
|
self.initialized_lock = Event()
|
||||||
|
self.klippy_identified_evt: Optional[Event] = None
|
||||||
|
|
||||||
# Auto Status Refresh
|
# Auto Status Refresh
|
||||||
self.last_refresh_time: float = 0
|
self.last_refresh_time: float = 0
|
||||||
|
@ -157,6 +159,9 @@ class UpdateManager:
|
||||||
self.server.register_endpoint(
|
self.server.register_endpoint(
|
||||||
"/machine/update/client", ["POST"],
|
"/machine/update/client", ["POST"],
|
||||||
self._handle_update_request)
|
self._handle_update_request)
|
||||||
|
self.server.register_endpoint(
|
||||||
|
"/machine/update/full", ["POST"],
|
||||||
|
self._handle_full_update_request)
|
||||||
self.server.register_endpoint(
|
self.server.register_endpoint(
|
||||||
"/machine/update/status", ["GET"],
|
"/machine/update/status", ["GET"],
|
||||||
self._handle_status_request)
|
self._handle_status_request)
|
||||||
|
@ -187,6 +192,8 @@ class UpdateManager:
|
||||||
self.initialized_lock.set()
|
self.initialized_lock.set()
|
||||||
|
|
||||||
async def _set_klipper_repo(self) -> None:
|
async def _set_klipper_repo(self) -> None:
|
||||||
|
if self.klippy_identified_evt is not None:
|
||||||
|
self.klippy_identified_evt.set()
|
||||||
kinfo = self.server.get_klippy_info()
|
kinfo = self.server.get_klippy_info()
|
||||||
if not kinfo:
|
if not kinfo:
|
||||||
logging.info("No valid klippy info received")
|
logging.info("No valid klippy info received")
|
||||||
|
@ -283,6 +290,67 @@ class UpdateManager:
|
||||||
self.cmd_helper.clear_update_info()
|
self.cmd_helper.clear_update_info()
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
|
async def _handle_full_update_request(self,
|
||||||
|
web_request: WebRequest
|
||||||
|
) -> str:
|
||||||
|
async with self.cmd_request_lock:
|
||||||
|
app_name = ""
|
||||||
|
self.cmd_helper.set_update_info('full', id(web_request),
|
||||||
|
full_complete=False)
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
"Preparing full software update...")
|
||||||
|
try:
|
||||||
|
# Perform system updates
|
||||||
|
if 'system' in self.updaters:
|
||||||
|
app_name = 'system'
|
||||||
|
await self.updaters['system'].update()
|
||||||
|
|
||||||
|
# Update clients
|
||||||
|
for name, updater in self.updaters.items():
|
||||||
|
if name in ['klipper', 'moonraker', 'system']:
|
||||||
|
continue
|
||||||
|
app_name = name
|
||||||
|
if not await self._check_need_reinstall(app_name):
|
||||||
|
await updater.update()
|
||||||
|
|
||||||
|
# Update Klipper
|
||||||
|
app_name = 'klipper'
|
||||||
|
kupdater = self.updaters.get('klipper')
|
||||||
|
if isinstance(kupdater, AppDeploy):
|
||||||
|
self.klippy_identified_evt = Event()
|
||||||
|
if not await self._check_need_reinstall(app_name):
|
||||||
|
await kupdater.update()
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
"Waiting for Klippy to reconnect (this may take"
|
||||||
|
" up to 2 minutes)...")
|
||||||
|
try:
|
||||||
|
await self.klippy_identified_evt.wait(
|
||||||
|
time.time() + 120.)
|
||||||
|
except tornado.util.TimeoutError:
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
"Klippy reconnect timed out...")
|
||||||
|
else:
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
f"Klippy Reconnected")
|
||||||
|
self.klippy_identified_evt = None
|
||||||
|
|
||||||
|
# Update Moonraker
|
||||||
|
app_name = 'moonraker'
|
||||||
|
if not await self._check_need_reinstall(app_name):
|
||||||
|
await self.updaters['moonraker'].update()
|
||||||
|
self.cmd_helper.set_full_complete(True)
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
"Full Update Complete", is_complete=True)
|
||||||
|
except Exception as e:
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
f"Error updating {app_name}")
|
||||||
|
self.cmd_helper.set_full_complete(True)
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
str(e), is_complete=True)
|
||||||
|
finally:
|
||||||
|
self.cmd_helper.clear_update_info()
|
||||||
|
return "ok"
|
||||||
|
|
||||||
async def _check_need_reinstall(self, name: str) -> bool:
|
async def _check_need_reinstall(self, name: str) -> bool:
|
||||||
if name not in self.updaters:
|
if name not in self.updaters:
|
||||||
return False
|
return False
|
||||||
|
@ -399,6 +467,7 @@ class CommandHelper:
|
||||||
# Update In Progress Tracking
|
# Update In Progress Tracking
|
||||||
self.cur_update_app: Optional[str] = None
|
self.cur_update_app: Optional[str] = None
|
||||||
self.cur_update_id: Optional[int] = None
|
self.cur_update_id: Optional[int] = None
|
||||||
|
self.full_complete: bool = False
|
||||||
|
|
||||||
def get_server(self) -> Server:
|
def get_server(self) -> Server:
|
||||||
return self.server
|
return self.server
|
||||||
|
@ -406,12 +475,21 @@ class CommandHelper:
|
||||||
def is_debug_enabled(self) -> bool:
|
def is_debug_enabled(self) -> bool:
|
||||||
return self.debug_enabled
|
return self.debug_enabled
|
||||||
|
|
||||||
def set_update_info(self, app: str, uid: int) -> None:
|
def set_update_info(self,
|
||||||
|
app: str,
|
||||||
|
uid: int,
|
||||||
|
full_complete: bool = True
|
||||||
|
) -> None:
|
||||||
self.cur_update_app = app
|
self.cur_update_app = app
|
||||||
self.cur_update_id = uid
|
self.cur_update_id = uid
|
||||||
|
self.full_complete = full_complete
|
||||||
|
|
||||||
|
def set_full_complete(self, complete: bool = False):
|
||||||
|
self.full_complete = complete
|
||||||
|
|
||||||
def clear_update_info(self) -> None:
|
def clear_update_info(self) -> None:
|
||||||
self.cur_update_app = self.cur_update_id = None
|
self.cur_update_app = self.cur_update_id = None
|
||||||
|
self.full_complete = False
|
||||||
|
|
||||||
def is_app_updating(self, app_name: str) -> bool:
|
def is_app_updating(self, app_name: str) -> bool:
|
||||||
return self.cur_update_app == app_name
|
return self.cur_update_app == app_name
|
||||||
|
@ -511,6 +589,7 @@ class CommandHelper:
|
||||||
resp: HTTPResponse
|
resp: HTTPResponse
|
||||||
resp = await tornado.gen.with_timeout(timeout, fut)
|
resp = await tornado.gen.with_timeout(timeout, fut)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
fut.cancel()
|
||||||
retries -= 1
|
retries -= 1
|
||||||
if retries > 0:
|
if retries > 0:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
|
@ -564,13 +643,14 @@ class CommandHelper:
|
||||||
retries = 5
|
retries = 5
|
||||||
while retries:
|
while retries:
|
||||||
try:
|
try:
|
||||||
timeout = time.time() + timeout + 10.
|
timeout = time.time() + timeout
|
||||||
fut = self.http_client.fetch(
|
fut = self.http_client.fetch(
|
||||||
url, headers={"Accept": content_type},
|
url, headers={"Accept": content_type},
|
||||||
connect_timeout=5., request_timeout=timeout)
|
connect_timeout=5., request_timeout=timeout)
|
||||||
resp: HTTPResponse
|
resp: HTTPResponse
|
||||||
resp = await tornado.gen.with_timeout(timeout, fut)
|
resp = await tornado.gen.with_timeout(timeout + 10., fut)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
fut.cancel()
|
||||||
retries -= 1
|
retries -= 1
|
||||||
logging.exception("Error Processing Download")
|
logging.exception("Error Processing Download")
|
||||||
if not retries:
|
if not retries:
|
||||||
|
@ -594,14 +674,15 @@ class CommandHelper:
|
||||||
while retries:
|
while retries:
|
||||||
dl = StreamingDownload(self, dest, size)
|
dl = StreamingDownload(self, dest, size)
|
||||||
try:
|
try:
|
||||||
timeout = time.time() + timeout + 10.
|
timeout = time.time() + timeout
|
||||||
fut = self.http_client.fetch(
|
fut = self.http_client.fetch(
|
||||||
url, headers={"Accept": content_type},
|
url, headers={"Accept": content_type},
|
||||||
connect_timeout=5., request_timeout=timeout,
|
connect_timeout=5., request_timeout=timeout,
|
||||||
streaming_callback=dl.on_chunk_recd)
|
streaming_callback=dl.on_chunk_recd)
|
||||||
resp: HTTPResponse
|
resp: HTTPResponse
|
||||||
resp = await tornado.gen.with_timeout(timeout, fut)
|
resp = await tornado.gen.with_timeout(timeout + 10., fut)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
fut.cancel()
|
||||||
retries -= 1
|
retries -= 1
|
||||||
logging.exception("Error Processing Download")
|
logging.exception("Error Processing Download")
|
||||||
if not retries:
|
if not retries:
|
||||||
|
@ -622,11 +703,12 @@ class CommandHelper:
|
||||||
resp = resp.strip()
|
resp = resp.strip()
|
||||||
if isinstance(resp, bytes):
|
if isinstance(resp, bytes):
|
||||||
resp = resp.decode()
|
resp = resp.decode()
|
||||||
|
done = is_complete and self.full_complete
|
||||||
notification = {
|
notification = {
|
||||||
'message': resp,
|
'message': resp,
|
||||||
'application': self.cur_update_app,
|
'application': self.cur_update_app,
|
||||||
'proc_id': self.cur_update_id,
|
'proc_id': self.cur_update_id,
|
||||||
'complete': is_complete}
|
'complete': done}
|
||||||
self.server.send_event(
|
self.server.send_event(
|
||||||
"update_manager:update_response", notification)
|
"update_manager:update_response", notification)
|
||||||
|
|
||||||
|
@ -833,6 +915,8 @@ class WebClientDeploy(BaseDeploy):
|
||||||
if self.remote_version == "?":
|
if self.remote_version == "?":
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Client {self.repo}: Unable to locate update")
|
f"Client {self.repo}: Unable to locate update")
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
f"Updating Web Client {self.name}...")
|
||||||
dl_url, content_type, size = self.dl_info
|
dl_url, content_type, size = self.dl_info
|
||||||
if dl_url == "?":
|
if dl_url == "?":
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
|
|
|
@ -347,6 +347,8 @@ class ZipDeploy(AppDeploy):
|
||||||
if self.short_version == self.latest_version:
|
if self.short_version == self.latest_version:
|
||||||
# already up to date
|
# already up to date
|
||||||
return
|
return
|
||||||
|
self.cmd_helper.notify_update_response(
|
||||||
|
f"Updating Application {self.name}...")
|
||||||
npm_hash = await self._get_file_hash(self.npm_pkg_json)
|
npm_hash = await self._get_file_hash(self.npm_pkg_json)
|
||||||
dl_url, content_type, size = self.release_download_info
|
dl_url, content_type, size = self.release_download_info
|
||||||
self.notify_status("Starting Download...")
|
self.notify_status("Starting Download...")
|
||||||
|
|
Loading…
Reference in New Issue