forked from michael.heier/citadel-core
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
61f5f9f1e0 | ||
|
07aa73b59e | ||
|
a8d224f597 | ||
|
85f9886137 | ||
|
66c0ca350b | ||
|
22bb680c35 | ||
|
9288bf424e | ||
|
94bc9de19b | ||
|
25c5c2dd32 |
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -22,10 +22,9 @@ statuses/*
|
|||
app-data
|
||||
apps
|
||||
electrs/*
|
||||
fulcrumx/*
|
||||
fulcrum/*
|
||||
nginx/*
|
||||
redis/*
|
||||
events/signals/*
|
||||
docker-compose.override.yml
|
||||
|
||||
# Commit these empty directories
|
||||
|
@ -36,7 +35,6 @@ docker-compose.override.yml
|
|||
!db/citadel-seed
|
||||
db/citadel-seed/*
|
||||
!db/citadel-seed/.gitkeep
|
||||
!events/signals/.gitkeep
|
||||
!lnd/.gitkeep
|
||||
!logs/.gitkeep
|
||||
!tor/data/.gitkeep
|
||||
|
@ -45,10 +43,10 @@ db/citadel-seed/*
|
|||
!db/.gitkeep
|
||||
!nginx/.gitkeep
|
||||
!redis/.gitkeep
|
||||
!fulcrumx/.gitkeep
|
||||
!events/signals/.gitkeep
|
||||
!fulcrum/.gitkeep
|
||||
|
||||
!**/*.license
|
||||
services/installed.json
|
||||
services/installed.yml
|
||||
|
||||
events/karen
|
||||
|
|
|
@ -75,7 +75,7 @@ def is_builtin_type(obj):
|
|||
def classToDict(theClass):
|
||||
obj: dict = {}
|
||||
for key, value in theClass.__dict__.items():
|
||||
if value is None or (isinstance(value, list) and len(value) == 0):
|
||||
if type(value).__name__ == "NoneType" or (isinstance(value, list) and len(value) == 0):
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
newList = []
|
||||
|
@ -83,7 +83,8 @@ def classToDict(theClass):
|
|||
if is_builtin_type(element):
|
||||
newList.append(element)
|
||||
else:
|
||||
newList.append(classToDict(element))
|
||||
if type(element).__name__ != "NoneType":
|
||||
newList.append(classToDict(element))
|
||||
obj[key] = newList
|
||||
elif isinstance(value, dict):
|
||||
newDict = {}
|
||||
|
@ -95,8 +96,7 @@ def classToDict(theClass):
|
|||
obj[key] = newDict
|
||||
elif is_builtin_type(value):
|
||||
obj[key] = value
|
||||
else:
|
||||
#print(value)
|
||||
elif type(value).__name__ != "NoneType":
|
||||
obj[key] = classToDict(value)
|
||||
return obj
|
||||
|
||||
|
|
24
app/lib/composegenerator/next/stage1.py
Normal file
24
app/lib/composegenerator/next/stage1.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from lib.citadelutils import classToDict
|
||||
from lib.composegenerator.shared.env import validateEnv
|
||||
|
||||
from lib.composegenerator.v3.types import App, generateApp
|
||||
from lib.composegenerator.v3.generate import convertContainerPermissions
|
||||
|
||||
def createCleanConfigFromV3(app: dict, nodeRoot: str):
|
||||
parsedApp: App = generateApp(app)
|
||||
for container in range(len(parsedApp.containers)):
|
||||
# TODO: Make this dynamic and not hardcoded
|
||||
if parsedApp.containers[container].requires and "c-lightning" in parsedApp.containers[container].requires:
|
||||
parsedApp.containers[container] = None
|
||||
parsedApp = convertContainerPermissions(parsedApp)
|
||||
parsedApp = validateEnv(parsedApp)
|
||||
finalApp = classToDict(parsedApp)
|
||||
try:
|
||||
finalApp['permissions'] = finalApp['metadata']['dependencies']
|
||||
except:
|
||||
finalApp['permissions'] = []
|
||||
finalApp['id'] = finalApp['metadata']['id']
|
||||
del finalApp['metadata']
|
||||
# Set version of the cache file format
|
||||
finalApp['version'] = "1"
|
||||
return finalApp
|
|
@ -40,13 +40,14 @@ def validateEnvStringOrListorDict(env: Union[str, Union[list, dict]], existingEn
|
|||
def validateEnv(app: App):
|
||||
# For every container of the app, check if all env vars in the strings in environment are defined in env
|
||||
for container in app.containers:
|
||||
if container.environment_allow:
|
||||
existingEnv = container.environment_allow
|
||||
del container.environment_allow
|
||||
else:
|
||||
existingEnv = []
|
||||
if container.environment:
|
||||
validateEnvStringOrListorDict(container.command, existingEnv, app.metadata.id, container.name)
|
||||
validateEnvStringOrListorDict(container.entrypoint, existingEnv, app.metadata.id, container.name)
|
||||
validateEnvStringOrListorDict(container.environment, existingEnv, app.metadata.id, container.name)
|
||||
if container is not None:
|
||||
if container.environment_allow:
|
||||
existingEnv = container.environment_allow
|
||||
del container.environment_allow
|
||||
else:
|
||||
existingEnv = []
|
||||
if container.environment:
|
||||
validateEnvStringOrListorDict(container.command, existingEnv, app.metadata.id, container.name)
|
||||
validateEnvStringOrListorDict(container.entrypoint, existingEnv, app.metadata.id, container.name)
|
||||
validateEnvStringOrListorDict(container.environment, existingEnv, app.metadata.id, container.name)
|
||||
return app
|
||||
|
|
|
@ -47,7 +47,7 @@ def getContainerHiddenService(
|
|||
if isinstance(container.hiddenServicePorts, int):
|
||||
return getHiddenServiceString(
|
||||
"{} {}".format(metadata.name, container.name),
|
||||
metadata.id,
|
||||
metadata.id if isMainContainer else "{}-{}".format(metadata.id, container.name),
|
||||
container.hiddenServicePorts,
|
||||
containerIp,
|
||||
container.hiddenServicePorts,
|
||||
|
@ -55,7 +55,7 @@ def getContainerHiddenService(
|
|||
elif isinstance(container.hiddenServicePorts, list):
|
||||
return getHiddenServiceMultiPort(
|
||||
"{} {}".format(metadata.name, container.name),
|
||||
metadata.id,
|
||||
metadata.id if isMainContainer else "{}-{}".format(metadata.id, container.name),
|
||||
containerIp,
|
||||
container.hiddenServicePorts,
|
||||
)
|
||||
|
@ -77,14 +77,14 @@ def getContainerHiddenService(
|
|||
else:
|
||||
additionalHiddenServices[key] = value
|
||||
for key, value in additionalHiddenServices.items():
|
||||
otherHiddenServices += "\n"
|
||||
if isinstance(value, int):
|
||||
otherHiddenServices += "# {} {} {} Hidden Service\nHiddenServiceDir /var/lib/tor/app-{}-{}\n".format(
|
||||
metadata.name, container.name, key, metadata.id, container.name
|
||||
metadata.name, container.name, key, metadata.id, key
|
||||
)
|
||||
otherHiddenServices += "HiddenServicePort {} {}:{}".format(
|
||||
value, containerIp, value
|
||||
)
|
||||
otherHiddenServices += "\n"
|
||||
elif isinstance(value, list):
|
||||
otherHiddenServices += getHiddenServiceMultiPort(
|
||||
"{} {}".format(metadata.name, key), "{}-{}".format(metadata.id, key), containerIp, value
|
||||
|
|
|
@ -16,20 +16,21 @@ from lib.composegenerator.shared.const import permissions
|
|||
|
||||
def convertContainerPermissions(app: App) -> App:
|
||||
for container in app.containers:
|
||||
for permission in app.metadata.dependencies:
|
||||
if isinstance(permission, str):
|
||||
if permission in permissions():
|
||||
container.environment_allow.extend(permissions()[permission]['environment_allow'])
|
||||
container.volumes.extend(permissions()[permission]['volumes'])
|
||||
else:
|
||||
print("Warning: app {} defines unknown permission {}".format(app.metadata.name, permission))
|
||||
else:
|
||||
for subPermission in permission:
|
||||
if subPermission in permissions():
|
||||
container.environment_allow.extend(permissions()[subPermission]['environment_allow'])
|
||||
container.volumes.extend(permissions()[subPermission]['volumes'])
|
||||
if container is not None:
|
||||
for permission in app.metadata.dependencies:
|
||||
if isinstance(permission, str):
|
||||
if permission in permissions():
|
||||
container.environment_allow.extend(permissions()[permission]['environment_allow'])
|
||||
container.volumes.extend(permissions()[permission]['volumes'])
|
||||
else:
|
||||
print("Warning: app {} defines unknown permission {}".format(app.metadata.name, subPermission))
|
||||
print("Warning: app {} defines unknown permission {}".format(app.metadata.name, permission))
|
||||
else:
|
||||
for subPermission in permission:
|
||||
if subPermission in permissions():
|
||||
container.environment_allow.extend(permissions()[subPermission]['environment_allow'])
|
||||
container.volumes.extend(permissions()[subPermission]['volumes'])
|
||||
else:
|
||||
print("Warning: app {} defines unknown permission {}".format(app.metadata.name, subPermission))
|
||||
return app
|
||||
|
||||
def convertDataDirToVolumeGen3(app: App) -> AppStage2:
|
||||
|
@ -84,11 +85,12 @@ def createComposeConfigFromV3(app: dict, nodeRoot: str):
|
|||
newApp = configureIps(newApp, networkingFile, envFile)
|
||||
# This is validated earlier
|
||||
for container in newApp.containers:
|
||||
container.ports = container.requiredPorts
|
||||
for tcpPort in container.requiredPorts:
|
||||
container.ports.append("{}:{}".format(tcpPort, tcpPort))
|
||||
del container.requiredPorts
|
||||
for container in newApp.containers:
|
||||
for udpPort in container.requiredUdpPorts:
|
||||
container.ports.append("{}/udp".format(udpPort))
|
||||
container.ports.append("{}:{}/udp".format(udpPort, udpPort))
|
||||
del container.requiredUdpPorts
|
||||
newApp = configureMainPort(newApp, nodeRoot)
|
||||
newApp = configureHiddenServices(newApp, nodeRoot)
|
||||
|
|
|
@ -77,6 +77,8 @@ def configureMainPort(app: AppStage2, nodeRoot: str) -> AppStage3:
|
|||
# If it doesn't contain a :, it's the port itself
|
||||
if mainPort == False:
|
||||
mainPort = mainContainer.ports[0]
|
||||
if mainPort.find(":") != -1:
|
||||
mainPort = mainPort.split(":")[1]
|
||||
else:
|
||||
mainContainer.ports = [portToAppend]
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class Container:
|
|||
port: Union[int, None] = None
|
||||
requiredPorts: list = field(default_factory=list)
|
||||
requiredUdpPorts: list = field(default_factory=list)
|
||||
preferredOutsidePort: Union[int, None] = None
|
||||
#preferredOutsidePort: Union[int, None] = None
|
||||
requiresPort: Union[bool, None] = None
|
||||
environment: Union[dict, None] = None
|
||||
data: list = field(default_factory=list)
|
||||
|
|
|
@ -118,26 +118,6 @@ def getUserData():
|
|||
userData = json.load(f)
|
||||
return userData
|
||||
|
||||
def checkUpdateAvailable(name: str) -> bool:
|
||||
latestAppYml = yaml.safe_load(getAppYml(name))
|
||||
with open(os.path.join(appsDir, name, "app.yml"), "r") as f:
|
||||
originalAppYml = yaml.safe_load(f)
|
||||
if not "metadata" in latestAppYml or not "version" in latestAppYml["metadata"] or not "metadata" in originalAppYml or not "version" in originalAppYml["metadata"]:
|
||||
print("App {} is not valid".format(name))
|
||||
return False
|
||||
return semver.compare(latestAppYml["metadata"]["version"], originalAppYml["metadata"]["version"]) > 0
|
||||
|
||||
def getAvailableUpdates():
|
||||
availableUpdates = []
|
||||
apps = findAndValidateApps(appsDir)
|
||||
for app in apps:
|
||||
try:
|
||||
if checkUpdateAvailable(app):
|
||||
availableUpdates.append(app)
|
||||
except Exception:
|
||||
print("Warning: Can't check app {} yet".format(app), file=sys.stderr)
|
||||
return availableUpdates
|
||||
|
||||
def startInstalled():
|
||||
# If userfile doesn't exist, just do nothing
|
||||
userData = {}
|
||||
|
@ -364,3 +344,59 @@ def updateRepos():
|
|||
shutil.rmtree(tempDir)
|
||||
with open(os.path.join(appsDir, "sourceMap.json"), "w") as f:
|
||||
json.dump(sourceMap, f)
|
||||
|
||||
|
||||
def getAvailableUpdates():
|
||||
availableUpdates = {}
|
||||
repos = []
|
||||
ignoreApps = []
|
||||
with open(sourcesList) as f:
|
||||
repos = f.readlines()
|
||||
try:
|
||||
with open(updateIgnore) as f:
|
||||
ignoreApps = f.readlines()
|
||||
except: pass
|
||||
# For each repo, clone the repo to a temporary dir, checkout the branch,
|
||||
# and overwrite the current app dir with the contents of the temporary dir/apps/app
|
||||
# Set this to ignoreApps. Normally, it keeps track of apps already installed from repos higher in the list,
|
||||
# but apps specified in updateignore have the highest priority
|
||||
alreadyDefined = [s.strip() for s in ignoreApps]
|
||||
for repo in repos:
|
||||
repo = repo.strip()
|
||||
if repo == "":
|
||||
continue
|
||||
# Also ignore comments
|
||||
if repo.startswith("#"):
|
||||
continue
|
||||
# Split the repo into the git url and the branch
|
||||
repo = repo.split(" ")
|
||||
if len(repo) != 2:
|
||||
print("Error: Invalid repo format in " + sourcesList, file=sys.stderr)
|
||||
exit(1)
|
||||
gitUrl = repo[0]
|
||||
branch = repo[1]
|
||||
# Clone the repo to a temporary dir
|
||||
tempDir = tempfile.mkdtemp()
|
||||
# Git clone with a depth of 1 to avoid cloning the entire repo
|
||||
# Don't print anything to stdout, as we don't want to see the git clone output
|
||||
subprocess.run("git clone --depth 1 --branch {} {} {}".format(branch, gitUrl, tempDir), shell=True, stdout=subprocess.DEVNULL)
|
||||
# Overwrite the current app dir with the contents of the temporary dir/apps/app
|
||||
for app in os.listdir(os.path.join(tempDir, "apps")):
|
||||
try:
|
||||
# if the app is already installed (or a simple file instead of a valid app), skip it
|
||||
if app in alreadyDefined or not os.path.isdir(os.path.join(tempDir, "apps", app)):
|
||||
continue
|
||||
with open(os.path.join(appsDir, app, "app.yml"), "r") as f:
|
||||
originalAppYml = yaml.safe_load(f)
|
||||
with open(os.path.join(tempDir, "apps", app, "app.yml"), "r") as f:
|
||||
latestAppYml = yaml.safe_load(f)
|
||||
if semver.compare(latestAppYml["metadata"]["version"], originalAppYml["metadata"]["version"]) > 0:
|
||||
availableUpdates[app] = {
|
||||
"updateFrom": originalAppYml["metadata"]["version"],
|
||||
"updateTo": latestAppYml["metadata"]["version"]
|
||||
}
|
||||
except Exception:
|
||||
print("Warning: Can't check app {} (yet)".format(app), file=sys.stderr)
|
||||
# Remove the temporary dir
|
||||
shutil.rmtree(tempDir)
|
||||
return availableUpdates
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import os
|
||||
import yaml
|
||||
|
||||
from lib.composegenerator.next.stage1 import createCleanConfigFromV3
|
||||
from lib.composegenerator.v2.networking import getMainContainer
|
||||
from lib.composegenerator.v1.networking import getFreePort
|
||||
from lib.entropy import deriveEntropy
|
||||
|
@ -40,6 +41,7 @@ def getAppRegistry(apps, app_path):
|
|||
app_metadata = []
|
||||
for app in apps:
|
||||
app_yml_path = os.path.join(app_path, app, 'app.yml')
|
||||
app_cache_path = os.path.join(app_path, app, 'app.cache.json')
|
||||
if os.path.isfile(app_yml_path):
|
||||
try:
|
||||
with open(app_yml_path, 'r') as f:
|
||||
|
@ -57,6 +59,8 @@ def getAppRegistry(apps, app_path):
|
|||
getPortsOldApp(app_yml, app)
|
||||
else:
|
||||
getPortsV3App(app_yml, app)
|
||||
with open(app_cache_path, 'w') as f:
|
||||
json.dump(createCleanConfigFromV3(app_yml, os.path.dirname(app_path)), f)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("App {} is invalid!".format(app))
|
||||
|
|
|
@ -7,18 +7,18 @@ import yaml
|
|||
from jsonschema import validate
|
||||
import yaml
|
||||
|
||||
scriptDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
||||
|
||||
with open(os.path.join(scriptDir, 'app-standard-v1.yml'), 'r') as f:
|
||||
schemaVersion1 = yaml.safe_load(f)
|
||||
with open(os.path.join(scriptDir, 'app-standard-v2.yml'), 'r') as f:
|
||||
schemaVersion2 = yaml.safe_load(f)
|
||||
with open(os.path.join(scriptDir, 'app-standard-v3.yml'), 'r') as f:
|
||||
schemaVersion3 = yaml.safe_load(f)
|
||||
|
||||
# Validates app data
|
||||
# Returns true if valid, false otherwise
|
||||
scriptDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
||||
def validateApp(app: dict):
|
||||
with open(os.path.join(scriptDir, 'app-standard-v1.yml'), 'r') as f:
|
||||
schemaVersion1 = yaml.safe_load(f)
|
||||
with open(os.path.join(scriptDir, 'app-standard-v2.yml'), 'r') as f:
|
||||
schemaVersion2 = yaml.safe_load(f)
|
||||
with open(os.path.join(scriptDir, 'app-standard-v3.yml'), 'r') as f:
|
||||
schemaVersion3 = yaml.safe_load(f)
|
||||
|
||||
if 'version' in app and str(app['version']) == "1":
|
||||
try:
|
||||
validate(app, schemaVersion1)
|
||||
|
|
1
bin/citadel
Symbolic link
1
bin/citadel
Symbolic link
|
@ -0,0 +1 @@
|
|||
../cli/citadel
|
|
@ -13,7 +13,7 @@ CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
|||
result=$(docker compose \
|
||||
--file "${CITADEL_ROOT}/docker-compose.yml" \
|
||||
--env-file "${CITADEL_ROOT}/.env" \
|
||||
exec lnd lncli "$@")
|
||||
exec lightning lncli "$@")
|
||||
|
||||
# We need to echo with quotes to preserve output formatting
|
||||
echo "$result"
|
||||
|
|
487
cli/citadel
Executable file
487
cli/citadel
Executable file
|
@ -0,0 +1,487 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2022 Citadel and contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
CLI_NAME="$(basename $0)"
|
||||
CLI_VERSION="0.0.1"
|
||||
CLI_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
SERVICE_NAME="citadel-startup"
|
||||
EDITOR="${EDITOR:-micro}"
|
||||
|
||||
source $CLI_DIR/utils/functions.sh
|
||||
source $CLI_DIR/utils/multiselect.sh
|
||||
source $CLI_DIR/utils/spinner.sh
|
||||
source $CLI_DIR/utils/helpers.sh
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
else
|
||||
command="$1"
|
||||
fi
|
||||
|
||||
# Check Citadel Status
|
||||
if [[ "$command" = "status" ]]; then
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
long=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-l | --long)
|
||||
long=true
|
||||
shift
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}"
|
||||
|
||||
free -m | awk 'NR==2{printf "Memory Usage: %s/%sMB (%.2f%%)\n", $3,$2,$3*100/$2 }'
|
||||
df -h | awk '$NF=="/"{printf "Disk Usage: %d/%dGB (%s)\n", $3,$2,$5}'
|
||||
top -bn1 | grep load | awk '{printf "CPU Load: %.2f\n", $(NF-2)}'
|
||||
|
||||
echo
|
||||
|
||||
if $long; then
|
||||
docker container ls --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
|
||||
else
|
||||
docker container ls --format "table {{.Names}}\t{{.Status}}"
|
||||
fi
|
||||
|
||||
if [[ $(pgrep -f karen) ]]; then
|
||||
printf "\nKaren is listening.\n"
|
||||
else
|
||||
printf "\nERROR: Karen is not listening.\n"
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Update Citadel
|
||||
if [[ "$command" = "update" ]]; then
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
branch=$(get_update_channel)
|
||||
force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-b | --branch)
|
||||
branch="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--force)
|
||||
force=true
|
||||
shift # past argument
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
if $force; then
|
||||
sudo rm -f $CITADEL_ROOT/statuses/update-in-progress
|
||||
fi
|
||||
|
||||
sudo $CITADEL_ROOT/scripts/update/update --repo runcitadel/core#$branch
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Start Citadel
|
||||
if [[ "$command" = "start" ]]; then
|
||||
if $(is_managed_by_systemd); then
|
||||
if $(is_service_active); then
|
||||
echo 'Citadel is already running.'
|
||||
else
|
||||
sudo systemctl start citadel-startup
|
||||
fi
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Stop Citadel
|
||||
if [[ "$command" = "stop" ]]; then
|
||||
active=$(is_service_active)
|
||||
|
||||
if $(is_managed_by_systemd) && $active; then
|
||||
if $(is_service_active); then
|
||||
sudo systemctl stop citadel-startup
|
||||
else
|
||||
echo 'Citadel is not running.'
|
||||
fi
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Restart Citadel
|
||||
if [[ "$command" = "restart" ]]; then
|
||||
shift
|
||||
|
||||
# TODO: enable restarting services
|
||||
|
||||
if [ ! -z ${1+x} ]; then
|
||||
echo "Too many arguments."
|
||||
echo "Usage: \`$CLI_NAME $command\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $(is_managed_by_systemd); then
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
else
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Reboot the system
|
||||
if [[ "$command" = "reboot" ]]; then
|
||||
$CLI_NAME stop || true
|
||||
sudo reboot
|
||||
exit
|
||||
fi
|
||||
|
||||
# Shutdown the system
|
||||
if [[ "$command" = "shutdown" ]]; then
|
||||
$CLI_NAME stop || true
|
||||
sudo shutdown now
|
||||
exit
|
||||
fi
|
||||
|
||||
# List all installed services apps
|
||||
if [[ "$command" = "list" ]]; then
|
||||
echo 'karen'
|
||||
docker ps --format "{{.Names}}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Run a command inside a container
|
||||
if [[ "$command" = "run" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Specify an app or service."
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify a command to run."
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec -t $1 sh -c "$2" || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service> \"<command>\"\`"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Configure Citadel
|
||||
if [[ "$command" = "set" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Missing subcommand."
|
||||
echo "Usage: \`$CLI_NAME $command <subcommand>\`"
|
||||
exit 1
|
||||
else
|
||||
subcommand="$1"
|
||||
fi
|
||||
|
||||
# Switch update channel
|
||||
if [[ "$subcommand" = "update-channel" ]]; then
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify an update channel to switch to."
|
||||
echo "Usage: \`$CLI_NAME $subcommand <stable|beta|c-lightning>\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $2 in "stable" | "beta" | "c-lightning")
|
||||
# continue
|
||||
;;
|
||||
*)
|
||||
echo "Not a valid update channel: \"$2\""
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sudo $CITADEL_ROOT/scripts/set-update-channel $2
|
||||
$CLI_NAME update
|
||||
exit
|
||||
fi
|
||||
|
||||
# Switch Bitcoin/Electrum implementation
|
||||
if [[ "$subcommand" = "implementation" ]] || [[ "$subcommand" = "impl" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/services/manage.py set $@
|
||||
$CLI_NAME restart
|
||||
exit
|
||||
fi
|
||||
|
||||
# Switch Bitcoin network
|
||||
if [[ "$subcommand" = "network" ]]; then
|
||||
shift
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
echo "Specify a network to switch to."
|
||||
echo "Usage: \`$CLI_NAME $subcommand <mainnet|signet|testnet|regtest>\`"
|
||||
else
|
||||
case $1 in
|
||||
"mainnet" | "testnet" | "signet" | "regtest")
|
||||
sudo $CITADEL_ROOT/scripts/stop
|
||||
sudo OVERWRITE_NETWORK=$1 $CITADEL_ROOT/scripts/configure
|
||||
sudo $CITADEL_ROOT/scripts/start
|
||||
;;
|
||||
*)
|
||||
echo "Not a valid value for network"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "\"$subcommand\" is not a valid subcommand."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# App commands
|
||||
if [[ "$command" = "app" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/scripts/app $@
|
||||
exit
|
||||
fi
|
||||
|
||||
# Edit common app configuration files
|
||||
if [[ "$command" = "configure" ]]; then
|
||||
if [ -z ${2+x} ]; then
|
||||
echo "Specify an app or service to configure."
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
persist=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--persist)
|
||||
persist=true
|
||||
shift # past argument
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
# These service and app configs are already persisted
|
||||
# TODO: add more apps
|
||||
|
||||
if [[ "$2" = "nextcloud" ]]; then
|
||||
edit_file --priviledged $CITADEL_ROOT/app-data/nextcloud/data/nextcloud/config/config.php
|
||||
prompt_apply_config nextcloud-web-1 false
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "nginx" ]]; then
|
||||
edit_file $CITADEL_ROOT/nginx/nginx.conf
|
||||
prompt_apply_config nginx false
|
||||
exit
|
||||
fi
|
||||
|
||||
if $persist; then
|
||||
echo "NOTE: As of now persisted config changes will not be kept when updating Citadel."
|
||||
else
|
||||
echo "NOTE: Some changes to this configuration file may be overwritten the next time you start Citadel."
|
||||
echo "To persist the changes run the command again with \`$CLI_NAME configure $2 --persist\`"
|
||||
fi
|
||||
|
||||
read -p "Continue? [Y/n] " should_continue
|
||||
echo
|
||||
if [[ $should_continue =~ [Nn]$ ]]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
# Service and app configs below may be overwritten
|
||||
# TODO: check which implementation is running
|
||||
# and do "bitcoin" / "lightning" / "electrum"
|
||||
|
||||
if [[ "$2" = "bitcoin" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/bitcoin-sample.conf
|
||||
prompt_apply_config bitcoin true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/bitcoin/bitcoin.conf
|
||||
prompt_apply_config bitcoin false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "lnd" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/lnd-sample.conf
|
||||
prompt_apply_config lightning true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/lnd/lnd.conf
|
||||
prompt_apply_config lightning false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "electrs" ]]; then
|
||||
edit_file $CITADEL_ROOT/templates/electrs-sample.toml
|
||||
prompt_apply_config electrum true
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$2" = "fulcrum" ]]; then
|
||||
if $persist; then
|
||||
edit_file $CITADEL_ROOT/templates/fulcrum-sample.conf
|
||||
prompt_apply_config electrum true
|
||||
else
|
||||
edit_file $CITADEL_ROOT/fulcrum/fulcrum.conf
|
||||
prompt_apply_config electrum false
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "No service or app \"$2\" not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show logs for apps & services
|
||||
if [[ "$command" = "logs" ]]; then
|
||||
shift
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
follow=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-f | --follow)
|
||||
follow=true
|
||||
shift # past argument
|
||||
;;
|
||||
-n | --tail)
|
||||
number_of_lines="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-* | --*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
# Set default number_of_lines if not set by user
|
||||
if [ -z ${number_of_lines+x} ]; then
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 0 ]] || [[ ${#POSITIONAL_ARGS[@]} == 1 ]]; then
|
||||
number_of_lines=40
|
||||
else
|
||||
number_of_lines=10
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z ${1+x} ] || [[ "$1" = "karen" ]]; then
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 2 ]]; then
|
||||
echo "Karen logs cannot be viewed together with other services."
|
||||
echo "Usage: \`$CLI_NAME $command karen\`"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tail $($follow && echo "-f") -n $number_of_lines $CITADEL_ROOT/logs/karen.log
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ ${#POSITIONAL_ARGS[@]} == 1 ]]; then
|
||||
docker logs $($follow && echo "-f") --tail $number_of_lines $@ || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# TODO: can only show logs for services in docker-compose.yml
|
||||
docker compose logs $($follow && echo "-f") --tail $number_of_lines $@ || {
|
||||
echo "To see all installed services & apps use \`$CLI_NAME list\`"
|
||||
echo "Usage: \`$CLI_NAME $command <service>\`"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Debug Citadel
|
||||
if [[ "$command" = "debug" ]]; then
|
||||
shift
|
||||
sudo $CITADEL_ROOT/scripts/debug $@
|
||||
exit
|
||||
fi
|
||||
|
||||
# Show version information for this CLI
|
||||
if [[ "$command" = "--version" ]] || [[ "$command" = "-v" ]]; then
|
||||
citadel_version=$(jq -r '.version' $CITADEL_ROOT/info.json)
|
||||
echo "Citadel v$citadel_version"
|
||||
echo "citadel-cli v$CLI_VERSION"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Show usage information for this CLI
|
||||
if [[ "$command" = "--help" ]] || [[ "$command" = "-h" ]]; then
|
||||
show_help
|
||||
exit
|
||||
fi
|
||||
|
||||
# If we get here it means no valid command was supplied
|
||||
# Show help and exit
|
||||
show_help
|
||||
exit
|
111
cli/utils/functions.sh
Normal file
111
cli/utils/functions.sh
Normal file
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
${CLI_NAME}-cli v${CLI_VERSION}
|
||||
Manage your Citadel.
|
||||
|
||||
Usage: ${CLI_NAME} <command> [options]
|
||||
|
||||
Flags:
|
||||
-h, --help Show this help message
|
||||
-v, --version Show version information for this CLI
|
||||
|
||||
Commands:
|
||||
status Check the status of all services
|
||||
start Start the Citadel service
|
||||
stop Stop the Citadel service safely
|
||||
restart Restart the Citadel service
|
||||
reboot Reboot the system
|
||||
shutdown Shutdown the system
|
||||
update Update Citadel
|
||||
list List all installed services apps
|
||||
run <service> "<command>" Run a command inside a container
|
||||
set <command> Switch between Bitcoin & Lightning implementations
|
||||
app <command> Install, update or restart apps
|
||||
configure <service> Edit service & app configuration files
|
||||
logs <service> Show logs for an app or service
|
||||
debug View logs for troubleshooting
|
||||
EOF
|
||||
}
|
||||
|
||||
is_managed_by_systemd() {
|
||||
if systemctl --all --type service | grep -q "$SERVICE_NAME"; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
is_service_active() {
|
||||
service_status=$(systemctl is-active $SERVICE_NAME)
|
||||
if [[ "$service_status" = "active" ]]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
}
|
||||
|
||||
edit_file() {
|
||||
if [[ $1 = "--priviledged" ]]; then
|
||||
echo "Editing this file requires elevated priviledges."
|
||||
|
||||
if ! sudo test -f $2; then
|
||||
echo "File not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if sudo test -w $2; then
|
||||
sudo $EDITOR $2
|
||||
else
|
||||
echo "File not writable."
|
||||
fi
|
||||
else
|
||||
if ! test -f $1; then
|
||||
echo "File not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -w $1; then
|
||||
$EDITOR $1
|
||||
else
|
||||
echo "File not writable."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
get_update_channel() {
|
||||
update_channel_line=$(cat $CITADEL_ROOT/.env | grep UPDATE_CHANNEL)
|
||||
update_channel=(${update_channel_line//=/ })
|
||||
|
||||
if [ -z ${update_channel[1]+x} ]; then
|
||||
# fall back to stable
|
||||
echo "stable"
|
||||
else
|
||||
echo ${update_channel[1]}
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_apply_config() {
|
||||
service=$1
|
||||
persisted=$2
|
||||
|
||||
read -p "Do you want to apply the changes now? [y/N] " should_restart
|
||||
echo
|
||||
if [[ $should_restart =~ [Yy]$ ]]; then
|
||||
if $persisted; then
|
||||
sudo $CITADEL_ROOT/scripts/configure
|
||||
fi
|
||||
|
||||
printf "\nRestarting service \"$service\"...\n"
|
||||
docker restart $service
|
||||
echo "Done."
|
||||
else
|
||||
if $persisted; then
|
||||
echo "To apply the changes, restart Citadel by running \`$CLI_NAME restart\`."
|
||||
else
|
||||
echo "To apply the changes, restart service \"$service\" by running \`docker restart $service\`."
|
||||
fi
|
||||
fi
|
||||
}
|
11
cli/utils/helpers.sh
Normal file
11
cli/utils/helpers.sh
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
trim() {
|
||||
local var="$*"
|
||||
# remove leading whitespace characters
|
||||
var="${var#"${var%%[![:space:]]*}"}"
|
||||
# remove trailing whitespace characters
|
||||
var="${var%"${var##*[![:space:]]}"}"
|
||||
printf '%s' "$var"
|
||||
}
|
123
cli/utils/multiselect.sh
Normal file
123
cli/utils/multiselect.sh
Normal file
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
function multiselect() {
|
||||
# little helpers for terminal print control and key input
|
||||
ESC=$(printf "\033")
|
||||
cursor_blink_on() { printf "$ESC[?25h"; }
|
||||
cursor_blink_off() { printf "$ESC[?25l"; }
|
||||
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
|
||||
print_inactive() { printf "$2 $1 "; }
|
||||
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
|
||||
get_cursor_row() {
|
||||
IFS=';' read -sdR -p $'\E[6n' ROW COL
|
||||
echo ${ROW#*[}
|
||||
}
|
||||
|
||||
local return_value=$1
|
||||
local -n options=$2
|
||||
local -n defaults=$3
|
||||
|
||||
local selected=()
|
||||
for ((i = 0; i < ${#options[@]}; i++)); do
|
||||
if [[ ${defaults[i]} = "true" ]]; then
|
||||
selected+=("true")
|
||||
else
|
||||
selected+=("false")
|
||||
fi
|
||||
printf "\n"
|
||||
done
|
||||
|
||||
# determine current screen position for overwriting the options
|
||||
local lastrow=$(get_cursor_row)
|
||||
local startrow=$(($lastrow - ${#options[@]}))
|
||||
|
||||
# ensure cursor and input echoing back on upon a ctrl+c during read -s
|
||||
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
|
||||
cursor_blink_off
|
||||
|
||||
key_input() {
|
||||
local key
|
||||
IFS= read -rsn1 key 2>/dev/null >&2
|
||||
if [[ $key = "" ]]; then echo enter; fi
|
||||
if [[ $key = $'\x20' ]]; then echo space; fi
|
||||
if [[ $key = "k" ]]; then echo up; fi
|
||||
if [[ $key = "j" ]]; then echo down; fi
|
||||
if [[ $key = $'\x1b' ]]; then
|
||||
read -rsn2 key
|
||||
if [[ $key = [A || $key = k ]]; then echo up; fi
|
||||
if [[ $key = [B || $key = j ]]; then echo down; fi
|
||||
fi
|
||||
}
|
||||
|
||||
toggle_option() {
|
||||
local option=$1
|
||||
if [[ ${selected[option]} == true ]]; then
|
||||
selected[option]=false
|
||||
else
|
||||
selected[option]=true
|
||||
fi
|
||||
}
|
||||
|
||||
print_options() {
|
||||
# print options by overwriting the last lines
|
||||
local idx=0
|
||||
|
||||
for option in "${options[@]}"; do
|
||||
local prefix="[ ]"
|
||||
|
||||
if [[ ${selected[idx]} == true ]]; then
|
||||
prefix="[\e[38;5;46m✔\e[0m]"
|
||||
fi
|
||||
|
||||
cursor_to $(($startrow + $idx))
|
||||
if [ $idx -eq $1 ]; then
|
||||
print_active "$option" "$prefix"
|
||||
else
|
||||
print_inactive "$option" "$prefix"
|
||||
fi
|
||||
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
}
|
||||
|
||||
get_result() {
|
||||
local result=()
|
||||
for ((i = 0; i < ${#options[@]}; i++)); do
|
||||
if ${selected[i]}; then
|
||||
result+=(${options[i]})
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${result[@]}"
|
||||
}
|
||||
|
||||
local active=0
|
||||
while true; do
|
||||
print_options $active
|
||||
|
||||
# user key control
|
||||
case $(key_input) in
|
||||
space) toggle_option $active ;;
|
||||
enter)
|
||||
print_options -1
|
||||
break
|
||||
;;
|
||||
up)
|
||||
active=$((active - 1))
|
||||
if [ $active -lt 0 ]; then active=$((${#options[@]} - 1)); fi
|
||||
;;
|
||||
down)
|
||||
active=$((active + 1))
|
||||
if [ $active -ge ${#options[@]} ]; then active=0; fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# cursor position back to normal
|
||||
cursor_to $lastrow
|
||||
printf "\n"
|
||||
cursor_blink_on
|
||||
|
||||
eval $return_value='("$(get_result)")'
|
||||
}
|
87
cli/utils/spinner.sh
Normal file
87
cli/utils/spinner.sh
Normal file
|
@ -0,0 +1,87 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Author: Tasos Latsas
|
||||
|
||||
# spinner.sh
|
||||
#
|
||||
# Display an awesome 'spinner' while running your long shell commands
|
||||
#
|
||||
# Do *NOT* call _spinner function directly.
|
||||
# Use {start,stop}_spinner wrapper functions
|
||||
|
||||
# usage:
|
||||
# 1. source this script in your's
|
||||
# 2. start the spinner:
|
||||
# start_spinner [display-message-here]
|
||||
# 3. run your command
|
||||
# 4. stop the spinner:
|
||||
# stop_spinner [your command's exit status]
|
||||
#
|
||||
# Also see: test.sh
|
||||
|
||||
function _spinner() {
|
||||
# $1 start/stop
|
||||
#
|
||||
# on start: $2 display message
|
||||
# on stop : $2 process exit status
|
||||
# $3 spinner function pid (supplied from stop_spinner)
|
||||
|
||||
local on_success="DONE"
|
||||
local on_fail="FAIL"
|
||||
local white="\e[1;37m"
|
||||
local green="\e[1;32m"
|
||||
local red="\e[1;31m"
|
||||
local nc="\e[0m"
|
||||
|
||||
case $1 in
|
||||
start)
|
||||
# display message with some space
|
||||
echo -ne "${2} "
|
||||
|
||||
# start spinner
|
||||
i=1
|
||||
sp='\|/-'
|
||||
delay=${SPINNER_DELAY:-0.15}
|
||||
|
||||
while :; do
|
||||
printf "\b${sp:i++%${#sp}:1}"
|
||||
sleep $delay
|
||||
done
|
||||
;;
|
||||
stop)
|
||||
if [[ -z ${3} ]]; then
|
||||
echo "spinner is not running.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kill $3 >/dev/null 2>&1
|
||||
|
||||
# inform the user upon success or failure
|
||||
echo -en "\b["
|
||||
if [[ $2 -eq 0 ]]; then
|
||||
echo -en "${green}${on_success}${nc}"
|
||||
else
|
||||
echo -en "${red}${on_fail}${nc}"
|
||||
fi
|
||||
echo -e "]"
|
||||
;;
|
||||
*)
|
||||
echo "invalid argument, try {start/stop}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function start_spinner() {
|
||||
# $1 : msg to display
|
||||
_spinner "start" "${1}" &
|
||||
# set global spinner pid
|
||||
_sp_pid=$!
|
||||
disown
|
||||
}
|
||||
|
||||
function stop_spinner() {
|
||||
# $1 : command exit status
|
||||
_spinner "stop" $1 $_sp_pid
|
||||
unset _sp_pid
|
||||
}
|
|
@ -2,7 +2,7 @@ version: '3.8'
|
|||
services:
|
||||
tor:
|
||||
container_name: tor
|
||||
image: lncm/tor:0.4.6.8@sha256:c262923ffd0bd224a4a4123cf1c88eea11e2314566b7b7e8a1f77969deeb0208
|
||||
image: lncm/tor:0.4.7.8@sha256:aab30ebb496aa25934d6096951d8b200347c3c3ce5db3493695229efa2601f7b
|
||||
user: toruser
|
||||
restart: on-failure
|
||||
volumes:
|
||||
|
@ -15,7 +15,7 @@ services:
|
|||
ipv4_address: $TOR_PROXY_IP
|
||||
app-tor:
|
||||
container_name: app-tor
|
||||
image: lncm/tor:0.4.6.8@sha256:c262923ffd0bd224a4a4123cf1c88eea11e2314566b7b7e8a1f77969deeb0208
|
||||
image: lncm/tor:0.4.7.8@sha256:aab30ebb496aa25934d6096951d8b200347c3c3ce5db3493695229efa2601f7b
|
||||
user: toruser
|
||||
restart: on-failure
|
||||
volumes:
|
||||
|
@ -26,7 +26,7 @@ services:
|
|||
ipv4_address: $APPS_TOR_IP
|
||||
app-2-tor:
|
||||
container_name: app-2-tor
|
||||
image: lncm/tor:0.4.6.8@sha256:c262923ffd0bd224a4a4123cf1c88eea11e2314566b7b7e8a1f77969deeb0208
|
||||
image: lncm/tor:0.4.7.8@sha256:aab30ebb496aa25934d6096951d8b200347c3c3ce5db3493695229efa2601f7b
|
||||
user: toruser
|
||||
restart: on-failure
|
||||
volumes:
|
||||
|
@ -37,7 +37,7 @@ services:
|
|||
ipv4_address: $APPS_2_TOR_IP
|
||||
app-3-tor:
|
||||
container_name: app-3-tor
|
||||
image: lncm/tor:0.4.6.8@sha256:c262923ffd0bd224a4a4123cf1c88eea11e2314566b7b7e8a1f77969deeb0208
|
||||
image: lncm/tor:0.4.7.8@sha256:aab30ebb496aa25934d6096951d8b200347c3c3ce5db3493695229efa2601f7b
|
||||
user: toruser
|
||||
restart: on-failure
|
||||
volumes:
|
||||
|
@ -65,7 +65,7 @@ services:
|
|||
ipv4_address: $NGINX_IP
|
||||
bitcoin:
|
||||
container_name: bitcoin
|
||||
image: nolim1t/bitcoinknots:v22.0.knots20211108@sha256:a475da2b2ecda55fcc65ea23e1a36c58b2c10549f1c3d3bb3c31c7dda1127354
|
||||
image: ghcr.io/runcitadel/bitcoinknots:main@sha256:5fbee0f6f0d09d42aacc11c373ffe6162210c42ce21e6eba294e547e3ad80219
|
||||
depends_on:
|
||||
- tor
|
||||
volumes:
|
||||
|
@ -79,7 +79,7 @@ services:
|
|||
ipv4_address: $BITCOIN_IP
|
||||
lightning:
|
||||
container_name: lightning
|
||||
image: lightninglabs/lnd:v0.14.3-beta@sha256:6a2234b0aad4caed3d993736816b198d6228f32c59b27ba2218d5ebf516ae905
|
||||
image: lightninglabs/lnd:v0.15.0-beta@sha256:d227a9db0727ff56020c8d6604c8c369757123d238ab6ce679579c2dd0d0d259
|
||||
user: 1000:1000
|
||||
depends_on:
|
||||
- tor
|
||||
|
@ -100,7 +100,7 @@ services:
|
|||
ipv4_address: $LND_IP
|
||||
dashboard:
|
||||
container_name: dashboard
|
||||
image: ghcr.io/runcitadel/dashboard:v0.0.12@sha256:102a44b938765be8224c63785949b90bf796670f01aef4e282d58a4c26a42595
|
||||
image: ghcr.io/runcitadel/dashboard:v0.0.15@sha256:a2cf5ad79367fb083db0f61e5a296aafee655c99af0c228680644c248ec674a5
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m30s
|
||||
networks:
|
||||
|
@ -108,7 +108,7 @@ services:
|
|||
ipv4_address: $DASHBOARD_IP
|
||||
manager:
|
||||
container_name: manager
|
||||
image: ghcr.io/runcitadel/manager:feat-karen-v2@sha256:61dfc6a23bb6603096673e517ec058730ca7e1fbe251df38760c2860d03b1dd0
|
||||
image: ghcr.io/runcitadel/manager:v0.0.15@sha256:9fb5a86d9e40a04f93d5b6110d43a0f9a5c4ad6311a843b5442290013196a5ce
|
||||
depends_on:
|
||||
- tor
|
||||
- redis
|
||||
|
@ -120,7 +120,6 @@ services:
|
|||
volumes:
|
||||
- ${PWD}/info.json:/info.json
|
||||
- ${PWD}/db:/db
|
||||
- ${PWD}/events/signals:/signals
|
||||
- ${PWD}/events:/events
|
||||
- ${PWD}/apps:/apps
|
||||
- ${PWD}/lnd:/lnd:ro
|
||||
|
@ -163,7 +162,7 @@ services:
|
|||
ipv4_address: $MANAGER_IP
|
||||
middleware:
|
||||
container_name: middleware
|
||||
image: ghcr.io/runcitadel/middleware:v0.0.10@sha256:afd6e2b6f5ba27cde32f6f6d630ddc6dd46d1072871f7834d7424d0554d0f53d
|
||||
image: ghcr.io/runcitadel/middleware:v0.0.11@sha256:e472da8cbfa67d9a9dbf321334fe65cdf20a0f9b6d6bab33fdf07210f54e7002
|
||||
depends_on:
|
||||
- manager
|
||||
- bitcoin
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
|
||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
"${CITADEL_ROOT}/scripts/set-update-channel" beta
|
||||
"${CITADEL_ROOT}/scripts/set-update-channel" "${1}"
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
"${CITADEL_ROOT}/scripts/set-update-channel" stable
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": "0.0.3",
|
||||
"name": "Citadel 0.0.3",
|
||||
"version": "0.0.7",
|
||||
"name": "Citadel 0.0.7",
|
||||
"requires": ">=0.0.1",
|
||||
"notes": "This update fixes multiple bugs on the dashboard and contains a rewritten app system to prepare for Core Lightning compatibility. We also added a new dark mode toggle and updated the instructions to get a Lightning address."
|
||||
"notes": "While we are busy with the next huge update, you may need to wait longer for updates. This update updates Bitcoin Knots and LND to their latest versions to ensure apps can utilize their latest features. In addition, this update includes the Citadel CLI. More information on that will be published soon."
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2020 Umbrel. https://getumbrel.com
|
||||
# SPDX-FileCopyrightText: 2022 Citadel and contributors. https://runcitadel.space
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
@ -98,39 +99,19 @@ tar \
|
|||
# --verbose \
|
||||
# --gzip
|
||||
|
||||
# Check if a file exists on Firebase by checking if
|
||||
# "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media"
|
||||
# returns a 200 response code.
|
||||
#
|
||||
check_if_exists() {
|
||||
curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X GET \
|
||||
"https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F${1}?alt=media"
|
||||
}
|
||||
|
||||
# Upload a file to Firebase Cloud Storage by uloading its name + a random ID.
|
||||
# Before uploading, we need to check if a file with the same name already exists, and if it does, change the random ID.
|
||||
# For example, if we want to upload a file named "<THE_FILE_NAME>" with the content AA, we'd use this command to upload:
|
||||
# curl -X POST "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media" -d "AA" -H "Content-Type: text/plain"
|
||||
# To check if a file exists, we can check if this endpoint returns an error 404:
|
||||
# curl "https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F<THE_FILE_NAME>?alt=media"
|
||||
# To download a file, we can the same endpoint
|
||||
upload_file() {
|
||||
local file_to_send="${1}"
|
||||
local file_name="${2}"
|
||||
# A random ID to avoid collisions
|
||||
local random_id="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 60 ; echo '')"
|
||||
# Check if the file already exists
|
||||
# While a file with the same name exists, we'll try to upload it with a different ID
|
||||
while [[ $(check_if_exists "${file_name}-${random_id}") == "200" ]]; do
|
||||
random_id="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 60 ; echo '')"
|
||||
done
|
||||
# Upload the file
|
||||
local backup_id="${2}"
|
||||
local upload_data=$(jq --null-input \
|
||||
--arg name "$backup_id" \
|
||||
--arg data "$(base64 $file_to_send)" \
|
||||
'{"name": $name, "data": $data}')
|
||||
curl -X POST \
|
||||
"https://firebasestorage.googleapis.com/v0/b/citadel-user-backups.appspot.com/o/backups%2F${file_name}-${random_id}?alt=media" \
|
||||
-d @"${file_to_send}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
> /dev/null
|
||||
"https://account.runcitadel.space/api/upload" \
|
||||
-d "${upload_data}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--socks5 "localhost:${TOR_PROXY_PORT}" \
|
||||
> /dev/null
|
||||
}
|
||||
|
||||
if [[ $BITCOIN_NETWORK == "testnet" ]]; then
|
||||
|
|
|
@ -39,7 +39,7 @@ main () {
|
|||
echo "Sleeping for ${delay} seconds..."
|
||||
sleep $delay
|
||||
echo "Triggering decoy backup..."
|
||||
touch "${CITADEL_ROOT}/events/signals/backup"
|
||||
"${CITADEL_ROOT}/scripts/backup/backup"
|
||||
done
|
||||
}
|
||||
|
||||
|
|
|
@ -48,10 +48,10 @@ monitor_file () {
|
|||
sleep 1
|
||||
done
|
||||
echo "$file_path created! Triggering backup..."
|
||||
touch "${CITADEL_ROOT}/events/signals/backup"
|
||||
"${CITADEL_ROOT}/scripts/backup/backup"
|
||||
fi
|
||||
|
||||
fswatch -0 --event Updated $file_path | xargs -0 -n 1 -I {} touch "${CITADEL_ROOT}/events/signals/backup"
|
||||
fswatch -0 --event Updated $file_path | xargs -0 -n 1 -I {} "${CITADEL_ROOT}/scripts/backup/backup"
|
||||
}
|
||||
|
||||
if [[ ! -d "${CITADEL_ROOT}" ]]; then
|
||||
|
|
2
scripts/configure
vendored
2
scripts/configure
vendored
|
@ -334,7 +334,7 @@ templates_to_build = {
|
|||
"./templates/bitcoin-sample.conf": "./bitcoin/bitcoin.conf",
|
||||
"./templates/.env-sample": "./.env",
|
||||
"./templates/electrs-sample.toml": "./electrs/electrs.toml",
|
||||
"./templates/fulcrumx-sample.conf": "./fulcrumx/fulcrumx.conf",
|
||||
"./templates/fulcrum-sample.conf": "./fulcrum/fulcrum.conf",
|
||||
"./templates/nginx-sample.conf": "./nginx/nginx.conf"
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,17 @@ function get_memory_usage() {
|
|||
function mem_usage_to_percent() {
|
||||
local mem_usage="$1"
|
||||
local total_mem="$(free -m | awk 'NR==2 {print $2}')"
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", $mem_usage / $total_mem * 100}")"
|
||||
echo "$(awk "BEGIN {printf \"%.1f\", ${mem_usage/,/.} / ${total_mem/,/.} * 100}")"
|
||||
}
|
||||
|
||||
function app_mem_usage() {
|
||||
# For every container of the app, get the mem usage, save it, and at the end, print the total mem usage of the app
|
||||
local mem_usage=0
|
||||
for container in $(get_app_containers "$1"); do
|
||||
# Use awk to add, it supports floating point numbers
|
||||
mem_usage=$(awk "BEGIN {printf \"%.2f\", $mem_usage + $(get_memory_usage "$container")}")
|
||||
done
|
||||
echo "${1}: $mem_usage%"
|
||||
}
|
||||
|
||||
get_total_used_mem_raw() {
|
||||
|
@ -35,7 +45,7 @@ get_total_used_mem() {
|
|||
# To get the containers of the app, list every container whose name starts with the name of the app
|
||||
get_app_containers () {
|
||||
local app_name="$1"
|
||||
"${CITADEL_ROOT}/scripts/app" compose "${app_name}" ps | awk '{print $1}' | grep -v 'Name\|-----'
|
||||
"${CITADEL_ROOT}/scripts/app" compose "${app_name}" ps | awk '{print $1}' | grep -v 'NAME'
|
||||
}
|
||||
|
||||
# Get the memory usage of the whole system, excluding docker containers
|
||||
|
@ -48,22 +58,17 @@ get_system_memory_usage() {
|
|||
}
|
||||
|
||||
main() {
|
||||
echo "total: $(get_total_used_mem)%"&
|
||||
echo "total: $(get_total_used_mem)%"
|
||||
echo "system: $(get_system_memory_usage)%"
|
||||
for service in bitcoin lightning electrum tor; do
|
||||
echo "${service}: $(get_memory_usage "$service")%" &
|
||||
done
|
||||
for app in $("${CITADEL_ROOT}/scripts/app" ls-installed); do
|
||||
# For every container of the app, get the mem usage, save it, and at the end, print the total mem usage of the app
|
||||
local mem_usage=0
|
||||
for container in $(get_app_containers "$app"); do
|
||||
# Use awk to add, it supports floating point numbers
|
||||
mem_usage=$(awk "BEGIN {printf \"%.2f\", $mem_usage + $(get_memory_usage "$container")}")
|
||||
done
|
||||
wait
|
||||
echo "${app}: $mem_usage%"
|
||||
app_mem_usage "${app}" &
|
||||
done
|
||||
echo "system: $(get_system_memory_usage)%"
|
||||
wait
|
||||
}
|
||||
|
||||
echo "Calculating memory usage..."
|
||||
echo "This may take a while, please wait..."
|
||||
main | sort --key 2 --numeric-sort --reverse
|
||||
|
|
|
@ -90,11 +90,11 @@ echo
|
|||
echo "Starting status monitors..."
|
||||
echo
|
||||
pkill -f ./scripts/status-monitor || true
|
||||
./scripts/status-monitor memory 60 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor memory 300 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor storage 60 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor temperature 15 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor uptime 15 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor app-updates 300 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor app-updates 600 &>> "${CITADEL_LOGS}/status-monitor.log" &
|
||||
|
||||
echo "Starting backup monitor..."
|
||||
echo
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# To prevent tools we use from outputting in another language
|
||||
LANG=C
|
||||
|
||||
CITADEL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
memory_total_bytes() {
|
||||
|
@ -23,7 +26,7 @@ get_app_memory_use() {
|
|||
|
||||
local app_memory=0
|
||||
|
||||
local app_containers=$("${CITADEL_ROOT}/app/app-manager.py" compose "${app}" ps | awk '{print $1}' | grep -v 'Name\|-----')
|
||||
local app_containers=$("${CITADEL_ROOT}/scripts/app" compose "${app}" ps | awk '{print $1}' | grep -v 'NAME')
|
||||
for container in $app_containers; do
|
||||
local container_memory=$(get_container_memory_use "${container}")
|
||||
app_memory=$(awk "BEGIN {print ${app_memory}+${container_memory}}")
|
||||
|
@ -43,7 +46,7 @@ used=$(memory_used_bytes)
|
|||
json=$(echo $json | jq --arg used "${used}" '. + {used: $used|tonumber}')
|
||||
|
||||
cumulative_app_memory="0"
|
||||
for app in $( "${CITADEL_ROOT}/app/app-manager.py" ls-installed); do
|
||||
for app in $( "${CITADEL_ROOT}/scripts/app" ls-installed); do
|
||||
app_memory=$(get_app_memory_use "${app}")
|
||||
cumulative_app_memory=$(($cumulative_app_memory+$app_memory))
|
||||
json=$(echo $json | jq --arg app "${app}" --arg app_memory "${app_memory}" '.breakdown |= .+ [{id: $app, used: $app_memory|tonumber}]')
|
||||
|
|
|
@ -5,7 +5,6 @@ lnd/*
|
|||
statuses
|
||||
tor/*
|
||||
electrs/*
|
||||
events/signals
|
||||
logs/*
|
||||
app-data/*
|
||||
apps/networking.json
|
||||
|
@ -13,4 +12,4 @@ nginx/*
|
|||
services/installed.yml
|
||||
apps/sourceMap.json
|
||||
apps/.updateignore
|
||||
fulcrumx/*
|
||||
fulcrum/*
|
||||
|
|
|
@ -6,3 +6,6 @@ tor/torrc-apps-3
|
|||
tor/torrc-core
|
||||
electrs/electrs.toml
|
||||
apps/docker-compose.common.yml
|
||||
services/bitcoin/*
|
||||
services/electrum/*
|
||||
services/lightning/*
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
bitcoin:
|
||||
container_name: bitcoin
|
||||
image: ghcr.io/runcitadel/bitcoin-custom:main@sha256:d0af506f8dc92a434e845305ac4252d0601b699c4b3bc4443073a0a2e237f3a0
|
||||
depends_on:
|
||||
- tor
|
||||
volumes:
|
||||
- ${PWD}/bitcoin:/data/.bitcoin
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m
|
||||
ports:
|
||||
- $BITCOIN_P2P_PORT:$BITCOIN_P2P_PORT
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: $BITCOIN_IP
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
bitcoin:
|
||||
container_name: bitcoin
|
||||
image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507
|
||||
image: lncm/bitcoind:v23.0@sha256:57317c90d89156a30327fe1b8e51b836e0fd1a8ba13721eb2e75e6b35a570e26
|
||||
depends_on:
|
||||
- tor
|
||||
volumes:
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
bitcoin:
|
||||
container_name: bitcoin
|
||||
image: nolim1t/bitcoinknots:v22.0.knots20211108@sha256:a475da2b2ecda55fcc65ea23e1a36c58b2c10549f1c3d3bb3c31c7dda1127354
|
||||
image: ghcr.io/runcitadel/bitcoinknots:main@sha256:5fbee0f6f0d09d42aacc11c373ffe6162210c42ce21e6eba294e547e3ad80219
|
||||
depends_on:
|
||||
- tor
|
||||
volumes:
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
electrum:
|
||||
container_name: electrum
|
||||
image: ghcr.io/runcitadel/fulcrumx:latest@sha256:a74abdfe8397f02482faed6bd828477c452df071129f66ad6596d0ab8d29cf39
|
||||
image: cculianu/fulcrum:latest@sha256:c0543f8b8a5bf6b0c447d8525d6b4360a6c07532f7741f19cc2c179968e71848
|
||||
working_dir: /data
|
||||
volumes:
|
||||
- ${PWD}/bitcoin:/bitcoin:ro
|
||||
- ${PWD}/fulcrumx:/data
|
||||
command: /usr/bin/FulcrumX /data/fulcrumx.conf
|
||||
- ${PWD}/fulcrum:/data
|
||||
command: /usr/bin/Fulcrum /data/fulcrum.conf
|
||||
restart: on-failure
|
||||
stop_grace_period: 5m
|
||||
ports:
|
|
@ -1,19 +0,0 @@
|
|||
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
electrum:
|
||||
container_name: electrum
|
||||
image: ghcr.io/runcitadel/fulcrumx:latest@sha256:557a54a5652b475b01c59eb6cffee3568f18b74d875c4cc125e5ac190c4b0706
|
||||
working_dir: /data
|
||||
volumes:
|
||||
- ${PWD}/bitcoin:/bitcoin:ro
|
||||
- ${PWD}/fulcrumx:/data
|
||||
command: /usr/bin/FulcrumX /data/fulcrumx.conf
|
||||
restart: on-failure
|
||||
stop_grace_period: 5m
|
||||
ports:
|
||||
- "$ELECTRUM_PORT:$ELECTRUM_PORT"
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: $ELECTRUM_IP
|
|
@ -1,6 +1,6 @@
|
|||
lightning:
|
||||
container_name: lightning
|
||||
image: lightninglabs/lnd:v0.14.3-beta@sha256:6a2234b0aad4caed3d993736816b198d6228f32c59b27ba2218d5ebf516ae905
|
||||
image: lightninglabs/lnd:v0.15.0-beta@sha256:d227a9db0727ff56020c8d6604c8c369757123d238ab6ce679579c2dd0d0d259
|
||||
user: 1000:1000
|
||||
depends_on:
|
||||
- tor
|
||||
|
|
|
@ -31,6 +31,21 @@ args = parser.parse_args()
|
|||
# Function to install a service
|
||||
# To install it, read the service's YAML file (nodeRoot/services/name.yml) and add it to the main compose file (nodeRoot/docker-compose.yml)
|
||||
def setService(name, implementation):
|
||||
# Get all available services
|
||||
services = next(os.walk(os.path.join(nodeRoot, "services")))[1]
|
||||
|
||||
if not name in services:
|
||||
print("\"{}\" is not a valid service.".format(name))
|
||||
exit(1)
|
||||
|
||||
# Get all available implementations
|
||||
implementations = next(os.walk(os.path.join(nodeRoot, "services", name)), (None, None, []))[2]
|
||||
implementations = [x.split('.')[0] for x in implementations]
|
||||
|
||||
if not implementation in implementations:
|
||||
print("\"{}\" is not a valid implementation.".format(implementation))
|
||||
exit(1)
|
||||
|
||||
# Read the YAML file
|
||||
with open(os.path.join(nodeRoot, "services", name, implementation + ".yml"), 'r') as stream:
|
||||
service = yaml.safe_load(stream)
|
||||
|
|
Loading…
Reference in New Issue
Block a user