feat: add Citadel CLI (#33)

add commands for:
- Show status for all services
- Run a command inside a (app) container
- Switch branch / update channel
- Switch Bitcoin/Electrum/Lightning implementation
- app commands
- debug command
- Fix update / backup / starting stuck
- Edit node configs (Bitcoin Core, LND)
- Edit app configs (Nextcloud etc.)
- Memory usage / System info
- Start
- Stop
- Restart
- Reboot
- Shutdown
- List all installed apps & services
- Logs
- Version
- Help
This commit is contained in:
Philipp Walter 2022-07-15 13:26:23 +02:00 committed by GitHub
parent a8d224f597
commit 07aa73b59e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 820 additions and 0 deletions

1
bin/citadel Symbolic link
View File

@ -0,0 +1 @@
../cli/citadel

487
cli/citadel Executable file
View 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" = "fulcrumx" ]]; then
if $persist; then
edit_file $CITADEL_ROOT/templates/fulcrumx-sample.conf
prompt_apply_config electrum true
else
edit_file $CITADEL_ROOT/fulcrumx/fulcrumx.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
View 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
View 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
View 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
View 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
}