blitz_api/app/apps/impl/raspiblitz.py
2024-02-25 19:03:04 +01:00

453 lines
16 KiB
Python

# ruff: noqa: E722
import asyncio
import json
import os
import random
from typing import List
from decouple import config
from fastapi import HTTPException, status
from fastapi.encoders import jsonable_encoder
from loguru import logger as logging
from app.api.utils import SSE, broadcast_sse_msg, call_sudo_script, parse_key_value_text
from app.apps.impl.apps_base import AppsBase
available_app_ids = {
"btc-rpc-explorer",
"rtl",
# Specter is deactivated for now because it uses its own self signed HTTPS cert that
# makes trouble in Chrome on last test
# "specter",
"btcpayserver",
"lnbits",
"mempool",
"thunderhub",
"jam",
"electrs",
}
SHELL_SCRIPT_PATH = config("shell_script_path")
node_type = config("ln_node")
class RaspiBlitzApps(AppsBase):
async def get_app_status_single(self, app_id):
if app_id not in available_app_ids:
return {
"id": f"{app_id}",
"error": "appID not in list",
}
script_call = (
os.path.join(SHELL_SCRIPT_PATH, "config.scripts", f"bonus.{app_id}.sh")
+ " status"
)
try:
result = await call_sudo_script(script_call)
except:
# script had error or was not able to deliver all requested data fields
logging.warning(f"error on calling: {script_call}")
return {
"id": f"{app_id}",
"error": f"script not working for api: {script_call}",
}
try:
data = parse_key_value_text(result)
except:
logging.warning(f"error on parsing: {result}")
return {
"id": f"{app_id}",
"error": f"script result parsing error: {script_call}",
}
try:
keys = data.keys()
error = data["error"] if "error" in keys else ""
installed = False
version = data["version"] if "version" in keys else ""
status = "offline"
if "installed" in keys and data["installed"] == "1":
status = "online"
installed = True
if not installed:
return {
"id": app_id,
"version": version,
"installed": False,
"status": "offline",
"error": error,
}
configured = False
if "configured" in keys and data["configured"] == "1":
configured = True
httpsSelfsigned = False
if "httpsSelfsigned" in keys and data["httpsSelfsigned"] == "1":
httpsSelfsigned = True
localIP = data["localIP"] if "localIP" in keys else ""
httpPort = data["httpPort"] if "httpPort" in keys else ""
httpsPort = data["httpsPort"] if "httpsPort" in keys else ""
httpsForced = data["httpsForced"] == "1" if "httpsForced" in keys else False
address = f"http://{localIP}:{httpPort}"
if httpsForced:
address = f"https://{localIP}:{httpsPort}"
hiddenService = data["toraddress"] if "toraddress" in keys else ""
authMethod = "none"
if "authMethod" in data.keys():
authMethod = data["authMethod"]
details = {}
# set details for certain apps
if app_id == "mempool" or app_id == "btc-rpc-explorer":
details = {
"isIndexed": data["isIndexed"],
"indexInfo": data["indexInfo"],
}
return {
"id": app_id,
"installed": installed,
"configured": configured,
"status": status,
"localIP": localIP,
"httpPort": httpPort,
"httpsPort": httpsPort,
"httpsForced": httpsForced,
"httpsSelfsigned": httpsSelfsigned,
"hiddenService": hiddenService,
"address": address,
"authMethod": authMethod,
"details": details,
}
except Exception as e:
logging.error(e)
logging.info(f"error on repackage data: {result}")
return {
"id": f"{app_id}",
"error": f"script result processing error: {script_call}",
}
async def get_app_status_advanced(self, app_id):
if app_id not in available_app_ids:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"App id invalid. Available app ids: {available_app_ids}",
)
if app_id == "electrs":
return await _do_electrs_status_advanced()
return {}
async def get_app_status(self):
appStatusList: List = []
for appID in available_app_ids:
# skip app based on node running
if node_type == "" or node_type == "none":
if appID == "rtl":
continue
if appID == "lnbits":
continue
if appID == "thunderhub":
continue
elif node_type == "cln_grpc":
if appID == "thunderhub":
continue
# elif node_type="lnd_grpc":
# get status (installed, etc) and append
appStatusList.append(await self.get_app_status_single(appID))
return appStatusList
async def get_app_status_sub(self):
switch = True
while True:
status = "online" if switch else "offline"
app_list = [
# Specter is deactivated for now because it uses its own self signed
# HTTPS cert that makes trouble in Chrome on last test
# also see: app/constants.py where specter is deactivated
# {"id": "specter", "name": "Specter Desktop", "status": status},
{"id": "sphinx", "name": "Sphinx Chat", "status": status},
{"id": "btc-pay", "name": "BTCPay Server", "status": status},
{"id": "rtl", "name": "Ride the Lightning", "status": status},
{"id": "bos", "name": "Balance of Satoshis", "status": status},
]
i = random.randint(1, len(app_list))
yield json.dumps(app_list[i - 1])
await asyncio.sleep(4)
switch = not switch
async def install_app_sub(self, app_id: str):
if app_id not in available_app_ids:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=app_id + "install script does not exist / is not supported",
)
await broadcast_sse_msg(
SSE.INSTALL_APP,
{"id": app_id, "mode": "on", "result": "running", "details": ""},
)
loop = asyncio.get_event_loop()
loop.create_task(self.run_bonus_script(app_id, "on"))
return jsonable_encoder({"id": app_id})
async def uninstall_app_sub(self, app_id: str, delete_data: bool):
if app_id not in available_app_ids:
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail="script not exist/supported"
)
await broadcast_sse_msg(
SSE.INSTALL_APP,
{"id": app_id, "mode": "off", "result": "running", "details": ""},
)
deleteDataFlag = " --keep-data"
if delete_data:
deleteDataFlag = " --delete-data"
loop = asyncio.get_event_loop()
loop.create_task(self.run_bonus_script(app_id, f"off{deleteDataFlag}"))
return jsonable_encoder({"id": app_id})
async def run_bonus_script(self, app_id: str, params: str):
# to satisfy CodeQL: test again against predefined array and
# don't use 'user value'
tested_app_id = ""
for id in available_app_ids:
if id == app_id:
tested_app_id = id
# run script and get results
script_path = f"{SHELL_SCRIPT_PATH}/config.scripts/bonus.{tested_app_id}.sh"
cmd = f"bash {script_path} {params}"
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
# extracting mode from params
mode = params.split()[0]
# logging to console
if stdout:
logging.debug(f"[stdout]\n{stdout.decode()}")
else:
logging.debug("NO [stdout]")
if stderr:
logging.debug(f"[stderr]\n{stderr.decode()}")
else:
logging.debug("NO [stderr]")
# create log file
logFileName = f"/var/cache/raspiblitz/temp/install.{app_id}.log"
logging.info(f"WRITING LOG FILE: {logFileName}")
with open(logFileName, "w", encoding="utf-8") as f:
f.write(f"API triggered script: {cmd}\n")
f.write("###### STDOUT #######\n")
if stdout:
f.write(stdout.decode())
f.write("\n###### STDERR #######\n")
if stderr:
f.write(stderr.decode())
# sending final feedback event
logging.debug("SENDING RESULT EVENT ...")
if stdout:
stdoutData = parse_key_value_text(stdout.decode())
logging.debug(f"PARSED STDOUT DATA: {stdoutData}")
# when there is a defined error message (if multiple it will
# be the last one)
if "error" in stdoutData:
logging.error(
f"FOUND `error=` returned by script: {stdoutData['error']}"
)
await broadcast_sse_msg(
SSE.INSTALL_APP,
{
"id": app_id,
"mode": mode,
"result": "fail",
"details": stdoutData["error"],
},
)
# when there is no result (e.g. result="OK") at the end of install script
# stdout - consider also script had error
elif "result" not in stdoutData:
logging.error("NO `result=` returned by script:")
await broadcast_sse_msg(
SSE.INSTALL_APP,
{
"id": app_id,
"mode": mode,
"result": "fail",
"details": "install script threw an error",
},
)
# nothing above consider success
else:
# check if script was effective
updatedAppData = await self.get_app_status_single(app_id)
# in case of script error
if updatedAppData["error"] != "":
logging.warning("Error Detected ...")
logging.warning(f"updatedAppData: {updatedAppData}")
await broadcast_sse_msg(
SSE.INSTALL_APP,
{
"id": app_id,
"mode": mode,
"result": "fail",
"details": updatedAppData["error"],
},
)
# if install was running
elif mode == "on":
if updatedAppData["installed"]:
logging.info(f"WIN - install of {app_id} was effective")
await broadcast_sse_msg(
SSE.INSTALL_APP,
{
"id": app_id,
"mode": mode,
"result": "win",
"httpsForced": updatedAppData["httpsForced"],
"httpsSelfsigned": updatedAppData["httpsSelfsigned"],
"details": stdoutData["result"],
},
)
await broadcast_sse_msg(
SSE.INSTALLED_APP_STATUS, [updatedAppData]
)
else:
logging.error(f"FAIL - {app_id} was not installed")
logging.debug(f"updatedAppData: {updatedAppData}")
logging.debug(f"params: {params}")
await broadcast_sse_msg(
SSE.INSTALL_APP,
{
"id": app_id,
"mode": mode,
"result": "fail",
"details": "install was not effective",
},
)
await broadcast_sse_msg(
SSE.INSTALLED_APP_STATUS, [updatedAppData]
)
elif mode == "off":
await broadcast_sse_msg(
SSE.INSTALL_APP,
{"id": app_id, "mode": mode, "result": "win"},
)
await broadcast_sse_msg(SSE.INSTALLED_APP_STATUS, [updatedAppData])
if not updatedAppData["installed"]:
logging.info(f"WIN - uninstall of {app_id} was effective")
return
logging.error(f"FAIL - {app_id} was not uninstalled")
logging.debug(f"updatedAppData: {updatedAppData}")
logging.debug(f"params: {params}")
return
async def _do_electrs_status_advanced():
app_id = "electrs"
script_call_status = (
os.path.join(SHELL_SCRIPT_PATH, "config.scripts", f"bonus.{app_id}.sh")
+ " status showAddress"
)
script_call_sync = (
os.path.join(SHELL_SCRIPT_PATH, "config.scripts", f"bonus.{app_id}.sh")
+ " status-sync"
)
try:
result_status = await call_sudo_script(script_call_status)
result_sync = await call_sudo_script(script_call_sync)
result = result_status + result_sync
except Exception as e:
# script had error or was not able to deliver all requested data fields
logging.error(e)
logging.debug(f"error on calling: {script_call_status} and {script_call_sync}")
return {
"id": f"{app_id}",
"error": "Unable to get Electrs data from script.",
}
try:
data = parse_key_value_text(result)
except Exception as e:
logging.error(e)
logging.debug(f"error on parsing: {result}")
return {
"id": f"{app_id}",
"error": "Unable to get Electrs data from script.",
}
try:
if data["installed"] == "0":
return {
"id": app_id,
"error": "Service not installed.",
}
if data["configured"] == "0":
return {
"id": app_id,
"error": "Service not configured.",
}
if data["serviceRunning"] == "0":
return {
"id": app_id,
"error": "Service installed, but not running.",
}
if "initialSynced" not in data:
logging.error(
(
f"The Raspiblitz {script_call_sync} doesn't "
"return the required data (initialSynced)."
)
)
logging.debug(data)
return {
"id": app_id,
"error": f"script not working for api: {script_call_status}",
}
return {
"version": data["version"],
"localIP": data["localIP"],
"publicIP": data["publicIP"],
"portTCP": data["portTCP"],
"portSSL": data["portSSL"],
"TORaddress": data["TORaddress"],
"initialSyncDone": data["initialSynced"] == "1",
}
except Exception as e:
logging.error(e)
logging.debug(data)