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:
Eric Callahan 2021-07-06 15:53:02 -04:00
parent 10da7b714f
commit 9133b59dbf
3 changed files with 94 additions and 6 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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...")