From 924f0643eda34876c97d4d9cec7eb4d54fcf673b Mon Sep 17 00:00:00 2001 From: Christoph Stenglein <9399034+cstenglein@users.noreply.github.com> Date: Wed, 23 Mar 2022 20:20:58 +0100 Subject: [PATCH 1/2] implement reboot & shutdown (#70) * implement reboot & shutdown * fix waiting for script by using event loop * implemented suggestions * remove unused import * implement suggestions #2 --- app/repositories/system_impl/raspiblitz.py | 27 ++++++++++++++++++++++ app/routers/system.py | 24 +++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/repositories/system_impl/raspiblitz.py b/app/repositories/system_impl/raspiblitz.py index 339d5f8..37342c0 100644 --- a/app/repositories/system_impl/raspiblitz.py +++ b/app/repositories/system_impl/raspiblitz.py @@ -1,3 +1,6 @@ +import asyncio +import os + from app.constants import API_VERSION from app.models.system import ( APIPlatform, @@ -7,6 +10,7 @@ from app.models.system import ( SystemInfo, ) from app.repositories.lightning import get_ln_info +from app.repositories.system import SHELL_SCRIPT_PATH from app.utils import redis_get @@ -34,3 +38,26 @@ async def get_system_info_impl() -> SystemInfo: ssh_address=f"admin@{lan}", chain=lninfo.chains[0].network, ) + + +async def shutdown(reboot: bool): + params = "" + if reboot: + params = "reboot" + + script = os.path.join(SHELL_SCRIPT_PATH, "config.scripts", "blitz.shutdown.sh") + cmd = f"sudo bash {script} {params}" + + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await proc.communicate() + + print(f"[{cmd!r} exited with {proc.returncode}]") + if stdout: + print(f"[stdout]\n{stdout.decode()}") + if stderr: + print(f"[stderr]\n{stderr.decode()}") diff --git a/app/routers/system.py b/app/routers/system.py index 5168e0d..b7383d1 100644 --- a/app/routers/system.py +++ b/app/routers/system.py @@ -8,9 +8,10 @@ from fastapi.params import Depends from app.auth.auth_bearer import JWTBearer from app.auth.auth_handler import signJWT from app.external.sse_startlette import EventSourceResponse -from app.models.system import LoginInput, RawDebugLogData, SystemInfo +from app.models.system import APIPlatform, LoginInput, RawDebugLogData, SystemInfo from app.repositories.system import ( HW_INFO_YIELD_TIME, + PLATFORM, get_debug_logs_raw, get_hardware_info, get_system_info, @@ -19,6 +20,7 @@ from app.repositories.system import ( parseKeyValueText, passwordValid ) +from app.repositories.system_impl.raspiblitz import shutdown from app.routers.system_docs import ( get_debug_logs_raw_desc, get_debug_logs_raw_resp_desc, @@ -132,8 +134,14 @@ async def hw_info_sub(request: Request): summary="Reboots the system", dependencies=[Depends(JWTBearer())], ) -def reboot_system(): - return HTTPException(status.HTTP_501_NOT_IMPLEMENTED) +async def reboot_system() -> bool: + if PLATFORM == APIPlatform.RASPIBLITZ: + await shutdown(True) + return True + else: + raise HTTPException( + status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" + ) @router.post( @@ -142,5 +150,11 @@ def reboot_system(): summary="Shuts the system down", dependencies=[Depends(JWTBearer())], ) -def reboot_system(): - return HTTPException(status.HTTP_501_NOT_IMPLEMENTED) +async def shutdown() -> bool: + if PLATFORM == APIPlatform.RASPIBLITZ: + await shutdown(False) + return True + else: + raise HTTPException( + status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" + ) From 85d2de19e4ccf8b4406647690e772ef93ae4d7ef Mon Sep 17 00:00:00 2001 From: fusion44 Date: Wed, 23 Mar 2022 21:03:50 +0100 Subject: [PATCH 2/2] chore: fix some naming issues --- app/auth/auth_handler.py | 4 +- .../LICENSE.md | 0 .../__init__.py | 0 .../sse_starlette.py | 0 app/main.py | 2 +- app/repositories/system.py | 36 +++++--- app/repositories/system_impl/raspiblitz.py | 5 +- app/routers/apps.py | 2 +- app/routers/bitcoin.py | 2 +- app/routers/setup.py | 86 ++++++++----------- app/routers/system.py | 29 ++++--- 11 files changed, 86 insertions(+), 80 deletions(-) rename app/external/{sse_startlette => sse_starlette}/LICENSE.md (100%) rename app/external/{sse_startlette => sse_starlette}/__init__.py (100%) rename app/external/{sse_startlette => sse_starlette}/sse_starlette.py (100%) diff --git a/app/auth/auth_handler.py b/app/auth/auth_handler.py index b18df83..cf2794b 100644 --- a/app/auth/auth_handler.py +++ b/app/auth/auth_handler.py @@ -11,7 +11,7 @@ JWT_ALGORITHM = config("algorithm") JWT_EXPIRY_TIME = config("jwt_expiry_time", default=300, cast=int) -def signJWT() -> Dict[str, str]: +def sign_jwt() -> Dict[str, str]: payload = { "user_id": "admin", "expires": int(round(time.time() * 1000) + JWT_EXPIRY_TIME), @@ -45,7 +45,7 @@ def handle_local_cookie(): if enabled: f = open(full_cookie_file_path, "w") - f.write(signJWT()["access_token"]) + f.write(sign_jwt()["access_token"]) f.close() diff --git a/app/external/sse_startlette/LICENSE.md b/app/external/sse_starlette/LICENSE.md similarity index 100% rename from app/external/sse_startlette/LICENSE.md rename to app/external/sse_starlette/LICENSE.md diff --git a/app/external/sse_startlette/__init__.py b/app/external/sse_starlette/__init__.py similarity index 100% rename from app/external/sse_startlette/__init__.py rename to app/external/sse_starlette/__init__.py diff --git a/app/external/sse_startlette/sse_starlette.py b/app/external/sse_starlette/sse_starlette.py similarity index 100% rename from app/external/sse_startlette/sse_starlette.py rename to app/external/sse_starlette/sse_starlette.py diff --git a/app/main.py b/app/main.py index 243d650..2b13eee 100644 --- a/app/main.py +++ b/app/main.py @@ -22,7 +22,7 @@ from app.auth.auth_handler import ( remove_local_cookie, ) from app.external.fastapi_versioning import VersionedFastAPI -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse from app.models.system import APIPlatform from app.repositories.bitcoin import ( register_bitcoin_status_gatherer, diff --git a/app/repositories/system.py b/app/repositories/system.py index fa407e6..60162ff 100644 --- a/app/repositories/system.py +++ b/app/repositories/system.py @@ -1,7 +1,7 @@ import asyncio -from os import path import logging import re +from os import path from decouple import config from fastapi import HTTPException, Request, status @@ -39,7 +39,8 @@ def _check_shell_scripts_status(): _check_shell_scripts_status() -async def callScript(scriptPath) -> str: + +async def call_script(scriptPath) -> str: cmd = f"bash {scriptPath}" logging.warning(f"running script: {cmd}") proc = await asyncio.create_subprocess_shell( @@ -54,24 +55,33 @@ async def callScript(scriptPath) -> str: logging.error(stderr.decode()) return "" -def parseKeyValueLines(lines:list) -> dict: + +def parse_key_value_lines(lines: list) -> dict: Dict = {} for line in lines: logging.warning(f"line({line})") - if len(line.strip()) == 0: continue - if line.strip().startswith('#'): continue - if line.find('=') <=0: continue - key, value = line.strip().split('=',1) + if len(line.strip()) == 0: + continue + if line.strip().startswith("#"): + continue + if line.find("=") <= 0: + continue + key, value = line.strip().split("=", 1) Dict[key] = value.strip('"').strip("'") return Dict -def parseKeyValueText(text:str) -> dict: - return parseKeyValueLines(text.splitlines()) -def passwordValid(password : str): - if len(password) < 8: return False - if password.find(' ') >= 0: return False - return re.match('^[a-zA-Z0-9]*$', password) +def parse_key_value_text(text: str) -> dict: + return parse_key_value_lines(text.splitlines()) + + +def password_valid(password: str): + if len(password) < 8: + return False + if password.find(" ") >= 0: + return False + return re.match("^[a-zA-Z0-9]*$", password) + async def get_system_info() -> SystemInfo: try: diff --git a/app/repositories/system_impl/raspiblitz.py b/app/repositories/system_impl/raspiblitz.py index 37342c0..59d2a8a 100644 --- a/app/repositories/system_impl/raspiblitz.py +++ b/app/repositories/system_impl/raspiblitz.py @@ -1,6 +1,8 @@ import asyncio import os +from decouple import config + from app.constants import API_VERSION from app.models.system import ( APIPlatform, @@ -10,9 +12,10 @@ from app.models.system import ( SystemInfo, ) from app.repositories.lightning import get_ln_info -from app.repositories.system import SHELL_SCRIPT_PATH from app.utils import redis_get +SHELL_SCRIPT_PATH = config("shell_script_path") + async def get_system_info_impl() -> SystemInfo: lninfo = await get_ln_info() diff --git a/app/routers/apps.py b/app/routers/apps.py index 8335237..3f93e63 100644 --- a/app/routers/apps.py +++ b/app/routers/apps.py @@ -4,7 +4,7 @@ from fastapi.params import Depends import app.repositories.apps as repo import app.routers.apps_docs as docs from app.auth.auth_bearer import JWTBearer -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse _PREFIX = "apps" diff --git a/app/routers/bitcoin.py b/app/routers/bitcoin.py index 64deb44..5775831 100644 --- a/app/routers/bitcoin.py +++ b/app/routers/bitcoin.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, HTTPException, Request, status from fastapi.params import Depends, Query from app.auth.auth_bearer import JWTBearer -from app.external.sse_startlette import EventSourceResponse +from app.external.sse_starlette import EventSourceResponse from app.models.bitcoind import BlockchainInfo, BtcInfo, FeeEstimationMode, NetworkInfo from app.repositories.bitcoin import ( estimate_fee, diff --git a/app/routers/setup.py b/app/routers/setup.py index e5a83a0..04721c8 100644 --- a/app/routers/setup.py +++ b/app/routers/setup.py @@ -1,5 +1,6 @@ import asyncio -#from asyncio.windows_events import NULL + +# from asyncio.windows_events import NULL import logging import re @@ -9,27 +10,22 @@ from fastapi.params import Depends from fastapi_plugins import depends_redis from setuptools import setup -from app.repositories.system import ( - callScript, - parseKeyValueLines, - passwordValid -) - from app.auth.auth_bearer import JWTBearer -from app.auth.auth_handler import signJWT +from app.auth.auth_handler import sign_jwt +from app.repositories.system import call_script, parse_key_value_lines, password_valid from app.utils import redis_get router = APIRouter(prefix="/setup", tags=["Setup"]) -setupFilePath="/var/cache/raspiblitz/temp/raspiblitz.setup" -configFilePath="/mnt/hdd/raspiblitz.conf" +setupFilePath = "/var/cache/raspiblitz/temp/raspiblitz.setup" +configFilePath = "/mnt/hdd/raspiblitz.conf" # can always be called without credentials to check if # the system needs or is in setup (setupPhase!="done") # for example in the beginning setupPhase can be (see controlSetupDialog.sh) # 1) recovery = same version on fresh sd card # 2) update = updated version on fresh sd card -# 3) migration = hdd got data from another node projcect +# 3) migration = hdd got data from another node project # 4) setup = a fresh blitz to setup @router.get("/status") async def get_status(): @@ -44,6 +40,7 @@ async def get_status(): # We can do the "MIGRATION" option later - because it would need an additional step after formatting hdd # People that need to migrate can do for now by SSH option + @router.get("/setup-start-info") async def setup_start_info(): @@ -51,9 +48,7 @@ async def setup_start_info(): setupPhase = await redis_get("setupPhase") state = await redis_get("state") if state != "waitsetup": - logging.warning( - f"/setup-start-info can only be called when nodes awaits setup" - ) + logging.warning(f"/setup-start-info can only be called when nodes awaits setup") return HTTPException(status.status.HTTP_405_METHOD_NOT_ALLOWED) # get all the additional info needed to do setup dialog @@ -73,17 +68,16 @@ async def setup_start_info(): } -def writeTextFile(filename: str, arrayOfLines): +def write_text_file(filename: str, arrayOfLines): logging.warning(f"writing {filename}") - with open(filename, 'w', encoding='utf-8') as f: - f.write('\n'.join(arrayOfLines)) + with open(filename, "w", encoding="utf-8") as f: + f.write("\n".join(arrayOfLines)) + # With all this info the WebUi can run its own runs its dialogs and in the end makes a call to @router.post("/setup-start-done") -async def setup_start_done( - passwordA : str = "" -): - logging.warning(f"START /setup-start-done") +async def setup_start_done(passwordA: str = ""): + logging.warning(f"START /setup-start-done") # first check that node is really in setup state setupPhase = await redis_get("setupPhase") @@ -92,18 +86,17 @@ async def setup_start_done( logging.warning(f"/setup-start-done can only be called when nodes awaits setup") return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - if setupPhase == "recovery": + if setupPhase == "recovery": logging.warning(f"check recovery data") - if passwordValid(passwordA) == False: + if password_valid(passwordA) == False: logging.warning(f"passwordA is not valid") return HTTPException(status.HTTP_400_BAD_REQUEST) - writeTextFile(setupFilePath,[ - f"setupType={setupPhase}", - "setPasswordA=1", - f"passwordA='{passwordA}'" - ]) + write_text_file( + setupFilePath, + [f"setupType={setupPhase}", "setPasswordA=1", f"passwordA='{passwordA}'"], + ) logging.warning(f"kicking off recovery") - await callScript("/home/admin/_cache.sh set state waitprovision") + await call_script("/home/admin/_cache.sh set state waitprovision") else: logging.warning(f"not handled setupPhase state ({setupPhase})") return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) @@ -124,10 +117,10 @@ async def setup_start_done( # those values get stored in: /var/cache/raspiblitz/temp/raspiblitz.setup # also a skeleton raspiblitz.conf gets created (see controlSetupDialog.sh Line 318) # and then API sets state to `waitprovision` to kick-off provision - + # await redis.publish_json("default", {"data": "Starting setup"}) # await asyncio.sleep(1) - return signJWT() + return sign_jwt() # WebUI now loops getting status until state=`waitfinal` then calls: @@ -143,29 +136,26 @@ async def setup_final_info(): setupPhase = await redis_get("setupPhase") state = await redis_get("state") if state != "waitfinal": - logging.warning(f"/setup-final-info can only be called when nodes awaits final ({state})") + logging.warning( + f"/setup-final-info can only be called when nodes awaits final ({state})" + ) return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - resultlines=[] - with open (setupFilePath, "r") as setupfile: - resultlines=setupfile.readlines() - data=parseKeyValueLines(resultlines) + resultlines = [] + with open(setupFilePath, "r") as setupfile: + resultlines = setupfile.readlines() + data = parse_key_value_lines(resultlines) logging.warning(f"data({data})") try: setupType = data["setupType"] except: logging.warning("missing setupType in raspiblitz.setup") - setupType="" + setupType = "" if setupType == "setup": - return { - "setupType": setupType, - "seedwordsNEW": data["seedwordsNEW"] - } + return {"setupType": setupType, "seedwordsNEW": data["seedwordsNEW"]} else: - return { - "setupType": setupType, - "seedwordsNEW": "" - } + return {"setupType": setupType, "seedwordsNEW": ""} + # When WebUI displayed seed words & user confirmed write the calls: @router.post("/setup-final-done", dependencies=[Depends(JWTBearer())]) @@ -178,7 +168,5 @@ async def setup_final_done(): logging.warning(f"/setup-final-done can only be called when nodes awaits final") return HTTPException(status.HTTP_405_METHOD_NOT_ALLOWED) - await callScript("/home/admin/_cache.sh set state donefinal") - return { - "state": "donefinal" - } + await call_script("/home/admin/_cache.sh set state donefinal") + return {"state": "donefinal"} diff --git a/app/routers/system.py b/app/routers/system.py index b7383d1..8bdf686 100644 --- a/app/routers/system.py +++ b/app/routers/system.py @@ -1,24 +1,24 @@ -import secrets import logging +import secrets from decouple import config from fastapi import APIRouter, HTTPException, Request, status from fastapi.params import Depends from app.auth.auth_bearer import JWTBearer -from app.auth.auth_handler import signJWT -from app.external.sse_startlette import EventSourceResponse +from app.auth.auth_handler import sign_jwt +from app.external.sse_starlette import EventSourceResponse from app.models.system import APIPlatform, LoginInput, RawDebugLogData, SystemInfo from app.repositories.system import ( HW_INFO_YIELD_TIME, PLATFORM, + call_script, get_debug_logs_raw, get_hardware_info, get_system_info, + parse_key_value_text, + password_valid, subscribe_hardware_info, - callScript, - parseKeyValueText, - passwordValid ) from app.repositories.system_impl.raspiblitz import shutdown from app.routers.system_docs import ( @@ -50,16 +50,21 @@ async def login(i: LoginInput): if platform == "raspiblitz": # script does not work when called from api yet - if passwordValid(i.password): - result = await callScript(f"/home/admin/config.scripts/blitz.setpassword.sh check-a {i.password}") - data = parseKeyValueText(result) - if data["correct"] == "1": return signJWT() + if password_valid(i.password): + result = await call_script( + f"/home/admin/config.scripts/blitz.setpassword.sh check-a {i.password}" + ) + data = parse_key_value_text(result) + if data["correct"] == "1": + return sign_jwt() raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Password is wrong") else: match = secrets.compare_digest(i.password, config("login_password", cast=str)) - if match: return signJWT() + if match: + return sign_jwt() raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Password is wrong") + @router.post( "/refresh-token", name=f"{_PREFIX}.refresh-token", @@ -68,7 +73,7 @@ async def login(i: LoginInput): dependencies=[Depends(JWTBearer())], ) def refresh_token(): - return signJWT() + return sign_jwt() @router.get(