Compare commits

..

74 Commits

Author SHA1 Message Date
a828100878 dropshell release 2025.0514.2226
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:26:45 +12:00
fcc517a115 dropshell release 2025.0514.2225
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:25:20 +12:00
47dcfca5f2 dropshell release 2025.0514.2222
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:22:15 +12:00
283b88effc Working on install.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 22:26:24 +12:00
97776b4642 dropshell release 2025.0513.2220
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 22:20:16 +12:00
89095cdc50 dropshell release 2025.0513.2200
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 22:02:02 +12:00
8d5296d9ea Merge branch 'main' of https://gitea.jde.nz/public/dropshell
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:56:47 +12:00
524d3df1b2 dropshell release 2025.0513.2156 2025-05-13 21:56:24 +12:00
54dce4862f Update README.md
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:54:18 +12:00
1dd4a7958d dropshell release 2025.0513.2151
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:51:53 +12:00
4b4b99634c Super simple assert for now.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:46:04 +12:00
972d5f4db7 gitignore
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:39:00 +12:00
365dc2c60a Remove build!
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:37:18 +12:00
bd1ad20990 dropshell release 2025.0513.2134
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:34:59 +12:00
adcb3567d4 Move templates.
Remove docker stuff.
2025-05-13 21:17:30 +12:00
30cfd591a3 Add version.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:05:33 +12:00
296434ff1f .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:02:32 +12:00
225ca12e83 Version fixed.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 21:02:07 +12:00
ae15a1b7c4 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 20:56:39 +12:00
a9b1758503 ./
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 20:55:06 +12:00
2cd0e8bba2 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 20:45:17 +12:00
b260c9813e Making install work.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 20:16:39 +12:00
5201e92cb3 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-12 23:53:52 +12:00
d946c18d7c move to bb64
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-12 23:08:56 +12:00
8fc3384c03 Tidy
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-12 20:11:08 +12:00
df281d2f91 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-12 19:14:03 +12:00
83d06a1680 ?
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-11 21:58:20 +12:00
1d2e223547 Fixed ssh commands. 2025-05-11 21:55:25 +12:00
bcf0f18006 better.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-11 21:48:15 +12:00
23e3d731c9 Something wrong with the ssh commands./
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-11 21:45:23 +12:00
df03f5ef91 execute_ssh_command is not returning the correct return code.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 22s
2025-05-11 21:33:49 +12:00
a27dd7638f Nuke and uninstall. 2025-05-11 21:05:17 +12:00
a2340dcb80 Fix autocomplete bugs.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-11 19:26:25 +12:00
7e95779d98 Install working
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-11 16:26:25 +12:00
25bc0b4655 ./
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-11 13:54:13 +12:00
64639adcf0 ...
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-11 13:47:51 +12:00
72df234290 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-11 13:40:40 +12:00
92b80d6bb7 list implemented
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-11 13:20:15 +12:00
62191cceed Working
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 27s
2025-05-11 13:04:12 +12:00
46396369d7 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-11 12:48:30 +12:00
78dbf4aff3 Implementing commands
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-11 12:22:36 +12:00
3c8a66c241 Pre big change
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-11 11:33:21 +12:00
ea3367d8d9 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-11 10:55:05 +12:00
a6dafc3a51 Tidy!
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 22:36:58 +12:00
d1d880a3a8 Simplify interactive
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 22:27:51 +12:00
ed68b58e5c ...
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 21:46:56 +12:00
fe571e1cc9 Back in business
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 21:41:52 +12:00
e9d4529d85 Working!
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 21:27:20 +12:00
535eed2ece .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-10 20:12:58 +12:00
4d6702b099 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 20:06:05 +12:00
330bdf9941 Revert to last night.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-10 19:44:28 +12:00
5d42db7331 All broken
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 19:40:38 +12:00
dbcef96bc2 EXPERIMENTAL
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 18:19:02 +12:00
f9dca5fea1 Sigh
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 17:32:03 +12:00
0f7ed5d657 Edit now works
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 17:02:41 +12:00
61218f8866 colour
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 16:56:54 +12:00
547482edc6 Nearly
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 15:58:19 +12:00
35c97728c9 Integrating runner.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 23s
2025-05-10 15:24:53 +12:00
ec5f4ad38d Seems to work
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 14:43:35 +12:00
409f532409 Works
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 14:36:42 +12:00
de337f51f3 GPT 4.1 has failed.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 14:32:12 +12:00
34f2763ef4 Broken
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 14:27:24 +12:00
5973d63d3e No WHATSUUUUPPP!
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 14:22:40 +12:00
bc45f60b6e ./
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 14:17:04 +12:00
39e083898f GPT 4.1 trying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-10 14:06:30 +12:00
c9c5108254 Local works 2025-05-10 13:54:38 +12:00
8827ea5a42 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 13:47:17 +12:00
068ef34709 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 20s
2025-05-10 13:32:09 +12:00
00571d8091 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 13:28:13 +12:00
4087b6e596 . 2025-05-10 13:14:11 +12:00
a3914f8167 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 13:01:24 +12:00
d070baed0a .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 12:51:19 +12:00
9e9d80570c .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 18s
2025-05-10 12:44:37 +12:00
b5bc7b611d Runner.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-10 12:43:25 +12:00
134 changed files with 2448 additions and 2253 deletions

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
# Build directories
build/
build_amd64/
build_arm64/
cmake-build-*/
out/
bin/
@ -56,4 +58,4 @@ Thumbs.db
# Dependencies
deps/
vendor/
vendor/

View File

@ -40,28 +40,19 @@ file(GLOB_RECURSE HEADERS "src/*.hpp")
add_executable(dropshell ${SOURCES})
# Set include directories
# build dir goes first so that we can use the generated version.hpp
target_include_directories(dropshell PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
${CMAKE_CURRENT_SOURCE_DIR}/src/contrib
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
${CMAKE_CURRENT_SOURCE_DIR}/src/commands
)
include(FetchContent)
FetchContent_Declare(
libassert
GIT_REPOSITORY https://github.com/jeremy-rifkin/libassert.git
GIT_TAG v2.1.5 # <HASH or TAG>
)
FetchContent_MakeAvailable(libassert)
target_link_libraries(dropshell libassert::assert)
# On windows copy libassert.dll to the same directory as the executable for your_target
if(WIN32)
add_custom_command(
TARGET dropshell POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:libassert::assert>
$<TARGET_FILE_DIR:dropshell>
)
endif()

View File

@ -1,3 +1,17 @@
# Dropshell
A system management tool for server operations, written in C++.
## Installation
```
curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/install.sh | bash
```
## Installation of Agent
Install the Agent on each server you wish to manage. Supports amd64 (x86_64) and arm64 (aarch64) architectures.
```
curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/install.sh | bash
```

View File

@ -17,6 +17,8 @@ SCRIPT_DIR="$(dirname "$0")"
# // |-- service.env
# // |-- template
# // |-- (script files)
# // |-- shared
# // |-- _allservicesstatus.sh
# // |-- config
# // |-- service.env
# // |-- (other config files for specific server&service)
@ -74,7 +76,7 @@ function command_exists() {
}
# Get all services on the server
SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../../")
SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../../../")
# Get all service names
SERVICE_NAMES=$(ls "${SERVICES_PATH}")

View File

@ -1,13 +1,10 @@
#!/bin/bash
# This script contains the common code for the autocommands.
_check_required_env_vars "BACKUP_FILE" "TEMP_DIR"
MYID=$(id -u)
MYGRP=$(id -g)
BACKUP_TEMP_PATH="$TEMP_DIR/backup"
_autocommandrun_volume() {
local command="$1"
local volume_name="$2"
@ -47,10 +44,10 @@ _autocommandrun_path() {
;;
nuke)
echo "Nuking path ${path}"
PATHPARENT=$(dirname ${path})
PATHCHILD=$(basename ${path})
if [ -d "${PATHPARENT}/${PATHCHILD}" ]; then
docker run --rm -v ${PATHPARENT}:/volume debian bash -c "rm -rf /volume/${PATHCHILD}" || echo "Failed to nuke path ${path}"
local path_parent=$(dirname ${path})
local path_child=$(basename ${path})
if [ -d "${path_parent}/${path_child}" ]; then
docker run --rm -v ${path_parent}:/volume debian bash -c "rm -rf /volume/${path_child}" || echo "Failed to nuke path ${path}"
else
echo "Path ${path} does not exist - nothing to nuke"
fi
@ -83,18 +80,18 @@ _autocommandrun_file() {
;;
backup)
echo "Backing up file ${filepath}"
FILEPARENT=$(dirname ${filepath})
FILENAME=$(basename ${filepath})
if [ -f "${FILEPARENT}/${FILENAME}" ]; then
docker run --rm-v ${FILEPARENT}:/volume -v ${backup_folder}:/backup debian bash -c "cp /volume/${FILENAME} /backup/${FILENAME} && chown -R $MYID:$MYGRP /backup"
local file_parent=$(dirname ${filepath})
local file_name=$(basename ${filepath})
if [ -f "${file_parent}/${file_name}" ]; then
docker run --rm -v ${file_parent}:/volume -v ${backup_folder}:/backup debian bash -c "cp /volume/${file_name} /backup/${file_name} && chown -R $MYID:$MYGRP /backup"
else
echo "File ${filepath} does not exist - nothing to backup"
fi
;;
restore)
echo "Restoring file ${filepath}"
local FILENAME=$(basename ${filepath})
cp ${backup_folder}/${FILENAME} ${filepath}
local file_name=$(basename ${filepath})
cp ${backup_folder}/${file_name} ${filepath}
;;
esac
}
@ -112,7 +109,11 @@ _autocommandparse() {
local command="$1"
shift
echo "autocommandparse: command=$command"
local backup_temp_path="$1"
shift
echo "autocommandparse: command=$command backup_temp_path=$backup_temp_path"
# Extract the backup file and temp path (last two arguments)
local args=("$@")
@ -132,7 +133,7 @@ _autocommandparse() {
# create backup folder unique to key/value.
local bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-')
local targetpath="${BACKUP_TEMP_PATH}/${bfolder}"
local targetpath="${backup_temp_path}/${bfolder}"
mkdir -p ${targetpath}
# Key must be one of volume, path or file
@ -155,31 +156,34 @@ _autocommandparse() {
autocreate() {
_autocommandparse create "$@"
_autocommandparse create none "$@"
}
autonuke() {
_autocommandparse nuke "$@"
_autocommandparse nuke none "$@"
}
autobackup() {
_check_required_env_vars "BACKUP_FILE" "TEMP_DIR"
BACKUP_TEMP_PATH="$TEMP_DIR/backup"
mkdir -p "$BACKUP_TEMP_PATH"
echo "_autocommandparse [backup] [$@]"
_autocommandparse backup "$@"
echo "_autocommandparse [backup] [$BACKUP_TEMP_PATH] [$@]"
_autocommandparse backup "$BACKUP_TEMP_PATH" "$@"
tar zcvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" .
}
autorestore() {
echo "_autocommandparse [restore] [$@]"
_check_required_env_vars "BACKUP_FILE" "TEMP_DIR"
BACKUP_TEMP_PATH="$TEMP_DIR/restore"
echo "_autocommandparse [restore] [$BACKUP_TEMP_PATH] [$@]"
mkdir -p "$BACKUP_TEMP_PATH"
tar zxvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" --strip-components=1
_autocommandparse restore "$@"
_autocommandparse restore "$BACKUP_TEMP_PATH" "$@"
}

View File

@ -9,15 +9,11 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Parse command line arguments
AUTO_INSTALL=false
for arg in "$@"; do
case $arg in
--auto-install)
AUTO_INSTALL=true
;;
esac
done
JOBS=4
# Determine number of CPU cores for parallel build
if command -v nproc >/dev/null 2>&1; then
JOBS=$(nproc)
fi
# Function to print status messages
@ -79,7 +75,7 @@ cmake .. -DCMAKE_BUILD_TYPE=Debug
# Build the project
print_status "Building project..."
make -j$(nproc)
make -j"$JOBS"
# Check if build was successful
if [ $? -eq 0 ]; then
@ -90,31 +86,16 @@ else
exit 1
fi
# Check if user wants to install
if [ $AUTO_INSTALL = true ]; then
print_status "Auto-installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
print_status "Auto-installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
read -p "Do you want to install the program? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
print_status "Installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
fi
print_error "Installation failed!"
exit 1
fi
# Return to original directory
cd ..

View File

@ -1,24 +0,0 @@
FROM debian:bullseye AS builder
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
pkg-config \
bash \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
binutils-aarch64-linux-gnu \
qemu-user-static
WORKDIR /app
COPY . .
COPY --chmod=755 docker/_create_dropshell.sh /scripts/
RUN rm -rf build
ENV CXXFLAGS="-static-libstdc++ -static-libgcc"
ENV LDFLAGS="-static -pthread -Wl,--whole-archive -lpthread -Wl,--no-whole-archive"
CMD ["/bin/bash","/scripts/_create_dropshell.sh"]

View File

@ -1,10 +0,0 @@
#!/bin/bash
set -x
mkdir -p /app/build
cd /app/build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4
cp /app/build/dropshell /output/
chown $CHOWN_USER:$CHOWN_GROUP /output/dropshell

View File

@ -1,8 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ROOT_DIR=$(dirname $SCRIPT_DIR)
docker build -t gitea.jde.nz/j/dropshell_builder:latest $ROOT_DIR -f $SCRIPT_DIR/Dockerfile.build

View File

@ -1,47 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ROOT_DIR=$(dirname $SCRIPT_DIR)
echo "Building dropshell from $ROOT_DIR"
# Build the builder image
#docker build -t dropshell_alpine_builder $ROOT_DIR -f $SCRIPT_DIR/Dockerfile.build
rm -rf $SCRIPT_DIR/output
mkdir -p $SCRIPT_DIR/output
MYUID=$(id -u)
MYGID=$(id -g)
# Build for x86_64
echo "Building for x86_64..."
docker run --rm -tt --env CHOWN_USER=$MYUID --env CHOWN_GROUP=$MYGID \
-v $SCRIPT_DIR/output:/output \
-e TARGET_ARCH=x86_64 \
-e CC=gcc \
-e CXX=g++ \
gitea.jde.nz/j/dropshell_builder:latest
mv $SCRIPT_DIR/output/dropshell $SCRIPT_DIR/output/dropshell_x86_64
$SCRIPT_DIR/output/dropshell_x86_64 version
echo "dropshell built in $SCRIPT_DIR/output/dropshell_x86_64"
# Build for arm64
echo "Building for arm64..."
docker run --rm -tt --env CHOWN_USER=$MYUID --env CHOWN_GROUP=$MYGID \
-v $SCRIPT_DIR/output:/output \
-e TARGET_ARCH=aarch64 \
-e CC=aarch64-linux-gnu-gcc \
-e CXX=aarch64-linux-gnu-g++ \
gitea.jde.nz/j/dropshell_builder:latest
mv $SCRIPT_DIR/output/dropshell $SCRIPT_DIR/output/dropshell_aarch64
echo "dropshell built in $SCRIPT_DIR/output/dropshell_aarch64"

View File

@ -1,3 +0,0 @@
#!/bin/bash
docker push gitea.jde.nz/j/dropshell_builder:latest

View File

@ -1,2 +0,0 @@
#!/bin/bash

50
install.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/bash
set -e
# download and install dropshell
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run this script as root (use sudo)"
exit 1
fi
# 1. Determine architecture
# -----------------------------------------------------------------------------
ARCH=$(uname -m)
if [[ "$ARCH" == "x86_64" ]]; then
BIN=dropshell.amd64
elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
BIN=dropshell.arm64
else
echo "Unsupported architecture: $ARCH" >&2
exit 1
fi
# 2. Download the appropriate binary to a temp directory
# -----------------------------------------------------------------------------
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
URL="https://gitea.jde.nz/public/dropshell/releases/download/latest/$BIN"
echo "Downloading $BIN from $URL..."
curl -fsSL -o "$TMPDIR/dropshell" "$URL"
if [ ! -f "$TMPDIR/dropshell" ]; then
echo "Failed to download dropshell" >&2
exit 1
fi
chmod +x "$TMPDIR/dropshell"
cp "$TMPDIR/dropshell" /usr/local/bin/dropshell
if [ -f /usr/local/bin/ds ]; then
rm -f /usr/local/bin/ds
fi
ln -s /usr/local/bin/dropshell /usr/local/bin/ds
rm -rf "$TMPDIR"
echo "dropshell installed successfully to /usr/local/bin/dropshell"

View File

@ -82,5 +82,69 @@ for tool in cmake make g++; do
fi
done
# Install other required packages
apt install -y musl-tools wget tar
# Set install directory
if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
USER_HOME=$(eval echo ~$SUDO_USER)
else
USER_HOME="$HOME"
fi
INSTALL_DIR="$USER_HOME/.musl-cross"
mkdir -p "$INSTALL_DIR"
MUSL_CC_URL="https://musl.cc"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# x86_64
if [ ! -d "$INSTALL_DIR/x86_64-linux-musl-cross" ]; then
echo "Downloading x86_64 musl cross toolchain..."
wget -nc -O "$TMPDIR/x86_64-linux-musl-cross.tgz" $MUSL_CC_URL/x86_64-linux-musl-cross.tgz
tar -C "$INSTALL_DIR" -xvf "$TMPDIR/x86_64-linux-musl-cross.tgz"
fi
# aarch64
if [ ! -d "$INSTALL_DIR/aarch64-linux-musl-cross" ]; then
echo "Downloading aarch64 musl cross toolchain..."
wget -nc -O "$TMPDIR/aarch64-linux-musl-cross.tgz" $MUSL_CC_URL/aarch64-linux-musl-cross.tgz
tar -C "$INSTALL_DIR" -xvf "$TMPDIR/aarch64-linux-musl-cross.tgz"
fi
# Print instructions for adding to PATH
# cat <<EOF
# To use the musl cross compilers, add the following to your shell:
# export PATH="$INSTALL_DIR/x86_64-linux-musl-cross/bin:$INSTALL_DIR/aarch64-linux-musl-cross/bin:$PATH"
# Or run:
# export PATH="$INSTALL_DIR/x86_64-linux-musl-cross/bin:$INSTALL_DIR/aarch64-linux-musl-cross/bin:\$PATH"
# EOF
# Clean up
rm -rf "$TMPDIR"
# If run with sudo, add to invoking user's ~/.bashrc
if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
BASHRC="$USER_HOME/.bashrc"
EXPORT_LINE="export PATH=\"$INSTALL_DIR/x86_64-linux-musl-cross/bin:$INSTALL_DIR/aarch64-linux-musl-cross/bin:\$PATH\""
if ! grep -Fxq "$EXPORT_LINE" "$BASHRC"; then
echo "" >> "$BASHRC"
echo "# Add musl cross compilers to PATH for bb64" >> "$BASHRC"
echo "$EXPORT_LINE" >> "$BASHRC"
echo "Added musl cross compilers to $BASHRC"
else
echo "musl cross compiler PATH already present in $BASHRC"
fi
fi
print_status "All dependencies installed successfully!"
print_status "You can now run ./build.sh to build the project"

52
multibuild.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
# build amd64 and arm64 versions of dropshell, to:
# build/dropshell.amd64
# build/dropshell.arm64
set -e
rm -f build_amd64/dropshell build_arm64/dropshell build/dropshell.amd64 build/dropshell.arm64
# Determine number of CPU cores for parallel build
if command -v nproc >/dev/null 2>&1; then
JOBS=$(nproc)
else
JOBS=4 # fallback default
fi
# Build for amd64 (musl)
echo "Building for amd64 (musl)..."
cmake -B build_amd64 -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=x86_64-linux-musl-gcc \
-DCMAKE_CXX_COMPILER=x86_64-linux-musl-g++ \
-DCMAKE_EXE_LINKER_FLAGS="-static" \
-DCMAKE_CXX_FLAGS="-march=x86-64" .
cmake --build build_amd64 --target dropshell --config Release -j"$JOBS"
mkdir -p build
cp build_amd64/dropshell build/dropshell.amd64
# Build for arm64 (musl)
echo "Building for arm64 (musl)..."
cmake -B build_arm64 -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=aarch64-linux-musl-gcc \
-DCMAKE_CXX_COMPILER=aarch64-linux-musl-g++ \
-DCMAKE_EXE_LINKER_FLAGS="-static" \
-DCMAKE_CXX_FLAGS="-march=armv8-a" \
-DCMAKE_SYSTEM_PROCESSOR=aarch64 .
cmake --build build_arm64 --target dropshell --config Release -j"$JOBS"
mkdir -p build
cp build_arm64/dropshell build/dropshell.arm64
if [ ! -f build/dropshell.amd64 ]; then
echo "build/dropshell.amd64 not found!" >&2
exit 1
fi
if [ ! -f build/dropshell.arm64 ]; then
echo "build/dropshell.arm64 not found!" >&2
exit 1
fi
echo "Builds complete:"
ls -lh build/dropshell.*

95
publish.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/bash
set -e
# Check for GITEA_TOKEN_DEPLOY or GITEA_TOKEN
if [ -n "$GITEA_TOKEN_DEPLOY" ]; then
TOKEN="$GITEA_TOKEN_DEPLOY"
elif [ -n "$GITEA_TOKEN" ]; then
TOKEN="$GITEA_TOKEN"
else
echo "GITEA_TOKEN_DEPLOY or GITEA_TOKEN environment variable not set!" >&2
exit 1
fi
./multibuild.sh
if [ ! -f "build/dropshell.amd64" ]; then
echo "build/dropshell.amd64 not found!" >&2
echo "Please run multibuild.sh first." >&2
exit 1
fi
if [ ! -f "build/dropshell.arm64" ]; then
echo "build/dropshell.arm64 not found!" >&2
echo "Please run multibuild.sh first." >&2
exit 1
fi
TAG=$(./build/dropshell.amd64 --version)
echo "Publishing dropshell version $TAG"
# make sure we've commited.
git add . && git commit -m "dropshell release $TAG" && git push
# Find repo info from .git/config
REPO_URL=$(git config --get remote.origin.url)
if [[ ! $REPO_URL =~ gitea ]]; then
echo "Remote origin is not a Gitea repository: $REPO_URL" >&2
exit 1
fi
# Extract base URL, owner, and repo
# Example: https://gitea.example.com/username/reponame.git
BASE_URL=$(echo "$REPO_URL" | sed -E 's#(https?://[^/]+)/.*#\1#')
OWNER=$(echo "$REPO_URL" | sed -E 's#.*/([^/]+)/[^/]+(\.git)?$#\1#')
REPO=$(echo "$REPO_URL" | sed -E 's#.*/([^/]+)(\.git)?$#\1#')
API_URL="$BASE_URL/api/v1/repos/$OWNER/$REPO"
# Create release
RELEASE_DATA=$(cat <<EOF
{
"tag_name": "$TAG",
"name": "$TAG",
"body": "dropshell release $TAG",
"draft": false,
"prerelease": false
}
EOF
)
RELEASE_ID=$(curl -s -X POST "$API_URL/releases" \
-H "Content-Type: application/json" \
-H "Authorization: token $TOKEN" \
-d "$RELEASE_DATA" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -z "$RELEASE_ID" ]; then
echo "Failed to create release on Gitea." >&2
exit 1
fi
# Upload binaries and install.sh
for FILE in dropshell.amd64 dropshell.arm64 install.sh server_autosetup.sh; do
if [ -f "build/$FILE" ]; then
filetoupload="build/$FILE"
elif [ -f "$FILE" ]; then
filetoupload="$FILE"
else
echo "File $FILE not found!" >&2
continue
fi
# Auto-detect content type
ctype=$(file --mime-type -b "$filetoupload")
curl -s -X POST "$API_URL/releases/$RELEASE_ID/assets?name=$FILE" \
-H "Content-Type: $ctype" \
-H "Authorization: token $TOKEN" \
--data-binary @"$filetoupload"
echo "Uploaded $FILE to release $TAG as $ctype."
done
echo "Published dropshell version $TAG to $REPO_URL (tag $TAG) with binaries."

View File

@ -55,6 +55,13 @@ if ! command -v docker &> /dev/null; then
rm get-docker.sh
fi
# check bb64 installation
if ! command -v bb64 &> /dev/null; then
echo "bb64 is not installed."
echo "Installing bb64..."
curl -fsSL https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh | bash
fi
# check dropshell user exists
if ! id "dropshell" &> /dev/null; then
echo "Dropshell user does not exist."
@ -101,6 +108,7 @@ if [ "$ARCH" != "aarch64" ] && [ "$ARCH" != "x86_64" ]; then
exit 1
fi
echo "Installation complete."
#--------------------------------

View File

@ -5,11 +5,30 @@
#include "services.hpp"
#include "servers.hpp"
#include <libassert/assert.hpp>
#include <assert.hpp>
#include <algorithm>
#include <iostream>
bool dropshell::autocomplete(const std::vector<std::string> &args)
namespace autocomplete {
const std::set<std::string> system_commands_noargs = {"templates","autocomplete_list_servers","autocomplete_list_services","autocomplete_list_commands"};
const std::set<std::string> system_commands_always_available = {"help","edit"};
const std::set<std::string> system_commands_require_config = {"server","templates","create-service","create-template","create-server","ssh","list"};
const std::set<std::string> system_commands_hidden = {"nuke","_allservicesstatus"};
const std::set<std::string> service_commands_require_config = {"ssh","edit","nuke","_allservicesstatus"};
void merge_commands(std::set<std::string> &commands, const std::set<std::string> &new_commands)
{
commands.insert(new_commands.begin(), new_commands.end());
}
bool is_no_arg_cmd(std::string cmd)
{
return system_commands_noargs.find(cmd) != system_commands_noargs.end();
}
bool autocomplete(const std::vector<std::string> &args)
{
if (args.size() < 3) // dropshell autocomplete ???
{
@ -17,7 +36,7 @@ bool dropshell::autocomplete(const std::vector<std::string> &args)
return true;
}
ASSERT(args.size() >= 3);
ASSERT(args.size() >= 3, "Invalid number of arguments");
std::string cmd = args[2];
// std::cout<<" cmd = ["<<cmd<<"]"<<std::endl;
@ -30,10 +49,8 @@ bool dropshell::autocomplete(const std::vector<std::string> &args)
return true;
}
std::string noargcmds[] = {"templates","autocomplete_list_servers","autocomplete_list_services","autocomplete_list_commands"};
if (std::find(std::begin(noargcmds), std::end(noargcmds), cmd) != std::end(noargcmds))
return true;
if (autocomplete::is_no_arg_cmd(cmd))
return true; // no arguments needed.
if (!dropshell::gConfig().is_config_set())
return false; // can't help without working config.
@ -83,23 +100,21 @@ bool dropshell::autocomplete(const std::vector<std::string> &args)
return false; // catch-all.
}
bool dropshell::autocomplete_list_commands()
bool autocomplete_list_commands()
{
std::set<std::string> commands;
dropshell::get_all_used_commands(commands);
// add in commmands hard-coded and handled in main
commands.merge(std::set<std::string>{
"help","edit" // these are always available.
});
autocomplete::merge_commands(commands, autocomplete::system_commands_always_available);
if (dropshell::gConfig().is_config_set())
commands.merge(std::set<std::string>{
"server","templates","create-service","create-template","create-server","ssh",
"list" // only if we have a config.
});
autocomplete::merge_commands(commands, autocomplete::system_commands_require_config);
for (const auto& command : commands) {
std::cout << command << std::endl;
}
return true;
}
} // namespace autocomplete

View File

@ -3,14 +3,26 @@
#include <string>
#include <vector>
#include <set>
namespace autocomplete {
namespace dropshell {
extern const std::set<std::string> system_commands_noargs;
extern const std::set<std::string> system_commands_always_available;
extern const std::set<std::string> system_commands_require_config;
extern const std::set<std::string> system_commands_hidden;
extern const std::set<std::string> service_commands_require_config;
void merge_commands(std::set<std::string> &commands, const std::set<std::string> &new_commands);
bool is_no_arg_cmd(std::string cmd);
bool autocomplete(const std::vector<std::string> &args);
bool autocomplete_list_commands();
} // namespace dropshell
}
#endif

View File

@ -0,0 +1,75 @@
#include "command_registry.hpp"
#include "utils/utils.hpp"
#include "config.hpp"
namespace dropshell {
CommandRegistry& CommandRegistry::instance() {
static CommandRegistry reg;
return reg;
}
void CommandRegistry::register_command(const CommandInfo& info) {
auto ptr = std::make_shared<CommandInfo>(info);
for (const auto& name : info.names) {
command_map_[name] = ptr;
}
all_commands_.push_back(ptr);
}
const CommandInfo* CommandRegistry::find_command(const std::string& name) const {
auto it = command_map_.find(name);
if (it != command_map_.end()) return it->second.get();
return nullptr;
}
std::vector<std::string> CommandRegistry::list_commands(bool include_hidden) const {
std::set<std::string> out;
for (const auto& cmd : all_commands_) {
if (!cmd->hidden || include_hidden) {
for (const auto& name : cmd->names) out.insert(name);
}
}
return std::vector<std::string>(out.begin(), out.end());
}
std::vector<std::string> CommandRegistry::list_primary_commands(bool include_hidden) const {
std::set<std::string> out;
for (const auto& cmd : all_commands_) {
if (!cmd->hidden || include_hidden) {
if (cmd->names.size() > 0)
{
if (cmd->requires_config && !gConfig().is_config_set())
continue;
if (cmd->requires_install && !gConfig().is_agent_installed())
continue;
out.insert(cmd->names[0]);
}
}
}
return std::vector<std::string>(out.begin(), out.end());
}
void CommandRegistry::autocomplete(const CommandContext& ctx) const {
// dropshell autocomplete <command> <arg> <arg> ...
if (ctx.args.size() < 1) {
for (const auto& name : list_primary_commands(false)) {
std::cout << name << std::endl;
}
return;
}
// ctx command is autocomplete, so recreate ctx with the first arg removed
CommandContext childcontext = {
ctx.exename,
ctx.args[0],
std::vector<std::string>(ctx.args.begin() + 1, ctx.args.end())
};
auto* info = find_command(childcontext.command);
if (info && info->autocomplete) {
info->autocomplete(childcontext);
}
}
} // namespace dropshell

View File

@ -0,0 +1,60 @@
#ifndef COMMAND_REGISTRY_HPP
#define COMMAND_REGISTRY_HPP
#include <string>
#include <vector>
#include <functional>
#include <map>
#include <set>
#include <memory>
#include <iostream>
namespace dropshell {
struct CommandContext {
std::string exename;
std::string command;
std::vector<std::string> args;
// Add more fields as needed (e.g., config pointer, output stream, etc.)
};
struct CommandInfo {
std::vector<std::string> names;
std::function<int(const CommandContext&)> handler;
std::function<void(const CommandContext&)> autocomplete; // optional
bool hidden = false;
bool requires_config = true;
bool requires_install = true;
int min_args = 0;
int max_args = -1; // -1 = unlimited
std::string help_usage; // install SERVER [SERVICE]
std::string help_description; // Install/reinstall/update service(s). Safe/non-destructive.
std::string full_help; // detailed help for the command
};
class CommandRegistry {
public:
static CommandRegistry& instance();
void register_command(const CommandInfo& info);
// Returns nullptr if not found
const CommandInfo* find_command(const std::string& name) const;
// List all commands (optionally including hidden)
std::vector<std::string> list_commands(bool include_hidden = false) const;
std::vector<std::string> list_primary_commands(bool include_hidden = false) const;
// For autocomplete
void autocomplete(const CommandContext& ctx) const;
private:
CommandRegistry() = default;
std::map<std::string, std::shared_ptr<CommandInfo>> command_map_;
std::vector<std::shared_ptr<CommandInfo>> all_commands_;
};
} // namespace dropshell
#endif // COMMAND_REGISTRY_HPP

180
src/commands/edit.cpp Normal file
View File

@ -0,0 +1,180 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/assert.hpp"
namespace dropshell {
int edit_handler(const CommandContext& ctx);
static std::vector<std::string> edit_name_list={"edit"};
// Static registration
struct EditCommandRegister {
EditCommandRegister() {
CommandRegistry::instance().register_command({
edit_name_list,
edit_handler,
std_autocomplete,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
2, // max_args (after command)
"edit [SERVER] [SERVICE]",
"Edit dropshell, server or service configuration",
// heredoc
R"(
Edit dropshell, server or service configuration.
edit edit the dropshell config.
edit <server> edit the server config.
edit <server> <service> edit the service config.
)"
});
}
} edit_command_register;
// ------------------------------------------------------------------------------------------------
// edit command implementation
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// utility function to edit a file
// ------------------------------------------------------------------------------------------------
bool edit_file(const std::string &file_path, bool has_bb64)
{
// make sure parent directory exists.
std::string parent_dir = get_parent(file_path);
std::filesystem::create_directories(parent_dir);
std::string editor_cmd;
const char* editor_env = std::getenv("EDITOR");
if (editor_env && std::strlen(editor_env) > 0) {
editor_cmd = std::string(editor_env) + " " + quote(file_path);
} else if (isatty(STDIN_FILENO)) {
// Check if stdin is connected to a terminal if EDITOR is not set
editor_cmd = "nano -w " + quote(file_path);
} else {
std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
std::cerr << "You can manually edit the file at: " << file_path << std::endl;
return false;
}
std::cout << "Editing file: " << file_path << std::endl;
if (has_bb64) {
return execute_local_command(editor_cmd, nullptr, cMode::Interactive);
}
else {
// might not have bb64 at this early stage. Direct edit.
int ret = system(editor_cmd.c_str());
return EXITSTATUSCHECK(ret);
}
}
// ------------------------------------------------------------------------------------------------
// edit config
// ------------------------------------------------------------------------------------------------
int edit_config()
{
if (!gConfig().is_config_set())
gConfig().save_config(false); // save defaults.
std::string config_file = localfile::dropshell_json();
if (!edit_file(config_file, false) || !std::filesystem::exists(config_file))
return die("Error: Failed to edit config file.");
gConfig().load_config();
if (!gConfig().is_config_set())
return die("Error: Failed to load and parse edited config file!");
gConfig().save_config(true);
std::cout << "Successfully edited config file at " << config_file << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit server
// ------------------------------------------------------------------------------------------------
int edit_server(const std::string &server_name)
{
std::string serverpath = localpath::server(server_name);
if (serverpath.empty()) {
std::cerr << "Error: Server not found: " << server_name << std::endl;
return -1;
}
std::ostringstream aftertext;
aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n"
<< "You can ssh in to the remote server with: dropshell ssh "<<server_name<<"\n"
<< "Once moved, reinstall all services with: dropshell install " << server_name;
std::string config_file = serverpath + "/server.env";
if (!edit_file(config_file, true)) {
std::cerr << "Error: Failed to edit server.env" << std::endl;
std::cerr << "You can manually edit this file at: " << config_file << std::endl;
std::cerr << "After editing, " << aftertext.str() << std::endl;
}
else
std::cout << aftertext.str() << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit service config
// ------------------------------------------------------------------------------------------------
int edit_service_config(const std::string &server, const std::string &service)
{
std::string config_file = localfile::service_env(server, service);
if (!std::filesystem::exists(config_file))
{
std::cerr << "Error: Service config file not found: " << config_file << std::endl;
return 1;
}
if (edit_file(config_file, true) && std::filesystem::exists(config_file))
std::cout << "To apply your changes, run:\n dropshell install " + server + " " + service << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// edit command handler
// ------------------------------------------------------------------------------------------------
int edit_handler(const CommandContext& ctx) {
// edit dropshell config
if (ctx.args.size() < 1)
return edit_config();
// edit server config
if (ctx.args.size() < 2) {
edit_server(safearg(ctx.args,0));
return 0;
}
// edit service config
if (ctx.args.size() < 3) {
edit_service_config(safearg(ctx.args,0), safearg(ctx.args,1));
return 0;
}
std::cout << "Edit handler called with " << ctx.args.size() << " args\n";
return -1;
}
} // namespace dropshell

138
src/commands/health.cpp Normal file
View File

@ -0,0 +1,138 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "server_env_manager.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "transwarp.hpp"
namespace dropshell
{
int health_handler(const CommandContext &ctx);
static std::vector<std::string> health_name_list = {"health", "check", "healthcheck", "status"};
// Static registration
struct HealthCommandRegister
{
HealthCommandRegister()
{
CommandRegistry::instance().register_command({health_name_list,
health_handler,
std_autocomplete_allowallservices,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
2, // max_args (after command)
"health SERVER",
"Check the health of a server.",
R"(
health <server>
)"});
}
} health_command_register;
// ------------------------------------------------------------------------------------------------
// health command implementation
HealthStatus is_healthy(const std::string &server, const std::string &service)
{
server_env_manager env(server);
if (!env.is_valid())
{
std::cerr << "Error: Server service not initialized" << std::endl;
return HealthStatus::ERROR;
}
if (!env.check_remote_dir_exists(remotepath::service(server, service)))
{
return HealthStatus::NOTINSTALLED;
}
std::string script_path = remotepath::service_template(server, service) + "/status.sh";
if (!env.check_remote_file_exists(script_path))
{
return HealthStatus::UNKNOWN;
}
// Run status script, does not display output.
if (!env.run_remote_template_command(service, "status", {}, true, {}))
return HealthStatus::UNHEALTHY;
return HealthStatus::HEALTHY;
}
std::string healthtick(const std::string &server, const std::string &service)
{
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
std::string yellow_exclamation = "\033[33m!\033[0m";
std::string unknown = "\033[37m✓\033[0m";
HealthStatus status = is_healthy(server, service);
if (status == HealthStatus::HEALTHY)
return green_tick;
else if (status == HealthStatus::UNHEALTHY)
return red_cross;
else if (status == HealthStatus::UNKNOWN)
return unknown;
else
return yellow_exclamation;
}
std::string HealthStatus2String(HealthStatus status)
{
if (status == HealthStatus::HEALTHY)
return ":tick:";
else if (status == HealthStatus::UNHEALTHY)
return ":cross:";
else if (status == HealthStatus::UNKNOWN)
return ":greytick:";
else if (status == HealthStatus::NOTINSTALLED)
return ":warning:";
else
return ":error:";
}
std::string healthmark(const std::string &server, const std::string &service)
{
HealthStatus status = is_healthy(server, service);
return HealthStatus2String(status);
}
// ------------------------------------------------------------------------------------------------
// health command implementation
// ------------------------------------------------------------------------------------------------
int health_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{
std::cerr << "Error: Server name is required" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
if (ctx.args.size() == 1) {
// get all services on server
std::vector<LocalServiceInfo> services = get_server_services_info(server);
transwarp::parallel exec{services.size()};
auto task = transwarp::for_each(exec, services.begin(), services.end(), [&](const LocalServiceInfo& service) {
std::string status = healthtick(server, service.service_name);
std::cout << status << " " << service.service_name << " (" << service.template_name << ")" << std::endl << std::flush;
});
task->wait();
return 0;
} else {
// get service status
std::string service = safearg(ctx.args, 1);
LocalServiceInfo service_info = get_service_info(server, service);
std::cout << healthtick(server, service) << " " << service << " (" << service_info.template_name << ")" << std::endl << std::flush;
}
return 0;
}
} // namespace dropshell

178
src/commands/help.cpp Normal file
View File

@ -0,0 +1,178 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "version.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/assert.hpp"
namespace dropshell {
void help_autocomplete(const CommandContext& ctx);
int help_handler(const CommandContext& ctx);
static std::vector<std::string> help_name_list={"help","h","--help","-h"};
// Static registration
struct HelpCommandRegister {
HelpCommandRegister() {
CommandRegistry::instance().register_command({
help_name_list,
help_handler,
help_autocomplete,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
1, // max_args (after command)
"help [COMMAND]",
"Show help for dropshell, or detailed help for a specific command.",
// heredoc
R"(
Show help for dropshell, or detailed help for a specific command.
If you want to see documentation, please visit the DropShell website:
https://dropshell.app
)"
});
}
} help_command_register;
void help_autocomplete(const CommandContext& ctx) {
if (ctx.args.size() == 1) {
// list all commands
for (const auto& cmd : CommandRegistry::instance().list_primary_commands(false)) {
std::cout << cmd << std::endl;
}
}
return;
}
void show_command(const std::string& cmd) {
const auto& cmd_info = CommandRegistry::instance().find_command(cmd);
if (!cmd_info)
std::cout << "Unknown command: " << cmd << std::endl;
std::cout << " ";
print_left_aligned(cmd_info->help_usage, 30);
std::cout << cmd_info->help_description << std::endl;
}
extern const std::string VERSION;
extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
int show_command_help(const std::string& cmd) {
const auto& cmd_info = CommandRegistry::instance().find_command(cmd);
if (!cmd_info)
{
std::cout << "Unknown command: " << cmd << std::endl;
return 1;
}
std::cout << std::endl;
std::cout << "Usage: " << std::endl;
std::cout << " ";
print_left_aligned(cmd_info->help_usage, 30);
std::cout << cmd_info->help_description << std::endl;
std::cout << std::endl;
std::cout << " Equivalent names: ";
bool first = true;
for (const auto& name : cmd_info->names) {
if (!first) std::cout << ", ";
std::cout << name;
first = false;
}
std::cout << std::endl << std::endl;
std::cout << cmd_info->full_help << std::endl << std::endl;
return 0;
}
int help_handler(const CommandContext& ctx) {
if (ctx.args.size() > 0)
return show_command_help(ctx.args[0]);
std::cout << std::endl;
maketitle("DropShell version " + VERSION);
std::cout << std::endl;
std::cout << "A tool for managing remote servers, by " << AUTHOR << std::endl;
std::cout << std::endl;
std::cout << "dropshell ..." << std::endl;
show_command("help");
show_command("edit");
if (gConfig().is_config_set())
{
// show more!
show_command("list");
}
return 0;
}
// void show_command(const std::string& cmd) {
// const auto& cmd_info = CommandRegistry::instance().find_command(cmd);
// if (cmd_info) {
// std::cout << " " << cmd_info->help_usage
// << std::string(' ', std::min(1,(int)(30-cmd_info->help_usage.length())))
// << cmd_info->help_description << std::endl;
// }
// }
// bool print_help() {
// std::cout << std::endl;
// maketitle("DropShell version " + VERSION);
// std::cout << std::endl;
// std::cout << "A tool for managing server configurations" << std::endl;
// std::cout << std::endl;
// std::cout << "dropshell ..." << std::endl;
// show_command("help");
// show_command("edit");
// if (gConfig().is_config_set()) {
// std::cout << " templates List all available templates" << std::endl;
// std::cout << std::endl;
// std::cout << std::endl;
// std::cout << "Service commands: (if no service is specified, all services for the server are affected)" << std::endl;
// std::cout << " list [SERVER] [SERVICE] List status/details of all servers/server/service." << std::endl;
// std::cout << " edit [SERVER] [SERVICE] Edit the configuration of dropshell/server/service." << std::endl;
// std::cout << std::endl;
// std::cout << " install SERVER [SERVICE] Install/reinstall/update service(s). Safe/non-destructive." << std::endl;
// std::cout << " uninstall SERVER [SERVICE] Uninstalls the service on the remote server. Leaves data intact." << std::endl;
// std::cout << " nuke SERVER SERVICE Nuke the service, deleting ALL local and remote data." << std::endl;
// std::cout << std::endl;
// std::cout << " COMMAND SERVER [SERVICE] Run a command on service(s), e.g." << std::endl;
// std::cout << " backup, restore, start, stop, logs" << std::endl;
// std::cout << std::endl;
// std::cout << " ssh SERVER SERVICE Launch an interactive shell on a server or service" << std::endl;
// std::cout << std::endl;
// std::cout << "Creation commands: (apply to the first local config directory)"<<std::endl;
// std::cout << " create-template TEMPLATE" << std::endl;
// std::cout << " create-server SERVER" << std::endl;
// std::cout << " create-service SERVER TEMPLATE SERVICE" << std::endl;
// }
// else {
// show_command("help");
// show_command("edit");
// std::cout << std::endl;
// std::cout << "Other commands available once initialised." << std::endl;
// }
// return true;
// }
} // namespace dropshell

369
src/commands/install.cpp Normal file
View File

@ -0,0 +1,369 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "templates.hpp"
#include "shared_commands.hpp"
#include "utils/hash.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/assert.hpp"
namespace dropshell
{
int install_handler(const CommandContext &ctx);
static std::vector<std::string> install_name_list = {"install","reinstall","update"};
// Static registration
struct InstallCommandRegister
{
InstallCommandRegister()
{
CommandRegistry::instance().register_command({install_name_list,
install_handler,
std_autocomplete_allowallservices,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
2, // max_args (after command)
"install SERVER [SERVICE]",
"Install/reinstall service(s). Safe/non-destructive way to update.",
// heredoc
R"(
Install components on a server. This is safe to re-run (non-destructive) and used to update
servers and their services.
install (re)install dropshell components on this computer.
install SERVER (re)install dropshell agent on the given server.
install SERVER [SERVICE|*] (re)install the given service (or all services) on the given server.
Note you need to create the service first with:
dropshell create-service <server> <template> <service>
)"});
}
} install_command_register;
// ------------------------------------------------------------------------------------------------
// rsync_tree_to_remote : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool rsync_tree_to_remote(
const std::string &local_path,
const std::string &remote_path,
server_env_manager &server_env,
bool silent)
{
ASSERT(!local_path.empty() && !remote_path.empty(), "Local or remote path not specified. Can't rsync.");
std::string rsync_cmd = "rsync --delete --mkpath -zrpc -e 'ssh -p " + server_env.get_SSH_PORT() + "' " +
quote(local_path + "/") + " " +
quote(server_env.get_SSH_USER() + "@" + server_env.get_SSH_HOST() + ":" +
remote_path + "/");
return execute_local_command(rsync_cmd, nullptr, (silent ? cMode::Silent : cMode::None));
}
// ------------------------------------------------------------------------------------------------
// install service over ssh : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool install_service(const std::string &server, const std::string &service, bool silent)
{
LocalServiceInfo service_info = get_service_info(server, service);
if (!SIvalid(service_info))
return false;
server_env_manager server_env(server);
if (!server_env.is_valid())
return false;
maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server);
if (!server_env.is_valid())
return false; // should never hit this.
// Check if template exists
template_info tinfo = gTemplateManager().get_template_info(service_info.template_name);
if (!tinfo.is_set())
return false;
// Create service directory
std::string remote_service_path = remotepath::service(server, service);
std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path);
if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", mkdir_cmd, {}), cMode::Silent))
{
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
return false;
}
// Check if rsync is installed on remote host
std::string check_rsync_cmd = "which rsync";
if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", check_rsync_cmd, {}), cMode::Silent))
{
std::cerr << "rsync is not installed on the remote host" << std::endl;
return false;
}
// Copy template files
std::cout << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl
<< std::string(8, ' ') << "[REMOTE] " << remotepath::service_template(server, service) << "/" << std::endl;
if (!rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath::service_template(server, service),
server_env, silent))
{
std::cerr << "Failed to copy template files using rsync" << std::endl;
return false;
}
// Copy service files
std::cout << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl
<< std::string(8, ' ') << "[REMOTE] " << remotepath::service_config(server, service) << std::endl;
if (!rsync_tree_to_remote(localpath::service(server, service), remotepath::service_config(server, service),
server_env, silent))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
// Run install script
{
server_env.run_remote_template_command(service, "install", {}, silent, {});
}
// print health tick
std::cout << "Health: " << healthtick(server, service) << std::endl;
return true;
}
// ------------------------------------------------------------------------------------------------
// get_arch : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
std::string get_arch()
{
// determine the architecture of the system
std::string arch;
#ifdef __aarch64__
arch = "arm64";
#elif __x86_64__
arch = "amd64";
#endif
return arch;
}
// ------------------------------------------------------------------------------------------------
// update_dropshell
// ------------------------------------------------------------------------------------------------
std::string _exec(const char* cmd) {
char buffer[128];
std::string result = "";
FILE* pipe = popen(cmd, "r");
if (!pipe) {
throw std::runtime_error("popen() failed!");
}
while (!feof(pipe)) {
if (fgets(buffer, 128, pipe) != nullptr)
result += buffer;
}
pclose(pipe);
return trim(result);
}
int update_dropshell()
{
// determine path to this executable
std::filesystem::path dropshell_path = std::filesystem::canonical("/proc/self/exe");
std::filesystem::path parent_path = dropshell_path.parent_path();
// determine the architecture of the system
std::string arch = get_arch();
std::string url = "https://gitea.jde.nz/public/dropshell/releases/download/latest/dropshell." + arch;
// download new version, preserve permissions and ownership
std::string bash_script;
bash_script += "docker run --rm -v " + parent_path.string() + ":/target";
bash_script += " gitea.jde.nz/public/debian-curl:latest";
bash_script += " sh -c \"";
bash_script += " curl -fsSL " + url + " -o /target/dropshell_temp &&";
bash_script += " chmod --reference=/target/dropshell /target/dropshell_temp &&";
bash_script += " chown --reference=/target/dropshell /target/dropshell_temp";
bash_script += "\"";
std::string cmd = "bash -c '" + bash_script + "'";
int rval = system(cmd.c_str());
if (rval != 0)
{
std::cerr << "Failed to download new version of dropshell." << std::endl;
return -1;
}
// check if the new version is the same as the old version
uint64_t new_hash = hash_file(parent_path / "dropshell_temp");
uint64_t old_hash = hash_file(parent_path / "dropshell");
if (new_hash == old_hash)
{
std::cout << "Confirmed dropshell is the latest version." << std::endl;
return 0;
}
std::string runvercmd = (parent_path / "dropshell").string() + " version";
std::string currentver = _exec(runvercmd.c_str());
runvercmd = (parent_path / "dropshell_temp").string() + " version";
std::string newver = _exec(runvercmd.c_str());
if (currentver >= newver)
{
std::cout << "Current dropshell version: " << currentver << ", published version: " << newver << std::endl;
std::cout << "No update needed." << std::endl;
return 0;
}
return 0;
std::string bash_script_2 = "docker run --rm -v " + parent_path.string() + ":/target gitea.jde.nz/public/debian-curl:latest " +
"sh -c \"mv /target/dropshell_temp /target/dropshell\"";
rval = system(bash_script_2.c_str());
if (rval != 0)
{
std::cerr << "Failed to install new version of dropshell." << std::endl;
return -1;
}
std::cout << "Successfully updated " << dropshell_path << " to the latest " << arch << " version." << std::endl;
// execute the new version
execlp("bash", "bash", "-c", (parent_path / "dropshell").c_str(), "install", (char *)nullptr);
std::cerr << "Failed to execute new version of dropshell." << std::endl;
return -1;
}
int install_local_agent()
{
std::vector<std::filesystem::path> paths = {
gConfig().get_local_template_cache_path(),
gConfig().get_local_backup_path(),
gConfig().get_local_tempfiles_path(),
localpath::agent()
};
for (auto &p : gConfig().get_local_server_definition_paths())
paths.push_back(p);
for (auto &p : paths)
if (!std::filesystem::exists(p))
{
std::cout << "Creating directory: " << p << std::endl;
std::filesystem::create_directories(p);
}
// download bb64.
if (!std::filesystem::exists(localpath::agent()+"bb64"))
{
std::string cmd = "cd " + localpath::agent() + " && curl -fsSL -o bb64 https://gitea.jde.nz/public/bb64/releases/download/latest/bb64.amd64 && chmod a+x bb64";
int ret = system(cmd.c_str());
if (EXITSTATUSCHECK(ret))
std::cout << "Downloaded bb64 to " << localpath::agent() << std::endl;
else
std::cerr << "Failed to download bb64 to " << localpath::agent() << std::endl;
} else {
std::cout << "Updating bb64..." << std::endl;
system((localpath::agent()+"bb64 -u").c_str()); // update.
}
return 0;
}
int install_host()
{
// update dropshell.
// install the local dropshell agent.
int rval = update_dropshell();
if (rval != 0)
return rval;
rval = install_local_agent();
if (rval != 0)
return rval;
std::cout << "Installation complete." << std::endl;
return 0;
}
int install_server(const std::string &server)
{
// install the dropshell agent on the given server.
std::cout << "Installing dropshell agent on " << server << std::endl;
std::string agent_path = remotepath::agent(server);
if (agent_path.empty())
{
std::cerr << "Failed to get agent path for " << server << std::endl;
return 1;
}
// first install bb64.
std::string remote_cmd =
"curl -fsSL https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh | bash -s -- " + agent_path;
server_env_manager server_env(server);
if (!server_env.is_valid()) {
std::cerr << "Invalid server environment for " << server << std::endl;
return 1;
}
std::string fullcmd = "ssh -p " + server_env.get_SSH_INFO().port + " " + server_env.get_SSH_INFO().user + "@" + server_env.get_SSH_INFO().host;
fullcmd += " bash -c '" + remote_cmd + "'";
std::cout << "Executing: " << fullcmd << std::endl;
if (!execute_local_command(fullcmd, nullptr, cMode::Silent)) {
std::cerr << "Failed to download bb64 to " << agent_path << " on remote server." << std::endl;
} else {
std::cout << "Downloaded bb64 to " << agent_path << " on remote server." << std::endl;
}
return 0; // NOTIMPL
}
// ------------------------------------------------------------------------------------------------
// install command implementation
// ------------------------------------------------------------------------------------------------
int install_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{ // install host
return install_host();
}
std::string server = safearg(ctx.args, 0);
if (ctx.args.size() == 1)
{ // install server
return install_server(server);
}
// install service(s)
if (safearg(ctx.args, 1) == "*")
{
// install all services on the server
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &service : services)
{
if (!install_service(server, service.service_name, false))
okay = false;
}
return okay ? 0 : 1;
}
else
{ // install the specific service.
std::string service = safearg(ctx.args, 1);
return install_service(server, service, false) ? 0 : 1;
}
}
} // namespace dropshell

184
src/commands/list.cpp Normal file
View File

@ -0,0 +1,184 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "servers.hpp"
#include "tableprint.hpp"
#include "transwarp.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/assert.hpp"
namespace dropshell {
int list_handler(const CommandContext& ctx);
void show_server_details(const std::string& server_name);
void list_servers();
static std::vector<std::string> list_name_list={"list","ls","info","-l"};
// Static registration
struct ListCommandRegister {
ListCommandRegister() {
CommandRegistry::instance().register_command({
list_name_list,
list_handler,
std_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
0, // min_args (after command)
2, // max_args (after command)
"list [SERVER] [SERVICE]",
"List server or service information and status",
// heredoc
R"(
List details for servers and services controller by dropshell.
list list all servers.
list server list all services for the given server.
list server service list the given service details on the given server.
)"
});
}
} list_command_register;
// ------------------------------------------------------------------------------------------------
// list command handler
// ------------------------------------------------------------------------------------------------
int list_handler(const CommandContext& ctx) {
if (ctx.args.size() == 0) {
list_servers();
return 0;
}
if (ctx.args.size() == 1) {
show_server_details(ctx.args[0]);
return 0;
}
std::cout << "List handler called with " << ctx.args.size() << " args\n";
return 0;
}
// https://github.com/bloomen/transwarp?tab=readme-ov-file#range-functions
void list_servers() {
auto servers = get_configured_servers();
if (servers.empty()) {
std::cout << "No servers found" << std::endl;
std::cout << "Please run 'dropshell edit' to set up dropshell." << std::endl;
std::cout << "Then run 'dropshell create-server' to create a server." << std::endl;
return;
}
tableprint tp("All DropShell Servers");
tp.add_row({"Name", "User", "Address", "Health", "Ports"});
std::cout << "Checking "<<servers.size() << " servers: " << std::flush;
int checked = 0;
transwarp::parallel exec{servers.size()};
auto task = transwarp::for_each(exec, servers.begin(), servers.end(), [&](const ServerInfo& server) {
std::map<std::string, ServiceStatus> status = service_runner::get_all_services_status(server.name);
std::set<int> ports_used;
std::string serviceticks = "";
for (const auto& [service_name, service_status] : status) {
ports_used.insert(service_status.ports.begin(), service_status.ports.end());
serviceticks += HealthStatus2String(service_status.health) + " ";
}
std::string ports_used_str = "";
for (const auto& port : ports_used)
ports_used_str += std::to_string(port) + " ";
tp.add_row({server.name, server.ssh_user, server.ssh_host, serviceticks, ports_used_str});
++checked;
// print out a tick character for each server checked.
std::cout << checked << "" << std::flush;
});
task->wait();
std::cout << std::endl << std::endl;
tp.print();
}
void show_server_details(const std::string& server_name) {
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
return;
}
//---------------------
// Check if server is reachable via SSH
std::string ssh_address = env.get_SSH_HOST();
std::string ssh_user = env.get_SSH_USER();
std::string ssh_port = env.get_SSH_PORT();
if (!ssh_address.empty()) {
std::cout << std::endl << "Server Status:" << std::endl;
std::cout << std::string(40, '-') << std::endl;
// Try to connect to the server
std::string cmd = "ssh -o ConnectTimeout=5 " + ssh_user + "@" + ssh_address + " -p " + ssh_port + " 'echo connected' 2>/dev/null";
int result = system(cmd.c_str());
if (result == 0) {
std::cout << "Status: Online" << std::endl;
// // Get uptime if possible
// cmd = "ssh " + ssh_address + " 'uptime' 2>/dev/null";
// int rval = system(cmd.c_str());
// if (rval != 0) {
// std::cout << "Error: Failed to get uptime" << std::endl;
// }
} else {
std::cout << "Status: Offline" << std::endl;
}
}
std::cout << std::endl;
//---------------------
{
std::cout << std::endl;
tableprint tp("Server Configuration: " + server_name, true);
tp.add_row({"Key", "Value"});
for (const auto& [key, value] : env.get_variables()) {
tp.add_row({key, value});
}
tp.print();
}
//---------------------
// list services, and run healthcheck on each
{
tableprint tp("Services: " + server_name, false);
tp.add_row({"Status", "Service", "Ports"});
std::map<std::string, ServiceStatus> status = service_runner::get_all_services_status(server_name);
std::set<int> ports_used;
std::string serviceticks = "";
for (const auto& [service_name, service_status] : status) {
std::string healthy = HealthStatus2String(service_status.health);
std::string ports_str = "";
for (const auto& port : service_status.ports)
ports_str += std::to_string(port) + " ";
tp.add_row({healthy, service_name, ports_str});
} // end of for (const auto& service : services)
tp.print();
} // end of list services
} // end of show_server_details
} // namespace dropshell

106
src/commands/nuke.cpp Normal file
View File

@ -0,0 +1,106 @@
#include "command_registry.hpp"
#include "shared_commands.hpp"
#include "config.hpp"
#include "services.hpp"
#include "server_env_manager.hpp"
#include "utils/directories.hpp"
#include "servers.hpp"
#include "templates.hpp"
#include "utils/assert.hpp"
#pragma message ("TODO: Fix issues with Nuke below.")
/*
j@twelve:~/code/dropshell$ ds nuke localhost test-squashkiwi
---------------------------------------
| Nuking test-squashkiwi on localhost |
---------------------------------------
---------------------------------------------
| Uninstalling test-squashkiwi on localhost |
---------------------------------------------
Service is not installed: test-squashkiwi
bash: line 1: cd: /home/dropshell/dropshell_deploy/services/test-squashkiwi/template: Permission denied
bash: line 1: /home/dropshell/dropshell_deploy/services/test-squashkiwi/template/nuke.sh: Permission denied
Warning: Failed to run nuke script: test-squashkiwi
*/
namespace dropshell {
int nuke_handler(const CommandContext& ctx);
static std::vector<std::string> nuke_name_list={"nuke"};
// Static registration
struct NukeCommandRegister {
NukeCommandRegister() {
CommandRegistry::instance().register_command({
nuke_name_list,
nuke_handler,
std_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
2, // min_args (after command)
2, // max_args (after command)
"nuke SERVER [SERVICE|*] ",
"Nuke a service on a server. Destroys everything, both local and remote!",
// heredoc
R"(
Nuke a service on a server. Destroys everything, both local and remote!
nuke SERVER SERVICE nuke the given service on the given server.
nuke SERVER * nuke all services on the given server.
)"
});
}
} nuke_command_register;
int nuke_handler(const CommandContext &ctx)
{
ASSERT(ctx.args.size() > 1, "Usage: nuke <server> <service>");
ASSERT(gConfig().is_config_set(), "No configuration found. Please run 'dropshell config' to set up your configuration.");
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
maketitle("Nuking " + service + " on " + server);
server_env_manager server_env(server);
LocalServiceInfo service_info;
if (server_env.is_valid())
{
service_info = get_service_info(server, service);
if (!SIvalid(service_info))
std::cerr << "Warning: Invalid service: " << service << std::endl;
if (!uninstall_service(server, service, false))
std::cerr << "Warning: Failed to uninstall service: " << service << std::endl;
// run the nuke script on the remote server if it exists.
if (gTemplateManager().template_command_exists(service_info.template_name, "nuke"))
{
if (!server_env.run_remote_template_command(service, "nuke", {}, false, {}))
std::cerr << "Warning: Failed to run nuke script: " << service << std::endl;
}
}
else
std::cerr << "Warning: Invalid server: " << server << std::endl;
// remove the local service directory
std::string local_service_path = service_info.local_service_path;
if (local_service_path.empty() || !std::filesystem::exists(local_service_path))
{
std::cerr << "Warning: Local service directory not found: " << local_service_path << std::endl;
return 1;
}
auto ret = std::filesystem::remove_all(local_service_path);
if (ret != 0)
std::cerr << "Warning: Failed to remove local service directory" << std::endl;
return ret == 0 ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,31 @@
#ifndef SHARED_COMMANDS_HPP
#define SHARED_COMMANDS_HPP
#include "servers.hpp"
#include "command_registry.hpp"
namespace dropshell {
// defined in install.cpp
bool rsync_tree_to_remote(
const std::string &local_path,
const std::string &remote_path,
server_env_manager &server_env,
bool silent);
// defined in install.cpp
bool install_service(const std::string& server, const std::string& service, bool silent);
bool uninstall_service(const std::string& server, const std::string& service, bool silent);
std::string get_arch();
// defined in health.cpp
std::string healthtick(const std::string& server, const std::string& service);
std::string HealthStatus2String(HealthStatus status);
// defined in standard_autocomplete.cpp
void std_autocomplete(const CommandContext& ctx);
void std_autocomplete_allowallservices(const CommandContext& ctx);
} // namespace dropshell
#endif

View File

@ -0,0 +1,36 @@
#include "shared_commands.hpp"
#include "command_registry.hpp"
#include "servers.hpp"
#include "services.hpp"
#include "utils/assert.hpp"
namespace dropshell {
void std_autocomplete(const CommandContext &ctx)
{
if (ctx.args.size() == 0) { // just the command, no args yet.
// list servers
std::vector<ServerInfo> servers = get_configured_servers();
for (const auto& server : servers) {
std::cout << server.name << std::endl;
}
}
else if (ctx.args.size() == 1) {
// list services
std::vector<LocalServiceInfo> services = get_server_services_info(ctx.args[0]);
for (const auto& service : services) {
std::cout << service.service_name << std::endl;
}
}
}
void std_autocomplete_allowallservices(const CommandContext &ctx)
{
std_autocomplete(ctx);
if (ctx.args.size() == 1)
std::cout << "*" << std::endl;
}
} // namespace dropshell

108
src/commands/uninstall.cpp Normal file
View File

@ -0,0 +1,108 @@
#include "command_registry.hpp"
#include "directories.hpp"
#include "shared_commands.hpp"
#include "templates.hpp"
#include "utils/assert.hpp"
namespace dropshell
{
int uninstall_handler(const CommandContext &ctx);
static std::vector<std::string> uninstall_name_list = {"uninstall", "remove"};
// Static registration
struct UninstallCommandRegister
{
UninstallCommandRegister()
{
CommandRegistry::instance().register_command({uninstall_name_list,
uninstall_handler,
std_autocomplete_allowallservices,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
2, // max_args (after command)
"uninstall SERVER [SERVICE|*]",
"Uninstall a service on a server. Does not remove configuration or user data.",
// heredoc
R"(
Uninstall a service on a server. Does not remove configuration or user data.
uninstall SERVER SERVICE uninstall the given service on the given server.
uninstall SERVER * uninstall all services on the given server.
)"});
}
} uninstall_command_register;
int uninstall_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{
std::cerr << "Error: uninstall requires a server and (optionally) a service" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
if (ctx.args.size() == 1)
{
// uninstall all services on the server
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &service : services)
{
if (!uninstall_service(server, service.service_name, false))
okay = false;
}
return okay ? 0 : 1;
}
std::string service = safearg(ctx.args, 1);
return uninstall_service(server, service, false) ? 0 : 1;
}
bool uninstall_service(const std::string &server, const std::string &service, bool silent)
{
if (!silent)
maketitle("Uninstalling " + service + " on " + server);
server_env_manager server_env(server);
if (!server_env.is_valid())
{
std::cerr << "Invalid server: " << server << std::endl;
return false; // should never hit this.
}
// 2. Check if service directory exists on server
if (!server_env.check_remote_dir_exists(remotepath::service(server, service)))
{
std::cerr << "Service is not installed: " << service << std::endl;
return true; // Nothing to uninstall
}
// 3. Run uninstall script if it exists
std::string uninstall_script = remotepath::service_template(server, service) + "/uninstall.sh";
if (gTemplateManager().template_command_exists(service, "uninstall"))
if (server_env.check_remote_file_exists(uninstall_script))
if (!server_env.run_remote_template_command(service, "uninstall", {}, silent, {}))
if (!silent)
std::cerr << "Warning: Uninstall script failed, but continuing with directory removal" << std::endl;
// 4. Remove the service directory from the server, running in a docker container as root.
if (server_env.remove_remote_dir(remotepath::service(server, service), silent))
{
ASSERT(!server_env.check_remote_dir_exists(remotepath::service(server, service)), "Service directory still found on server after uninstall");
if (!silent)
std::cout << "Removed remote service directory " << remotepath::service(server, service) << std::endl;
}
else if (!silent)
std::cerr << "Warning: Failed to remove remote service directory" << std::endl;
if (!silent)
std::cout << "Completed service " << service << " uninstall on " << server << std::endl;
return true;
}
} // namespace dropshell

45
src/commands/version.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "command_registry.hpp"
#include "version.hpp"
namespace dropshell {
int version_handler(const CommandContext &ctx);
static std::vector<std::string> version_name_list = {"version","v","ver","-v","-ver","--version"};
void version_autocomplete(const CommandContext &ctx)
{
}
// Static registration
struct VersionCommandRegister
{
VersionCommandRegister()
{
CommandRegistry::instance().register_command({version_name_list,
version_handler,
version_autocomplete,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
0, // max_args (after command)
"version",
"Uninstall a service on a server. Does not remove configuration or user data.",
// heredoc
R"(
Uninstall a service on a server. Does not remove configuration or user data.
uninstall <server> <service> uninstall the given service on the given server.
uninstall <server> uninstall all services on the given server.
)"});
}
} version_command_register;
int version_handler(const CommandContext &ctx)
{
std::cout << VERSION << std::endl;
return 0;
}
} // namespace dropshell

View File

@ -5,7 +5,7 @@
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include <filesystem>
#include "utils/execute.hpp"
namespace dropshell {
@ -61,6 +61,7 @@ bool config::save_config(bool create_aux_directories)
std::string dropshell_base = homedir + "/.dropshell";
mConfig["tempfiles"] = dropshell_base + "/tmp";
mConfig["backups"] = dropshell_base + "/backups";
mConfig["template_cache"] = dropshell_base + "/template_cache";
mConfig["template_registry_URLs"] = {
"https://templates.dropshell.app"
@ -68,6 +69,7 @@ bool config::save_config(bool create_aux_directories)
mConfig["template_local_paths"] = {
dropshell_base + "/local_templates"
};
mConfig["server_definition_paths"] = {
dropshell_base + "/servers"
};
@ -94,16 +96,20 @@ bool config::save_config(bool create_aux_directories)
std::filesystem::create_directories(p);
}
}
return true;
}
bool config::is_config_set() const
{
return mIsConfigSet;
}
bool config::is_agent_installed()
{
return std::filesystem::exists(localpath::agent() + "/bb64");
}
std::string config::get_local_tempfiles_path() {
return mConfig["tempfiles"];
}

View File

@ -15,12 +15,11 @@ class config {
bool save_config(bool create_aux_directories);
bool is_config_set() const;
static bool is_agent_installed();
std::string get_local_tempfiles_path();
std::string get_local_backup_path();
std::string get_local_template_cache_path();
std::vector<std::string> get_template_registry_urls();
std::vector<std::string> get_template_local_paths();
std::vector<std::string> get_local_server_definition_paths();

View File

@ -1,282 +0,0 @@
/*
base64.cpp and base64.h
base64 encoding and decoding with C++.
More information at
https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp
Version: 2.rc.09 (release candidate)
Copyright (C) 2004-2017, 2020-2022 René Nyffenegger
This source code is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this source code must not be misrepresented; you must not
claim that you wrote the original source code. If you use this source code
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original source code.
3. This notice may not be removed or altered from any source distribution.
René Nyffenegger rene.nyffenegger@adp-gmbh.ch
*/
#include "contrib/base64.hpp"
#include <algorithm>
#include <stdexcept>
//
// Depending on the url parameter in base64_chars, one of
// two sets of base64 characters needs to be chosen.
// They differ in their last two characters.
//
static const char* base64_chars[2] = {
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"+/",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"-_"};
static unsigned int pos_of_char(const unsigned char chr) {
//
// Return the position of chr within base64_encode()
//
if (chr >= 'A' && chr <= 'Z') return chr - 'A';
else if (chr >= 'a' && chr <= 'z') return chr - 'a' + ('Z' - 'A') + 1;
else if (chr >= '0' && chr <= '9') return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2;
else if (chr == '+' || chr == '-') return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters (
else if (chr == '/' || chr == '_') return 63; // Ditto for '/' and '_'
else
//
// 2020-10-23: Throw std::exception rather than const char*
//(Pablo Martin-Gomez, https://github.com/Bouska)
//
throw std::runtime_error("Input is not valid base64-encoded data.");
}
static std::string insert_linebreaks(std::string str, size_t distance) {
//
// Provided by https://github.com/JomaCorpFX, adapted by me.
//
if (!str.length()) {
return "";
}
size_t pos = distance;
while (pos < str.size()) {
str.insert(pos, "\n");
pos += distance + 1;
}
return str;
}
template <typename String, unsigned int line_length>
static std::string encode_with_line_breaks(String s) {
return insert_linebreaks(base64_encode(s, false), line_length);
}
template <typename String>
static std::string encode_pem(String s) {
return encode_with_line_breaks<String, 64>(s);
}
template <typename String>
static std::string encode_mime(String s) {
return encode_with_line_breaks<String, 76>(s);
}
template <typename String>
static std::string encode(String s, bool url) {
return base64_encode(reinterpret_cast<const unsigned char*>(s.data()), s.length(), url);
}
std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url) {
size_t len_encoded = (in_len +2) / 3 * 4;
unsigned char trailing_char = url ? '.' : '=';
//
// Choose set of base64 characters. They differ
// for the last two positions, depending on the url
// parameter.
// A bool (as is the parameter url) is guaranteed
// to evaluate to either 0 or 1 in C++ therefore,
// the correct character set is chosen by subscripting
// base64_chars with url.
//
const char* base64_chars_ = base64_chars[url];
std::string ret;
ret.reserve(len_encoded);
unsigned int pos = 0;
while (pos < in_len) {
ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]);
if (pos+1 < in_len) {
ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]);
if (pos+2 < in_len) {
ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]);
ret.push_back(base64_chars_[ bytes_to_encode[pos + 2] & 0x3f]);
}
else {
ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]);
ret.push_back(trailing_char);
}
}
else {
ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]);
ret.push_back(trailing_char);
ret.push_back(trailing_char);
}
pos += 3;
}
return ret;
}
template <typename String>
static std::string decode(String const& encoded_string, bool remove_linebreaks) {
//
// decode(…) is templated so that it can be used with String = const std::string&
// or std::string_view (requires at least C++17)
//
if (encoded_string.empty()) return std::string();
if (remove_linebreaks) {
std::string copy(encoded_string);
copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end());
return base64_decode(copy, false);
}
size_t length_of_string = encoded_string.length();
size_t pos = 0;
//
// The approximate length (bytes) of the decoded string might be one or
// two bytes smaller, depending on the amount of trailing equal signs
// in the encoded string. This approximation is needed to reserve
// enough space in the string to be returned.
//
size_t approx_length_of_decoded_string = length_of_string / 4 * 3;
std::string ret;
ret.reserve(approx_length_of_decoded_string);
while (pos < length_of_string) {
//
// Iterate over encoded input string in chunks. The size of all
// chunks except the last one is 4 bytes.
//
// The last chunk might be padded with equal signs or dots
// in order to make it 4 bytes in size as well, but this
// is not required as per RFC 2045.
//
// All chunks except the last one produce three output bytes.
//
// The last chunk produces at least one and up to three bytes.
//
size_t pos_of_char_1 = pos_of_char(encoded_string.at(pos+1) );
//
// Emit the first output byte that is produced in each chunk:
//
ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char(encoded_string.at(pos+0)) ) << 2 ) + ( (pos_of_char_1 & 0x30 ) >> 4)));
if ( ( pos + 2 < length_of_string ) && // Check for data that is not padded with equal signs (which is allowed by RFC 2045)
encoded_string.at(pos+2) != '=' &&
encoded_string.at(pos+2) != '.' // accept URL-safe base 64 strings, too, so check for '.' also.
)
{
//
// Emit a chunk's second byte (which might not be produced in the last chunk).
//
unsigned int pos_of_char_2 = pos_of_char(encoded_string.at(pos+2) );
ret.push_back(static_cast<std::string::value_type>( (( pos_of_char_1 & 0x0f) << 4) + (( pos_of_char_2 & 0x3c) >> 2)));
if ( ( pos + 3 < length_of_string ) &&
encoded_string.at(pos+3) != '=' &&
encoded_string.at(pos+3) != '.'
)
{
//
// Emit a chunk's third byte (which might not be produced in the last chunk).
//
ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char_2 & 0x03 ) << 6 ) + pos_of_char(encoded_string.at(pos+3)) ));
}
}
pos += 4;
}
return ret;
}
std::string base64_decode(std::string const& s, bool remove_linebreaks) {
return decode(s, remove_linebreaks);
}
std::string base64_encode(std::string const& s, bool url) {
return encode(s, url);
}
std::string base64_encode_pem (std::string const& s) {
return encode_pem(s);
}
std::string base64_encode_mime(std::string const& s) {
return encode_mime(s);
}
#if __cplusplus >= 201703L
//
// Interface with std::string_view rather than const std::string&
// Requires C++17
// Provided by Yannic Bonenberger (https://github.com/Yannic)
//
std::string base64_encode(std::string_view s, bool url) {
return encode(s, url);
}
std::string base64_encode_pem(std::string_view s) {
return encode_pem(s);
}
std::string base64_encode_mime(std::string_view s) {
return encode_mime(s);
}
std::string base64_decode(std::string_view s, bool remove_linebreaks) {
return decode(s, remove_linebreaks);
}
#endif // __cplusplus >= 201703L

View File

@ -1,35 +0,0 @@
//
// base64 encoding and decoding with C++.
// Version: 2.rc.09 (release candidate)
//
#ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A
#define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A
#include <string>
#if __cplusplus >= 201703L
#include <string_view>
#endif // __cplusplus >= 201703L
std::string base64_encode (std::string const& s, bool url = false);
std::string base64_encode_pem (std::string const& s);
std::string base64_encode_mime(std::string const& s);
std::string base64_decode(std::string const& s, bool remove_linebreaks = false);
std::string base64_encode(unsigned char const*, size_t len, bool url = false);
#if __cplusplus >= 201703L
//
// Interface with std::string_view rather than const std::string&
// Requires C++17
// Provided by Yannic Bonenberger (https://github.com/Yannic)
//
std::string base64_encode (std::string_view s, bool url = false);
std::string base64_encode_pem (std::string_view s);
std::string base64_encode_mime(std::string_view s);
std::string base64_decode(std::string_view s, bool remove_linebreaks = false);
#endif // __cplusplus >= 201703L
#endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */

View File

@ -8,6 +8,7 @@
#include "utils/utils.hpp"
#include "autocomplete.hpp"
#include "utils/hash.hpp"
#include "command_registry.hpp"
#include <filesystem>
#include <iostream>
@ -15,8 +16,9 @@
#include <vector>
#include <iomanip>
#include <chrono>
#include <libassert/assert.hpp>
#include <assert.hpp>
#include <sstream>
#include <algorithm>
namespace dropshell {
extern const std::string VERSION;
@ -24,52 +26,75 @@ extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
bool print_help() {
std::cout << std::endl;
maketitle("DropShell version " + VERSION);
std::cout << std::endl;
std::cout << "A tool for managing server configurations" << std::endl;
std::cout << std::endl;
std::cout << "dropshell ..." << std::endl;
std::cout << " help Show this help message" << std::endl;
std::cout << " edit Edit the configuration of dropshell" << std::endl;
if (gConfig().is_config_set()) {
std::cout << " server NAME Show details for specific server" << std::endl;
std::cout << " templates List all available templates" << std::endl;
std::cout << std::endl;
std::cout << std::endl;
std::cout << "Service commands: (if no service is specified, all services for the server are affected)" << std::endl;
std::cout << " list [SERVER] [SERVICE] List status/details of all servers/server/service." << std::endl;
std::cout << " edit [SERVER] [SERVICE] Edit the configuration of dropshell/server/service." << std::endl;
std::cout << std::endl;
std::cout << " install SERVER [SERVICE] Install/reinstall/update service(s). Safe/non-destructive." << std::endl;
std::cout << " uninstall SERVER [SERVICE] Uninstalls the service on the remote server. Leaves data intact." << std::endl;
std::cout << " nuke SERVER SERVICE Nuke the service on the remote server, deleting all remote data." << std::endl;
std::cout << std::endl;
std::cout << " COMMAND SERVER [SERVICE] Run a command on service(s), e.g." << std::endl;
std::cout << " backup, restore, start, stop, logs" << std::endl;
std::cout << std::endl;
std::cout << " ssh SERVER SERVICE Launch an interactive shell on a server or service" << std::endl;
std::cout << std::endl;
std::cout << "Creation commands: (apply to the first local config directory)"<<std::endl;
std::cout << " create-template TEMPLATE" << std::endl;
std::cout << " create-server SERVER" << std::endl;
std::cout << " create-service SERVER TEMPLATE SERVICE" << std::endl;
int main(int argc, char* argv[]) {
try {
// silently attempt to load the config file and templates.
gConfig().load_config();
if (gConfig().is_config_set())
gTemplateManager().load_sources();
// process the command line arguments.
std::vector<std::string> args(argv, argv + argc);
if (args.size() < 2)
args.push_back("help");
ASSERT(args.size() > 1, "No command provided, logic error.");
CommandContext ctx{args[0], args[1], std::vector<std::string>(args.begin() + 2, args.end())};
if (ctx.command == "autocomplete") {
CommandRegistry::instance().autocomplete(ctx);
return 0;
}
const CommandInfo* info = CommandRegistry::instance().find_command(ctx.command);
if (!info) {
std::cerr << "Unknown command: " << ctx.command << std::endl;
return 1;
}
if (info->requires_config && !gConfig().is_config_set()) {
std::cerr << "Valid dropshell configuration required for command: " << ctx.command << std::endl;
std::cerr << "Please run 'dropshell edit' to set up the dropshell configuration." << std::endl;
return 1;
}
if (info->requires_install && !gConfig().is_agent_installed()) {
std::cerr << "Dropshell agent not installed for command: " << ctx.command << std::endl;
std::cerr << "Please run 'dropshell install' to install the local dropshell agent." << std::endl;
return 1;
}
int arg_count = ctx.args.size();
if (arg_count < info->min_args || (info->max_args != -1 && arg_count > info->max_args)) {
std::cerr << "Invalid number of arguments for command: " << ctx.command << std::endl;
std::cerr << "Usage: " << std::endl;
std::cout << " ";
print_left_aligned(info->help_usage,30);
std::cout << info->help_description << std::endl;
return 1;
}
return info->handler(ctx);
}
else {
std::cout << " edit Edit the configuration of dropshell" << std::endl;
std::cout << std::endl;
std::cout << "Other commands available once initialised." << std::endl;
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return true;
}
int die(const std::string & msg) {
std::cerr << msg << std::endl;
return 1;
}
// ------------------------------------------------------------------------------------------------
struct ServerAndServices {
std::string server_name;
@ -90,12 +115,6 @@ bool getCLIServices(const std::string & arg2, const std::string & arg3,
return true;
}
std::string safearg(int argc, char *argv[], int index)
{
if (index >= argc) return "";
return argv[index];
}
void printversion() {
maketitle("DropShell version " + VERSION);
std::cout << "Release date: " << RELEASE_DATE << std::endl;
@ -103,10 +122,32 @@ void printversion() {
std::cout << "License: " << LICENSE << std::endl;
}
#define HAPPYEXIT(CMD, RUNCMD) {if (safearg(argc,argv,1) == CMD) {RUNCMD; return 0;}}
#define BOOLEXIT(CMD, RUNCMD) {if (safearg(argc,argv,1) == CMD) {return (RUNCMD) ? 0 : 1;}}
auto command_match = [](const std::string& cmd_list, int argc, char* argv[]) -> bool {
std::istringstream iss(cmd_list);
std::string cmd_item;
while (iss >> cmd_item) {
if (cmd_item == safearg(argc, argv, 1)) {
return true;
}
}
return false;
};
int main(int argc, char* argv[]) {
#define BOOLEXIT(CMD_LIST, RUNCMD) { \
if (command_match(CMD_LIST, argc, argv)) { \
return (RUNCMD) ? 0 : 1; \
} \
}
#define HAPPYEXIT(CMD_LIST, RUNCMD) { \
if (command_match(CMD_LIST, argc, argv)) { \
RUNCMD; \
return 0; \
} \
}
int old_main(int argc, char* argv[]) {
HAPPYEXIT("hash", hash_demo_raw(safearg(argc,argv,2)))
HAPPYEXIT("version", printversion())
BOOLEXIT("test-template", gTemplateManager().test_template(safearg(argc,argv,2)))
@ -115,46 +156,18 @@ int main(int argc, char* argv[]) {
try {
// silently attempt to load the config file and templates.
gConfig().load_config();
if (gConfig().is_config_set())
if (gConfig().is_config_set())
gTemplateManager().load_sources();
if (argc < 2)
return print_help() ? 0 : 1;
std::string cmd = argv[1];
if (cmd == "autocomplete") {
std::vector<std::string> argvec;
for (int i=0; i<argc; i++)
argvec.push_back(argv[i]);
return autocomplete(argvec) ? 0 : 1;
}
if (cmd == "help" || cmd == "-h" || cmd == "--help" || cmd== "h" || cmd=="halp")
return print_help() ? 0 : 1;
if (cmd == "edit" && argc < 3) {
if (!gConfig().is_config_set())
gConfig().save_config(false);
std::string config_file = localfile::dropshell_json();
if (!service_runner::edit_file(config_file) || !std::filesystem::exists(config_file))
return die("Error: Failed to edit config file.");
gConfig().load_config();
if (!gConfig().is_config_set())
return die("Error: Failed to load and parse edited config file.");
gConfig().save_config(true);
std::cout << "Successfully edited config file at " << config_file << std::endl;
return 0;
}
// ------------------------------------------------------------
// from here we require the config file to be loaded.
if (!gConfig().is_config_set())
return die("Please run 'dropshell edit' to set up the dropshell configuration.");
const std::vector<std::string> & server_definition_paths = gConfig().get_local_server_definition_paths();
if (server_definition_paths.size()>1) { // only show if there are multiple.
std::cout << "Server definition paths: ";
@ -165,26 +178,7 @@ int main(int argc, char* argv[]) {
if (gTemplateManager().is_loaded() && gTemplateManager().get_source_count() > 0)
gTemplateManager().print_sources();
if (cmd == "server" || cmd == "servers" || cmd == "list" || cmd == "view")
switch (argc)
{
case 2:
list_servers();
return 0;
case 3:
show_server_details(argv[2]);
return 0;
case 4:
cmd="logs";
break;
default:
return die("dropshell server: too many arguments");
}
if (cmd == "templates") {
gTemplateManager().list_templates();
return 0;
}
HAPPYEXIT("templates", gTemplateManager().list_templates());
if (cmd == "create-template") {
if (argc < 3) return die("Error: create-template requires a template name");
@ -207,16 +201,12 @@ int main(int argc, char* argv[]) {
return 0;
}
if (cmd == "edit" && argc < 4) {
ASSERT(argc>=3, "Error: logic error!");
service_runner::edit_server(safearg(argc,argv,2));
return 0;
}
// handle running a command.
std::set<std::string> commands;
get_all_used_commands(commands);
commands.merge(std::set<std::string>{"ssh","edit","_allservicesstatus","fullnuke"}); // handled by service_runner, but not in template_shell_commands.
autocomplete::merge_commands(commands, autocomplete::service_commands_require_config); // handled by service_runner, but not in template_shell_commands.
if (commands.count(cmd)) {
std::set<std::string> safe_commands = {"nuke", "fullnuke"};

View File

@ -101,11 +101,10 @@ std::string server_env_manager::get_variable(const std::string& name) const {
}
return it->second;
}
sCommand server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent) const
std::optional<sCommand> server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, const std::vector<std::string> args, const bool silent) const
{
if (command.empty())
return sCommand();
return std::nullopt;
std::string remote_service_template_path = remotepath::service_template(mServerName,service_name);
std::string script_path = remote_service_template_path + "/" + command + ".sh";
@ -113,7 +112,7 @@ sCommand server_env_manager::construct_standard_template_run_cmd(const std::stri
std::map<std::string, std::string> env_vars;
if (!get_all_service_env_vars(mServerName, service_name, env_vars)) {
std::cerr << "Error: Failed to get all service env vars for " << service_name << std::endl;
return sCommand();
return std::nullopt;
}
std::string argstr = "";
@ -121,28 +120,29 @@ sCommand server_env_manager::construct_standard_template_run_cmd(const std::stri
argstr += " " + quote(dequote(trim(arg)));
}
std::vector<std::string> cmd = {script_path};
cmd.insert(cmd.end(), args.begin(), args.end());
sCommand scommand(remote_service_template_path, cmd, env_vars);
sCommand sc(
remote_service_template_path,
quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""),
env_vars
);
if (scommand.empty())
if (sc.empty()) {
std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl;
return scommand;
return std::nullopt;
}
return sc;
}
bool server_env_manager::check_remote_dir_exists(const std::string &dir_path) const
{
std::vector<std::string> cmd = {"test", "-d", dir_path};
sCommand scommand(cmd);
return execute_command(get_SSH_INFO(), scommand, cMode::Silent);
sCommand scommand("", "test -d " + quote(dir_path),{});
return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
}
bool server_env_manager::check_remote_file_exists(const std::string& file_path) const {
std::vector<std::string> cmd = {"test", "-f", file_path};
sCommand scommand(cmd);
return execute_command(get_SSH_INFO(), scommand, cMode::Silent);
sCommand scommand("", "test -f " + quote(file_path),{});
return execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
}
bool server_env_manager::check_remote_items_exist(const std::vector<std::string> &file_paths) const
@ -155,11 +155,9 @@ bool server_env_manager::check_remote_items_exist(const std::vector<std::string>
file_names_str += std::filesystem::path(file_path).filename().string() + " ";
}
// check if all items in the vector exist on the remote server, in a single command.
sCommand scommand({
"bash", "-c", "for item in " + file_paths_str + "; do test -f $item; done"
});
sCommand scommand("", "for item in " + file_paths_str + "; do test -f $item; done",{});
bool okay = execute_command(get_SSH_INFO(), scommand, cMode::Silent);
bool okay = execute_ssh_command(get_SSH_INFO(), scommand, cMode::Silent);
if (!okay) {
std::cerr << "Error: Required items not found on remote server: " << file_names_str << std::endl;
return false;
@ -167,35 +165,65 @@ bool server_env_manager::check_remote_items_exist(const std::vector<std::string>
return true;
}
bool server_env_manager::remove_remote_dir(const std::string &dir_path, bool silent) const
{
std::filesystem::path path(dir_path);
std::filesystem::path parent_path = path.parent_path();
std::string target_dir = path.filename().string();
if (parent_path.empty())
parent_path="/";
if (target_dir.empty())
return false;
if (!silent)
std::cout << "Removing remote directory " << target_dir << " in " << parent_path << " on " << mServerName << std::endl;
std::string remote_cmd =
"docker run --rm -v " + quote(parent_path.string()) + ":/parent " +
" alpine rm -rf \"/parent/" + target_dir + "\"";
if (!silent)
std::cout << "Running command: " << remote_cmd << std::endl;
sCommand scommand("", remote_cmd,{});
cMode mode = (silent ? cMode::Silent : cMode::None);
return execute_ssh_command(get_SSH_INFO(), scommand, mode);
}
bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const
{
sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
auto scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
if (!scommand.has_value())
return false;
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand.add_env_var(key, value);
scommand->add_env_var(key, value);
if (scommand.get_command_to_run().empty())
if (scommand->get_command_to_run().empty())
return false;
cMode mode = (command=="ssh" ? cMode::Defaults : cMode::Silent);
return execute_command(get_SSH_INFO(), scommand, mode);
cMode mode = (command=="ssh") ? (cMode::Interactive) : cMode::Silent;
return execute_ssh_command(get_SSH_INFO(), scommand.value(), mode);
}
bool server_env_manager::run_remote_template_command_and_capture_output(const std::string &service_name, const std::string &command, std::vector<std::string> args, std::string &output, bool silent, std::map<std::string, std::string> extra_env_vars) const
{
sCommand scommand = construct_standard_template_run_cmd(service_name, command, args, false);
if (scommand.get_command_to_run().empty())
auto scommand = construct_standard_template_run_cmd(service_name, command, args, false);
if (!scommand.has_value())
return false;
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand.add_env_var(key, value);
scommand->add_env_var(key, value);
cMode mode = cMode::CaptureOutput;
return execute_command(get_SSH_INFO(), scommand, mode, output);
return execute_ssh_command(get_SSH_INFO(), scommand.value(), mode, &output);
}
// base64 <<< "FOO=BAR WHEE=YAY bash ./test.sh"
// echo YmFzaCAtYyAnRk9PPUJBUiBXSEVFPVlBWSBiYXNoIC4vdGVzdC5zaCcK | base64 -d | bash

View File

@ -10,7 +10,7 @@
#include <memory>
#include <vector>
#include "utils/execute.hpp"
#include <optional>
namespace dropshell {
class server_env_manager;
@ -43,7 +43,7 @@ class server_env_manager {
std::string get_SSH_USER() const { return get_variable("SSH_USER"); }
std::string get_SSH_PORT() const { return get_variable("SSH_PORT"); }
std::string get_DROPSHELL_DIR() const { return get_variable("DROPSHELL_DIR"); }
sSSHInfo get_SSH_INFO() const { return sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT()}; }
sSSHInfo get_SSH_INFO() const { return sSSHInfo{get_SSH_HOST(), get_SSH_USER(), get_SSH_PORT(), get_server_name()}; }
bool is_valid() const { return mValid; }
std::string get_server_name() const { return mServerName; }
@ -53,13 +53,15 @@ class server_env_manager {
bool check_remote_file_exists(const std::string& file_path) const;
bool check_remote_items_exist(const std::vector<std::string>& file_paths) const;
bool remove_remote_dir(const std::string &dir_path, bool silent) const;
bool run_remote_template_command(const std::string& service_name, const std::string& command,
std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const;
bool run_remote_template_command_and_capture_output(const std::string& service_name, const std::string& command,
std::vector<std::string> args, std::string & output, bool silent, std::map<std::string, std::string> extra_env_vars) const;
private:
sCommand construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent) const;
std::optional<sCommand> construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, const std::vector<std::string> args, const bool silent) const;
private:
std::string mServerName;

View File

@ -71,108 +71,6 @@ ServerInfo get_server_info(const std::string &server_name)
return ServerInfo();
}
// https://github.com/bloomen/transwarp?tab=readme-ov-file#range-functions
void list_servers() {
auto servers = get_configured_servers();
tableprint tp("All DropShell Servers");
tp.add_row({"Name", "User", "Address", "Health", "Ports"});
std::cout << "Checking "<<servers.size() << " servers: " << std::flush;
int checked = 0;
transwarp::parallel exec{servers.size()};
auto task = transwarp::for_each(exec, servers.begin(), servers.end(), [&](const ServerInfo& server) {
std::map<std::string, ServiceStatus> status = service_runner::get_all_services_status(server.name);
std::set<int> ports_used;
std::string serviceticks = "";
for (const auto& [service_name, service_status] : status) {
ports_used.insert(service_status.ports.begin(), service_status.ports.end());
serviceticks += service_runner::HealthStatus2String(service_status.health) + " ";
}
std::string ports_used_str = "";
for (const auto& port : ports_used)
ports_used_str += std::to_string(port) + " ";
tp.add_row({server.name, server.ssh_user, server.ssh_host, serviceticks, ports_used_str});
++checked;
// print out a tick character for each server checked.
std::cout << checked << "" << std::flush;
});
task->wait();
std::cout << std::endl << std::endl;
tp.print();
}
void show_server_details(const std::string& server_name) {
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
return;
}
//---------------------
// Check if server is reachable via SSH
std::string ssh_address = env.get_SSH_HOST();
std::string ssh_user = env.get_SSH_USER();
std::string ssh_port = env.get_SSH_PORT();
if (!ssh_address.empty()) {
std::cout << std::endl << "Server Status:" << std::endl;
std::cout << std::string(40, '-') << std::endl;
// Try to connect to the server
std::string cmd = "ssh -o ConnectTimeout=5 " + ssh_user + "@" + ssh_address + " -p " + ssh_port + " 'echo connected' 2>/dev/null";
int result = system(cmd.c_str());
if (result == 0) {
std::cout << "Status: Online" << std::endl;
// // Get uptime if possible
// cmd = "ssh " + ssh_address + " 'uptime' 2>/dev/null";
// int rval = system(cmd.c_str());
// if (rval != 0) {
// std::cout << "Error: Failed to get uptime" << std::endl;
// }
} else {
std::cout << "Status: Offline" << std::endl;
}
}
std::cout << std::endl;
//---------------------
{
std::cout << std::endl;
tableprint tp("Server Configuration: " + server_name, true);
tp.add_row({"Key", "Value"});
for (const auto& [key, value] : env.get_variables()) {
tp.add_row({key, value});
}
tp.print();
}
//---------------------
// list services, and run healthcheck on each
{
tableprint tp("Services: " + server_name, false);
tp.add_row({"Status", "Service", "Ports"});
std::map<std::string, ServiceStatus> status = service_runner::get_all_services_status(server_name);
std::set<int> ports_used;
std::string serviceticks = "";
for (const auto& [service_name, service_status] : status) {
std::string healthy = service_runner::HealthStatus2String(service_status.health);
std::string ports_str = "";
for (const auto& port : service_status.ports)
ports_str += std::to_string(port) + " ";
tp.add_row({healthy, service_name, ports_str});
} // end of for (const auto& service : services)
tp.print();
} // end of list services
} // end of show_server_details
bool create_server(const std::string &server_name)
{

View File

@ -20,10 +20,6 @@ namespace dropshell {
ServerInfo get_server_info(const std::string& server_name);
void list_servers();
void show_server_details(const std::string& server_name);
bool create_server(const std::string& server_name);
void get_all_used_commands(std::set<std::string> &commands);

View File

@ -1,3 +1,4 @@
#include <iostream>
#include <fstream>
#include <sstream>
@ -6,7 +7,7 @@
#include <iomanip>
#include <filesystem>
#include <unistd.h>
#include <libassert/assert.hpp>
#include "utils/assert.hpp"
#include "config.hpp"
#include "service_runner.hpp"
@ -15,7 +16,8 @@
#include "services.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "command_registry.hpp"
#include "shared_commands.hpp"
namespace fs = std::filesystem;
@ -42,109 +44,7 @@ service_runner::service_runner(const std::string& server_name, const std::string
mValid = !mServiceInfo.local_template_path.empty();
}
bool service_runner::install(bool silent) {
maketitle("Installing " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
if (!mServerEnv.is_valid()) return false; // should never hit this.
// Check if template exists
template_info tinfo = gTemplateManager().get_template_info(mServiceInfo.template_name);
if (!tinfo.is_set())
return false;
// Create service directory
std::string remote_service_path = remotepath::service(mServer, mService);
if (!execute_command(mServerEnv.get_SSH_INFO(), sCommand({"mkdir", "-p", quote(remote_service_path)}), cMode::Silent))
{
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
return false;
}
// Check if rsync is installed on remote host
if (!execute_command(mServerEnv.get_SSH_INFO(), sCommand({"which", "rsync", ">", "/dev/null", "2>&1"}), cMode::Silent))
{
std::cerr << "rsync is not installed on the remote host" << std::endl;
return false;
}
// Copy template files
{
std::cout << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl << std::string(8,' ')<<"[REMOTE] " << remotepath::service_template(mServer, mService) << "/" << std::endl;
std::vector<std::string> rsync_cmd = {"rsync", "--delete", "-zrpc", "-e", quote("ssh -p " + mServerEnv.get_SSH_PORT()),
quote(tinfo.local_template_path().string()+"/"),
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remotepath::service_template(mServer, mService)+"/")
};
if (!execute_command(rsync_cmd, silent ? cMode::Silent : cMode::Defaults))
{
std::cerr << "Failed to copy template files using rsync" << std::endl;
return false;
}
}
// Copy service files
{
std::string local_service_path = localpath::service(mServer,mService);
if (local_service_path.empty() || !fs::exists(local_service_path)) {
std::cerr << "Error: Service directory not found: " << local_service_path << std::endl;
return false;
}
std::cout << "Copying: [LOCAL] " << local_service_path << std::endl <<std::string(8,' ')<<"[REMOTE] " << remotepath::service_config(mServer,mService) << std::endl;
std::vector<std::string> rsync_cmd = {"rsync", "--delete", "-zrpc", "-e", quote("ssh -p " + mServerEnv.get_SSH_PORT()),
quote(local_service_path + "/"),
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remotepath::service_config(mServer,mService) + "/")
};
if (!execute_command(rsync_cmd, silent ? cMode::Silent : cMode::Defaults))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
}
// Run install script
{
mServerEnv.run_remote_template_command(mService, "install", {}, silent, {});
}
// print health tick
std::cout << "Health: " << healthtick() << std::endl;
return true;
}
bool service_runner::uninstall(bool silent) {
maketitle("Uninstalling " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
if (!mServerEnv.is_valid()) return false; // should never hit this.
// 2. Check if service directory exists on server
if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) {
std::cerr << "Service is not installed: " << mService << std::endl;
return true; // Nothing to uninstall
}
// 3. Run uninstall script if it exists
std::string uninstall_script = remotepath::service_template(mServer, mService) + "/uninstall.sh";
bool script_exists = mServerEnv.check_remote_file_exists(uninstall_script);
if (script_exists) {
if (!mServerEnv.run_remote_template_command(mService, "uninstall", {}, silent, {})) {
std::cerr << "Warning: Uninstall script failed, but continuing with directory removal" << std::endl;
}
} else {
std::cerr << "Warning: No uninstall script found. Unable to uninstall service." << std::endl;
return false;
}
// 4. Remove the service directory from the server
std::vector<std::string> rm_cmd = {"rm", "-rf", remotepath::service(mServer, mService)};
if (!execute_command(mServerEnv.get_SSH_INFO(), sCommand(rm_cmd), cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
return true;
}
bool service_runner::nuke(bool silent)
{
@ -184,8 +84,8 @@ bool service_runner::fullnuke()
return false;
}
std::vector<std::string> rm_cmd = {"rm", "-rf", local_service_path};
if (!execute_command(sCommand(rm_cmd), cMode::Silent)) {
std::string rm_cmd = "rm -rf " + quote(local_service_path);
if (!execute_local_command(rm_cmd, nullptr, cMode::Silent)) {
std::cerr << "Failed to remove service directory" << std::endl;
return false;
}
@ -208,12 +108,6 @@ bool service_runner::run_command(const std::string& command, std::vector<std::st
return false;
}
// don't need a script for edit!
if (command == "edit") {
edit_service_config();
return true;
}
if (command == "fullnuke")
return fullnuke();
@ -230,7 +124,7 @@ bool service_runner::run_command(const std::string& command, std::vector<std::st
// install doesn't require anything on the server yet.
if (command == "install")
return install();
return install_service(mServer, mService, false);
std::string script_path = remotepath::service_template(mServer, mService) + "/" + command + ".sh";
@ -252,8 +146,8 @@ bool service_runner::run_command(const std::string& command, std::vector<std::st
return false;
}
if (command == "uninstall")
return uninstall();
// if (command == "uninstall")
// return uninstall();
if (command == "ssh") {
interactive_ssh_service();
@ -284,7 +178,7 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
std::string command = "_allservicesstatus";
std::string service_name = "dropshell-agent";
if (!gTemplateManager().template_command_exists(service_name, command))
if (!gTemplateManager().template_command_exists(service_name, "shared/"+command))
{
std::cerr << "Error: " << service_name << " does not contain the " << command << " script" << std::endl;
return status;
@ -297,7 +191,7 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
}
std::string output;
if (!env.run_remote_template_command_and_capture_output(service_name, command, {}, output, true, {}))
if (!env.run_remote_template_command_and_capture_output(service_name, "shared/"+command, {}, output, true, {}))
return status;
std::stringstream ss(output);
@ -334,66 +228,6 @@ std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std
return status;
}
HealthStatus service_runner::is_healthy()
{
if (!mServerEnv.is_valid()) {
std::cerr << "Error: Server service not initialized" << std::endl;
return HealthStatus::ERROR;
}
if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) {
return HealthStatus::NOTINSTALLED;
}
std::string script_path = remotepath::service_template(mServer, mService) + "/status.sh";
if (!mServerEnv.check_remote_file_exists(script_path)) {
return HealthStatus::UNKNOWN;
}
// Run status script, does not display output.
if (!mServerEnv.run_remote_template_command(mService, "status", {}, true, {}))
return HealthStatus::UNHEALTHY;
return HealthStatus::HEALTHY;
}
std::string service_runner::healthtick()
{
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
std::string yellow_exclamation = "\033[33m!\033[0m";
std::string unknown = "\033[37m✓\033[0m";
HealthStatus status = is_healthy();
if (status == HealthStatus::HEALTHY)
return green_tick;
else if (status == HealthStatus::UNHEALTHY)
return red_cross;
else if (status == HealthStatus::UNKNOWN)
return unknown;
else
return yellow_exclamation;
}
std::string service_runner::HealthStatus2String(HealthStatus status)
{
if (status == HealthStatus::HEALTHY)
return ":tick:";
else if (status == HealthStatus::UNHEALTHY)
return ":cross:";
else if (status == HealthStatus::UNKNOWN)
return ":greytick:";
else if (status == HealthStatus::NOTINSTALLED)
return ":warning:";
else
return ":error:";
}
std::string service_runner::healthmark()
{
HealthStatus status = is_healthy();
return HealthStatus2String(status);
}
bool service_runner::interactive_ssh(const std::string & server_name, const std::string & command) {
std::string serverpath = localpath::server(server_name);
@ -402,62 +236,16 @@ bool service_runner::interactive_ssh(const std::string & server_name, const std:
return false;
}
sCommand scommand({"bash"});
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
return false;
}
return execute_command(env.get_SSH_INFO(), scommand, cMode::Defaults);
sCommand scommand("", "bash",{});
return execute_ssh_command(env.get_SSH_INFO(), scommand, cMode::Interactive);
}
void service_runner::edit_server(const std::string &server_name)
{
std::string serverpath = localpath::server(server_name);
if (serverpath.empty()) {
std::cerr << "Error: Server not found: " << server_name << std::endl;
return;
}
std::ostringstream aftertext;
aftertext << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW.\n"
<< "You can ssh in to the remote server with: dropshell ssh "<<server_name<<"\n"
<< "Once moved, reinstall all services with: dropshell install " << server_name;
std::string config_file = serverpath + "/server.env";
if (!edit_file(config_file)) {
std::cerr << "Error: Failed to edit server.env" << std::endl;
std::cerr << "You can manually edit this file at: " << config_file << std::endl;
std::cerr << "After editing, " << aftertext.str() << std::endl;
}
else
std::cout << aftertext.str() << std::endl;
}
bool service_runner::edit_file(const std::string &file_path)
{
// make sure parent directory exists.
std::string parent_dir = get_parent(file_path);
std::filesystem::create_directories(parent_dir);
std::vector<std::string> editor_cmd;
const char* editor_env = std::getenv("EDITOR");
if (editor_env && std::strlen(editor_env) > 0) {
editor_cmd = {std::string(editor_env), file_path};
} else if (isatty(STDIN_FILENO)) {
// Check if stdin is connected to a terminal if EDITOR is not set
editor_cmd = {"nano", "-w", file_path};
} else {
std::cerr << "Error: Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
std::cerr << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
std::cerr << "You can manually edit the file at: " << file_path << std::endl;
return false;
}
std::cout << "Editing file: " << file_path << std::endl;
return execute_command(editor_cmd, cMode::Defaults);
}
bool service_runner::interactive_ssh_service()
{
@ -471,19 +259,17 @@ bool service_runner::interactive_ssh_service()
return mServerEnv.run_remote_template_command(mService, "ssh", args, false, {});
}
void service_runner::edit_service_config()
bool service_runner::scp_file_to_remote(const std::string &local_path, const std::string &remote_path, bool silent)
{
std::string config_file = localfile::service_env(mServer,mService);
if (!fs::exists(config_file)) {
std::cerr << "Error: Service config file not found: " << config_file << std::endl;
return;
}
if (edit_file(config_file) && std::filesystem::exists(config_file))
std::cout << "To apply your changes, run:\n dropshell install " + mServer + " " + mService << std::endl;
std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + quote(local_path) + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : "");
return execute_local_command(scp_cmd, nullptr, (silent ? cMode::Silent : cMode::None));
}
bool service_runner::scp_file_from_remote(const std::string &remote_path, const std::string &local_path, bool silent)
{
std::string scp_cmd = "scp -P " + mServerEnv.get_SSH_PORT() + " " + mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_path) + " " + quote(local_path) + (silent ? " > /dev/null 2>&1" : "");
return execute_local_command(scp_cmd, nullptr, (silent ? cMode::Silent : cMode::None));
}
bool service_runner::restore(std::string backup_file, bool silent)
{
@ -547,12 +333,12 @@ bool service_runner::restore(std::string backup_file, bool silent)
{ // uninstall service, then nuke it.
maketitle("2) Uninstalling old service...");
if (!uninstall(true))
return false;
// if (!uninstall(true))
// return false;
maketitle("3) Nuking old service...");
if (!nuke(true))
return false;
// if (!nuke(true))
// return false;
}
@ -562,9 +348,8 @@ bool service_runner::restore(std::string backup_file, bool silent)
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file;
// Copy backup file from local to server
std::vector<std::string> scp_cmd = {"scp", "-P", mServerEnv.get_SSH_PORT(), quote(local_backup_file_path), mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" + quote(remote_backup_file_path) + (silent ? " > /dev/null 2>&1" : "")};
if (!execute_command(sCommand(scp_cmd), silent ? cMode::Silent : cMode::Defaults)) {
std::cerr << "Failed to copy backup file from server" << std::endl;
if (!scp_file_to_remote(local_backup_file_path, remote_backup_file_path, silent)) {
std::cerr << "Failed to copy backup file from local to server" << std::endl;
return false;
}
@ -575,7 +360,7 @@ bool service_runner::restore(std::string backup_file, bool silent)
{ // installing fresh service
maketitle("5) Non-destructive install of fresh service...");
if (!install(true))
if (!install_service(mServer, mService, true))
return false;
}
@ -640,7 +425,8 @@ bool service_runner::backup(bool silent) {
// Create backups directory on server if it doesn't exist
std::string remote_backups_dir = remotepath::backups(mServer);
if (!silent) std::cout << "Remote backups directory on "<< mServer <<": " << remote_backups_dir << std::endl;
if (!execute_command(mServerEnv.get_SSH_INFO(), sCommand({"mkdir","-p",remote_backups_dir}), cMode::Silent)) {
std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir);
if (!execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand("",mkdir_cmd, {}), cMode::Silent)) {
std::cerr << "Failed to create backups directory on server" << std::endl;
return false;
}
@ -671,7 +457,7 @@ bool service_runner::backup(bool silent) {
std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_filename).string();
// assert that the backup filename is valid - -_- appears exactly 3 times in local_backup_file_path.
ASSERT(3 == count_substring(magic_string, local_backup_file_path));
ASSERT(3 == count_substring(magic_string, local_backup_file_path), "Invalid backup filename");
{ // Run backup script
cRemoteTempFolder remote_temp_folder(mServerEnv);
@ -681,11 +467,7 @@ bool service_runner::backup(bool silent) {
}
// Copy backup file from server to local
std::vector<std::string> scp_cmd = {"scp", "-P", mServerEnv.get_SSH_PORT(),
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
remote_backup_file_path,
local_backup_file_path};
if (!execute_command(sCommand(scp_cmd), silent ? cMode::Silent : cMode::Defaults)) {
if (!scp_file_from_remote(remote_backup_file_path, local_backup_file_path, silent)) {
std::cerr << "Failed to copy backup file from server" << std::endl;
return false;
}
@ -701,8 +483,8 @@ bool service_runner::backup(bool silent) {
cRemoteTempFolder::cRemoteTempFolder(const server_env_manager &server_env) : mServerEnv(server_env)
{
std::string p = remotepath::temp_files(server_env.get_server_name()) + "/" + random_alphanumeric_string(10);
std::vector<std::string> mkdir_cmd = {"mkdir", "-p", p};
if (!execute_command(server_env.get_SSH_INFO(), sCommand(mkdir_cmd), cMode::Silent))
std::string mkdir_cmd = "mkdir -p " + quote(p);
if (!execute_ssh_command(server_env.get_SSH_INFO(), sCommand("", mkdir_cmd, {}), cMode::Silent))
std::cerr << "Failed to create temp directory on server" << std::endl;
else
mPath = p;
@ -710,8 +492,8 @@ cRemoteTempFolder::cRemoteTempFolder(const server_env_manager &server_env) : mSe
cRemoteTempFolder::~cRemoteTempFolder()
{
std::vector<std::string> rm_cmd = {"rm", "-rf", mPath};
execute_command(mServerEnv.get_SSH_INFO(), sCommand(rm_cmd), cMode::Silent);
std::string rm_cmd = "rm -rf " + quote(mPath);
execute_ssh_command(mServerEnv.get_SSH_INFO(), sCommand("", rm_cmd, {}), cMode::Silent);
}
std::string cRemoteTempFolder::path() const

View File

@ -55,23 +55,7 @@ class service_runner {
std::string healthtick();
std::string healthmark();
private:
// install the service over ssh, using the credentials from server.env (via server_env.hpp), by:
// 1. check if the server_name exists, and the service_name refers to a valid template
// 2. check if service_name is valid for the server_name
// 3. create the service directory on the server at {DROPSHELL_DIR}/{service_name}
// 3. copy the template files into {DROPSHELL_DIR}/{service_name}/template (from the templates directory for the specified server, using templates.hpp to identify the path)
// 4. copying the local service directory into {DROPSHELL_DIR}/{service_name}/config (from the server directory for the specified server)
// 5. running the install.sh script on the server, passing the {service_name}.env file as an argument
bool install(bool silent=false);
// uninstall the service over ssh, using the credentials from server.env (via server_env.hpp)
// 1. check if the server_name exists, and the service_name refers to a valid template
// 2. check if service_name is valid for the server_name
// 3. run the uninstall.sh script on the server, passing the {service_name}.env file as an argument
// 4. remove the service directory from the server
bool uninstall(bool silent=false);
public:
// backup and restore
bool backup(bool silent=false);
bool restore(std::string backup_file, bool silent=false);
@ -84,18 +68,13 @@ class service_runner {
// replaces the current dropshell process with the ssh process
bool interactive_ssh_service();
// edit the service configuration file
void edit_service_config();
bool scp_file_to_remote(const std::string& local_path, const std::string& remote_path, bool silent=false);
bool scp_file_from_remote(const std::string& remote_path, const std::string& local_path, bool silent=false);
public:
// utility functions
static std::string get_latest_backup_file(const std::string& server, const std::string& service);
static bool interactive_ssh(const std::string & server_name, const std::string & command);
static void edit_server(const std::string & server_name);
static bool edit_file(const std::string & file_path);
static std::map<std::string, ServiceStatus> get_all_services_status(std::string server_name);
static std::string HealthStatus2String(HealthStatus status);
private:
std::string mServer;

View File

@ -229,7 +229,7 @@ bool get_all_service_env_vars(const std::string &server_name, const std::string
all_env_vars["CONFIG_PATH"] = remotepath::service_config(server_name,service_name);
all_env_vars["SERVER"] = server_name;
all_env_vars["SERVICE"] = service_name;
all_env_vars["AGENT_PATH"] = remotepath::service_template(server_name, "dropshell-agent") + "/shared";
all_env_vars["AGENT_PATH"] = remotepath::agent(server_name);
ServerInfo server_info = get_server_info(server_name);
if (server_info.ssh_host.empty())

View File

@ -6,7 +6,7 @@
#include <algorithm>
#include <iomanip>
#include <map>
#include <libassert/assert.hpp>
#include "utils/assert.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
@ -105,7 +105,7 @@
// ------------------------------------------------------------------------------------------------
void template_manager::list_templates() const {
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
auto templates = get_template_list();
if (templates.empty()) {
@ -128,7 +128,7 @@
std::set<std::string> template_manager::get_template_list() const
{
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
std::set<std::string> templates;
for (const auto& source : mSources) {
auto source_templates = source->get_template_list();
@ -139,7 +139,7 @@
bool template_manager::has_template(const std::string &template_name) const
{
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
template_source_interface* source = get_source(template_name);
if (!source)
return false;
@ -148,7 +148,7 @@
template_info template_manager::get_template_info(const std::string &template_name) const
{
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
template_source_interface* source = get_source(template_name);
if (source)
return source->get_template_info(template_name);
@ -159,7 +159,7 @@
bool template_manager::template_command_exists(const std::string &template_name, const std::string &command) const
{
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
template_source_interface* source = get_source(template_name);
if (!source) {
std::cerr << "Error: Template '" << template_name << "' not found" << std::endl;
@ -249,9 +249,9 @@
void template_manager::load_sources()
{
ASSERT(mSources.empty());
ASSERT(gConfig().is_config_set());
ASSERT(!mLoaded);
ASSERT(mSources.empty(), "Template manager already loaded (sources are not empty).");
ASSERT(gConfig().is_config_set(), "Config not set.");
ASSERT(!mLoaded, "Template manager already loaded.");
auto local_template_paths = gConfig().get_template_local_paths();
if (local_template_paths.empty())
return;
@ -285,7 +285,7 @@
template_source_interface *template_manager::get_source(const std::string &template_name) const
{
ASSERT(mLoaded && mSources.size() > 0);
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
for (const auto& source : mSources) {
if (source->has_template(template_name)) {
return source.get();

11
src/utils/assert.hpp Normal file
View File

@ -0,0 +1,11 @@
#ifndef ASSERT_HPP
#define ASSERT_HPP
#define ASSERT(condition, message) \
if (!(condition)) { \
std::cerr << "Assertion failed: " << message << std::endl; \
std::exit(1); \
}
#endif // ASSERT_HPP

42
src/utils/b64ed.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "b64ed.hpp"
#include <vector>
// Custom base64 encoding/decoding tables
static const std::string custom_base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+_";
std::string base64_encode(const std::string &in) {
std::string out;
int val = 0, valb = -6;
for (unsigned char c : in) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(custom_base64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(custom_base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
std::string base64_decode(const std::string &in) {
std::vector<int> T(256, -1);
for (int i = 0; i < 64; i++) T[custom_base64_chars[i]] = i;
std::string out;
int val = 0, valb = -8;
for (unsigned char c : in) {
if (T[c] == -1) break;
val = (val << 6) + T[c];
valb += 6;
if (valb >= 0) {
out.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return out;
}

9
src/utils/b64ed.hpp Normal file
View File

@ -0,0 +1,9 @@
#ifndef B64ED_HPP
#define B64ED_HPP
#include <string>
std::string base64_decode(const std::string &in);
std::string base64_encode(const std::string &in);
#endif

View File

@ -63,8 +63,8 @@ namespace localpath {
return ((template_cache_path.empty() || service_name.empty()) ? "" :
(template_cache_path+"/remote_versions/"+service_name+".json"));
}
std::string local_bin(){
return current_user_home() + "/.local/bin";
std::string agent(){
return current_user_home() + "/.local/dropshell_agent";
}
std::string current_user_home(){
char * homedir = std::getenv("HOME");
@ -79,16 +79,20 @@ namespace localpath {
} // namespace localpath
// ------------------------------------------------------------------------------------------
// remote paths
// DROPSHELL_DIR
// |-- service name
// |-- config
// |-- service.env
// |-- (user config files)
// |-- template
// |-- (script files)
// |-- backups
// |-- backups
// |-- temp_files
// |-- agent
// |-- services
// |-- service name
// |-- config
// |-- service.env
// |-- template
// |-- (script files)
// |-- config
// |-- service.env
// |-- (other config files for specific server&service)
namespace remotefile {
@ -142,6 +146,12 @@ namespace remotepath {
return (dsp.empty() ? "" : (dsp + "/temp_files"));
}
std::string agent(const std::string &server_name)
{
std::string dsp = DROPSHELL_DIR(server_name);
return (dsp.empty() ? "" : (dsp + "/agent"));
}
std::string service_env(const std::string &server_name, const std::string &service_name)
{
std::string service_path = service_config(server_name, service_name);

View File

@ -12,6 +12,7 @@ namespace dropshell {
// local user config directories
// ~/.config/dropshell/dropshell.json
// ~/.local/dropshell_agent/bb64
// server_definition_path
// |-- <server_name>
@ -54,7 +55,7 @@ namespace dropshell {
std::string remote_versions(const std::string &server_name, const std::string &service_name);
std::string local_bin();
std::string agent();
std::string current_user_home();
} // namespace local
@ -64,6 +65,7 @@ namespace dropshell {
// DROPSHELL_DIR
// |-- backups
// |-- temp_files
// |-- agent
// |-- services
// |-- service name
// |-- config
@ -86,6 +88,7 @@ namespace dropshell {
std::string service_template(const std::string &server_name, const std::string &service_name);
std::string backups(const std::string &server_name);
std::string temp_files(const std::string &server_name);
std::string agent(const std::string &server_name);
} // namespace remotepath
//------------------------------------------------------------------------------------------------

View File

@ -6,170 +6,174 @@
#include <string>
#include <cstdlib>
#include <sstream>
#include <libassert/assert.hpp>
#include <algorithm>
#include "utils/assert.hpp"
#include "execute.hpp"
#include "contrib/base64.hpp"
#include "utils/utils.hpp"
#include "utils/b64ed.hpp"
#include "config.hpp"
#include "utils/directories.hpp"
#include <termios.h>
bool EXITSTATUSCHECK(int ret) {
return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute.
}
namespace dropshell {
bool __execute_command(std::vector<std::string> command, std::string * output)
namespace dropshell
{
// Save TTY state if possible
struct termios orig_termios;
int tty_fd = isatty(STDIN_FILENO) ? STDIN_FILENO : -1;
if (tty_fd != -1) {
tcgetattr(tty_fd, &orig_termios);
bool EXITSTATUSCHECK(int ret)
{
return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute.
}
std::cout << "Executing command: ";
for (auto & x : command) std::cout << "[" << x << "] ";
std::cout << std::endl << std::flush;
bool execute_local_command_interactive(const sCommand &command)
{
if (command.get_command_to_run().empty())
return false;
std::string full_command = command.construct_cmd(localpath::agent()); // Get the command string
int pipefd[2];
bool capture = (output != nullptr);
if (capture && pipe(pipefd) == -1) {
perror("pipe failed");
return false;
pid_t pid = fork();
if (pid == -1)
{
// Fork failed
perror("fork failed");
return false;
}
else if (pid == 0)
{
int rval = system(full_command.c_str());
exit(rval);
}
else
{
// Parent process
int ret;
// Wait for the child process to complete
waitpid(pid, &ret, 0);
return EXITSTATUSCHECK(ret);
}
}
pid_t pid = fork();
if (pid == -1) {
// Fork failed
perror("fork failed");
return false;
} else if (pid == 0) {
// Child process
if (capture) {
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO); // Redirect stdout to pipe
dup2(pipefd[1], STDERR_FILENO); // Redirect stderr to pipe
close(pipefd[1]);
bool execute_local_command_and_capture_output(const sCommand &command, std::string *output)
{
ASSERT(output != nullptr, "Output string must be provided");
if (command.get_command_to_run().empty())
return false;
std::string full_cmd = command.construct_cmd(localpath::agent()) + " 2>&1";
FILE *pipe = popen(full_cmd.c_str(), "r");
if (!pipe)
{
return false;
}
std::vector<const char *> commandvec;
for (auto & x : command) {
commandvec.push_back(x.c_str());
char buffer[128];
while (fgets(buffer, sizeof(buffer), pipe) != nullptr)
{
(*output) += buffer;
}
commandvec.push_back(NULL);
// if (!silent) {
// std::cout << "Executing command: ";
// for (auto & x : commandvec) std::cout << x << " ";
// std::cout << std::endl;
// }
execvp(commandvec[0], const_cast<char* const*>(commandvec.data()));
// If execvp returns, it means an error occurred
perror("execvp failed");
exit(EXIT_FAILURE); // Exit child process on error
} else {
// Parent process
if (capture) {
close(pipefd[1]); // Close write end
char buffer[256];
ssize_t count;
while ((count = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
output->append(buffer, count);
}
close(pipefd[0]);
}
int ret;
// Wait for the child process to complete
waitpid(pid, &ret, 0);
// Restore TTY state if possible
if (tty_fd != -1) {
tcsetattr(tty_fd, TCSANOW, &orig_termios);
}
int ret = pclose(pipe);
return EXITSTATUSCHECK(ret);
}
}
bool execute_command(const sSSHInfo * ssh_info, const sCommand command, cMode mode, std::string * output)
{
if (command.get_command_to_run().empty())
return false;
std::vector<std::string> commandvec;
// Construct the shell command with proper environment variables and directory
std::string shell_command;
// Add working directory if provided
if (!command.get_directory_to_run_in().empty()) {
shell_command += "cd " + quote(command.get_directory_to_run_in()) + " && ";
bool execute_local_command(std::string command, std::string *output, cMode mode)
{
return execute_local_command("", command, {}, output, mode);
}
// Add environment variables if provided
for (const auto& env_var : command.get_env_vars()) {
shell_command += env_var.first + "=" + quote(env_var.second) + " ";
bool execute_local_command(std::string directory_to_run_in, std::string command_to_run, const std::map<std::string, std::string> &env_vars, std::string *output, cMode mode)
{
sCommand command(directory_to_run_in, command_to_run, env_vars);
if (hasFlag(mode, cMode::Interactive))
{
ASSERT(!hasFlag(mode, cMode::CaptureOutput), "Interactive mode and capture output mode cannot be used together");
ASSERT(output == nullptr, "Interactive mode and an output string cannot be used together");
return execute_local_command_interactive(command);
}
if (hasFlag(mode, cMode::CaptureOutput))
{
ASSERT(output != nullptr, "Capture output mode requires an output string to be provided");
ASSERT(!hasFlag(mode, cMode::Silent), "Silent mode is not allowed with capture output mode");
return execute_local_command_and_capture_output(command, output);
}
if (command.get_command_to_run().empty())
return false;
bool silent = hasFlag(mode, cMode::Silent);
std::string full_cmd = command.construct_cmd(localpath::agent()) + " 2>&1" + (silent ? " > /dev/null" : "");
int ret = system(full_cmd.c_str());
bool ok = EXITSTATUSCHECK(ret);
if (!ok && !silent)
{
std::cerr << "Error: Failed to execute command: " << std::endl;
std::cerr << full_cmd << std::endl;
}
return ok;
}
// Add the command arguments properly joined
const auto& args = command.get_command_to_run();
std::ostringstream cmd_stream;
for (size_t i = 0; i < args.size(); ++i) {
if (i > 0) cmd_stream << " ";
cmd_stream << args[i];
bool execute_ssh_command(const sSSHInfo &ssh_info, const sCommand &remote_command, cMode mode, std::string *output)
{
if (remote_command.get_command_to_run().empty())
return false;
ASSERT(!(hasFlag(mode, cMode::CaptureOutput) && output == nullptr), "Capture output mode must be used with an output string");
std::stringstream ssh_cmd;
ssh_cmd << "ssh -p " << ssh_info.port << " " << (hasFlag(mode, cMode::Interactive) ? "-tt " : "")
<< ssh_info.user << "@" << ssh_info.host;
std::string remote_agent_path = remotepath::agent(ssh_info.server_ID);
bool rval = execute_local_command(
"", // directory to run in
ssh_cmd.str() + " " + remote_command.construct_cmd(remote_agent_path), // local command to run
{}, // environment variables
output, // output string
mode // mode
);
if (!rval && !hasFlag(mode, cMode::Silent))
{
std::cerr << std::endl
<< std::endl;
std::cerr << "Error: Failed to execute ssh command:" << std::endl;
std::cerr << "\033[90m" << ssh_cmd.str() + " " + remote_command.construct_cmd(remote_agent_path) << "\033[0m" << std::endl;
std::cerr << std::endl
<< std::endl;
}
return rval;
}
shell_command += cmd_stream.str();
if (ssh_info) {
// Use bash -c for SSH to ensure proper command execution
commandvec = {
"/usr/bin/ssh",
"-p", ssh_info->port,
(hasFlag(mode, cMode::CaptureOutput) ? "" : "-tt"),
ssh_info->user + "@" + ssh_info->host,
"bash", "-c", halfquote(shell_command)
};
} else {
// Local execution
commandvec = {"bash", "-c", halfquote(shell_command)};
std::string sCommand::makesafecmd(std::string agent_path, const std::string &command) const
{
if (command.empty())
return "";
std::string encoded = base64_encode(dequote(trim(command)));
std::string commandstr = agent_path + "/bb64 " + encoded;
return commandstr;
}
// Remove any empty strings
commandvec.erase(
std::remove_if(commandvec.begin(), commandvec.end(),
[](const std::string& s) { return s.empty(); }),
commandvec.end());
if (hasFlag(mode, cMode::CaptureOutput)) {
ASSERT(output, "Capture output mode requires an output string to be provided");
return __execute_command(commandvec, output);
} else {
return __execute_command(commandvec, nullptr);
std::string sCommand::construct_cmd(std::string agent_path) const
{
if (mCmd.empty())
return "";
// need to construct to change directory and set environment variables
std::string cmdstr;
if (!mDir.empty())
cmdstr += "cd " + quote(mDir) + " && ";
if (!mVars.empty())
for (const auto &env_var : mVars)
cmdstr += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " ";
cmdstr += mCmd;
if (!agent_path.empty())
cmdstr = makesafecmd(agent_path, cmdstr);
return cmdstr;
}
}
bool execute_command(const sSSHInfo ssh_info, const sCommand command, const cMode mode)
{
return execute_command(&ssh_info, command, mode, nullptr);
}
bool execute_command(const sSSHInfo ssh_info, const sCommand command, const cMode mode, std::string & output)
{
return execute_command(&ssh_info, command, mode, &output);
}
bool execute_command(const sCommand command, const cMode mode)
{
return execute_command(nullptr, command, mode, nullptr);
}
bool execute_command(const sCommand command, const cMode mode, std::string & output)
{
return execute_command(nullptr, command, mode, &output);
}
} // namespace dropshell

View File

@ -3,7 +3,6 @@
#include <string>
#include <map>
#include <optional>
namespace dropshell {
@ -11,9 +10,10 @@ class sCommand;
// mode bitset
enum class cMode {
Defaults = 0,
Silent = 1,
CaptureOutput = 2
None = 0,
Interactive = 1,
Silent = 2,
CaptureOutput = 4
};
inline cMode operator&(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & static_cast<int>(rhs));}
@ -28,40 +28,44 @@ typedef struct sSSHInfo {
std::string host;
std::string user;
std::string port;
std::string server_ID; // dropshell name for server.
} sSSHInfo;
bool execute_command(const sSSHInfo ssh_info, const sCommand command, const cMode mode);
bool execute_command(const sSSHInfo ssh_info, const sCommand command, const cMode mode, std::string & output);
bool execute_command(const sCommand command, const cMode mode);
bool execute_command(const sCommand command, const cMode mode, std::string & output);
bool execute_local_command(std::string command, std::string * output = nullptr, cMode mode = cMode::None);
bool execute_local_command(std::string directory_to_run_in, std::string command_to_run, const std::map<std::string, std::string> & env_vars, std::string * output = nullptr, cMode mode = cMode::None);
bool execute_ssh_command(const sSSHInfo & ssh_info, const sCommand & remote_command, cMode mode = cMode::None, std::string * output = nullptr);
// ------------------------------------------------------------------------------------------------
// class to hold a command to run on the remote server.
class sCommand {
public:
sCommand(std::string directory_to_run_in, const std::vector<std::string> & command_to_run, const std::map<std::string, std::string> & env_vars) :
mDir(directory_to_run_in), mCmdArgs(command_to_run), mEnvVars(env_vars) {}
sCommand(const std::vector<std::string> & command_to_run) :
mDir(""), mCmdArgs(command_to_run), mEnvVars({}) {}
sCommand() : mDir(""), mCmdArgs({}), mEnvVars({}) {}
sCommand(std::string directory_to_run_in, std::string command_to_run, const std::map<std::string, std::string> & env_vars) :
mDir(directory_to_run_in), mCmd(command_to_run), mVars(env_vars) {}
std::string get_directory_to_run_in() const { return mDir; }
const std::vector<std::string>& get_command_to_run() const { return mCmdArgs; }
const std::map<std::string, std::string>& get_env_vars() const { return mEnvVars; }
std::string get_command_to_run() const { return mCmd; }
const std::map<std::string, std::string>& get_env_vars() const { return mVars; }
void add_env_var(const std::string& key, const std::string& value) { mEnvVars[key] = value; }
void add_env_var(const std::string& key, const std::string& value) { mVars[key] = value; }
bool empty() const { return mCmdArgs.empty(); }
bool empty() const { return mCmd.empty(); }
std::string construct_cmd(std::string agent_path) const;
private:
std::string makesafecmd(std::string agent_path, const std::string& command) const;
private:
std::string mDir;
std::vector<std::string> mCmdArgs;
std::map<std::string, std::string> mEnvVars;
std::string mCmd;
std::map<std::string, std::string> mVars;
};
bool EXITSTATUSCHECK(int ret);
} // namespace dropshell

View File

@ -316,4 +316,59 @@ std::string requote(std::string str) {
return quote(trim(dequote(trim(str))));
}
int die(const std::string & msg) {
std::cerr << msg << std::endl;
return 1;
}
std::string safearg(const std::vector<std::string> & args, int index)
{
if (index<0 || index >= args.size()) return "";
return args[index];
}
std::string safearg(int argc, char *argv[], int index)
{
if (index<0 || index >= argc) return "";
return argv[index];
}
void print_left_aligned(const std::string & str, int width) {
std::cout << left_align(str, width);
}
void print_centered(const std::string & str, int width) {
std::cout << center_align(str, width);
}
void print_right_aligned(const std::string & str, int width) {
std::cout << right_align(str, width);
}
std::string left_align(const std::string & str, int width) {
if (static_cast<int>(str.size()) >= width)
return str;
return str + std::string(width - str.size(), ' ');
}
std::string right_align(const std::string & str, int width) {
if (static_cast<int>(str.size()) >= width)
return str;
return std::string(width - str.size(), ' ') + str;
}
std::string center_align(const std::string & str, int width) {
int pad = width - static_cast<int>(str.size());
if (pad <= 0)
return str;
int pad_left = pad / 2;
int pad_right = pad - pad_left;
return std::string(pad_left, ' ') + str + std::string(pad_right, ' ');
}
} // namespace dropshell

View File

@ -41,4 +41,16 @@ std::string replace_with_environment_variables_like_bash(std::string str);
std::string random_alphanumeric_string(int length);
int die(const std::string & msg);
std::string safearg(int argc, char *argv[], int index);
std::string safearg(const std::vector<std::string> & args, int index);
void print_left_aligned(const std::string & str, int width);
void print_centered(const std::string & str, int width);
void print_right_aligned(const std::string & str, int width);
std::string left_align(const std::string & str, int width);
std::string right_align(const std::string & str, int width);
std::string center_align(const std::string & str, int width);
} // namespace dropshell

15
src/version.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
// DUMMY VERSION - replaced by build process.
#include <string>
namespace dropshell {
// Version information
const std::string VERSION = "DEV";
const std::string RELEASE_DATE = "NEVER";
const std::string AUTHOR = "j842";
const std::string LICENSE = "MIT";
} // namespace dropshell

View File

@ -1,3 +0,0 @@
Caddy!
Edit the static site, and the Caddyfile.

View File

@ -1,8 +0,0 @@
# Service settings specific to this server
# Image settings
IMAGE_REGISTRY="docker.io"
IMAGE_REPO="caddy"
DATA_VOLUME=caddy_data
CONFIG_VOLUME=caddy_config

View File

@ -1,11 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
_stop_container "$CONTAINER_NAME"
autobackup "$1" "$2" "volume=$DATA_VOLUME" "volume=$CONFIG_VOLUME" || _die "Failed to create backup"
_start_container "$CONTAINER_NAME"
echo "Backup created successfully: $BACKUP_FILE"

View File

@ -1,2 +0,0 @@
# Template to use - always required!
TEMPLATE=caddy

View File

@ -1,6 +0,0 @@
# See https://caddyserver.com/docs/caddyfile
localhost {
root * /srv
file_server
}

View File

@ -1,10 +0,0 @@
# Service settings specific to this server
# (can also override anything in the _default.env file in the template to make it specific to this server)
CONTAINER_NAME=caddy
IMAGE_TAG="latest"
# Scripts will have these environment variables set, plus those in _default.env, plus:
# SERVER, SERVICE, CONFIG_PATH
# CONFIG_PATH points to this directory!

View File

@ -1,9 +0,0 @@
<html>
<head>
<title>Static Site</title>
</head>
<body>
<h1>Static Site</h1>
<p>This is a static site.</p>
</body>
</html>

View File

@ -1,17 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" "DATA_VOLUME" "CONFIG_VOLUME" "CONFIG_PATH"
autocreate "volume=$DATA_VOLUME" "volume=$CONFIG_VOLUME" || _die "Failed to autocreate volumes $DATA_VOLUME and $CONFIG_VOLUME"
_check_docker_installed || _die "Docker test failed, aborting installation..."
docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image $IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"
[ -f "${CONFIG_PATH}/Caddyfile" ] || _die "Caddyfile not found in ${CONFIG_PATH}!"
bash ./stop.sh || _die "Failed to stop container ${CONTAINER_NAME}"
_remove_container $CONTAINER_NAME || _die "Failed to remove container ${CONTAINER_NAME}"
bash ./start.sh || _die "Failed to start container ${CONTAINER_NAME}"
echo "Installation of ${CONTAINER_NAME} complete"

View File

@ -1,9 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# Main script.
echo "Container ${CONTAINER_NAME} logs:"
_grey_start
docker logs "${CONTAINER_NAME}"
_grey_end

View File

@ -1,13 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# NUKE SCRIPT
# This is run after the uninstall.sh script to delete all data.
# dropshell handles the configuration files, so we just need to remove
# any docker volumes and any custom local data folders.
autonuke "volume=$DATA_VOLUME" "volume=$CONFIG_VOLUME" || _die "Failed to nuke"
echo "Nuking of ${CONTAINER_NAME} complete."

View File

@ -1,6 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
echo 80
echo 443

View File

@ -1,17 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# RESTORE SCRIPT
# uninstall container before restore
bash ./uninstall.sh || _die "Failed to uninstall service before restore"
# restore data from backup file
autorestore "$1" "$2" "volume=$DATA_VOLUME" "volume=$CONFIG_VOLUME" || _die "Failed to restore data from backup file"
# reinstall service
bash ./install.sh || _die "Failed to reinstall service after restore"
echo "Restore complete! Service is running again."

View File

@ -1,33 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# START SCRIPT
# The start script is required for all templates.
# It is used to start the service on the server.
DOCKER_RUN_CMD="docker run -d \
--restart unless-stopped \
--name ${CONTAINER_NAME} \
-p 80:80 \
-p 443:443 \
-p 443:443/udp \
-v ${CONFIG_PATH}/Caddyfile:/etc/caddy/Caddyfile \
-v ${DATA_VOLUME}:/data \
-v ${CONFIG_VOLUME}:/config \
-v ${CONFIG_PATH}/static:/srv \
${IMAGE_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}"
if ! _create_and_start_container "$DOCKER_RUN_CMD" "$CONTAINER_NAME"; then
_die "Failed to start container ${CONTAINER_NAME}"
fi
# Check if the container is running
if ! _is_container_running "$CONTAINER_NAME"; then
_die "Container ${CONTAINER_NAME} is not running"
fi
echo "Container ${CONTAINER_NAME} started"

View File

@ -1,15 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# STATUS SCRIPT
# The status script is OPTIONAL.
# It is used to return the status of the service (0 is healthy, 1 is unhealthy).
# This is an example of a status script that checks if the service is running.
# check if the service is running
_is_container_running $CONTAINER_NAME || _die "Service is not running - did not find container $CONTAINER_NAME."
echo "Service is healthy"
exit 0

View File

@ -1,12 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# STOP SCRIPT
# The stop script is required for all templates.
# It is used to stop the service on the server.
_stop_container $CONTAINER_NAME || _die "Failed to stop container ${CONTAINER_NAME}"
echo "Container ${CONTAINER_NAME} stopped"

View File

@ -1,18 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# UNINSTALL SCRIPT
# The uninstall script is required for all templates.
# It is used to uninstall the service from the server.
_remove_container $CONTAINER_NAME || _die "Failed to remove container ${CONTAINER_NAME}"
_is_container_running && _die "Couldn't stop existing container"
_is_container_exists && _die "Couldn't remove existing container"
# remove the image
docker rmi "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || echo "Failed to remove image $IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"
echo "Uninstallation of ${CONTAINER_NAME} complete."
echo "Local data still in place."

View File

@ -1,3 +0,0 @@
DropShell agent.
Required for health checks etc.

View File

@ -1 +0,0 @@
#

View File

@ -1,4 +0,0 @@
# Template to use - always required!
TEMPLATE=dropshell-agent

View File

@ -1,29 +0,0 @@
#!/bin/bash
# INSTALL SCRIPT
# The install script is required for all templates.
# It is used to install the service on the server.
# It is called with the path to the server specific env file as an argument.
check_prerequisites() {
# prerequisites:
# - bash
# - curl
# - wget
# - docker
PREREQUISITES=("bash" "curl" "wget" "jq" "docker")
# check if all prerequisites are installed
for prerequisite in "${PREREQUISITES[@]}"; do
if ! command -v "${prerequisite}" &> /dev/null; then
echo "Prerequisite: ${prerequisite} is not installed."
exit 1
fi
done
}
check_prerequisites
exit 0

View File

@ -1,32 +0,0 @@
#!/bin/bash
directory_to_run_in=${1:-}
command_to_run_base64=${2:-}
if [ -z "$directory_to_run_in" ]; then
echo "Usage: $0 <directory_to_run_in> <command_to_run>"
exit 1
fi
if [ -z "$command_to_run_base64" ]; then
echo "Usage: $0 <directory_to_run_in> <command_to_run>"
exit 1
fi
command_to_run=$(echo "$command_to_run_base64" | base64 -d)
if [ -z "$command_to_run" ]; then
echo "Usage: $0 <directory_to_run_in> <command_to_run>"
exit 1
fi
if [ -n "$directory_to_run_in" ]; then
cd "$directory_to_run_in"
fi
eval "$command_to_run"

View File

@ -1 +0,0 @@
#!/bin/bash

View File

@ -1 +0,0 @@
Caddy

View File

@ -1,5 +0,0 @@
# Service settings specific to this server
# Image settings
IMAGE_REGISTRY="docker.io"
IMAGE_REPO="nginx"

View File

@ -1,10 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "LOCAL_DATA_FOLDER" "BACKUP_FILE" "TEMP_DIR" "CONTAINER_NAME"
# Nginx Example Backup Script
# hot backup is fine for nginx website content.
autobackup "path=${LOCAL_DATA_FOLDER}" || _die "Failed to create backup"
echo "Backup complete for ${CONTAINER_NAME}"

View File

@ -1,2 +0,0 @@
# Template to use - always required!
TEMPLATE=example-nginx

View File

@ -1,12 +0,0 @@
# Service settings specific to this server
# (can also override anything in the _default.env file in the template to make it specific to this server)
HOST_PORT=60123
LOCAL_DATA_FOLDER="/home/dropshell/nginx-example-website"
CONTAINER_NAME="example-nginx"
IMAGE_TAG="latest"
# Scripts will have these environment variables set, plus those in _default.env, plus:
# SERVER, SERVICE, CONFIG_PATH
# CONFIG_PATH points to this directory!

View File

@ -1,23 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "LOCAL_DATA_FOLDER" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" "CONTAINER_NAME"
# Nginx Example Install Script
# Ensure local data folder exists
autocreate "path=${LOCAL_DATA_FOLDER}"
echo "Checking Docker installation..."
_check_docker_installed || _die "Docker test failed, aborting installation..."
echo "Pulling image ${IMAGE_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}..."
docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image $IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"
echo "Stopping and removing any existing container..."
bash ./stop.sh || _die "Failed to stop container ${CONTAINER_NAME}"
_remove_container $CONTAINER_NAME || _die "Failed to remove container ${CONTAINER_NAME}"
echo "Starting container..."
bash ./start.sh || _die "Failed to start container ${CONTAINER_NAME}"
echo "Installation complete for service ${CONTAINER_NAME}."

View File

@ -1,10 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME"
# Nginx Example Logs Script
echo "Showing logs for ${CONTAINER_NAME}... (Press Ctrl+C to stop)"
_grey_start
_get_container_logs $CONTAINER_NAME
_grey_end

View File

@ -1,13 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "LOCAL_DATA_FOLDER" "CONTAINER_NAME"
# Nginx Example Nuke Script
# Removes container and local data folder.
# Call uninstall script first
./uninstall.sh
autonuke "path=${LOCAL_DATA_FOLDER}" || _die "Failed to nuke ${LOCAL_DATA_FOLDER}"
echo "Nuke complete for service ${CONTAINER_NAME}."

View File

@ -1,7 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "HOST_PORT"
# Nginx Example Ports Script
echo $HOST_PORT

View File

@ -1,15 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "LOCAL_DATA_FOLDER" "BACKUP_FILE" "TEMP_DIR" "CONTAINER_NAME"
# Nginx Example Restore Script
echo "Uninstalling service before restore..."
bash ./uninstall.sh || _die "Failed to uninstall service before restore"
autorestore "path=${LOCAL_DATA_FOLDER}" || _die "Failed to restore data folder from backup"
echo "Restore complete. Reinstalling service..."
bash ./install.sh || _die "Failed to reinstall service after restore"
echo "Service ${CONTAINER_NAME} restored and reinstalled."

View File

@ -1,30 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME" "LOCAL_DATA_FOLDER" "HOST_PORT" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG"
# START SCRIPT
# The start script is required for all templates.
# It is used to start the service on the server.
# It is called with the path to the server specific env file as an argument.
[ -d "${LOCAL_DATA_FOLDER}" ] || _die "Local data folder ${LOCAL_DATA_FOLDER} does not exist."
DOCKER_RUN_CMD="docker run -d \
--restart unless-stopped \
--name ${CONTAINER_NAME} \
-p ${HOST_PORT}:80 \
-v ${LOCAL_DATA_FOLDER}:/usr/share/nginx/html:ro \
${IMAGE_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}"
if ! _create_and_start_container "$DOCKER_RUN_CMD" "$CONTAINER_NAME"; then
_die "Failed to start container ${CONTAINER_NAME}"
fi
# Check if the container is running
if ! _is_container_running "$CONTAINER_NAME"; then
_die "Container ${CONTAINER_NAME} is not running"
fi
echo "Container ${CONTAINER_NAME} started"

View File

@ -1,13 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME"
# STATUS SCRIPT
# The status script is OPTIONAL.
# It is used to return the status of the service (0 is healthy, 1 is unhealthy).
# check if the service is running
_is_container_running $CONTAINER_NAME || _die "Service is not running - did not find container $CONTAINER_NAME."
echo "Service is healthy"
exit 0

View File

@ -1,7 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME"
_stop_container $CONTAINER_NAME || _die "Failed to stop container ${CONTAINER_NAME}"
echo "Container ${CONTAINER_NAME} stopped"

View File

@ -1,13 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "CONTAINER_NAME"
# Nginx Example Uninstall Script
echo "Uninstalling service ${CONTAINER_NAME}..."
_remove_container $CONTAINER_NAME || _die "Failed to remove container ${CONTAINER_NAME}"
if _is_container_running $CONTAINER_NAME; then _die "Couldn't stop existing container"; fi
if _is_container_exists $CONTAINER_NAME; then _die "Couldn't remove existing container"; fi
echo "Service ${CONTAINER_NAME} uninstalled."
echo "Note: This does NOT remove the local data folder. Use nuke.sh for that."

View File

@ -1 +0,0 @@
simple-object-storage

View File

@ -1,12 +0,0 @@
# Service settings specific to this server
# Image settings
IMAGE_REGISTRY="gitea.jde.nz"
IMAGE_REPO="j/simple-object-storage"
IMAGE_TAG="latest"
# Container settings
CONTAINER_NAME="simple-object-storage"
# Volume settings
VOLUME_NAME="simple-object-storage"

View File

@ -1,13 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars "VOLUME_NAME" "BACKUP_FILE" "TEMP_DIR"
# Simple Object Storage Backup Script
# Creates a backup tarball of the volume contents.
# HOT backup is fine for simple-object-storage
autobackup "volume=${VOLUME_NAME}" || _die "Failed to create backup"
echo "Backup complete: ${BACKUP_FILE}"

View File

@ -1,2 +0,0 @@
# Template to use - always required!
TEMPLATE=simple-object-storage

View File

@ -1,4 +0,0 @@
# note the port and write tokens are not set here, they are set in the sos_config.json file.
CONTAINER_NAME="simple-object-storage"
VOLUME_NAME="simple-object-storage"

View File

@ -1,8 +0,0 @@
{
"write_tokens": [
"fizzle1",
"fizzle2",
"fizzle3"
],
"port": 8123
}

View File

@ -1,25 +0,0 @@
#!/bin/bash
source "${AGENT_PATH}/_common.sh"
_check_required_env_vars
# Simple Object Storage Install Script
# Pulls image, creates volume/config, starts container.
autocreate "volume=${VOLUME_NAME}"
# check can pull image on remote host and exit if fails
echo "Pulling image ${IMAGE_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}..."
docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image $IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"
echo "Stopping and removing any existing container..."
bash ./stop.sh || _die "Failed to stop container ${CONTAINER_NAME}"
_remove_container $CONTAINER_NAME || _die "Failed to remove container ${CONTAINER_NAME}"
bash ./start.sh || _die "Failed to start container ${CONTAINER_NAME}"
echo "Installation complete for service ${CONTAINER_NAME}."
# determine port.
command -v jq &> /dev/null || _die "jq could not be found, please install it"
[ -f "${CONFIG_PATH}/sos_config.json" ] || _die "sos_config.json does not exist"
PORT=$(jq -r '.port' "${CONFIG_PATH}/sos_config.json")
echo "You can access the service at http://${HOST_NAME}:${PORT}"

Some files were not shown because too many files have changed in this diff Show More