Compare commits

...

169 Commits

Author SHA1 Message Date
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
2bcf6c530d Compiles but no good
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 25s
2025-05-09 18:44:43 +12:00
ed93fa1aaa Finally got all the ssh stuff working
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 24s
2025-05-06 23:26:26 +12:00
14e43855b4 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 31s
2025-05-06 22:54:21 +12:00
c78deea037 tidy
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 28s
2025-05-06 22:46:58 +12:00
cca3ee9679 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 24s
2025-05-06 22:31:57 +12:00
ac797e111c Default to safe
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 26s
2025-05-06 22:22:24 +12:00
bc29705ead Seems to be working!! 2025-05-06 21:57:48 +12:00
d60b7364bc improvement.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 19s
2025-05-06 21:42:36 +12:00
bbc280a50a BROKEN
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 21s
2025-05-06 21:32:41 +12:00
484613d10d Merge branch 'main' of https://gitea.jde.nz/j/dropshell
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m49s
2025-05-06 19:20:33 +12:00
22b1f577c1 Remove output/
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 35s
2025-05-06 13:24:31 +12:00
699f5d1b73 remove 2025-05-06 13:23:43 +12:00
b6df4b61e7 .. 2025-05-06 13:23:15 +12:00
3d791653fc Tidy
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s
2025-05-06 13:01:31 +12:00
1f72141e13 Merge branch 'main' of https://gitea.jde.nz/j/dropshell
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 30s
2025-05-06 12:57:14 +12:00
f7026cddc8 Action! 2025-05-06 12:57:09 +12:00
01858d8601 All tests pass for squashkiwi 2025-05-06 00:03:46 +12:00
38cb23706d Better errors. 2025-05-05 23:37:14 +12:00
42e25b6353 Fixin' 2025-05-05 23:18:00 +12:00
b4b237c1b7 Use docker to root remove the local directory. 2025-05-05 23:09:38 +12:00
ce5a64a4c7 . 2025-05-05 22:53:41 +12:00
ac20fcec3d Return more error codes as return value of exe. 2025-05-05 22:19:04 +12:00
9d01554b13 Add 'latest' hidden option to restore 2025-05-05 21:48:36 +12:00
1776a7e45f . 2025-05-05 21:43:07 +12:00
63490d9ce3 Tidy 2025-05-05 21:12:50 +12:00
e727fc518f Pipe through temp folder to scripts. 2025-05-05 20:11:36 +12:00
7c9a45edf5 . 2025-05-05 19:27:17 +12:00
0462748336 . 2025-05-05 14:45:47 +12:00
c0dc928070 Caddy static 2025-05-04 22:44:37 +12:00
bbd7c635a8 Update scripts. 2025-05-04 22:35:51 +12:00
3460224e29 tidy 2025-05-04 22:09:04 +12:00
56184710a7 Tidying 2025-05-04 20:38:04 +12:00
3bfd6a3cba Vaguely working 2025-05-04 20:34:46 +12:00
8d2a66ee49 Starting to work 2025-05-04 20:19:52 +12:00
aeaf57e196 Remove bogus servers 2025-05-04 20:04:47 +12:00
008cf59c48 . 2025-05-04 19:56:52 +12:00
27e5cce367 Compiles. 2025-05-04 19:16:52 +12:00
95185d3149 Just need to write some functions... 2025-05-04 14:15:34 +12:00
080e384560 . 2025-05-04 14:10:46 +12:00
395c9deb45 ensure template valid 2025-05-04 14:07:27 +12:00
0bc78f353f change 'example' to 'config'. 2025-05-04 14:05:14 +12:00
5286ec542a Add template manager for remote templates. 2025-05-04 13:52:26 +12:00
0e9466365e Refaaaacttt00000rr 2025-05-04 11:30:57 +12:00
6380ce9178 Tidy 2025-05-03 23:37:53 +12:00
77c7315b0b Some basics working (squashkiwi, dropshell-agent). 2025-05-03 23:34:05 +12:00
3d9b1fa6d2 . 2025-05-03 23:18:52 +12:00
2fe7d4c3d9 . 2025-05-03 23:03:16 +12:00
340170b248 biiig wrench 2025-05-03 22:58:39 +12:00
107034cf7b Pipe through dropshell-agent shared script folder $AGENT_PATH 2025-05-03 22:09:49 +12:00
943ed79641 testing! 2025-05-03 22:03:53 +12:00
46ca0003af broken broken broken 2025-05-03 21:53:11 +12:00
0c2b90cf8e In a mess while refactor! 2025-05-03 21:46:32 +12:00
c6b7e2bb41 Tidying 2025-05-03 20:30:06 +12:00
9b9a536697 Tidying 2025-05-03 20:18:09 +12:00
866046b505 Nuke! 2025-05-03 20:13:21 +12:00
ec779e51c2 Working. 2025-05-03 19:56:12 +12:00
2fbe5307da Fixing 2025-05-03 19:36:33 +12:00
5909c90941 . 2025-05-03 19:06:12 +12:00
e10704636f Fix uninstall script 2025-05-03 18:59:38 +12:00
fc8a142e91 Fix server list. 2025-05-03 17:53:38 +12:00
be5493a11c . 2025-05-03 16:56:22 +12:00
f00523d149 Volumez 2025-05-03 16:41:53 +12:00
3a02933bab . 2025-05-03 16:37:58 +12:00
26df02b164 Simple object store. 2025-05-03 16:34:54 +12:00
dcdab05933 . 2025-05-03 16:34:28 +12:00
f80aefdc15 Builder 2025-05-03 09:13:14 +12:00
d1a739cdd0 Rework backup/restore 2025-05-02 23:00:41 +12:00
710a8c762f Updated scripts 2025-05-02 22:37:03 +12:00
2c40c10ea5 . 2025-05-02 22:33:42 +12:00
844ab60834 . 2025-05-02 22:22:52 +12:00
656c6431a5 Working on autosetup 2025-05-02 22:19:14 +12:00
9f9dd4b6e3 Magic! 2025-05-01 21:22:08 +12:00
35d3df0685 install script 2025-05-01 21:16:48 +12:00
198ddd7782 Allow hash of current directory 2025-05-01 19:41:29 +12:00
189a7c3ef1 remove bits 2025-04-30 23:36:30 +12:00
6d7a42e718 Fix ssh! 2025-04-30 23:35:02 +12:00
fce8a89491 . 2025-04-30 20:57:23 +12:00
194bde1a0d code tidying 2025-04-30 20:46:18 +12:00
32da3d5fbc . 2025-04-30 19:43:50 +12:00
feb7dc6da6 Fix segfault (infinite recursion) 2025-04-30 19:39:19 +12:00
87ab33dce9 Seg fault! 2025-04-30 19:31:50 +12:00
47d64a1a0d Tidying 2025-04-30 19:16:49 +12:00
10050f0c27 All working. 2025-04-29 23:43:54 +12:00
e6ed77dd78 Cross-platform compilation all working. 2025-04-29 23:35:56 +12:00
f5fbb85ed5 Static linking with musl!! 2025-04-29 23:12:51 +12:00
3b2f936cd2 working 2025-04-29 23:11:17 +12:00
b770558c68 Hashes match now. 2025-04-29 23:07:33 +12:00
38b3f33689 remove system xxhash 2025-04-29 22:52:52 +12:00
8c234cf88b Non dependency hashing 2025-04-29 22:49:03 +12:00
1ef4a16b66 . 2025-04-29 22:44:46 +12:00
3db05c0de9 Give up on packages for now 2025-04-29 21:56:08 +12:00
f5e2f48c64 Hmm 2025-04-29 21:44:55 +12:00
940e309e9c Fancy Version 2025-04-29 21:00:54 +12:00
e5b0d13311 . 2025-04-29 20:59:54 +12:00
ac314e9f67 . 2025-04-29 20:56:07 +12:00
11e50eae8c Niiice. 2025-04-29 20:52:17 +12:00
c1f961b96b Working 2025-04-29 20:50:10 +12:00
422e75c5d4 . 2025-04-29 20:11:49 +12:00
fb9397e8e7 . 2025-04-29 20:08:47 +12:00
106 changed files with 41292 additions and 3054 deletions

View File

@ -0,0 +1,23 @@
name: Dropshell Test
run-name: Test dropshell
on: [push]
jobs:
Build_and_Test:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y openssh-server
- name: Check out repository code
uses: actions/checkout@v4
- name: Build
run: |
cd ${{ gitea.workspace }}/docker
./compile.sh
- name: Test
run: |
cd ${{ gitea.workspace }}/docker/output
./dropshell_x86_64 list
./dropshell_x86_64 help

3
.gitignore vendored
View File

@ -1,9 +1,12 @@
# Build directories
build/
build_amd64/
build_arm64/
cmake-build-*/
out/
bin/
lib/
output/
# Compiled Object files
*.o

View File

@ -17,7 +17,9 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")
string(TIMESTAMP CURRENT_YEAR "%Y")
string(TIMESTAMP CURRENT_MONTH "%m")
string(TIMESTAMP CURRENT_DAY "%d")
set(PROJECT_VERSION "${CURRENT_YEAR}.${CURRENT_MONTH}.${CURRENT_DAY}")
string(TIMESTAMP CURRENT_HOUR "%H")
string(TIMESTAMP CURRENT_MINUTE "%M")
set(PROJECT_VERSION "${CURRENT_YEAR}.${CURRENT_MONTH}${CURRENT_DAY}.${CURRENT_HOUR}${CURRENT_MINUTE}")
string(TIMESTAMP RELEASE_DATE "%Y-%m-%d")
# Configure version.hpp file
@ -30,10 +32,6 @@ configure_file(
# Set CMAKE_MODULE_PATH to include our custom find modules
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
# Find required packages
find_package(TBB REQUIRED)
find_package(xxHash REQUIRED)
# Auto-detect source files
file(GLOB_RECURSE SOURCES "src/*.cpp")
file(GLOB_RECURSE HEADERS "src/*.hpp")
@ -42,16 +40,25 @@ 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
src
${CMAKE_CURRENT_BINARY_DIR}/src
${xxHash_INCLUDE_DIRS}
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
${CMAKE_CURRENT_SOURCE_DIR}/src/contrib
${CMAKE_CURRENT_SOURCE_DIR}/src/commands
)
if(WIN32)
add_custom_command(
TARGET dropshell POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE_DIR:dropshell>
)
endif()
# Link libraries
target_link_libraries(dropshell PRIVATE
TBB::tbb
${xxHash_LIBRARIES}
)
# Install targets
@ -126,14 +133,3 @@ install(CODE "
message(STATUS \"Command 'ds' not found. Skipping completion symlink.\")
endif()
")
# Create pre-install script to clean old templates
install(CODE "
message(STATUS \"Removing old template files...\")
execute_process(COMMAND rm -rf /opt/dropshell/templates)
")
# Install templates with pre-install script
install(DIRECTORY templates/
DESTINATION /opt/dropshell/templates
)

View File

@ -1,3 +1,9 @@
# 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
```

View File

@ -17,7 +17,9 @@ SCRIPT_DIR="$(dirname "$0")"
# // |-- service.env
# // |-- template
# // |-- (script files)
# // |-- example
# // |-- 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}")

189
agent/remote/_autocommands.sh Executable file
View File

@ -0,0 +1,189 @@
#!/bin/bash
# This script contains the common code for the autocommands.
MYID=$(id -u)
MYGRP=$(id -g)
_autocommandrun_volume() {
local command="$1"
local volume_name="$2"
local backup_folder="$3"
case "$command" in
create)
echo "Creating volume ${volume_name}"
docker volume create ${volume_name}
;;
nuke)
echo "Nuking volume ${volume_name}"
docker volume rm ${volume_name}
;;
backup)
echo "Backing up volume ${volume_name}"
docker run --rm -v ${volume_name}:/volume -v ${backup_folder}:/backup debian bash -c "tar -czvf /backup/backup.tgz -C /volume . && chown -R $MYID:$MYGRP /backup"
;;
restore)
echo "Restoring volume ${volume_name}"
docker volume rm ${volume_name}
docker volume create ${volume_name}
docker run --rm -v ${volume_name}:/volume -v ${backup_folder}:/backup debian bash -c "tar -xzvf /backup/backup.tgz -C /volume --strip-components=1"
;;
esac
}
_autocommandrun_path() {
local command="$1"
local path="$2"
local backup_folder="$3"
case "$command" in
create)
echo "Creating path ${path}"
mkdir -p ${path}
;;
nuke)
echo "Nuking 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
;;
backup)
echo "Backing up path ${path}"
if [ -d "${path}" ]; then
docker run --rm -v ${path}:/path -v ${backup_folder}:/backup debian bash -c "tar -czvf /backup/backup.tgz -C /path . && chown -R $MYID:$MYGRP /backup"
else
echo "Path ${path} does not exist - nothing to backup"
fi
;;
restore)
echo "Restoring path ${path}"
tar -xzvf ${backup_folder}/backup.tgz -C ${path} --strip-components=1
;;
esac
}
_autocommandrun_file() {
local command="$1"
local filepath="$2"
local backup_folder="$3"
case "$command" in
create)
;;
nuke)
rm -f ${filepath}
;;
backup)
echo "Backing up file ${filepath}"
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 file_name=$(basename ${filepath})
cp ${backup_folder}/${file_name} ${filepath}
;;
esac
}
_autocommandparse() {
# first argument is the command
# if the command is backup or restore, then the last two arguments are the backup file and the temporary path
# all other arguments are of form:
# key=value
# where key can be one of volume, path or file.
# value is the path or volume name.
# we iterate over the key=value arguments, and for each we call:
# autorun <command> <backupfile> <key> <value>
local command="$1"
shift
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=("$@")
local arg_count=${#args[@]}
# Process all key=value pairs
for ((i=0; i<$arg_count; i++)); do
local pair="${args[$i]}"
# Skip if not in key=value format
if [[ "$pair" != *"="* ]]; then
continue
fi
local key="${pair%%=*}"
local value="${pair#*=}"
# create backup folder unique to key/value.
local bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-')
local targetpath="${backup_temp_path}/${bfolder}"
mkdir -p ${targetpath}
# Key must be one of volume, path or file
case "$key" in
volume)
_autocommandrun_volume "$command" "$value" "$targetpath"
;;
path)
_autocommandrun_path "$command" "$value" "$targetpath"
;;
file)
_autocommandrun_file "$command" "$value" "$targetpath"
;;
*)
_die "Unknown key $key passed to auto${command}. We only support volume, path and file."
;;
esac
done
}
autocreate() {
_autocommandparse create none "$@"
}
autonuke() {
_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] [$BACKUP_TEMP_PATH] [$@]"
_autocommandparse backup "$BACKUP_TEMP_PATH" "$@"
tar zcvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" .
}
autorestore() {
_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 "$BACKUP_TEMP_PATH" "$@"
}

183
agent/remote/_common.sh Executable file
View File

@ -0,0 +1,183 @@
# COMMON FUNCTIONS
# JDE
# 2025-05-03
# This file is available TO ***ALL*** templates, as ${AGENT_PATH}/_common.sh
# ----------------------------------------------------------------------------------------------------------
# summary of functions:
# _die "message" : Prints an error message in red and exits with status code 1.
# _grey_start : Switches terminal output color to grey.
# _grey_end : Resets terminal output color from grey.
# _create_and_start_container "<run_cmd>" <container_name> : Creates/starts a container, verifying it runs.
# _create_folder <folder_path> : Creates a directory if it doesn't exist (chmod 777).
# _check_docker_installed : Checks if Docker is installed, running, and user has permission. Returns 1 on failure.
# _is_container_exists <container_name> : Checks if a container (any state) exists. Returns 1 if not found.
# _is_container_running <container_name>: Checks if a container is currently running. Returns 1 if not running.
# _get_container_id <container_name> : Prints the ID of the named container.
# _get_container_status <container_name>: Prints the status string of the named container.
# _start_container <container_name> : Starts an existing, stopped container.
# _stop_container <container_name> : Stops a running container.
# _remove_container <container_name> : Stops (if needed) and removes a container.
# _get_container_logs <container_name> : Prints the logs for a container.
# _check_required_env_vars "VAR1" ... : Checks if listed environment variables are set; calls _die() if any are missing.
# _root_remove_tree <path> : Removes a path using a root Docker container (for permissions).
# ----------------------------------------------------------------------------------------------------------
# Prints an error message in red and exits with status code 1.
_die() {
echo -e "\033[91mError: $1\033[0m"
exit 1
}
# Switches terminal output color to grey.
_grey_start() {
echo -e -n "\033[90m"
}
# Resets terminal output color from grey.
_grey_end() {
echo -e -n "\033[0m"
}
# Creates/starts a container, verifying it runs.
_create_and_start_container() {
if [ -z "$1" ] || [ -z "$2" ]; then
_die "Template error: create_and_start_container <run_cmd> <container_name>"
fi
local run_cmd="$1"
local container_name="$2"
if _is_container_exists $container_name; then
_is_container_running $container_name && return 0
_start_container $container_name
else
_grey_start
$run_cmd
_grey_end
fi
if ! _is_container_running $container_name; then
_die "Container ${container_name} failed to start"
fi
ID=$(_get_container_id $container_name)
echo "Container ${container_name} is running with ID ${ID}"
}
# Creates a directory if it doesn't exist (chmod 777).
_create_folder() {
local folder="$1"
if [ -d "$folder" ]; then
return 0
fi
if ! mkdir -p "$folder"; then
_die "Failed to create folder: $folder"
fi
chmod 777 "$folder"
echo "Folder created: $folder"
}
# Checks if Docker is installed, running, and user has permission. Returns 1 on failure.
_check_docker_installed() {
if ! command -v docker &> /dev/null; then
echo "Docker is not installed"
return 1
fi
# check if docker daemon is running
if ! docker info &> /dev/null; then
echo "Docker daemon is not running"
return 1
fi
# check if user has permission to run docker
if ! docker run --rm hello-world &> /dev/null; then
echo "User does not have permission to run docker"
return 1
fi
return 0
}
# Checks if a container (any state) exists. Returns 1 if not found.
_is_container_exists() {
if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# Checks if a container is currently running. Returns 1 if not running.
_is_container_running() {
if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# Prints the ID of the named container.
_get_container_id() {
docker ps --format "{{.ID}}" --filter "name=$1"
}
# Prints the status string of the named container.
_get_container_status() {
docker ps --format "{{.Status}}" --filter "name=$1"
}
# Starts an existing, stopped container.
_start_container() {
_is_container_exists $1 || return 1
_is_container_running $1 && return 0
docker start $1
}
# Stops a running container.
_stop_container() {
_is_container_running $1 || return 0;
docker stop $1
}
# Stops (if needed) and removes a container.
_remove_container() {
_stop_container $1
_is_container_exists $1 || return 0;
docker rm $1
}
# Prints the logs for a container.
_get_container_logs() {
if ! _is_container_exists $1; then
echo "Container $1 does not exist"
return 1
fi
docker logs $1
}
# Checks if listed environment variables are set; calls _die() if any are missing.
_check_required_env_vars() {
local required_vars=("$@")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
_die "Required environment variable $var is not set"
fi
done
}
# Removes a path using a root Docker container (for permissions).
_root_remove_tree() {
local to_remove="$1"
parent=$(dirname "$to_remove")
abs_parent=$(realpath "$parent")
child=$(basename "$to_remove")
docker run --rm -v "$abs_parent":/data alpine rm -rf "/data/$child"
}
# Load autocommands
source "${AGENT_PATH}/_autocommands.sh"

25
agent/remote/_nuke_other.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "$SCRIPT_DIR/shared/_common.sh"
A_SERVICE="$1"
A_SERVICE_PATH="$2"
# 1. Check if service directory exists on server
[ -d "$A_SERVICE_PATH" ] || _die "Service is not installed: $A_SERVICE"
# uninstall the service
if [ -f "$A_SERVICE_PATH/uninstall.sh" ]; then
$A_SERVICE_PATH/uninstall.sh
fi
# nuke the service
if [ -f "$A_SERVICE_PATH/nuke.sh" ]; then
$A_SERVICE_PATH/nuke.sh
fi
# remove the service directory
rm -rf "$A_SERVICE_PATH"

View File

@ -74,8 +74,8 @@ fi
# Configure with CMake
print_status "Configuring with CMake..."
#cmake .. -DCMAKE_BUILD_TYPE=Debug
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake .. -DCMAKE_BUILD_TYPE=Debug
#cmake .. -DCMAKE_BUILD_TYPE=Release
# Build the project
print_status "Building project..."
@ -101,17 +101,13 @@ if [ $AUTO_INSTALL = true ]; then
exit 1
fi
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
print_status "Installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
fi

View File

@ -1,38 +0,0 @@
# Find xxHash library
#
# This sets the following variables:
# xxHash_FOUND - True if xxHash was found
# xxHash_INCLUDE_DIRS - xxHash include directories
# xxHash_LIBRARIES - xxHash libraries
find_path(xxHash_INCLUDE_DIR
NAMES xxhash.h
PATHS
/usr/include
/usr/local/include
/opt/local/include
${CMAKE_INSTALL_PREFIX}/include
PATH_SUFFIXES xxhash
)
find_library(xxHash_LIBRARY
NAMES xxhash libxxhash
PATHS
/usr/lib
/usr/local/lib
/opt/local/lib
${CMAKE_INSTALL_PREFIX}/lib
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(xxHash
FOUND_VAR xxHash_FOUND
REQUIRED_VARS xxHash_LIBRARY xxHash_INCLUDE_DIR
)
if(xxHash_FOUND)
set(xxHash_LIBRARIES ${xxHash_LIBRARY})
set(xxHash_INCLUDE_DIRS ${xxHash_INCLUDE_DIR})
endif()
mark_as_advanced(xxHash_INCLUDE_DIR xxHash_LIBRARY)

8
debian/changelog vendored
View File

@ -1,8 +0,0 @@
dropshell (1.0.0-1) unstable; urgency=medium
* Initial release.
* Converted from bash script to C++ implementation
* Added proper system status monitoring
* Improved server management functionality
-- j842 Sun, 21 Apr 2025 12:00:00 +0000

15
debian/control vendored
View File

@ -1,15 +0,0 @@
Source: dropshell
Section: utils
Priority: optional
Maintainer: j842
Build-Depends: debhelper (>= 10), cmake, libboost-all-dev
Standards-Version: 4.5.0
Homepage: https://github.com/j842/dropshell
Package: dropshell
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, libboost-program-options1.74.0, libboost-filesystem1.74.0, libboost-system1.74.0
Description: A system management tool for server operations
dropshell is a command-line tool for managing and monitoring servers.
It provides functionality for checking system status, managing server
configurations, and performing common administrative tasks.

26
debian/copyright vendored
View File

@ -1,26 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dropshell
Source: https://github.com/j842/dropshell
Files: *
Copyright: 2025 j842
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

14
debian/rules vendored
View File

@ -1,14 +0,0 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_configure:
dh_auto_configure -- -DCMAKE_BUILD_TYPE=Release
override_dh_auto_install:
dh_auto_install
# Install bash completion
install -D -m 644 src/dropshell-completion.bash debian/dropshell/etc/bash_completion.d/dropshell
# Install configuration
install -D -m 644 src/dropshell.conf debian/dropshell/etc/dropshell.conf

View File

@ -1,21 +0,0 @@
FROM alpine:latest
RUN apk add --no-cache \
build-base \
cmake \
git \
ncurses-dev \
pkgconfig \
libtbb-dev \
xxhash-dev \
bash
WORKDIR /app
COPY . .
COPY --chmod=755 docker/dropshell_alpine/_create_dropshell.sh /scripts/
RUN rm -rf build
CMD ["/bin/bash","/scripts/_create_dropshell.sh"]

View File

@ -1,19 +0,0 @@
FROM alpine:latest
RUN apk add --no-cache \
libtbb \
xxhash-dev \
bash \
nano
WORKDIR /app
COPY docker/dropshell_alpine/output/dropshell /app/dropshell
RUN /bin/bash -c "mkdir -p /opt/dropshell/templates && mkdir -p /config"
COPY templates /opt/dropshell/templates
COPY src/dropshell-completion.bash /etc/bash_completion.d/
RUN /app/dropshell init /config
RUN /app/dropshell version
ENTRYPOINT ["/app/dropshell"]

View File

@ -1,12 +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
/app/build/dropshell version

View File

@ -1,23 +0,0 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ROOT_DIR=$(dirname $(dirname $SCRIPT_DIR))
echo "Building dropshell from $ROOT_DIR"
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)
docker run -it --env CHOWN_USER=$MYUID --env CHOWN_GROUP=$MYGID -v $SCRIPT_DIR/output:/output dropshell_alpine_builder
echo "dropshell built in $SCRIPT_DIR/output/dropshell"
docker build -t gitea.jde.nz/j/dropshell_alpine:latest -t dropshell_alpine $ROOT_DIR -f $SCRIPT_DIR/Dockerfile.run
docker run -it dropshell_alpine version

View File

@ -1,13 +0,0 @@
#!/bin/bash
if [ $# -lt 1 ]; then
echo " Use: example <dir>"
exit 0
fi
(
cd "$1"
echo ZG9ja2VyIHJ1biAtdHQgLS1ybSAtdi46L3ggZ2l0ZWEuamRlLm56L2ovZHJvcHNoZWxsX2FscGluZSBoYXNoIC94 | base64 -d | bash
)

View File

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

47
install.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
set -e
# download and install dropshell
# Check if running as root
if [ "$EUID" -ne 0 ]; then
print_error "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
ln -s /usr/local/bin/dropshell /usr/local/bin/ds
rm -rf "$TMPDIR"
echo "dropshell installed successfully to /usr/local/bin/dropshell"

150
install_build_prerequisites.sh Executable file
View File

@ -0,0 +1,150 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print status messages
print_status() {
echo -e "${GREEN}[*] $1${NC}"
}
print_error() {
echo -e "${RED}[!] $1${NC}"
}
print_warning() {
echo -e "${YELLOW}[!] $1${NC}"
}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
print_error "Please run this script as root (use sudo)"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
else
print_error "Could not detect distribution"
exit 1
fi
print_status "Detected OS: $OS $VER"
# Define packages based on distribution
case $OS in
"Ubuntu"|"Debian GNU/Linux")
# Common packages for both Ubuntu and Debian
PACKAGES="cmake make g++ devscripts debhelper"
;;
*)
print_error "Unsupported distribution: $OS"
exit 1
;;
esac
# Function to check if a package is installed
is_package_installed() {
dpkg -l "$1" 2>/dev/null | grep -q "^ii"
}
# Update package lists
print_status "Updating package lists..."
apt-get update
# Install missing packages
print_status "Checking and installing required packages..."
for pkg in $PACKAGES; do
if ! is_package_installed "$pkg"; then
print_status "Installing $pkg..."
apt-get install -y "$pkg"
if [ $? -ne 0 ]; then
print_error "Failed to install $pkg"
exit 1
fi
else
print_status "$pkg is already installed"
fi
done
# Verify all required tools are installed
print_status "Verifying installation..."
for tool in cmake make g++; do
if ! command -v "$tool" &> /dev/null; then
print_error "$tool is not installed properly"
exit 1
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"

View File

@ -1,98 +0,0 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print status messages
print_status() {
echo -e "${GREEN}[*] $1${NC}"
}
print_error() {
echo -e "${RED}[!] $1${NC}"
}
print_warning() {
echo -e "${YELLOW}[!] $1${NC}"
}
# Check if running as root
if [ "$EUID" -ne 0 ]; then
print_error "Please run this script as root (use sudo)"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
else
print_error "Could not detect distribution"
exit 1
fi
print_status "Detected OS: $OS $VER"
# Define packages based on distribution
case $OS in
"Ubuntu"|"Debian GNU/Linux")
# Common packages for both Ubuntu and Debian
PACKAGES="cmake make g++ devscripts debhelper libtbb-dev libxxhash-dev"
;;
*)
print_error "Unsupported distribution: $OS"
exit 1
;;
esac
# Function to check if a package is installed
is_package_installed() {
dpkg -l "$1" 2>/dev/null | grep -q "^ii"
}
# Install missing packages
print_status "Checking and installing required packages..."
for pkg in $PACKAGES; do
if ! is_package_installed "$pkg"; then
print_status "Installing $pkg..."
apt-get install -y "$pkg"
if [ $? -ne 0 ]; then
print_error "Failed to install $pkg"
exit 1
fi
else
print_status "$pkg is already installed"
fi
done
# Update package lists
print_status "Updating package lists..."
apt-get update
# Verify all required tools are installed
print_status "Verifying installation..."
for tool in cmake make g++; do
if ! command -v "$tool" &> /dev/null; then
print_error "$tool is not installed properly"
exit 1
fi
done
# Check TBB installation
if [ ! -d "/usr/include/tbb" ]; then
print_error "TBB headers not found"
exit 1
fi
# # Check Boost installation
# if [ ! -d "/usr/include/boost" ]; then
# print_error "Boost headers not found"
# exit 1
# 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; 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

@ -0,0 +1,108 @@
# can you make this script run in bash, but fall back to sh if bash is not installed?
# check if we are running as root
if [ "$(id -u)" -ne 0 ]; then
echo "Please run the script with sudo privileges or as root."
exit 1
fi
# install bash if not already installed.
if ! command -v bash > /dev/null 2>&1; then
echo "bash is not installed"
apt update
apt install -y bash
fi
# re-exec this script in bash if not already running in bash
if [ -z "$BASH_VERSION" ]; then
echo "Re-executing script in Bash..."
exec /bin/bash "$0" "$@"
fi
if [ -n "$BASH_VERSION" ]; then
echo "Running in Bash $BASH_VERSION"
else
echo "Not running in Bash"
exit 1
fi
# check if curl, wget, bash installed, and install if not
PREREQUISITES=("curl" "wget" "jq")
# check if all prerequisites are installed
ALLINSTALLED=true
for prerequisite in "${PREREQUISITES[@]}"; do
if ! command -v "${prerequisite}" &> /dev/null; then
echo "Prerequisite: ${prerequisite} is not installed."
ALLINSTALLED=false
fi
done
if [ "$ALLINSTALLED" = false ]; then
echo "Installing prerequisites..."
apt update
apt install -y "${PREREQUISITES[@]}"
fi
#--------------------------------
# check docker installation
if ! command -v docker &> /dev/null; then
echo "Docker is not installed."
echo "Installing docker..."
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
rm get-docker.sh
fi
# check dropshell user exists
if ! id "dropshell" &> /dev/null; then
echo "Dropshell user does not exist."
echo "Creating dropshell user..."
useradd -m dropshell
fi
# add dropshell user to docker group
# check if already in docker group
if ! id "dropshell" | grep -q "docker" &> /dev/null; then
echo "Adding dropshell user to docker group..."
usermod -aG docker dropshell
fi
# check .ssh/authorized_keys file exists
if [ ! -f "/home/dropshell/.ssh/authorized_keys" ]; then
echo "Creating .ssh/authorized_keys file..."
mkdir -p /home/dropshell/.ssh
cp /root/.ssh/authorized_keys /home/dropshell/.ssh/authorized_keys
chown -R dropshell:dropshell /home/dropshell/.ssh
chmod 600 /home/dropshell/.ssh/authorized_keys
fi
# also check known_hosts file exists
if [ ! -f "/home/dropshell/.ssh/known_hosts" ]; then
echo "Creating .ssh/known_hosts file..."
mkdir -p /home/dropshell/.ssh
touch /home/dropshell/.ssh/known_hosts
fi
# ensure default shell for dropshell user is bash
chsh -s /bin/bash dropshell
#--------------------------------
# download dropshell
# determine if x86_64 or arm64
ARCH=$(uname -m)
# check is aarch64 or x86_64 and error if neither
if [ "$ARCH" != "aarch64" ] && [ "$ARCH" != "x86_64" ]; then
echo "Unsupported architecture: $ARCH"
exit 1
fi
echo "Installation complete."
#--------------------------------

View File

@ -4,37 +4,63 @@
#include "templates.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/assert.hpp"
#include <assert.hpp>
#include <algorithm>
#include <iostream>
void 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 ???
{
autocomplete_list_commands();
return;
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;
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;
if (cmd=="hash")
{ // output files and folders in the current directory, one per line.
std::filesystem::directory_iterator dir_iter(std::filesystem::current_path());
for (const auto& entry : dir_iter)
std::cout << entry.path().filename().string() << std::endl;
return true;
}
if (autocomplete::is_no_arg_cmd(cmd))
return true; // no arguments needed.
if (!dropshell::gConfig().is_config_set())
return; // can't help without working config.
return false; // can't help without working config.
if (args.size()==3) // we have the command but nothing else. dropshell autocomplete command <server>
{
auto servers = dropshell::get_configured_servers();
for (const auto& server : servers)
std::cout << server.name << std::endl;
return;
std::cout << server.name << std::endl;
return true;
}
if (args.size()==4) // we have the command and the server. dropshell autocomplete command server <service>
@ -43,17 +69,16 @@ void dropshell::autocomplete(const std::vector<std::string> &args)
if (cmd=="create-service")
{ // create-service <server> <template> <service>
std::vector<template_info> templates;
get_templates(templates);
auto templates = dropshell::gTemplateManager().get_template_list();
for (const auto& t : templates)
std::cout << t.template_name << std::endl;
return;
std::cout << t << std::endl;
return true;
}
auto services = dropshell::get_server_services_info(server);
for (const auto& service : services)
std::cout << service.service_name << std::endl;
return;
return true;
}
if (args.size()==5) // we have the command and the server and the service. dropshell autocomplete command server service_name <command?>
@ -65,32 +90,31 @@ void dropshell::autocomplete(const std::vector<std::string> &args)
std::set<std::string> backups = dropshell::list_backups(server_name, service_name);
for (auto backup : backups)
std::cout << backup << std::endl;
return;
return true;
}
return; // no more autocompletion possible - don't know what the argument is for.
return false; // no more autocompletion possible - don't know what the argument is for.
}
// args>5 - no more autocompletion possible - don't know what the argument is for.
return; // catch-all.
return false; // catch-all.
}
void 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","init" // 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","edit","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;
void autocomplete(const std::vector<std::string> &args);
extern const std::set<std::string> service_commands_require_config;
void autocomplete_list_commands();
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

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

@ -0,0 +1,334 @@
#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 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
system((localpath::agent()+"bb64 -u").c_str()); // update.
}
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;
}
int install_server(const std::string &server)
{
// install the dropshell agent on the given server.
}
// ------------------------------------------------------------------------------------------------
// 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 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

@ -2,11 +2,10 @@
#include <iostream>
#include <fstream>
#include "config.hpp"
#include "utils/envmanager.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include <filesystem>
#include "utils/execute.hpp"
namespace dropshell {
@ -16,103 +15,152 @@ config & gConfig() {
}
config::config() {
config::config() : mIsConfigSet(false) {
}
config::~config() {
}
bool config::load_config() {
std::string config_path = localfile::dropshell_env();
bool config::load_config() { // load json config file.
std::string config_path = localfile::dropshell_json();
if (config_path.empty() || !std::filesystem::exists(config_path))
return false;
envmanager config_env(config_path);
if (!config_env.load())
std::ifstream config_file(config_path);
if (!config_file.is_open())
return false;
std::string directories = config_env.get_variable_substituted("local.config.directories");
if (directories.empty())
try {
mConfig = nlohmann::json::parse(config_file);
}
catch (nlohmann::json::parse_error& ex)
{
std::cerr << "Error: Failed to parse config file: " << ex.what() << std::endl;
return false;
}
// Split the directories string into a vector of strings
mLocalConfigPaths = string2multi(directories);
mLocalBackupPath = config_env.get_variable_substituted("local.backup.directory");
// legacy config file conversion
if (mLocalBackupPath.empty() && mLocalConfigPaths.size()>0)
mLocalBackupPath = mLocalConfigPaths[0] + "/backups";
//std::cout << "Local config path: " << mLocalConfigPath << std::endl;
mIsConfigSet = true;
return true;
}
void config::save_config()
bool config::save_config(bool create_aux_directories)
{
if (mLocalConfigPaths.empty())
std::string config_path = localfile::dropshell_json();
if (config_path.empty())
return false;
std::filesystem::create_directories(get_parent(config_path));
std::ofstream config_file(config_path);
if (!config_file.is_open())
return false;
if (!mIsConfigSet)
{
std::cerr << "Warning: Unable to save configuration file, as DropShell has not been initialised."<< std::endl;
std::cerr << "Please run 'dropshell init <path>' to initialise DropShell." << std::endl;
return;
std::string homedir = localpath::current_user_home();
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"
};
mConfig["template_local_paths"] = {
dropshell_base + "/local_templates"
};
mConfig["server_definition_paths"] = {
dropshell_base + "/servers"
};
mConfig["template_upload_registry_url"] = "https://templates.dropshell.app";
mConfig["template_upload_registry_token"] = "SECRETTOKEN";
}
std::string parent_path = dropshell::get_parent(localfile::dropshell_env());
std::filesystem::create_directories(parent_path);
config_file << mConfig.dump(4);
config_file.close();
std::string config_path = localfile::dropshell_env();
envmanager config_env(config_path);
if (create_aux_directories) {
std::vector<std::filesystem::path> paths = {
get_local_template_cache_path(),
get_local_backup_path(),
get_local_tempfiles_path()
};
for (auto & p : get_local_server_definition_paths())
paths.push_back(p);
config_env.set_variable("local.config.directories", multi2string(mLocalConfigPaths));
config_env.set_variable("local.backup.directory", mLocalBackupPath);
config_env.save();
for (auto & p : paths)
if (!std::filesystem::exists(p))
{
std::cout << "Creating directory: " << p << std::endl;
std::filesystem::create_directories(p);
}
}
return true;
}
bool config::is_config_set() const
{
return !mLocalConfigPaths.empty() && !mLocalBackupPath.empty();
return mIsConfigSet;
}
const std::vector<std::string> & config::get_local_config_directories() const
bool config::is_agent_installed()
{
return mLocalConfigPaths;
return std::filesystem::exists(localpath::agent() + "/bb64");
}
bool config::add_local_config_directory(const std::string &path)
{
if (path.empty())
return false;
std::string config::get_local_tempfiles_path() {
return mConfig["tempfiles"];
}
// Convert to canonical path, using std::filesystem
std::filesystem::path abs_path = std::filesystem::canonical(path);
std::string config::get_local_backup_path() {
return mConfig["backups"];
}
// The directory must exist
if (!std::filesystem::exists(abs_path)) {
std::cerr << "Error: The local config directory does not exist: " << abs_path.string() << std::endl;
return false;
std::string config::get_local_template_cache_path() {
return mConfig["template_cache"];
}
std::vector<std::string> config::get_template_registry_urls() {
nlohmann::json template_registry_urls = mConfig["template_registry_URLs"];
std::vector<std::string> urls;
for (auto &url : template_registry_urls) {
urls.push_back(url);
}
// Add to config paths if not already there
for (auto &p : mLocalConfigPaths)
{ // robustly compare the two paths.
if (p == abs_path.string())
{
std::cerr << "Warning: The local config directory is already registered: " << abs_path.string() << std::endl;
std::cerr << "No changes made to the DropShell configuration." << std::endl;
return false;
}
}
mLocalConfigPaths.push_back(abs_path.string());
if (mLocalBackupPath.empty())
mLocalBackupPath = abs_path.string() + "/backups";
return true;
return urls;
}
const std::string &config::get_local_backup_path() const
std::vector<std::string> config::get_template_local_paths()
{
return mLocalBackupPath;
nlohmann::json template_local_paths = mConfig["template_local_paths"];
std::vector<std::string> paths;
for (auto &path : template_local_paths) {
if (path.is_string() && !path.empty())
paths.push_back(path);
}
return paths;
}
std::vector<std::string> config::get_local_server_definition_paths() {
nlohmann::json server_definition_paths = mConfig["server_definition_paths"];
std::vector<std::string> paths;
for (auto &path : server_definition_paths) {
if (path.is_string() && !path.empty())
paths.push_back(path);
else
std::cerr << "Warning: Invalid server definition path: " << path << std::endl;
}
return paths;
}
std::string config::get_template_upload_registry_url() {
return mConfig["template_upload_registry_url"];
}
std::string config::get_template_upload_registry_token() {
return mConfig["template_upload_registry_token"];
}
} // namespace dropshell

View File

@ -2,6 +2,7 @@
#include <string>
#include <vector>
#include "utils/json.hpp"
namespace dropshell {
@ -9,19 +10,26 @@ class config {
public:
config();
~config();
bool load_config();
void save_config();
bool save_config(bool create_aux_directories);
bool is_config_set() const;
static bool is_agent_installed();
const std::vector<std::string> & get_local_config_directories() const;
bool add_local_config_directory(const std::string& path);
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();
const std::string & get_local_backup_path() const;
std::string get_template_upload_registry_url();
std::string get_template_upload_registry_token();
private:
std::vector<std::string> mLocalConfigPaths;
std::string mLocalBackupPath;
nlohmann::json mConfig;
bool mIsConfigSet;
};

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 */

3744
src/contrib/transwarp.hpp Normal file

File diff suppressed because it is too large Load Diff

7343
src/contrib/xxhash.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,9 @@
#include "utils/directories.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/readmes.hpp"
#include "autocomplete.hpp"
#include "main_commands.hpp"
#include "utils/hash.hpp"
#include "command_registry.hpp"
#include <filesystem>
#include <iostream>
@ -17,8 +16,9 @@
#include <vector>
#include <iomanip>
#include <chrono>
#include <assert.hpp>
#include <sstream>
#include <algorithm>
namespace dropshell {
extern const std::string VERSION;
@ -26,69 +26,95 @@ extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
void 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 << " init DIR Add DIR as a local server config directory (can add several)" << 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 << " install SERVER [SERVICE] Install/reinstall/update service(s). Non-destructive." << 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 << " COMMAND SERVER [SERVICE] Run a command on service(s)." << std::endl;
std::cout << std::endl;
std::cout << "Standard commands: install, uninstall, backup, restore, start, stop" << 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;
}
}
int die(const std::string & msg) {
std::cerr << msg << std::endl;
return 1;
}
bool parseargs(std::string arg2, std::string arg3, std::string & server_name, std::vector<ServiceInfo>& servicelist)
// ------------------------------------------------------------------------------------------------
struct ServerAndServices {
std::string server_name;
std::vector<LocalServiceInfo> servicelist;
};
bool getCLIServices(const std::string & arg2, const std::string & arg3,
ServerAndServices & server_and_services)
{
if (arg2.empty()) return false;
server_name = arg2;
server_and_services.server_name = arg2;
if (arg3.empty()) {
servicelist = get_server_services_info(server_name);
server_and_services.servicelist = get_server_services_info(arg2);
} else {
servicelist.push_back(get_service_info(server_name, arg3));
server_and_services.servicelist.push_back(get_service_info(arg2, 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;
@ -96,147 +122,121 @@ void printversion() {
std::cout << "License: " << LICENSE << std::endl;
}
#define HAPPYEXIT(CMD, RUNCMD) {if (safearg(argc,argv,1) == CMD) {RUNCMD; return 0;}}
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("makesafecmd", std::cout<<makesafecmd(safearg(argc,argv,2))<<std::endl)
HAPPYEXIT("version", printversion())
ASSERT_MSG(safearg(argc,argv,1) != "assert", "Hello! Here is an assert.");
BOOLEXIT("test-template", gTemplateManager().test_template(safearg(argc,argv,2)))
ASSERT(safearg(argc,argv,1) != "assert", "Hello! Here is an assert.");
try {
// silently attempt to load the config file.
// silently attempt to load the config file and templates.
gConfig().load_config();
if (gConfig().is_config_set())
gTemplateManager().load_sources();
if (argc < 2) {
print_help();
return 0;
}
std::string cmd = argv[1];
std::vector<std::string> argvec;
for (int i=0; i<argc; i++)
argvec.push_back(argv[i]);
if (cmd == "autocomplete") {
autocomplete(argvec);
return 0;
}
if (cmd == "init") {
return main_commands::init(argvec);
}
if (cmd == "help" || cmd == "-h" || cmd == "--help" || cmd== "h" || cmd=="halp") {
print_help();
return 0;
}
if (cmd == "edit" && argc < 3) {
std::string config_file = localfile::dropshell_env();
std::filesystem::create_directories( get_parent(config_file) );
edit_file(config_file, "Please ensure any directories you have introduced in the config file exist.");
return 0;
}
// ------------------------------------------------------------
// from here we require the config file to be loaded.
if (!gConfig().is_config_set())
return die("Please run 'dropshell init <path>' to initialise the user directory and create a configuration file.");
return die("Please run 'dropshell edit' to set up the dropshell configuration.");
const std::vector<std::string> & local_config_directories = gConfig().get_local_config_directories();
std::cout << "Config directories: ";
for (auto & dir : local_config_directories)
std::cout << "["<< dir << "] ";
std::cout << std::endl;
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") {
list_templates();
return 0;
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: ";
for (auto & dir : server_definition_paths)
std::cout << "["<< dir << "] ";
std::cout << std::endl;
}
if (gTemplateManager().is_loaded() && gTemplateManager().get_source_count() > 0)
gTemplateManager().print_sources();
HAPPYEXIT("templates", gTemplateManager().list_templates());
if (cmd == "create-template") {
if (argc < 3) return die("Error: create-template requires a template name");
create_template(argv[2]);
return 0;
return (gTemplateManager().create_template(argv[2])) ? 0 : 1;
}
if (cmd == "create-server") {
if (argc < 3) return die("Error: create-server requires a server name");
create_server(argv[2]);
return 0;
return (create_server(argv[2])) ? 0 : 1;
}
if (cmd == "create-service") {
if (argc < 5) return die("Error: not enough arguments.\ndropshell create-service server template service");
create_service(argv[2], argv[3], argv[4]);
return 0;
return (create_service(argv[2], argv[3], argv[4])) ? 0 : 1;
}
if (cmd == "ssh" && argc < 4) {
if (argc < 3) return die("Error: ssh requires a server name and optionally service name");
interactive_ssh(argv[2], "bash");
service_runner::interactive_ssh(argv[2], "bash");
return 0;
}
if (cmd == "edit" && argc < 4) {
ASSERT_MSG(argc>=3, "Error: logic error!");
edit_server(safearg(argc,argv,2));
return 0;
}
if (cmd == "backup" || cmd=="backups") {
if (argc < 4) return die("Error: backup requires a target server and target service to back up");
return main_commands::backup(argvec);
}
if (cmd == "restore") {
if (argc < 4) return die("Error: restore requires a target server, target service the backup file to restore");
return main_commands::restore(argvec);
}
// handle running a command.
std::set<std::string> commands;
get_all_used_commands(commands);
commands.merge(std::set<std::string>{"ssh","edit","_allservicesstatus"}); // handled by service_runner, but not in template_shell_commands.
for (const auto& command : commands) {
if (cmd == command) {
std::string server_name;
std::vector<ServiceInfo> servicelist;
if (!parseargs(safearg(argc, argv, 2), safearg(argc, argv, 3), server_name, servicelist)) {
std::cerr << "Error: " << command << " command requires server name and optionally service name" << std::endl;
return 1;
}
for (const auto& service_info : servicelist) {
service_runner runner(server_name, service_info.service_name);
if (!runner.isValid()) {
std::cerr << "Error: Failed to initialize service" << std::endl;
return 1;
}
if (!runner.run_command(command)) {
std::cerr << command +" failed." << std::endl;
return 1;
}
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"};
if (safe_commands.count(cmd) && argc < 4)
return die("Error: "+cmd+" requires a server name and service name. For safety, can't run on all services.");
// get all the services to run the command on.
ServerAndServices server_and_services;
if (!getCLIServices(safearg(argc, argv, 2), safearg(argc, argv, 3), server_and_services))
return die("Error: "+cmd+" command requires server name and optionally service name");
// run the command on each service.
for (const auto& service_info : server_and_services.servicelist) {
if (!SIvalid(service_info))
std::cerr<<"Error: Unable to get service information."<<std::endl;
else {
service_runner runner(server_and_services.server_name, service_info.service_name);
if (!runner.isValid())
return die("Error: Failed to initialize service");
std::vector<std::string> additional_args;
for (int i=4; i<argc; i++)
additional_args.push_back(argv[i]);
if (!runner.run_command(cmd, additional_args))
return die(cmd+" failed on service "+service_info.service_name);
}
return 0;
}
// success!
return 0;
}
// Unknown command

View File

@ -1,280 +0,0 @@
#include "main_commands.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "utils/readmes.hpp"
#include "service_runner.hpp"
#include "config.hpp"
#include "templates.hpp"
#include "server_env_manager.hpp"
#include <iostream>
#include <filesystem>
#include <algorithm>
namespace dropshell {
namespace main_commands {
static const std::string magic_string = "-_-";
int init(const std::vector<std::string> &args)
{
std::string lcd;
if (args.size() < 3) {
std::cerr << "Error: init command requires a directory argument" << std::endl;
return 1;
}
try {
if (!gConfig().add_local_config_directory(args[2]))
return 1; // error already reported
gConfig().save_config();
std::cout << "Config directory added: " << gConfig().get_local_config_directories().back() << std::endl;
dropshell::create_readme_local_config_dir(gConfig().get_local_config_directories().back());
if (gConfig().get_local_config_directories().size() ==1)
std::cout << "DropShell is now initialised and you can add a server with 'dropshell create-server <server-name>'" << std::endl;
else
{
std::cout << "DropShell will now use all of the following directories for configuration:" << std::endl;
for (const auto& dir : gConfig().get_local_config_directories()) {
std::cout << " " << dir << std::endl;
}
std::cout << "You can edit the config file manually with: dropshell edit" << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error in init: " << e.what() << std::endl;
return 1;
}
return 0;
}
int restore(const std::vector<std::string> &args, bool silent)
{
if (args.size() < 4) {
std::cerr << "Error: not enough arguments. dropshell restore <server> <service> <backup-file>" << std::endl;
return 1;
}
std::string server_name = args[2];
std::string service_name = args[3];
std::string backup_file = args[4];
ServiceInfo service_info = get_service_info(server_name, service_name);
if (service_info.local_service_path.empty()) {
std::cerr << "Error: Service not found" << std::endl;
return 1;
}
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment" << std::endl;
return 1;
}
std::string local_backups_dir = localpath::backups_path();
std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_file).string();
if (! std::filesystem::exists(local_backup_file_path)) {
std::cerr << "Error: Backup file not found at " << local_backup_file_path << std::endl;
return 1;
}
// split the backup filename into parts based on the magic string
std::vector<std::string> parts = dropshell::split(backup_file, magic_string);
if (parts.size() != 4) {
std::cerr << "Error: Backup file format is incompatible, - in one of the names?" << std::endl;
return 1;
}
std::string backup_server_name = parts[0];
std::string backup_template_name = parts[1];
std::string backup_service_name = parts[2];
std::string backup_datetime = parts[3];
if (backup_template_name != service_info.template_name) {
std::cerr << "Error: Backup template does not match service template. Can't restore." << std::endl;
return 1;
}
std::string nicedate = std::string(backup_datetime).substr(0, 10);
std::cout << "Restoring " << nicedate << " backup of " << backup_template_name << " taken from "<<backup_server_name<<", onto "<<server_name<<std::endl;
std::cout << std::endl;
std::cout << "*** ALL DATA FOR "<<server_name<<"/"<<service_name<<" WILL BE OVERWRITTEN! ***"<<std::endl;
std::cout << std::endl;
std::cout << "Are you sure you want to continue? (y/n)" << std::endl;
char confirm;
std::cin >> confirm;
if (confirm != 'y') {
std::cout << "Restore cancelled." << std::endl;
return 1;
}
// run the restore script
std::cout << "OK, here goes..." << std::endl;
{ // backup existing service
std::cout << "1) Backing up existing service... " << std::flush;
std::vector<std::string> backup_args = {"dropshell","backup",server_name, service_name};
if (!backup(backup_args,true)) // silent=true
{
std::cerr << std::endl;
std::cerr << "Error: Backup failed, restore aborted." << std::endl;
std::cerr << "You can try using dropshell install "<<server_name<<" "<<service_name<<" to install the service afresh." << std::endl;
std::cerr << "Otherwise, stop the service, create and initialise a new one, then restore to that." << std::endl;
return 1;
}
std::cout << "Backup complete." << std::endl;
}
{ // restore service from backup
std::cout << "2) Restoring service from backup..." << std::endl;
std::string remote_backups_dir = remotepath::backups(server_name);
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file;
// Copy backup file from local to server
std::string scp_cmd = "scp -P " + env.get_SSH_PORT() + " " + quote(local_backup_file_path) + " " + env.get_SSH_USER() + "@" + env.get_SSH_HOST() + ":" + quote(remote_backup_file_path) + (silent ? " > /dev/null 2>&1" : "");
if (!env.execute_local_command(scp_cmd)) {
std::cerr << "Failed to copy backup file from server" << std::endl;
return false;
}
env.run_remote_template_command(service_name, "restore", {remote_backup_file_path}, silent);
}
// healthcheck the service
std::cout << "3) Healthchecking service..." << std::endl;
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
bool healthy= (env.run_remote_template_command(service_name, "status", {}, silent));
if (!silent)
std::cout << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl;
return 0;
}
bool name_breaks_backups(std::string name)
{
// if name contains -_-, return true
return name.find("-_-") != std::string::npos;
}
// backup the service over ssh, using the credentials from server.env (via server_env.hpp)
// 1. run backup.sh on the server
// 2. create a backup file with format server-service-datetime.tgz
// 3. store it in the server's DROPSHELL_DIR/backups folder
// 4. copy it to the local user_dir/backups folder
// ------------------------------------------------------------------------------------------------
// Backup the service.
// ------------------------------------------------------------------------------------------------
int backup(const std::vector<std::string> & args, bool silent) {
if (args.size() < 4) {
std::cerr << "Error: backup command requires a server name and service name" << std::endl;
return 1;
}
std::string server_name = args[2];
std::string service_name = args[3];
ServiceInfo service_info = get_service_info(server_name, service_name);
if (service_info.local_service_path.empty()) {
std::cerr << "Error: Service not found" << std::endl;
return 1;
}
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment" << std::endl;
return 1;
}
const std::string command = "backup";
if (!template_command_exists(service_info.template_name, command)) {
std::cout << "No backup script for " << service_info.template_name << std::endl;
return true; // nothing to back up.
}
// Check if basic installed stuff is in place.
std::string remote_service_template_path = remotepath::service_template(server_name, service_name);
std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh";
std::string remote_service_config_path = remotepath::service_config(server_name, service_name);
if (!env.check_remote_items_exist({
remotepath::service(server_name, service_name),
remote_command_script_file,
remotefile::service_env(server_name, service_name)})
)
{
std::cerr << "Error: Required service directories not found on remote server" << std::endl;
std::cerr << "Is the service installed?" << std::endl;
return false;
}
// Create backups directory on server if it doesn't exist
std::string remote_backups_dir = remotepath::backups(server_name);
if (!silent) std::cout << "Remote backups directory on "<< server_name <<": " << remote_backups_dir << std::endl;
std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir);
if (!env.execute_ssh_command(mkdir_cmd)) {
std::cerr << "Failed to create backups directory on server" << std::endl;
return false;
}
// Create backups directory locally if it doesn't exist
std::string local_backups_dir = localpath::backups_path();
if (local_backups_dir.empty()) {
std::cerr << "Error: Local backups directory not found - is DropShell initialised?" << std::endl;
return false;
}
if (!std::filesystem::exists(local_backups_dir))
std::filesystem::create_directories(local_backups_dir);
// Get current datetime for backup filename
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::stringstream datetime;
datetime << std::put_time(std::localtime(&time), "%Y-%m-%d_%H-%M-%S");
if (name_breaks_backups(server_name)) {std::cerr << "Error: Server name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
if (name_breaks_backups(service_name)) {std::cerr << "Error: Service name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
if (name_breaks_backups(service_info.template_name)) {std::cerr << "Error: Service template name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
// Construct backup filename
std::string backup_filename = server_name + magic_string + service_info.template_name + magic_string + service_name + magic_string + datetime.str() + ".tgz";
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_filename;
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));
// Run backup script
if (!env.run_remote_template_command(service_name, command, {remote_backup_file_path}, silent)) {
std::cerr << "Backup script failed on remote server: " << remote_backup_file_path << std::endl;
return false;
}
// Copy backup file from server to local
std::string scp_cmd = "scp -P " + env.get_SSH_PORT() + " " +
env.get_SSH_USER() + "@" + env.get_SSH_HOST() + ":" +
quote(remote_backup_file_path) + " " + quote(local_backup_file_path) + (silent ? " > /dev/null 2>&1" : "");
if (!env.execute_local_command(scp_cmd)) {
std::cerr << "Failed to copy backup file from server" << std::endl;
return false;
}
if (!silent) {
std::cout << "Backup created successfully. Restore with:"<<std::endl;
std::cout << " dropshell restore " << server_name << " " << service_name << " " << backup_filename << std::endl;
}
return true;
}
} // namespace main_commands
} // namespace dropshell

View File

@ -1,18 +0,0 @@
#ifndef MAIN_COMMANDS_HPP
#define MAIN_COMMANDS_HPP
#include <string>
#include <vector>
namespace dropshell {
namespace main_commands {
int init(const std::vector<std::string> &args);
int restore(const std::vector<std::string> &args, bool silent=false);
int backup(const std::vector<std::string> &args, bool silent=false);
} // namespace main_commands
} // namespace dropshell
#endif

View File

@ -1,44 +1,68 @@
#include "server_env_manager.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "services.hpp"
#include "contrib/base64.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include "utils/execute.hpp"
#include <iostream>
#include <memory>
#include <filesystem>
#include <fstream>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <iostream>
#include <wordexp.h> // For potential shell-like expansion if needed
namespace dropshell {
server_env_manager::server_env_manager(const std::string& server_name) : mValid(false), mServer_name(server_name) {
server_env_manager::server_env_manager(const std::string& server_name) : mValid(false), mServerName(server_name) {
if (server_name.empty())
return;
// Construct the full path to server.env
std::string env_path = localfile::server_env(server_name);
std::string server_env_path = localfile::server_json(server_name);
// Check if file exists
if (!std::filesystem::exists(env_path)) {
std::cerr << "Server environment file not found: " + env_path << std::endl;
if (!std::filesystem::exists(server_env_path)) {
std::cerr << "Server environment file not found: " + server_env_path << " for server " << server_name << std::endl;
return;
}
try {
// Use envmanager to handle the environment file
m_env_manager = std::unique_ptr<envmanager>(new envmanager(env_path));
m_env_manager->load();
nlohmann::json server_env_json = nlohmann::json::parse(std::ifstream(server_env_path));
if (server_env_json.empty()) {
std::cerr << "Error: Failed to parse server environment file: " + server_env_path << std::endl;
return;
}
// Get all variables
m_env_manager->get_all_variables_substituted(variables);
// get the variables from the json
for (const auto& var : server_env_json.items()) {
std::string value;
if (var.value().is_string())
value = var.value();
else if (var.value().is_number_integer())
value = std::to_string(var.value().get<int>());
else if (var.value().is_boolean())
value = var.value() ? "true" : "false";
else
value = var.value().dump();
mVariables[var.key()] = replace_with_environment_variables_like_bash(value);
}
// Verify required variables exist
for (const auto& var : {"SSH_HOST", "SSH_USER", "SSH_PORT", "DROPSHELL_DIR"}) {
if (variables.find(var) == variables.end()) {
if (mVariables.find(var) == mVariables.end()) {
// Print the variables identified in the file
std::cout << "Variables identified in the file:" << std::endl;
for (const auto& v : variables) {
for (const auto& v : mVariables) {
std::cout << " " << v.first << std::endl;
}
throw std::runtime_error("Missing required variable: " + std::string(var));
@ -51,84 +75,74 @@ server_env_manager::server_env_manager(const std::string& server_name) : mValid(
}
}
bool server_env_manager::create_server_env(const std::string &server_env_path, const std::string &SSH_HOST, const std::string &SSH_USER, const std::string &SSH_PORT, const std::string &DROPSHELL_DIR)
{
nlohmann::json server_env_json;
server_env_json["SSH_HOST"] = SSH_HOST;
server_env_json["SSH_USER"] = SSH_USER;
server_env_json["SSH_PORT"] = SSH_PORT;
server_env_json["DROPSHELL_DIR"] = DROPSHELL_DIR;
try {
std::ofstream server_env_file(server_env_path);
server_env_file << server_env_json.dump(4);
server_env_file.close();
return true;
} catch (const std::exception& e) {
std::cerr << "Failed to create server environment file: " + std::string(e.what()) << std::endl;
return false;
}
}
std::string server_env_manager::get_variable(const std::string& name) const {
if (!m_env_manager) {
auto it = mVariables.find(name);
if (it == mVariables.end()) {
return "";
}
return m_env_manager->get_variable_substituted(name);
return it->second;
}
// Helper method implementations
std::string server_env_manager::construct_ssh_cmd() const {
std::stringstream ssh_cmd;
ssh_cmd << "ssh -p " << get_SSH_PORT() << " "
<< get_SSH_USER() << "@" << get_SSH_HOST();
return ssh_cmd.str();
}
std::string server_env_manager::construct_standard_command_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
{
std::string remote_service_template_path = remotepath::service_template(mServer_name,service_name);
std::string remote_service_config_path = remotepath::service_config(mServer_name,service_name);
if (command.empty())
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";
std::map<std::string, std::string> env_vars;
get_all_service_env_vars(service_name, 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 std::nullopt;
}
std::string argstr = "";
for (const auto& arg : args) {
argstr += " " + quote(dequote(trim(arg)));
}
sCommand scommand(remote_service_template_path, "bash " + quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""), env_vars);
std::string run_cmd = scommand.construct_safecmd();
return run_cmd;
}
sCommand sc(
remote_service_template_path,
quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""),
env_vars
);
void server_env_manager::get_all_service_env_vars(const std::string &service_name, std::map<std::string, std::string> & all_env_vars) const
{
all_env_vars.clear();
// add in some handy variables.
all_env_vars["CONFIG_PATH"] = remotepath::service_config(mServer_name,service_name);
all_env_vars["SERVER"] = mServer_name;
all_env_vars["SERVICE"] = service_name;
{ // load service.env from the service on this machine.
std::map<std::string, std::string> env_vars;
envmanager env_manager(localfile::service_env(mServer_name,service_name));
env_manager.load();
env_manager.get_all_variables(env_vars);
all_env_vars.merge(env_vars);
}
{ // load _default.env from the template on this machine.
std::map<std::string, std::string> env_vars;
ServiceInfo service_info = get_service_info(mServer_name, service_name);
std::string defaultenvpath = service_info.local_template_default_env_path;
if (std::filesystem::exists(defaultenvpath)) {
envmanager env_manager(defaultenvpath);
env_manager.load();
env_manager.get_all_variables(env_vars);
all_env_vars.merge(env_vars);
}
else
std::cerr << "Warning: _default.env not found in template: " << defaultenvpath << std::endl;
if (sc.empty()) {
std::cerr << "Error: Failed to construct command for " << service_name << " " << command << std::endl;
return std::nullopt;
}
return sc;
}
bool server_env_manager::check_remote_dir_exists(const std::string &dir_path) const
{
sCommand scommand("test -d " + quote(dir_path));
return execute_ssh_command(scommand);
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 {
sCommand scommand("test -f " + quote(file_path));
return execute_ssh_command(scommand);
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
@ -141,9 +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("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_ssh_command(scommand);
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;
@ -151,75 +165,67 @@ bool server_env_manager::check_remote_items_exist(const std::vector<std::string>
return true;
}
bool server_env_manager::execute_ssh_command(const sCommand& command) const {
std::string full_cmd = construct_ssh_cmd() + " " + quote(command.construct_safecmd());
return execute_local_command(full_cmd);
}
bool server_env_manager::execute_ssh_command_and_capture_output(const sCommand& command, std::string &output) const
bool server_env_manager::remove_remote_dir(const std::string &dir_path, bool silent) const
{
std::string full_cmd = construct_ssh_cmd() + " " + quote(command.construct_safecmd());
return execute_local_command_and_capture_output(full_cmd, output);
}
std::filesystem::path path(dir_path);
std::filesystem::path parent_path = path.parent_path();
std::string target_dir = path.filename().string();
bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent) const
{
std::string full_cmd = construct_standard_command_run_cmd(service_name, command, args, silent);
return execute_ssh_command(full_cmd);
}
if (parent_path.empty())
parent_path="/";
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) const
{
std::string full_cmd = construct_standard_command_run_cmd(service_name, command, args, silent);
return execute_ssh_command_and_capture_output(full_cmd, output);
}
bool server_env_manager::execute_local_command(const sCommand& command) {
return (system(command.construct_safecmd().c_str()) == 0);
}
bool server_env_manager::execute_local_command_and_capture_output(const sCommand& command, std::string &output)
{
std::string full_cmd = command.construct_safecmd() + " 2>&1";
FILE *pipe = popen(full_cmd.c_str(), "r");
if (!pipe) {
if (target_dir.empty())
return false;
}
char buffer[128];
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
output += buffer;
}
int status = pclose(pipe);
return (status == 0);
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);
}
std::string sCommand::construct_safecmd() const
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
{
std::string to_encode;
auto scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
if (!scommand.has_value())
return false;
for (const auto& env_var : mVars) {
to_encode += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " ";
}
to_encode += mCmd;
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand->add_env_var(key, value);
std::string encoded = base64_encode(to_encode);
std::string commandstr = "echo " + encoded + " | base64 -d | bash";
if (!mDir.empty())
commandstr = "cd " + quote(mDir) + " && " + commandstr;
return commandstr;
if (scommand->get_command_to_run().empty())
return false;
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
{
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);
cMode mode = cMode::CaptureOutput;
return execute_ssh_command(get_SSH_INFO(), scommand.value(), mode, &output);
}
// base64 <<< "FOO=BAR WHEE=YAY bash ./test.sh"
// echo YmFzaCAtYyAnRk9PPUJBUiBXSEVFPVlBWSBiYXNoIC4vdGVzdC5zaCcK | base64 -d | bash
std::string makesafecmd(const std::string &command)
{
std::string encoded = base64_encode(dequote(trim(command)));
std::string commandstr = "echo " + encoded + " | base64 -d | bash";
return commandstr;
}
} // namespace dropshell

View File

@ -8,33 +8,14 @@
#include <string>
#include <map>
#include <memory>
#include "utils/envmanager.hpp"
#include <vector>
#include "utils/execute.hpp"
#include <optional>
namespace dropshell {
// class to hold a command to run on the remote server.
class sCommand {
public:
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) {}
sCommand(std::string command_to_run) :
mDir(""), mCmd(command_to_run), mVars({}) {}
class server_env_manager;
std::string get_directory_to_run_in() const { return mDir; }
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) { mVars[key] = value; }
std::string construct_safecmd() const;
private:
std::string mDir;
std::string mCmd;
std::map<std::string, std::string> mVars;
};
std::string makesafecmd(const std::string& command);
// ------------------------------------------------------------------------------------------------
// reads path / server.env and provides a class to access the variables.
// each env file is required to have the following variables:
@ -46,15 +27,25 @@ std::string makesafecmd(const std::string& command);
class server_env_manager {
public:
server_env_manager(const std::string& server_name);
static bool create_server_env(
const std::string& server_env_path,
const std::string& SSH_HOST,
const std::string& SSH_USER,
const std::string& SSH_PORT,
const std::string& DROPSHELL_DIR);
std::string get_variable(const std::string& name) const;
// trivial getters.
const std::map<std::string, std::string>& get_variables() const { return variables; }
const std::map<std::string, std::string>& get_variables() const { return mVariables; }
std::string get_SSH_HOST() const { return get_variable("SSH_HOST"); }
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(), get_server_name()}; }
bool is_valid() const { return mValid; }
std::string get_server_name() const { return mServerName; }
// helper functions
public:
@ -62,30 +53,24 @@ 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 run_remote_template_command(const std::string& service_name, const std::string& command, std::vector<std::string> args, bool silent=false) 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=false) const;
bool remove_remote_dir(const std::string &dir_path, bool silent) const;
public:
bool execute_ssh_command(const sCommand& command) const;
bool execute_ssh_command_and_capture_output(const sCommand& command, std::string & output) const;
static bool execute_local_command(const sCommand& command);
static bool execute_local_command_and_capture_output(const sCommand& command, std::string & output);
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:
std::string construct_ssh_cmd() const;
std::string construct_standard_command_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:
void get_all_service_env_vars(const std::string& service_name, std::map<std::string, std::string> & all_env_vars) const;
private:
std::string mServer_name;
std::map<std::string, std::string> variables;
std::string mServerName;
std::map<std::string, std::string> mVariables;
bool mValid;
std::unique_ptr<envmanager> m_env_manager;
};
} // namespace dropshell

View File

@ -7,10 +7,11 @@
#include "services.hpp"
#include "config.hpp"
#include "templates.hpp"
#include "contrib/transwarp.hpp"
#include <iostream>
#include <fstream>
#include <iomanip>
#include <execution>
#include <filesystem>
namespace dropshell {
@ -18,18 +19,19 @@ namespace dropshell {
std::vector<ServerInfo> get_configured_servers() {
std::vector<ServerInfo> servers;
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
if (local_config_directories.empty())
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return servers;
for (int i = 0; i < local_config_directories.size(); i++) {
std::string servers_dir = localpath::config_servers(i);
for (auto servers_dir : lsdp) {
if (!servers_dir.empty() && std::filesystem::exists(servers_dir)) {
for (const auto& entry : std::filesystem::directory_iterator(servers_dir)) {
if (std::filesystem::is_directory(entry)) {
std::string server_name = entry.path().filename().string();
if (server_name.empty() || server_name[0]=='.' || server_name[0]=='_')
continue;
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << entry.path().string() << std::endl;
@ -49,113 +51,45 @@ std::vector<ServerInfo> get_configured_servers() {
return servers;
}
void list_servers() {
auto servers = get_configured_servers();
ServerInfo get_server_info(const std::string &server_name)
{
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return ServerInfo();
tableprint tp("All DropShell Servers");
tp.add_row({"Name", "Address", "Health", "Ports"});
std::for_each(std::execution::par, 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) + " ";
for (auto &config_dir : lsdp) {
std::string server_dir = config_dir + "/" + server_name;
if (std::filesystem::exists(server_dir)) {
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_dir << std::endl;
continue;
}
return ServerInfo({server_name, env.get_SSH_HOST(), env.get_SSH_USER(), env.get_SSH_PORT()});
}
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_host, serviceticks, ports_used_str});
});
tp.print();
}
return ServerInfo();
}
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
void create_server(const std::string &server_name)
bool create_server(const std::string &server_name)
{
// 1. check if server name already exists
std::string server_existing_dir = localpath::server(server_name);
if (!server_existing_dir.empty()) {
std::cerr << "Error: Server name already exists: " << server_name << std::endl;
std::cerr << "Current server path: " << server_existing_dir << std::endl;
return;
return false;
}
// 2. create a new directory in the user config directory
std::string config_servers_dir = localpath::config_servers();
std::string server_dir = config_servers_dir + "/" + server_name;
auto lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty() || lsdp[0].empty()) {
std::cerr << "Error: Local server definition path not found" << std::endl;
std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl;
return false;
}
std::string server_dir = lsdp[0] + "/" + server_name;
std::filesystem::create_directory(server_dir);
// 3. create a template server.env file in the server directory
@ -172,17 +106,13 @@ void create_server(const std::string &server_name)
// 4. add dropshell-agent service to server
create_service(server_name, "dropshell-agent", "dropshell-agent", true); // silently create service.
// std::string service_dir = server_dir + "/dropshell-agent";
// std::filesystem::create_directory(service_dir);
// std::string service_env_path = service_dir + "/service.env";
// std::filesystem::copy(get_local_system_templates_path() + "/dropshell-agent/example/service.env", service_env_path);
std::cout << "Server created successfully: " << server_name << std::endl;
std::cout << "Please complete the installation:" <<std::endl;
std::cout << "1) edit the server configuration: dropshell edit " << server_name << std::endl;
std::cout << "2) test ssh is working: dropshell ssh " << server_name << std::endl;
std::cout << "3) install dropshell-agent: dropshell install " << server_name << " dropshell-agent" << std::endl;
std::cout << std::endl;
return true;
}
void get_all_used_commands(std::set<std::string> &commands)
@ -190,7 +120,7 @@ void get_all_used_commands(std::set<std::string> &commands)
std::vector<ServerInfo> servers = get_configured_servers();
for (const auto& server : servers)
{
std::vector<dropshell::ServiceInfo> services = dropshell::get_server_services_info(server.name);
auto services = dropshell::get_server_services_info(server.name);
for (const auto& service : services)
commands.merge(dropshell::get_used_commands(server.name, service.service_name));
}

View File

@ -17,10 +17,10 @@ namespace dropshell {
};
std::vector<ServerInfo> get_configured_servers();
void list_servers();
void show_server_details(const std::string& server_name);
void create_server(const std::string& server_name);
ServerInfo get_server_info(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,10 +1,4 @@
#include "config.hpp"
#include "service_runner.hpp"
#include "server_env_manager.hpp"
#include "templates.hpp"
#include "services.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
@ -13,11 +7,24 @@
#include <iomanip>
#include <filesystem>
#include <unistd.h>
#include "utils/assert.hpp"
#include "config.hpp"
#include "service_runner.hpp"
#include "server_env_manager.hpp"
#include "templates.hpp"
#include "services.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "command_registry.hpp"
#include "shared_commands.hpp"
namespace fs = std::filesystem;
namespace dropshell {
static const std::string magic_string = "-_-";
service_runner::service_runner(const std::string& server_name, const std::string& service_name) :
mServerEnv(server_name), mServer(server_name), mService(service_name), mValid(false)
{
@ -29,116 +36,60 @@ service_runner::service_runner(const std::string& server_name, const std::string
return;
mServiceInfo = get_service_info(server_name, service_name);
if (mServiceInfo.service_name.empty())
return;
mService = mServiceInfo.service_name;
mValid = !mServiceInfo.local_template_path.empty();
}
bool service_runner::install() {
maketitle("Installing " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
bool service_runner::nuke(bool silent)
{
maketitle("Nuking " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
if (!mServerEnv.is_valid()) return false; // should never hit this.
// Check if template exists
template_info tinfo;
if (!get_template_info(mServiceInfo.template_name, tinfo))
return false;
// Create service directory
std::string remote_service_path = remotepath::service(mServer, mService);
std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path);
if (!mServerEnv.execute_ssh_command(mkdir_cmd))
bool okay = mServerEnv.run_remote_template_command("dropshell-agent", "_nuke_other", {mService, remote_service_path}, silent, {});
if (!okay)
{
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
std::cerr << "Warning: Nuke script failed" << std::endl;
return false;
}
// Check if rsync is installed on remote host
std::string check_rsync_cmd = "which rsync > /dev/null 2>&1";
if (!mServerEnv.execute_ssh_command(check_rsync_cmd))
{
std::cerr << "rsync is not installed on the remote host" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully nuked from " << mServer << std::endl;
// 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::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + mServerEnv.get_SSH_PORT() + "' " +
quote(tinfo.local_template_path + "/") + " "+
mServerEnv.get_SSH_USER() + "@" + mServerEnv.get_SSH_HOST() + ":" +
quote(remotepath::service_template(mServer, mService)+"/");
//std::cout << std::endl << rsync_cmd << std::endl << std::endl;
if (!mServerEnv.execute_local_command(rsync_cmd))
{
std::cerr << "Failed to copy template files using rsync" << std::endl;
std::cerr << "Is rsync installed on the remote host?" << std::endl;
return false;
}
if (!silent) {
std::cout << "There's nothing left on the remote server." << std::endl;
std::cout << "You can remove the local files with:" << std::endl;
std::cout << " rm -rf " << localpath::service(mServer,mService) << std::endl;
}
// Copy service files (including service.env)
{
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::string rsync_cmd = "rsync --delete -zrpc -e '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 (!mServerEnv.execute_local_command(rsync_cmd))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
}
// Run install script
{
mServerEnv.run_remote_template_command(mService, "install", {});
}
// print health tick
std::cout << "Health: " << healthtick() << std::endl;
return true;
}
bool service_runner::uninstall() {
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", {})) {
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;
bool service_runner::fullnuke()
{
if (!nuke(true))
{
std::cerr << "Warning: Nuke script failed, aborting fullnuke!" << std::endl;
return false;
}
// 4. Remove the service directory from the server
std::string rm_cmd = "'rm -rf " + quote(remotepath::service(mServer, mService)) + "'";
if (!mServerEnv.execute_ssh_command(rm_cmd)) {
std::string local_service_path = mServiceInfo.local_service_path;
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::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;
}
std::cout << "Service " << mService << " successfully uninstalled from " << mServer << std::endl;
return true;
}
@ -146,59 +97,77 @@ bool service_runner::uninstall() {
// ------------------------------------------------------------------------------------------------
// Run a command on the service.
// ------------------------------------------------------------------------------------------------
bool service_runner::run_command(const std::string& command) {
bool service_runner::run_command(const std::string& command, std::vector<std::string> additional_args, std::map<std::string, std::string> env_vars) {
if (!mServerEnv.is_valid()) {
std::cerr << "Error: Server service not initialized" << std::endl;
return false;
}
template_info tinfo;
if (!get_template_info(mServiceInfo.template_name, tinfo)) {
template_info tinfo = gTemplateManager().get_template_info(mServiceInfo.template_name);
if (!tinfo.is_set()) {
std::cerr << "Error: Template '" << mServiceInfo.template_name << "' not found" << std::endl;
return false;
}
// don't need a script for edit!
if (command == "edit") {
edit_service_config();
return true;
if (command == "fullnuke")
return fullnuke();
if (command == "nuke")
{
std::cout << "Nuking " << mService << " (" << mServiceInfo.template_name << ") on " << mServer << std::endl;
return nuke();
}
if (!template_command_exists(mServiceInfo.template_name, command)) {
if (!gTemplateManager().template_command_exists(mServiceInfo.template_name, command)) {
std::cout << "No command script for " << mServiceInfo.template_name << " : " << command << std::endl;
return true; // nothing to run.
}
// 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";
// Check if service directory exists
if (!mServerEnv.check_remote_dir_exists(remotepath::service(mServer, mService))) {
std::cerr << "Error: Service is not installed: " << mService << std::endl;
return false;
}
// Check if command script exists
if (!mServerEnv.check_remote_file_exists(script_path)) {
std::cerr << "Error: Remote command script not found: " << script_path << std::endl;
return false;
}
// Check if env file exists
if (!mServerEnv.check_remote_file_exists(remotefile::service_env(mServer, mService))) {
std::cerr << "Error: Service config file not found: " << remotefile::service_env(mServer, mService) << std::endl;
return false;
}
if (command == "uninstall")
return uninstall();
// if (command == "uninstall")
// return uninstall();
if (command == "ssh") {
interactive_ssh_service();
return true;
}
if (command == "restore") {
if (additional_args.size() < 1) {
std::cerr << "Error: restore requires a backup file:" << std::endl;
std::cerr << "dropshell restore <server> <service> <backup-file>" << std::endl;
return false;
}
return restore(additional_args[0], false);
}
if (command == "backup") {
return backup(false);
}
// Run the generic command
std::vector<std::string> args; // not passed through yet.
return mServerEnv.run_remote_template_command(mService, command, args);
return mServerEnv.run_remote_template_command(mService, command, args, false, env_vars);
}
@ -209,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 (!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;
@ -222,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))
if (!env.run_remote_template_command_and_capture_output(service_name, "shared/"+command, {}, output, true, {}))
return status;
std::stringstream ss(output);
@ -259,191 +228,324 @@ 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:";
}
bool service_runner::ensure_service_dropshell_files_up_to_date()
{
if (!mServerEnv.is_valid()) {
std::cerr << "Error: Server service not initialized" << std::endl;
return false;
}
// check if the service template and config are up to date on the remote server.
service_versions versions(mServer, mService);
if (versions.remote_up_to_date())
return true;
if (!versions.remote_template_is_up_to_date()) {
std::cerr << "Error: Service template is not up to date on the remote server" << std::endl;
return false;
}
if (!versions.remote_config_is_up_to_date()) {
std::cerr << "Error: Service config is not up to date on the remote server" << std::endl;
return false;
}
// TODO - actually update things!
versions.update_stored_remote_versions();
return versions.remote_up_to_date();
}
std::string service_runner::healthmark()
{
HealthStatus status = is_healthy();
return HealthStatus2String(status);
}
void interactive_ssh(const std::string & server_name, const std::string & command) {
bool service_runner::interactive_ssh(const std::string & server_name, const std::string & command) {
std::string serverpath = localpath::server(server_name);
if (serverpath.empty()) {
std::cerr << "Error: Server not found: " << server_name << std::endl;
return;
return false;
}
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
return;
return false;
}
std::string ssh_address = env.get_SSH_HOST();
std::string ssh_user = env.get_SSH_USER();
std::string ssh_port = env.get_SSH_PORT();
std::string login = ssh_user + "@" + ssh_address;
// Execute ssh with server_name and command
if (command.empty())
execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), nullptr);
else
execlp("ssh", "ssh", "-tt", login.c_str(), "-p", ssh_port.c_str(), command.c_str(), nullptr);
// If exec returns, it means there was an error
perror("ssh execution failed");
exit(EXIT_FAILURE);
sCommand scommand("", "bash",{});
return execute_ssh_command(env.get_SSH_INFO(), scommand, cMode::Interactive);
}
void 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";
edit_file(config_file, aftertext.str());
}
void edit_file(const std::string &file_path, const std::string & aftertext)
{
std::string cmd = "nano -w "+file_path+" ; echo \""+aftertext+"\"";
execlp("bash","bash","-c",cmd.c_str(), nullptr);
// If exec returns, it means there was an error
perror("ssh execution failed");
exit(EXIT_FAILURE);
}
bool service_runner::restore(std::string backup_file)
{
std::string command = "restore";
std::string script_path = remotepath::service_template(mServer, mService) + "/" + command + ".sh";
if (!template_command_exists(mServiceInfo.template_name, command)) {
std::cout << "No restore script for " << mServiceInfo.template_name << std::endl;
return true; // nothing to restore.
}
/// TOODOOOOOO!!!!!!
std::cout << "Restore not implemented yet" << std::endl;
return true;
// std::string run_cmd = construct_standard_command_run_cmd("restore");
// return execute_ssh_command(run_cmd, "Restore script failed");
}
void service_runner::interactive_ssh_service()
bool service_runner::interactive_ssh_service()
{
std::set<std::string> used_commands = get_used_commands(mServer, mService);
if (used_commands.find("ssh") == used_commands.end()) {
std::cerr << "Error: "<< mService <<" does not support ssh" << std::endl;
return;
return false;
}
std::vector<std::string> args; // not passed through yet.
mServerEnv.run_remote_template_command(mService, "ssh", args);
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;
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)
{
if (backup_file.empty()) {
std::cerr << "Error: not enough arguments. dropshell restore <server> <service> <backup-file>" << std::endl;
return false;
}
std::string aftertext = "To apply your changes, run:\n dropshell install " + mServer + " " + mService;
std::string local_backups_dir = gConfig().get_local_backup_path();
edit_file(config_file, aftertext);
if (backup_file == "latest") {
// get the latest backup file from the server
backup_file = get_latest_backup_file(mServer, mService);
}
std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_file).string();
if (! std::filesystem::exists(local_backup_file_path)) {
std::cerr << "Error: Backup file not found at " << local_backup_file_path << std::endl;
return false;
}
// split the backup filename into parts based on the magic string
std::vector<std::string> parts = dropshell::split(backup_file, magic_string);
if (parts.size() != 4) {
std::cerr << "Error: Backup file format is incompatible, - in one of the names?" << std::endl;
return false;
}
std::string backup_server_name = parts[0];
std::string backup_template_name = parts[1];
std::string backup_service_name = parts[2];
std::string backup_datetime = parts[3];
if (backup_template_name != mServiceInfo.template_name) {
std::cerr << "Error: Backup template does not match service template. Can't restore." << std::endl;
return false;
}
std::string nicedate = std::string(backup_datetime).substr(0, 10);
std::cout << "Restoring " << nicedate << " backup of " << backup_template_name << " taken from "<<backup_server_name<<", onto "<<mServer<<"/"<<mService<<std::endl;
std::cout << std::endl;
std::cout << "*** ALL DATA FOR "<<mServer<<"/"<<mService<<" WILL BE OVERWRITTEN! ***"<<std::endl;
// run the restore script
std::cout << "OK, here goes..." << std::endl;
{ // backup existing service
maketitle("1) Backing up old service... ");
if (!backup(true)) // silent=true
{
std::cerr << std::endl;
std::cerr << "Error: Backup failed, restore aborted." << std::endl;
std::cerr << "You can try using dropshell install "<<mServer<<" "<<mService<<" to install the service afresh." << std::endl;
std::cerr << "Otherwise, stop the service, create and initialise a new one, then restore to that." << std::endl;
return false;
}
std::cout << "Backup complete." << std::endl;
}
{ // uninstall service, then nuke it.
maketitle("2) Uninstalling old service...");
// if (!uninstall(true))
// return false;
maketitle("3) Nuking old service...");
// if (!nuke(true))
// return false;
}
{ // restore service from backup
maketitle("4) Restoring service data from backup...");
std::string remote_backups_dir = remotepath::backups(mServer);
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_file;
// Copy backup file from local to server
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;
}
cRemoteTempFolder remote_temp_folder(mServerEnv);
mServerEnv.run_remote_template_command(mService, "restore", {}, silent, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}});
} // dtor of remote_temp_folder will clean up the temp folder on the server
{ // installing fresh service
maketitle("5) Non-destructive install of fresh service...");
if (!install_service(mServer, mService, true))
return false;
}
bool healthy = false;
{// healthcheck the service
maketitle("6) Healthchecking service...");
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
healthy= (mServerEnv.run_remote_template_command(mService, "status", {}, silent, {}));
if (!silent)
std::cout << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl;
}
return healthy;
}
bool name_breaks_backups(std::string name)
{
// if name contains -_-, return true
return name.find("-_-") != std::string::npos;
}
// backup the service over ssh, using the credentials from server.env (via server_env.hpp)
// 1. run backup.sh on the server
// 2. create a backup file with format server-service-datetime.tgz
// 3. store it in the server's DROPSHELL_DIR/backups folder
// 4. copy it to the local user_dir/backups folder
// ------------------------------------------------------------------------------------------------
// Backup the service.
// ------------------------------------------------------------------------------------------------
bool service_runner::backup(bool silent) {
auto service_info = get_service_info(mServer, mService);
if (service_info.local_service_path.empty()) {
std::cerr << "Error: Service not found" << std::endl;
return 1;
}
const std::string command = "backup";
if (!gTemplateManager().template_command_exists(service_info.template_name, command)) {
std::cout << "No backup script for " << service_info.template_name << std::endl;
return true; // nothing to back up.
}
// Check if basic installed stuff is in place.
std::string remote_service_template_path = remotepath::service_template(mServer, mService);
std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh";
std::string remote_service_config_path = remotepath::service_config(mServer, mService);
if (!mServerEnv.check_remote_items_exist({
remotepath::service(mServer, mService),
remote_command_script_file,
remotefile::service_env(mServer, mService)})
)
{
std::cerr << "Error: Required service directories not found on remote server" << std::endl;
std::cerr << "Is the service installed?" << std::endl;
return false;
}
// 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;
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;
}
// Create backups directory locally if it doesn't exist
std::string local_backups_dir = gConfig().get_local_backup_path();
if (local_backups_dir.empty()) {
std::cerr << "Error: Local backups directory not found" << std::endl;
std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl;
return false;
}
if (!std::filesystem::exists(local_backups_dir))
std::filesystem::create_directories(local_backups_dir);
// Get current datetime for backup filename
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::stringstream datetime;
datetime << std::put_time(std::localtime(&time), "%Y-%m-%d_%H-%M-%S");
if (name_breaks_backups(mServer)) {std::cerr << "Error: Server name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
if (name_breaks_backups(mService)) {std::cerr << "Error: Service name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
if (name_breaks_backups(service_info.template_name)) {std::cerr << "Error: Service template name contains invalid character sequence ( -_- ) that would break backup naming scheme" << std::endl; return 1;}
// Construct backup filename
std::string backup_filename = mServer + magic_string + service_info.template_name + magic_string + mService + magic_string + datetime.str() + ".tgz";
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_filename;
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), "Invalid backup filename");
{ // Run backup script
cRemoteTempFolder remote_temp_folder(mServerEnv);
if (!mServerEnv.run_remote_template_command(mService, command, {}, silent, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}})) {
std::cerr << "Backup script failed on remote server: " << remote_backup_file_path << std::endl;
return false;
}
// Copy backup file from server to local
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;
}
} // dtor of remote_temp_folder will clean up the temp folder on the server
if (!silent) {
std::cout << "Backup created successfully. Restore with:"<<std::endl;
std::cout << " dropshell restore " << mServer << " " << mService << " " << backup_filename << std::endl;
}
return true;
}
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::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;
}
cRemoteTempFolder::~cRemoteTempFolder()
{
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
{
return mPath;
}
// Helper function to get the latest backup file for a given server and service
std::string service_runner::get_latest_backup_file(const std::string& server, const std::string& service) {
std::string local_backups_dir = gConfig().get_local_backup_path();
if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir)) {
std::cerr << "Error: Local backups directory not found: " << local_backups_dir << std::endl;
return "";
}
// Get the template name for this service
LocalServiceInfo info = get_service_info(server, service);
if (info.template_name.empty()) {
std::cerr << "Error: Could not determine template name for service: " << service << std::endl;
return "";
}
// Build the expected prefix for backup files
std::string prefix = server + magic_string + info.template_name + magic_string + service + magic_string;
std::string latest_file;
std::string latest_datetime;
std::cout << "Looking for backup files in " << local_backups_dir << std::endl;
for (const auto& entry : std::filesystem::directory_iterator(local_backups_dir)) {
if (!entry.is_regular_file()) continue;
std::string filename = entry.path().filename().string();
if (filename.rfind(prefix, 0) == 0) { // starts with prefix
// Extract the datetime part
size_t dt_start = prefix.size();
size_t dt_end = filename.find(".tgz", dt_start);
if (dt_end == std::string::npos) continue;
std::string datetime = filename.substr(dt_start, dt_end - dt_start);
std::cout << "Found backup file: " << filename << " with datetime: " << datetime << std::endl;
if (datetime > latest_datetime) {
latest_datetime = datetime;
latest_file = filename;
}
}
}
if (latest_file.empty()) {
std::cerr << "Error: No backup files found for " << server << ", " << service << std::endl;
}
std::cout << "Latest backup file: " << latest_file << std::endl;
return latest_file;
}
} // namespace dropshell

View File

@ -12,7 +12,6 @@
#include "server_env_manager.hpp"
#include "services.hpp"
#include "utils/utils.hpp"
#include "utils/assert.hpp"
#include "utils/hash.hpp"
namespace dropshell {
@ -30,6 +29,7 @@ typedef struct ServiceStatus {
std::vector<int> ports;
} ServiceStatus;
class service_runner {
public:
service_runner(const std::string& server_name, const std::string& service_name);
@ -44,7 +44,7 @@ class service_runner {
// checking that the command exists in the service directory.
// checking that the command is a valid .sh file.
// checking that the {service_name}.env file exists in the service directory.
bool run_command(const std::string& command);
bool run_command(const std::string& command, std::vector<std::string> additional_args={}, std::map<std::string, std::string> env_vars={});
// check health of service. Silent.
// 1. run status.sh on the server
@ -55,46 +55,31 @@ class service_runner {
std::string healthtick();
std::string healthmark();
// get the status of all services on the server
static std::map<std::string, ServiceStatus> get_all_services_status(std::string server_name);
static std::string HealthStatus2String(HealthStatus status);
public:
// backup and restore
bool backup(bool silent=false);
bool restore(std::string backup_file, bool silent=false);
// ensure the service related dropshell files (template and config) are up to date on the remote server.
bool ensure_service_dropshell_files_up_to_date();
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();
// 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();
// restore the service over ssh, using the credentials from server.env (via server_env.hpp)
// 1. copy the backup file to the server's DROPSHELL_DIR/backups folder
// 2. run the restore.sh script on the server, passing the {service_name}.env file as an argument
bool restore(std::string backup_file);
// nuke the service
bool nuke(bool silent=false); // nukes all data for this service on the remote server
bool fullnuke(); // nuke all data for this service on the remote server, and then nukes all the local service definitionfiles
// launch an interactive ssh session on a server or service
// replaces the current dropshell process with the ssh process
void interactive_ssh_service();
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 std::map<std::string, ServiceStatus> get_all_services_status(std::string server_name);
private:
std::string mServer;
server_env_manager mServerEnv;
ServiceInfo mServiceInfo;
LocalServiceInfo mServiceInfo;
std::string mService;
bool mValid;
@ -102,38 +87,15 @@ class service_runner {
public:
};
// other utility routines (not specific to service_runner)
void interactive_ssh(const std::string & server_name, const std::string & command);
void edit_server(const std::string & server_name);
void edit_file(const std::string & file_path, const std::string & aftertext);
// check if the service template and config are up to date on the remote server.
class service_versions {
public:
service_versions(const std::string & server_name, const std::string & service_name);
bool remote_up_to_date() { return remote_template_is_up_to_date() && remote_config_is_up_to_date(); }
bool remote_template_is_up_to_date() { return get_version_remote_service_template() == calculate_version_local_service_template(); }
bool remote_config_is_up_to_date() { return get_version_remote_config() == calculate_version_local_config(); }
XXH64_hash_t calculate_version_local_service_template();
XXH64_hash_t calculate_version_local_config();
void update_stored_remote_versions();
XXH64_hash_t get_version_remote_config();
XXH64_hash_t get_version_remote_service_template();
private:
XXH64_hash_t calculate_version_remote_service_template();
XXH64_hash_t calculate_version_remote_config();
private:
std::string m_server_name;
std::string m_service_name;
};
class cRemoteTempFolder {
public:
cRemoteTempFolder(const server_env_manager & server_env); // create a temp folder on the remote server
~cRemoteTempFolder(); // delete the temp folder on the remote server
std::string path() const; // get the path to the temp folder on the remote server
private:
std::string mPath;
const server_env_manager & mServerEnv;
};
} // namespace dropshell

View File

@ -1,70 +0,0 @@
#include "service_runner.hpp"
#include "utils/hash.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "templates.hpp"
#include <fstream>
#include <filesystem>
#include <unistd.h>
namespace fs = std::filesystem;
namespace dropshell {
service_versions::service_versions(const std::string & server_name, const std::string & service_name)
: m_server_name(server_name), m_service_name(service_name)
{
}
XXH64_hash_t service_versions::calculate_version_local_service_template()
{
template_info tinfo;
if (!get_template_info(m_service_name, tinfo)) {
return 0;
}
return hash_directory_recursive(tinfo.local_template_path);
}
XXH64_hash_t service_versions::calculate_version_local_config()
{
std::string config_path = localpath::service(m_server_name, m_service_name);
if (config_path.empty() || !fs::exists(config_path)) {
return 0;
}
return hash_directory_recursive(config_path);
}
void service_versions::update_stored_remote_versions()
{
XXH64_hash_t template_hash = calculate_version_remote_service_template();
XXH64_hash_t config_hash = calculate_version_remote_config();
// TODO - actually update things!
}
XXH64_hash_t service_versions::get_version_remote_config()
{
// TODO - actually get the version!
return 0;
}
XXH64_hash_t service_versions::get_version_remote_service_template()
{
// TODO - actually get the version!
return 0;
}
XXH64_hash_t service_versions::calculate_version_remote_service_template()
{
// TODO - actually get the version!
return 0;
}
XXH64_hash_t service_versions::calculate_version_remote_config()
{
// TODO - actually get the version!
return 0;
}
} // namespace dropshell

View File

@ -5,6 +5,8 @@
#include "config.hpp"
#include "utils/utils.hpp"
#include "server_env_manager.hpp"
#include "servers.hpp"
#include <iostream>
#include <filesystem>
@ -12,90 +14,82 @@ namespace fs = std::filesystem;
namespace dropshell {
std::vector<ServiceInfo> get_server_services_info(const std::string& server_name) {
std::vector<ServiceInfo> services;
bool SIvalid(const LocalServiceInfo& service_info) {
return !service_info.service_name.empty() &&
!service_info.template_name.empty() &&
!service_info.local_service_path.empty() &&
!service_info.local_template_path.empty();
}
std::vector<LocalServiceInfo> get_server_services_info(const std::string& server_name) {
std::vector<LocalServiceInfo> services;
if (server_name.empty())
return services;
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
if (local_config_directories.empty()) {
std::cerr << "Error: No local config directories found" << std::endl;
std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl;
std::vector<std::string> local_server_definition_paths = gConfig().get_local_server_definition_paths();
if (local_server_definition_paths.empty()) {
std::cerr << "Error: No local server definition paths found" << std::endl;
std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl;
return services;
}
for (int i = 0; i < localpath::num_config_directories(); i++) {
std::string serverpath = localpath::config_servers(i);
if (serverpath.empty()) {
std::cerr << "Error: Server directory not found: " << serverpath << std::endl;
return services;
}
fs::path server_dir = fs::path(serverpath) / server_name;
if (fs::exists(server_dir)) {
for (const auto& entry : fs::directory_iterator(server_dir)) {
for (const auto& server_definition_path : local_server_definition_paths) {
fs::path serverpath = server_definition_path + "/" + server_name;
if (fs::exists(serverpath)) // service is on that server...
for (const auto& entry : fs::directory_iterator(serverpath)) {
if (fs::is_directory(entry)) {
ServiceInfo service = get_service_info(server_name, entry.path().filename().string());
if (!service.template_name.empty()) {
std::string dirname = entry.path().filename().string();
if (dirname.empty() || dirname[0] == '.' || dirname[0] == '_')
continue;
auto service = get_service_info(server_name, dirname);
if (!service.local_service_path.empty())
services.push_back(service);
}
else
std::cerr << "Warning: Failed to get service info for " << dirname << " on server " << server_name << std::endl;
}
}
} // end of for (int i = 0; i < getNumConfigDirectories(); i++)
} // end of for
}
return services;
}
ServiceInfo get_service_info(const std::string &server_name, const std::string &service_name)
LocalServiceInfo get_service_info(const std::string &server_name, const std::string &service_name)
{
ServiceInfo service;
LocalServiceInfo service;
if (server_name.empty() || service_name.empty())
return ServiceInfo();
return LocalServiceInfo();
service.service_name = service_name;
service.local_service_path = localpath::service(server_name, service_name);
if (service.local_service_path.empty())
return ServiceInfo();
return LocalServiceInfo();
// now set the template name and path.
std::string local_service_env_path = localfile::service_env(server_name, service_name);
envmanager env(local_service_env_path);
if (!env.load()) {
if (std::filesystem::exists(localpath::service(server_name, service_name)))
std::cerr << "Error: service malformed - service.env missing from " << local_service_env_path << std::endl;
else
{
template_info tinfo;
get_template_info(service_name, tinfo);
std::string template_name = service_name;
if (tinfo.local_template_path.empty())
template_name = "TEMPLATE";
std::cerr << "Error: you need to create that service first, with: dropshell create-service " << server_name << " "<<template_name<<" " << service_name << std::endl;
}
return ServiceInfo();
}
service.template_name = env.get_variable("TEMPLATE");
std::map<std::string, std::string> variables;
if (!get_all_service_env_vars(server_name, service_name, variables))
return LocalServiceInfo();
if (service.template_name.empty()) {
std::cerr << "Error: TEMPLATE variable not defined in " << local_service_env_path << std::endl;
return ServiceInfo();
// confirm TEMPLATE is defined.
auto it = variables.find("TEMPLATE");
if (it == variables.end()) {
std::cerr << "Error: TEMPLATE variable not defined in service " << service_name << " on server " << server_name << std::endl;
return LocalServiceInfo();
}
service.template_name = it->second;
template_info tinfo;
if (!get_template_info(service.template_name, tinfo)) {
template_info tinfo = gTemplateManager().get_template_info(service.template_name);
if (!tinfo.is_set()) {
std::cerr << "Error: Template '" << service.template_name << "' not found" << std::endl;
return ServiceInfo();
return LocalServiceInfo();
}
// find the template path
service.local_template_path = tinfo.local_template_path;
service.local_template_default_env_path = tinfo.local_template_path + "/_default.env";
service.local_template_path = tinfo.local_template_path();
return service;
}
@ -107,7 +101,7 @@ std::set<std::string> get_used_commands(const std::string &server_name, const st
if (server_name.empty() || service_name.empty())
return commands;
ServiceInfo service_info = get_service_info(server_name, service_name);
auto service_info = get_service_info(server_name, service_name);
if (service_info.local_template_path.empty()) {
std::cerr << "Error: Service not found: " << service_name << std::endl;
return commands;
@ -130,13 +124,13 @@ std::set<std::string> list_backups(const std::string &server_name, const std::st
return backups;
// need to find the template for the service.
ServiceInfo service_info = get_service_info(server_name, service_name);
auto service_info = get_service_info(server_name, service_name);
if (service_info.local_template_path.empty()) {
std::cerr << "Error: Service not found: " << service_name << std::endl;
return backups;
}
std::string backups_dir = localpath::backups_path();
std::string backups_dir = gConfig().get_local_backup_path();
if (backups_dir.empty())
return backups;
@ -181,8 +175,8 @@ bool create_service(const std::string &server_name, const std::string &template_
return false;
}
template_info tinfo;
if (!get_template_info(template_name, tinfo))
template_info tinfo = gTemplateManager().get_template_info(template_name);
if (!tinfo.is_set())
{
if (!silent)
{
@ -194,21 +188,95 @@ bool create_service(const std::string &server_name, const std::string &template_
return false;
}
// check template is all good.
if (!gTemplateManager().test_template(tinfo.local_template_path()))
{
if (!silent)
std::cerr << "Error: Template '" << template_name << "' is not valid" << std::endl;
return false;
}
// create the service directory
fs::create_directory(service_dir);
// copy the template example files to the service directory
recursive_copy(tinfo.local_template_path+"/example", service_dir);
// copy the template config files to the service directory
recursive_copy(tinfo.local_template_path()/"config", service_dir);
if (!silent)
{
std::cout << "Service " << service_name <<" created successfully"<<std::endl;
std::cout << std::endl;
std::cout << "To complete the installation, please:" << std::endl;
std::cout << "1. edit the service.env file: dropshell edit " << server_name << " " << service_name << std::endl;
std::cout << "1. edit the service config file: dropshell edit " << server_name << " " << service_name << std::endl;
std::cout << "2. install the remote service: dropshell install " << server_name << " " << service_name << std::endl;
}
return true;
}
bool get_all_service_env_vars(const std::string &server_name, const std::string &service_name, std::map<std::string, std::string> & all_env_vars)
{
all_env_vars.clear();
if (localpath::service(server_name, service_name).empty() || !fs::exists(localpath::service(server_name, service_name)))
{
std::cerr << "Error: Service not found: " << service_name << std::endl;
return false;
}
// add in some handy variables.
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::agent(server_name);
ServerInfo server_info = get_server_info(server_name);
if (server_info.ssh_host.empty())
std::cerr << "Error: Server " << server_name << " not found - ssh_host empty, so HOST_NAME not set" << std::endl;
all_env_vars["HOST_NAME"] = server_info.ssh_host;
// Lambda function to load environment variables from a file
auto load_env_file = [&all_env_vars](const std::string& file) {
if (!file.empty() && std::filesystem::exists(file)) {
std::map<std::string, std::string> env_vars;
envmanager env_manager(file);
env_manager.load();
env_manager.get_all_variables(env_vars);
all_env_vars.merge(env_vars);
}
else
std::cout << "Warning: Expected environment file not found: " << file << std::endl;
};
// Load environment files
load_env_file(localfile::service_env(server_name, service_name));
load_env_file(localfile::template_info_env(server_name, service_name));
// determine template name.
auto it = all_env_vars.find("TEMPLATE");
if (it == all_env_vars.end()) {
std::cerr << std::endl << std::endl;
std::cerr << "Error: TEMPLATE variable not defined in service " << service_name << " on server " << server_name << std::endl;
std::cerr << "The TEMPLATE variable is required to determine the template name." << std::endl;
std::cerr << "Please check the service.env file and the .template_info.env file in:" << std::endl;
std::cerr << " " << localpath::service(server_name, service_name) << std::endl << std::endl;
return false;
}
template_info tinfo = gTemplateManager().get_template_info(it->second);
if (!tinfo.is_set()) {
std::cerr << "Error: Template '" << it->second << "' not found" << std::endl;
return false;
}
std::string default_env_file = tinfo.local_template_path()/"_default.env";
if (!fs::exists(default_env_file)) {
std::cerr << "Error: Template default env file '" << default_env_file << "' not found" << std::endl;
return false;
}
load_env_file(default_env_file);
return true;
}
} // namespace dropshell

View File

@ -4,21 +4,27 @@
#include <string>
#include <vector>
#include <set>
#include <map>
namespace dropshell {
struct ServiceInfo {
struct LocalServiceInfo {
std::string service_name;
std::string template_name;
std::string local_service_path;
std::string local_template_path;
std::string local_template_default_env_path;
};
std::vector<ServiceInfo> get_server_services_info(const std::string& server_name);
ServiceInfo get_service_info(const std::string& server_name, const std::string& service_name);
bool SIvalid(const LocalServiceInfo& service_info);
std::vector<LocalServiceInfo> get_server_services_info(const std::string& server_name);
LocalServiceInfo get_service_info(const std::string& server_name, const std::string& service_name);
std::set<std::string> get_used_commands(const std::string& server_name, const std::string& service_name);
// get all env vars for a given service
bool get_all_service_env_vars(const std::string& server_name, const std::string& service_name, std::map<std::string, std::string> & all_env_vars);
// list all backups for a given service (across all servers)
std::set<std::string> list_backups(const std::string& server_name, const std::string& service_name);

View File

@ -1,7 +1,3 @@
#include "templates.hpp"
#include "config.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include <filesystem>
#include <iostream>
#include <fstream>
@ -9,12 +5,23 @@
#include <string>
#include <algorithm>
#include <iomanip>
#include <map>
#include "utils/assert.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "templates.hpp"
#include "config.hpp"
namespace dropshell {
// ------------------------------------------------------------------------------------------------
// template_source_local
// ------------------------------------------------------------------------------------------------
bool get_templates(std::vector<template_info>& templates) {
templates.clear();
std::set<std::string> template_source_local::get_template_list() {
std::set<std::string> templates;
// Helper function to add templates from a directory
auto add_templates_from_dir = [&templates](const std::string& dir_path) {
@ -22,68 +29,84 @@
return;
for (const auto& entry : std::filesystem::directory_iterator(dir_path))
if (entry.is_directory()) {
template_info info(entry.path().filename().string(), entry.path().string());
// Check if template with same name already exists
bool duplicate = false;
auto it = std::find_if(templates.begin(), templates.end(),
[&info](const template_info& t) { return t.template_name == info.template_name; });
duplicate = (it!=templates.end());
if (!duplicate)
templates.push_back(info);
}
if (entry.is_directory())
templates.insert(entry.path().filename().string());
};
// add templates from the local config directories
std::vector<std::string> template_config_directories;
get_all_template_config_directories(template_config_directories);
for (const auto& path : template_config_directories) {
add_templates_from_dir(path);
}
return true;
add_templates_from_dir(mLocalPath);
return templates;
}
bool get_template_info(const std::string& template_name, template_info& info) {
// add templates from the local config directories
std::vector<std::string> paths_to_search;
get_all_template_config_directories(paths_to_search);
for (const auto& path : paths_to_search) {
std::filesystem::path full_path = path + "/" + template_name;
if (std::filesystem::exists(full_path))
{
info.template_name = template_name;
info.local_template_path = full_path.string();
return true;
}
}
std::cout << "Warning: Template '" << template_name << "' not found" << std::endl;
return false;
}
bool template_command_exists(const std::string &template_name, const std::string &command)
{
template_info info;
if (!get_template_info(template_name, info)) {
return false;
}
std::string path = info.local_template_path + "/" + command + ".sh";
bool template_source_local::has_template(const std::string& template_name) {
std::filesystem::path path = mLocalPath / template_name;
return (std::filesystem::exists(path));
}
void list_templates() {
std::vector<template_info> templates;
bool template_source_local::template_command_exists(const std::string& template_name, const std::string& command) {
std::filesystem::path path = mLocalPath / template_name / (command+".sh");
return std::filesystem::exists(path);
}
if (!get_templates(templates)) {
std::cerr << "Error: Failed to get templates" << std::endl;
return;
}
template_info template_source_local::get_template_info(const std::string& template_name) {
std::filesystem::path path = mLocalPath / template_name;
if (!std::filesystem::exists(path))
return template_info();
return template_info(
template_name,
mLocalPath.string(),
path
);
}
// ------------------------------------------------------------------------------------------------
// template_source_registry
// ------------------------------------------------------------------------------------------------
std::set<std::string> template_source_registry::get_template_list()
{
#pragma message("TODO: Implement template_source_registry::get_template_list")
return std::set<std::string>();
}
bool template_source_registry::has_template(const std::string& template_name)
{
#pragma message("TODO: Implement template_source_registry::has_template")
return false;
}
template_info template_source_registry::get_template_info(const std::string& template_name)
{
#pragma message("TODO: Implement template_source_registry::get_template_info")
return template_info();
}
bool template_source_registry::template_command_exists(const std::string& template_name, const std::string& command)
{
#pragma message("TODO: Implement template_source_registry::template_command_exists")
return false;
}
std::filesystem::path template_source_registry::get_cache_dir()
{
#pragma message("TODO: Implement template_source_registry::get_cache_dir")
return std::filesystem::path();
}
// ------------------------------------------------------------------------------------------------
// template_manager
// ------------------------------------------------------------------------------------------------
void template_manager::list_templates() const {
ASSERT(mLoaded && mSources.size() > 0, "Template manager not loaded, or no template sources found.");
auto templates = get_template_list();
if (templates.empty()) {
std::cout << "No templates found." << std::endl;
@ -91,43 +114,95 @@
}
std::cout << "Available templates:" << std::endl;
std::cout << std::left << std::setw(20) << "Name" << "Path" << std::endl;
std::cout << std::string(60, '-') << std::endl;
// print templates.
std::cout << std::string(60, '-') << std::endl;
bool first = true;
for (const auto& t : templates) {
std::cout << std::left << std::setw(20) << t.template_name << t.local_template_path << std::endl;
std::cout << (first ? "" : ", ") << t;
first = false;
}
std::cout << std::endl;
std::cout << std::string(60, '-') << std::endl;
}
void create_template(const std::string& template_name) {
std::set<std::string> template_manager::get_template_list() const
{
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();
templates.insert(source_templates.begin(), source_templates.end());
}
return templates;
}
bool template_manager::has_template(const std::string &template_name) const
{
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;
return true;
}
template_info template_manager::get_template_info(const std::string &template_name) const
{
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);
// fail
return template_info();
}
bool template_manager::template_command_exists(const std::string &template_name, const std::string &command) const
{
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;
return false;
}
return source->template_command_exists(template_name, command);
}
bool template_manager::create_template(const std::string &template_name) const
{
// 1. Create a new directory in the user templates directory
std::vector<std::string> local_config_directories = gConfig().get_local_config_directories();
std::vector<std::string> local_server_definition_paths = gConfig().get_local_server_definition_paths();
if (local_config_directories.empty()) {
std::cerr << "Error: No local config directories found" << std::endl;
std::cerr << "Run 'dropshell init' to initialise DropShell" << std::endl;
return;
if (local_server_definition_paths.empty()) {
std::cerr << "Error: No local server definition paths found" << std::endl;
std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl;
return false;
}
template_info info;
if (get_template_info(template_name, info)) {
std::cerr << "Error: Template '" << template_name << "' already exists at " << info.local_template_path << std::endl;
return;
auto info = get_template_info(template_name);
if (info.is_set()) {
std::cerr << "Error: Template '" << template_name << "' already exists at " << info.locationID() << std::endl;
return false;
}
std::string new_template_path = localpath::config_templates() + "/" + template_name;
auto local_template_paths = gConfig().get_template_local_paths();
if (local_template_paths.empty()) {
std::cerr << "Error: No local template paths found" << std::endl;
std::cerr << "Run 'dropshell edit' to add one to the DropShell config" << std::endl;
return false;
}
std::string new_template_path = local_template_paths[0] + "/" + template_name;
// Create the new template directory
std::filesystem::create_directories(new_template_path);
// 2. Copy the example template from the system templates directory
std::string system_templates_dir = localpath::system_templates();
std::string example_template_path = system_templates_dir + "/example-nginx";
if (!std::filesystem::exists(example_template_path)) {
std::cerr << "Error: Example template not found at " << example_template_path << std::endl;
return;
auto example_info = gTemplateManager().get_template_info("example-nginx");
if (!example_info.is_set()) {
std::cerr << "Error: Example template not found" << std::endl;
return false;
}
std::string example_template_path = example_info.local_template_path();
// Copy all files from example template to new template
for (const auto& entry : std::filesystem::recursive_directory_iterator(example_template_path)) {
@ -141,14 +216,13 @@
}
}
// modify the TEMPLATE=example line in the service.env file to TEMPLATE=<template_name>
// modify the TEMPLATE=example line in the .template_info.env file to TEMPLATE=<template_name>
std::string search_string = "TEMPLATE=";
std::string replacement_line = "TEMPLATE=" + template_name;
// replace the line in the example/service.env file with the replacement line
std::string service_env_path = new_template_path + "/example/service.env";
std::string service_env_path = new_template_path + "/config/.template_info.env";
if (!replace_line_in_file(service_env_path, search_string, replacement_line)) {
std::cerr << "Error: Failed to replace TEMPLATE= line in service.env file" << std::endl;
return;
std::cerr << "Error: Failed to replace TEMPLATE= line in the .template_info.env file" << std::endl;
return false;
}
// 3. Print out the README.txt file and the path
@ -166,27 +240,120 @@
readme_file.close();
}
std::cout << std::string(60, '-') << std::endl;
} else {
std::cout << "No README.txt file found in the template." << std::endl;
}
std::cout << std::endl;
std::cout << "Template '" << template_name << "' created at " << new_template_path << std::endl;
return test_template(new_template_path);
}
bool get_all_template_config_directories(std::vector<std::string> &template_config_directories)
void template_manager::load_sources()
{
template_config_directories.clear();
for (int i = 0; i < localpath::num_config_directories(); i++) {
std::string config_templates_path = localpath::config_templates(i);
if (config_templates_path.empty()) {
std::cerr << "Error: Templates directory not found: " << config_templates_path << std::endl;
return false;
}
template_config_directories.push_back(config_templates_path);
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;
for (const auto& path : local_template_paths)
mSources.push_back(std::make_unique<template_source_local>(path));
auto registry_urls = gConfig().get_template_registry_urls();
for (const auto& url : registry_urls)
mSources.push_back(std::make_unique<template_source_registry>(url));
mLoaded = true;
}
void template_manager::print_sources() const
{
std::cout << "Template sources: ";
for (const auto& source : mSources) {
std::cout << "[" << source->get_description() << "] ";
}
std::cout << std::endl;
}
bool template_manager::required_file(std::string path, std::string template_name)
{
if (!std::filesystem::exists(path)) {
std::cerr << "Error: " << path << " file not found in template - REQUIRED." << template_name << std::endl;
return false;
}
template_config_directories.push_back(localpath::system_templates());
return true;
}
template_source_interface *template_manager::get_source(const std::string &template_name) const
{
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();
}
}
return nullptr;
}
bool template_manager::test_template(const std::string &template_path)
{
std::string template_name = std::filesystem::path(template_path).filename().string();
std::vector<std::string> required_files = {
"config/service.env",
"config/.template_info.env",
"_default.env",
"install.sh",
"uninstall.sh",
"nuke.sh"
};
for (const auto& file : required_files) {
if (!required_file(template_path + "/" + file, template_name))
return false;
}
// ------------------------------------------------------------
// check TEMPLATE= line.
std::map<std::string, std::string> all_env_vars;
std::vector<std::string> env_files = {
"config/service.env",
"config/.template_info.env"
};
for (const auto& file : env_files) {
{ // load service.env from the service on this machine.
std::map<std::string, std::string> env_vars;
envmanager env_manager(template_path + "/" + file);
env_manager.load();
env_manager.get_all_variables(env_vars);
all_env_vars.merge(env_vars);
}
}
// determine template name.
auto it = all_env_vars.find("TEMPLATE");
if (it == all_env_vars.end()) {
std::cerr << "Error: TEMPLATE variable not found in " << template_path << std::endl;
return false;
}
std::string env_template_name = it->second;
if (env_template_name.empty()) {
std::cerr << "Error: TEMPLATE variable is empty in " << template_path << std::endl;
return false;
}
if (env_template_name != template_name) {
std::cerr << "Error: TEMPLATE variable is wrong in " << template_path << std::endl;
return false;
}
return true;
}
template_manager & gTemplateManager()
{
static template_manager instance;
return instance;
}
} // namespace dropshell

View File

@ -1,36 +1,110 @@
#include <string>
#include <vector>
#include <filesystem>
#include <memory>
#include <set>
#include "utils/json.hpp"
namespace dropshell {
typedef enum template_source_type {
TEMPLATE_SOURCE_TYPE_LOCAL,
TEMPLATE_SOURCE_TYPE_REGISTRY,
TEMPLATE_SOURCE_NOT_SET
} template_source_type;
class template_info {
public:
template_info() {}
template_info(std::string n, std::string p) : template_name(n), local_template_path(p) {}
std::string template_name;
std::string local_template_path;
template_info() : mIsSet(false) {}
template_info(const std::string& template_name, const std::string& location_id, const std::filesystem::path& local_template_path) : mTemplateName(template_name), mLocationID(location_id), mTemplateLocalPath(local_template_path), mIsSet(true) {}
virtual ~template_info() {}
bool is_set() { return mIsSet; }
std::string name() { return mTemplateName; }
std::string locationID() { return mLocationID; }
std::filesystem::path local_template_path() { return mTemplateLocalPath; }
private:
std::string mTemplateName;
std::string mLocationID;
std::filesystem::path mTemplateLocalPath; // source or cache.
bool mIsSet;
};
// templates are stored in multiple locations:
// 1. /opt/dropshell/templates
// 2. CONFIG_DIR/templates
class template_source_interface {
public:
virtual ~template_source_interface() {}
virtual std::set<std::string> get_template_list() = 0;
virtual bool has_template(const std::string& template_name) = 0;
virtual template_info get_template_info(const std::string& template_name) = 0;
virtual bool template_command_exists(const std::string& template_name,const std::string& command) = 0;
virtual std::string get_description() = 0;
};
bool get_templates(std::vector<template_info>& templates);
bool get_template_info(const std::string& template_name, template_info& info);
bool template_command_exists(const std::string& template_name,const std::string& command);
void list_templates();
class template_source_registry : public template_source_interface {
public:
template_source_registry(std::string URL) : mURL(URL) {}
~template_source_registry() {}
// create a template
// 1. create a new directory in the user templates directory
// 2. copy the example template from the system templates directory into the new directory
// 3. print out the README.txt file in the new template directory, and the path to the new template
void create_template(const std::string& template_name);
std::set<std::string> get_template_list();
bool has_template(const std::string& template_name);
template_info get_template_info(const std::string& template_name);
bool template_command_exists(const std::string& template_name,const std::string& command);
std::string get_description() { return "Registry: " + mURL; }
private:
std::filesystem::path get_cache_dir();
private:
std::string mURL;
std::vector<nlohmann::json> mTemplates; // cached list.
};
class template_source_local : public template_source_interface {
public:
template_source_local(std::string local_path) : mLocalPath(local_path) {}
~template_source_local() {}
std::set<std::string> get_template_list();
bool has_template(const std::string& template_name);
template_info get_template_info(const std::string& template_name);
bool template_command_exists(const std::string& template_name,const std::string& command);
std::string get_description() { return "Local: " + mLocalPath.string(); }
private:
std::filesystem::path mLocalPath;
};
class template_manager {
public:
template_manager() : mLoaded(false) {}
~template_manager() {}
std::set<std::string> get_template_list() const;
bool has_template(const std::string& template_name) const;
template_info get_template_info(const std::string& template_name) const;
bool template_command_exists(const std::string& template_name,const std::string& command) const;
bool create_template(const std::string& template_name) const;
static bool test_template(const std::string& template_path);
void list_templates() const;
void load_sources();
void print_sources() const;
bool is_loaded() const { return mLoaded; }
int get_source_count() const { return mSources.size(); }
private:
static bool required_file(std::string path, std::string template_name);
template_source_interface* get_source(const std::string& template_name) const;
private:
bool mLoaded;
mutable std::vector<std::unique_ptr<template_source_interface>> mSources;
};
template_manager & gTemplateManager();
bool get_all_template_config_directories(std::vector<std::string>& template_config_directories);
} // namespace dropshell

View File

@ -1,58 +1,11 @@
#ifndef DROPSHELL_ASSERT_HPP
#define DROPSHELL_ASSERT_HPP
#ifndef ASSERT_HPP
#define ASSERT_HPP
#include <iostream>
#include <string_view>
#include <cstdlib> // For std::exit and EXIT_FAILURE
namespace ds {
struct SourceLocation {
const char* file_name;
int line;
const char* function_name;
};
// Helper macro to create a SourceLocation with current context
#define DS_CURRENT_LOCATION ds::SourceLocation{__FILE__, __LINE__, __func__}
[[noreturn]] inline void assert_fail(
const char* expression,
const SourceLocation& location,
const char* message = nullptr) {
std::cerr << "\033[1;31mAssertion failed!\033[0m\n"
<< "Expression: \033[1;33m" << expression << "\033[0m\n"
<< "Location: \033[1;36m" << location.file_name << ":"
<< location.line << "\033[0m\n"
<< "Function: \033[1;36m" << location.function_name << "\033[0m\n";
if (message) {
std::cerr << "Message: \033[1;37m" << message << "\033[0m\n";
#define ASSERT(condition, message) \
if (!(condition)) { \
std::cerr << "Assertion failed: " << message << std::endl; \
std::exit(1); \
}
std::cerr << std::endl;
// Exit the program without creating a core dump
std::exit(EXIT_FAILURE);
}
} // namespace ds
// Standard assertion
#define ASSERT(condition) \
do { \
if (!(condition)) { \
ds::assert_fail(#condition, DS_CURRENT_LOCATION); \
} \
} while (false)
// Assertion with custom message
#define ASSERT_MSG(condition, message) \
do { \
if (!(condition)) { \
ds::assert_fail(#condition, DS_CURRENT_LOCATION, message); \
} \
} while (false)
#endif // DROPSHELL_ASSERT_HPP
#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

@ -11,40 +11,31 @@ namespace dropshell {
namespace localfile {
std::string dropshell_env() {
// Try ~/.config/dropshell/dropshell.env
const char* home = std::getenv("HOME");
if (home) {
fs::path user_path = fs::path(home) / ".config" / "dropshell" / "dropshell.env";
return user_path.string();
}
std::cerr << "Warning: Couldn't determine user directory" << std::endl;
return std::string();
std::string dropshell_json() {
// Try ~/.config/dropshell/dropshell.json
std::string homedir = localpath::current_user_home();
if (!homedir.empty()) {
fs::path user_path = fs::path(homedir) / ".config" / "dropshell" / "dropshell.json";
return user_path.string();
}
return std::string();
}
std::string server_env(const std::string &server_name) {
if (server_name.empty())
return std::string();
std::string server_json(const std::string &server_name) {
std::string serverpath = localpath::server(server_name);
if (serverpath.empty())
return std::string();
return (fs::path(serverpath) / "server.env").string();
return (serverpath.empty() ? "" : (fs::path(serverpath) / "server.json").string());
}
std::string service_env(const std::string &server_name, const std::string &service_name) {
if (server_name.empty() || service_name.empty())
return std::string();
std::string servicepath = localpath::service(server_name, service_name);
if (servicepath.empty())
return std::string();
return (fs::path(servicepath) / "service.env").string();
return (servicepath.empty() ? "" : (fs::path(servicepath) / "service.env").string());
}
std::string service_hash(const std::string &server_name, const std::string &service_name) {
std::string config_path = localpath::config();
if (server_name.empty() || service_name.empty() || config_path.empty())
return std::string();
return (fs::path(config_path) / ".remote_versions" / server_name / (service_name + ".hash.env")).string();
std::string template_info_env(const std::string &server_name, const std::string &service_name)
{
std::string servicepath = localpath::service(server_name, service_name);
return (servicepath.empty() ? "" : (fs::path(servicepath) / ".template_info.env").string());
}
} // namespace localfile
@ -53,57 +44,55 @@ namespace localfile {
// ------------------------------------------------------------------------------------------
namespace localpath {
// /opt/dropshell/templates
std::string system_templates() {
return "/opt/dropshell/templates";
}
// configured by user - defaults to first config_path/backups.
std::string backups_path() {
return gConfig().get_local_backup_path();
}
int num_config_directories() {
return gConfig().get_local_config_directories().size();
}
std::string config(int index) {
return (num_config_directories()>index) ? gConfig().get_local_config_directories()[index] : "";
}
std::string config_templates(int index) {
return (num_config_directories()>index) ? (gConfig().get_local_config_directories()[index]+"/templates") : "";
}
std::string config_servers(int index) {
return (num_config_directories()>index) ? (gConfig().get_local_config_directories()[index]+"/servers") : "";
}
std::string server(const std::string &server_name) {
for (auto &dir : gConfig().get_local_config_directories())
if (fs::exists(dir + "/servers/" + server_name))
return dir + "/servers/" + server_name;
for (std::filesystem::path dir : gConfig().get_local_server_definition_paths())
if (fs::exists(dir / server_name))
return dir / server_name;
return "";
}
std::string service(const std::string &server_name, const std::string &service_name) {
std::string serverpath = localpath::server(server_name);
return (serverpath.empty() ? "" : (serverpath+"/"+service_name));
return ((serverpath.empty() || service_name.empty()) ? "" : (serverpath+"/"+service_name));
}
std::string remote_versions(const std::string &server_name, const std::string &service_name)
{
std::string template_cache_path = gConfig().get_local_template_cache_path();
return ((template_cache_path.empty() || service_name.empty()) ? "" :
(template_cache_path+"/remote_versions/"+service_name+".json"));
}
std::string agent(){
return current_user_home() + "/.local/dropshell_agent";
}
std::string current_user_home(){
char * homedir = std::getenv("HOME");
if (homedir)
{
std::filesystem::path homedir_path(homedir);
return fs::canonical(homedir_path).string();
}
std::cerr << "Warning: Couldn't determine user directory" << std::endl;
return std::string();
}
} // 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 {
@ -151,6 +140,18 @@ namespace remotepath {
return (dsp.empty() ? "" : (dsp + "/backups"));
}
std::string temp_files(const std::string &server_name)
{
std::string dsp = DROPSHELL_DIR(server_name);
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);
@ -162,11 +163,18 @@ namespace remotepath {
// ------------------------------------------------------------------------------------------
// Utility functions
std::string get_parent(std::string path)
std::string get_parent(const std::filesystem::path path)
{
if (path.empty())
return std::string();
return fs::path(path).parent_path().string();
return path.parent_path().string();
}
std::string get_child(const std::filesystem::path path)
{
if (path.empty())
return std::string();
return path.filename().string();
}
} // namespace dropshell

View File

@ -2,6 +2,7 @@
#define DIRECTORIES_HPP
#include <string>
#include <filesystem>
namespace dropshell {
@ -9,45 +10,53 @@ namespace dropshell {
//------------------------------------------------------------------------------------------------
// local user config directories
// config_path
// |-- servers
// | |-- server_name
// | |-- server.env
// | |-- services
// | |-- service_name
// | |-- service.env
// | |-- (other config files for specific server&service)
// |-- templates
// | |-- template_name
// | |-- (script files)
// | |-- example
// | |-- service.env
// | |-- (other service config files)
// |-- .remote_versions
// | |-- server_name
// | |-- service_name.hash.env
// ~/.config/dropshell/dropshell.json
// ~/.local/dropshell_agent/bb64
// server_definition_path
// |-- <server_name>
// |-- server.json
// |-- services
// |-- <service_name>
// |-- service.env
// |-- .template_info.env
// |-- (...other config files for specific server&service...)
// backup path
// |-- katie-_-squashkiwi-_-squashkiwi-test-_-2025-04-28_21-23-59.tgz
// temp files path
// template cache path
// |-- templates
// | |-- <template_name>.json
// | |-- <template_name>
// | |-- (...script files...)
// | |-- _default.env
// | |-- config
// | |-- service.env
// | |-- .template_info.env
// | |-- (...other service config files...)
// |-- remote_versions
// | |-- server_name-service_name.json
namespace localfile {
// ~/.config/dropshell/dropshell.conf
std::string dropshell_env();
std::string server_env(const std::string &server_name);
// ~/.config/dropshell/dropshell.json
std::string dropshell_json();
std::string server_json(const std::string &server_name);
std::string service_env(const std::string &server_name, const std::string &service_name);
std::string service_hash(const std::string &server_name, const std::string &service_name);
std::string template_info_env(const std::string &server_name, const std::string &service_name);
} // namespace localfile
namespace localpath {
// /opt/dropshell/templates
std::string system_templates();
// configured by user - defaults to first config_path/backups.
std::string backups_path();
int num_config_directories();
std::string config(int index=0);
std::string config_templates(int index=0);
std::string config_servers(int index=0);
std::string server(const std::string &server_name);
std::string service(const std::string &server_name, const std::string &service_name);
std::string remote_versions(const std::string &server_name, const std::string &service_name);
std::string agent();
std::string current_user_home();
} // namespace local
@ -55,13 +64,15 @@ namespace dropshell {
// remote paths
// DROPSHELL_DIR
// |-- backups
// |-- temp_files
// |-- agent
// |-- services
// |-- service name
// |-- config
// |-- service.env
// |-- template
// |-- (script files)
// |-- example
// |-- config
// |-- service.env
// |-- (other config files for specific server&service)
@ -76,12 +87,15 @@ namespace dropshell {
std::string service_config(const std::string &server_name, const std::string &service_name);
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
//------------------------------------------------------------------------------------------------
// utility functions
std::string get_parent(std::string path);
std::string get_parent(const std::filesystem::path path);
std::string get_child(const std::filesystem::path path);
} // namespace dropshell
#endif
#endif // DIRECTORIES_HPP

View File

@ -72,18 +72,6 @@ void envmanager::get_all_variables(std::map<std::string, std::string>& variables
variables = m_variables;
}
std::string envmanager::get_variable_substituted(std::string key) const {
std::string value = get_variable(key);
return expand_patterns(value);
}
void envmanager::get_all_variables_substituted(std::map<std::string, std::string>& variables) const {
variables.clear();
for (const auto& pair : m_variables) {
variables[pair.first] = expand_patterns(pair.second);
}
}
void envmanager::add_variables(std::map<std::string, std::string> variables) {
for (auto& pair : variables) {
set_variable(pair.first, pair.second);
@ -98,26 +86,4 @@ void envmanager::clear_variables() {
m_variables.clear();
}
std::string envmanager::expand_patterns(std::string str) const {
// Combined regex pattern for both ${var} and $var formats
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
std::string result = str;
std::smatch match;
while (std::regex_search(result, match, var_pattern)) {
// match[1] will contain capture from ${var} format
// match[2] will contain capture from $var format
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
// Get value from system environment variables
const char* env_value = std::getenv(var_name.c_str());
std::string value = env_value ? env_value : "";
result = result.replace(match.position(), match.length(), value);
}
// dequote the result
return result;
}
} // namespace dropshell

View File

@ -25,20 +25,12 @@ class envmanager {
std::string get_variable(std::string key) const;
void get_all_variables(std::map<std::string, std::string>& variables) const;
// get variables, but replace patterns ${var} and $var with the actual environment variable in the returned string.
// trim whitespace from the values.
std::string get_variable_substituted(std::string key) const;
void get_all_variables_substituted(std::map<std::string, std::string>& variables) const;
// add variables to the environment files.
// trim whitespace from the values.
void add_variables(std::map<std::string, std::string> variables);
void set_variable(std::string key, std::string value);
void clear_variables();
private:
std::string expand_patterns(std::string str) const;
private:
std::string m_path;
std::map<std::string, std::string> m_variables;

179
src/utils/execute.cpp Normal file
View File

@ -0,0 +1,179 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include <iostream>
#include <string>
#include <cstdlib>
#include <sstream>
#include "utils/assert.hpp"
#include "execute.hpp"
#include "utils/utils.hpp"
#include "utils/b64ed.hpp"
#include "config.hpp"
#include "utils/directories.hpp"
namespace dropshell
{
bool EXITSTATUSCHECK(int ret)
{
return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute.
}
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
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);
}
}
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;
}
char buffer[128];
while (fgets(buffer, sizeof(buffer), pipe) != nullptr)
{
(*output) += buffer;
}
int ret = pclose(pipe);
return EXITSTATUSCHECK(ret);
}
bool execute_local_command(std::string command, std::string *output, cMode mode)
{
return execute_local_command("", command, {}, output, mode);
}
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;
}
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;
}
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;
}
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;
}
} // namespace dropshell

74
src/utils/execute.hpp Normal file
View File

@ -0,0 +1,74 @@
#ifndef EXECUTE_HPP
#define EXECUTE_HPP
#include <string>
#include <map>
namespace dropshell {
class sCommand;
// mode bitset
enum class cMode {
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));}
inline cMode operator+(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(rhs));}
inline cMode operator-(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & ~static_cast<int>(rhs));}
inline cMode operator|(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) | static_cast<int>(rhs));}
inline cMode operator|=(cMode & lhs, cMode rhs) {return lhs = lhs | rhs;}
inline bool hasFlag(cMode mode, cMode flag) {return (mode & flag) == flag;}
typedef struct sSSHInfo {
std::string host;
std::string user;
std::string port;
std::string server_ID; // dropshell name for server.
} sSSHInfo;
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, 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; }
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) { mVars[key] = value; }
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::string mCmd;
std::map<std::string, std::string> mVars;
};
bool EXITSTATUSCHECK(int ret);
} // namespace dropshell
#endif

View File

@ -1,11 +1,15 @@
#include "hash.hpp"
#include "utils/hash.hpp"
#define XXH_INLINE_ALL
#include "contrib/xxhash.hpp"
#include <fstream>
#include <filesystem>
#include <iostream>
namespace dropshell {
XXH64_hash_t hash_file(const std::string &path) {
uint64_t hash_file(const std::string &path) {
// Create hash state
XXH64_state_t* const state = XXH64_createState();
if (state == nullptr) {
@ -14,11 +18,8 @@ XXH64_hash_t hash_file(const std::string &path) {
}
// Initialize state with seed 0
if (XXH64_reset(state, 0) == XXH_ERROR) {
std::cerr << "Failed to reset hash state" << std::endl;
XXH64_freeState(state);
return 0;
}
XXH64_hash_t const seed = 0; /* or any other value */
if (XXH64_reset(state, seed) == XXH_ERROR) return 0;
// Open file
std::ifstream file(path, std::ios::binary);
@ -54,7 +55,7 @@ XXH64_hash_t hash_file(const std::string &path) {
return hash;
}
XXH64_hash_t hash_directory_recursive(const std::string &path) {
uint64_t hash_directory_recursive(const std::string &path) {
// Create hash state
XXH64_state_t* const state = XXH64_createState();
if (state == nullptr) {
@ -63,7 +64,8 @@ XXH64_hash_t hash_directory_recursive(const std::string &path) {
}
// Initialize state with seed 0
if (XXH64_reset(state, 0) == XXH_ERROR) {
XXH64_hash_t const seed = 0; /* or any other value */
if (XXH64_reset(state, seed) == XXH_ERROR) {
std::cerr << "Failed to reset hash state" << std::endl;
XXH64_freeState(state);
return 0;
@ -75,13 +77,7 @@ XXH64_hash_t hash_directory_recursive(const std::string &path) {
if (entry.is_regular_file()) {
// Get file hash
XXH64_hash_t file_hash = hash_file(entry.path().string());
// Update directory hash with file hash
if (XXH64_update(state, &file_hash, sizeof(file_hash)) == XXH_ERROR) {
std::cerr << "Failed to update hash" << std::endl;
XXH64_freeState(state);
return 0;
}
XXH64_update(state, &file_hash, sizeof(file_hash));
}
}
} catch (const std::filesystem::filesystem_error& e) {
@ -96,11 +92,27 @@ XXH64_hash_t hash_directory_recursive(const std::string &path) {
return hash;
}
uint64_t hash_path(const std::string &path) {
if (!std::filesystem::exists(path)) {
std::cerr << "Path does not exist: " << path << std::endl;
return 0;
}
if (std::filesystem::is_directory(path)) {
return hash_directory_recursive(path);
} else if (std::filesystem::is_regular_file(path)) {
return hash_file(path);
} else {
std::cerr << "Path is neither a file nor a directory: " << path << std::endl;
return 0;
}
}
void hash_demo(const std::string & path)
{
std::cout << "Hashing directory: " << path << std::endl;
std::cout << "Hashing path: " << path << std::endl;
auto start = std::chrono::high_resolution_clock::now();
XXH64_hash_t hash = hash_directory_recursive(path);
XXH64_hash_t hash = hash_path(path);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Hash: " << hash << " (took " << duration.count() << "ms)" << std::endl;
@ -111,7 +123,7 @@ int hash_demo_raw(const std::string & path)
if (!std::filesystem::exists(path)) {
std::cout << 0 <<std::endl; return 1;
}
XXH64_hash_t hash = hash_directory_recursive(path);
XXH64_hash_t hash = hash_path(path);
std::cout << hash << std::endl;
return 0;
}

View File

@ -1,14 +1,16 @@
#ifndef HASH_HPP
#define HASH_HPP
#include <xxhash.h>
#include <string>
#include <cstdint>
namespace dropshell {
XXH64_hash_t hash_file(const std::string &path);
uint64_t hash_file(const std::string &path);
XXH64_hash_t hash_directory_recursive(const std::string &path);
uint64_t hash_directory_recursive(const std::string &path);
uint64_t hash_path(const std::string &path);
void hash_demo(const std::string & path);

25578
src/utils/json.hpp Normal file

File diff suppressed because it is too large Load Diff

187
src/utils/json_fwd.hpp Normal file
View File

@ -0,0 +1,187 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

View File

@ -1,36 +0,0 @@
#include "readmes.hpp"
#include <fstream>
#include <filesystem>
void dropshell::create_readme_local_config_dir(const std::string &readme_path)
{
if (std::filesystem::exists(readme_path))
return; // already exists
std::ofstream readme_file(readme_path);
// use heredoc to write the readme
readme_file << R"(
use this directory to store your local config files for your servers and services.
dropshell create-server <server_name>
dropshell create-template <template_name>
dropshell create-service <server_name> <template_name> <service_name>
config_path
|-- servers
| |-- server_name
| |-- server.env
| |-- services
| |-- service_name
| |-- service.env
| |-- (other config files for specific server&service)
|-- templates
| |-- template_name
| |-- (script files)
| |-- example
| |-- service.env
| |-- (other service config files)
)" << std::endl;
readme_file.close();
}

View File

@ -1,12 +0,0 @@
#ifndef READMES_HPP
#define READMES_HPP
#include <string>
namespace dropshell {
void create_readme_local_config_dir(const std::string &readme_path);
} // namespace dropshell
#endif

View File

@ -5,6 +5,9 @@
#include <vector>
#include <algorithm>
#include <filesystem>
#include <regex>
#include <random>
namespace dropshell {
void maketitle(const std::string& title) {
@ -72,6 +75,16 @@ std::string quote(std::string str)
return "\""+str+"\"";
}
std::string halfquote(std::string str)
{
return "'" + str + "'";
}
std::string escapequotes(std::string str)
{
return std::regex_replace(str, std::regex("\""), "\\\"");
}
std::string multi2string(std::vector<std::string> values)
{
std::string result;
@ -263,4 +276,99 @@ std::vector<std::string> split(const std::string& str, const std::string& delimi
return tokens;
}
std::string replace_with_environment_variables_like_bash(std::string str) {
// Combined regex pattern for both ${var} and $var formats
std::regex var_pattern("\\$(?:\\{([^}]+)\\}|([a-zA-Z0-9_]+))");
std::string result = str;
std::smatch match;
while (std::regex_search(result, match, var_pattern)) {
// match[1] will contain capture from ${var} format
// match[2] will contain capture from $var format
std::string var_name = match[1].matched ? match[1].str() : match[2].str();
// Get value from system environment variables
const char* env_value = std::getenv(var_name.c_str());
std::string value = env_value ? env_value : "";
result = result.replace(match.position(), match.length(), value);
}
// dequote the result
return result;
}
std::string random_alphanumeric_string(int length)
{
static std::mt19937 generator(std::random_device{}());
static const std::string chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
std::uniform_int_distribution<> distribution(0, chars.size() - 1);
std::string random_string;
for (int i = 0; i < length; ++i) {
random_string += chars[distribution(generator)];
}
return random_string;
}
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

@ -19,6 +19,10 @@ bool replace_line_in_file(const std::string& file_path, const std::string& searc
std::string trim(std::string str);
std::string dequote(std::string str);
std::string quote(std::string str);
std::string halfquote(std::string str);
std::string requote(std::string str);
std::string escapequotes(std::string str);
std::string multi2string(std::vector<std::string> values);
std::vector<std::string> string2multi(std::string values);
std::vector<std::string> split(const std::string& str, const std::string& delimiter);
@ -33,4 +37,20 @@ void ensure_directories_exist(std::vector<std::string> directories);
std::vector<int> search(const std::string &pat, const std::string &txt);
int count_substring(const std::string &substring, const std::string &text);
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 @@
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,8 +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.
echo "Installation of dropshell-agent complete"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# UNINSTALL SCRIPT
# The uninstall script is required for all templates.
# It is used to uninstall the service from the server.
# It is called with the path to the server specific env file as an argument.

View File

@ -1,33 +0,0 @@
DropShell Template Example - Nginx simple webserver
A simple service example, creating a single docker container running nginx, with contant on the host
in the configurable LOCAL_DATA_FOLDER directory.
Shell scripts defined in this folder are run as DropShell commands on the remote server (not locally!).
When they are run, the following environment variables will be set:
- SERVER (server name), SERVICE (service name) and CONFIG_PATH (path to the user's service configuration folder)
- everything in _default.env
- everything in the server's particular service.env file (defaults in example/service.env)
The optional backup and restore scripts get a second argument, which is the backup file to create/restore
(must be a single tgz file).
Mandatory scripts are:
- install.sh
- uninstall.sh
Optional standard scripts are:
- start.sh
- stop.sh
- backup.sh
- restore.sh
- status.sh
- ports.sh
- logs.sh
The example/ folder gets copied to the service's configuration, edited for the particular server settings,
then copied onto the server. The location is server-specific, but can be accessed via CONFIG_PATH.
You can use it to store things like an nginx config file (this example does not).

View File

@ -1,155 +0,0 @@
#!/bin/bash
# COMMON FUNCTIONS
# JDE
# 2025-04-25
# This file is not required if you write your own template.
# Print error message and exit with code 1
# Usage: die "error message"
die() {
echo -e "\033[91mError: $1\033[0m"
exit 1
}
grey_start() {
echo -e -n "\033[90m"
}
grey_end() {
echo -e -n "\033[0m"
}
create_and_start_container() {
if [ -z "$1" ] || [ -z "$2" ]; then
die "Template error: create_and_start_container <run_cmd> <container_name>"
fi
local run_cmd="$1"
local container_name="$2"
if _is_container_exists $container_name; then
_is_container_running $container_name && return 0
_start_container $container_name
else
grey_start
$run_cmd
grey_end
fi
if ! _is_container_running $container_name; then
die "Container ${container_name} failed to start"
fi
ID=$(_get_container_id $container_name)
echo "Container ${container_name} is running with ID ${ID}"
}
function create_folder() {
local folder="$1"
if [ -d "$folder" ]; then
return 0
fi
if ! mkdir -p "$folder"; then
die "Failed to create folder: $folder"
fi
chmod 777 "$folder"
echo "Folder created: $folder"
}
# Check if docker is installed
_check_docker_installed() {
if ! command -v docker &> /dev/null; then
echo "Docker is not installed"
return 1
fi
# check if docker daemon is running
if ! docker info &> /dev/null; then
echo "Docker daemon is not running"
return 1
fi
# check if user has permission to run docker
if ! docker run --rm hello-world &> /dev/null; then
echo "User does not have permission to run docker"
return 1
fi
return 0
}
# Check if a container exists
_is_container_exists() {
if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# Check if a container is running
_is_container_running() {
if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# get contianer ID
_get_container_id() {
docker ps --format "{{.ID}}" --filter "name=$1"
}
# get container status
_get_container_status() {
docker ps --format "{{.Status}}" --filter "name=$1"
}
# start container that exists
_start_container() {
_is_container_exists $1 || return 1
_is_container_running $1 && return 0
docker start $1
}
# stop container that exists
_stop_container() {
_is_container_running $1 || return 0;
docker stop $1
}
# remove container that exists
_remove_container() {
_stop_container $1
_is_container_exists $1 || return 0;
docker rm $1
}
# get container logs
_get_container_logs() {
if ! _is_container_exists $1; then
echo "Container $1 does not exist"
return 1
fi
docker logs $1
}
check_required_env_vars() {
local required_vars=("$@")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
die "Required environment variable $var is not set in your service.env file"
fi
done
}
function _root_remove_tree() {
local to_remove="$1"
parent=$(dirname "$to_remove")
abs_parent=$(realpath "$parent")
child=$(basename "$to_remove")
docker run --rm -v "$abs_parent":/data alpine rm -rf "/data/$child"
}

View File

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

View File

@ -1,36 +0,0 @@
#!/bin/bash
# BACKUP SCRIPT
# The backup script is OPTIONAL.
# It is used to backup the service on the server.
# It is called with one argument: the path to the destination backup file.
# If the backup file already exists, the script should exit with a message.
source "$(dirname "$0")/_common.sh"
check_required_env_vars "CONTAINER_NAME" "LOCAL_DATA_FOLDER"
# Get backup file path from first argument
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
die "Backup file path not provided"
fi
# Check if backup file already exists
if [ -f "$BACKUP_FILE" ]; then
die "Backup file $BACKUP_FILE already exists"
fi
# Stop container before backup
_stop_container "$CONTAINER_NAME"
Create backup of data folder
echo "Creating backup of $LOCAL_DATA_FOLDER..."
if ! tar zcvf "$BACKUP_FILE" -C "$LOCAL_DATA_FOLDER" .; then
_start_container "$CONTAINER_NAME"
die "Failed to create backup"
fi
# Start container after backup
_start_container "$CONTAINER_NAME"
echo "Backup created successfully: $BACKUP_FILE"

View File

@ -1,15 +0,0 @@
# Template to use - always required!
TEMPLATE=example-nginx
# 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}/.example"
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,41 +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.
source "$(dirname "$0")/_common.sh"
# Required environment variables
check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" "LOCAL_DATA_FOLDER"
# Create local data folder if it doesn't exist
if [ -d "${LOCAL_DATA_FOLDER}" ]; then
echo "Local data folder ${LOCAL_DATA_FOLDER} exists, using existing data."
else
echo "Local data folder ${LOCAL_DATA_FOLDER} does not exist, creating..."
mkdir -p "${LOCAL_DATA_FOLDER}"
cat <<EOF > "${LOCAL_DATA_FOLDER}/index.html"
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
EOF
fi
# Test Docker
_check_docker_installed || die "Docker test failed, aborting installation..."
# check can pull image on remote host and exit if fails
docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || die "Failed to pull image $IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"
# remove and restart, as the env may have changed.
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"
echo "You can access the service at http://${SERVER}:${HOST_PORT}"

View File

@ -1,16 +0,0 @@
#!/bin/bash
# LOGS SCRIPT
# The logs script is OPTIONAL.
# It is used to return the logs of the service.
# It is called with the path to the server specific env file as an argument.
source "$(dirname "$0")/_common.sh"
# Required environment variables
check_required_env_vars "CONTAINER_NAME"
echo "Container ${CONTAINER_NAME} logs:"
grey_start
docker logs "${CONTAINER_NAME}"
grey_end

View File

@ -1,13 +0,0 @@
#!/bin/bash
# PORT SCRIPT
# The port script is OPTIONAL.
# It is used to return the ports used by the service.
# It is called with the path to the server specific env file as an argument.
source "$(dirname "$0")/_common.sh"
# Required environment variables
# check_required_env_vars "HOST_PORT"
echo $HOST_PORT

View File

@ -1,41 +0,0 @@
#!/bin/bash
# RESTORE SCRIPT
# The restore script is OPTIONAL.
# It is used to restore the service on the server from a backup file.
# It is called with one argument: the path to the backup file.
source "$(dirname "$0")/_common.sh"
check_required_env_vars "CONTAINER_NAME" "LOCAL_DATA_FOLDER"
# Get backup file path from first argument
BACKUP_FILE="$1"
if [ -z "$BACKUP_FILE" ]; then
die "Backup file path not provided"
fi
# Check if backup file already exists
if [ ! -f "$BACKUP_FILE" ]; then
die "Backup file $BACKUP_FILE does not exist"
fi
# # Stop container before backup
bash ./uninstall.sh || die "Failed to uninstall service before restore"
# Remove existing data folder
echo "Deleting ALL data in $LOCAL_DATA_FOLDER."
_root_remove_tree "$LOCAL_DATA_FOLDER"
[ ! -d "$LOCAL_DATA_FOLDER" ] || die "Failed to delete $LOCAL_DATA_FOLDER"
mkdir -p "$LOCAL_DATA_FOLDER"
[ -d "$LOCAL_DATA_FOLDER" ] || die "Failed to create $LOCAL_DATA_FOLDER"
# Restore data folder from backup
# --strip-components=1 removes the parent folder in the tgz from the restore paths.
if ! tar xzvf "$BACKUP_FILE" -C "$LOCAL_DATA_FOLDER" --strip-components=1; then
die "Failed to restore data folder from backup"
fi
# reinstall service
bash ./install.sh || die "Failed to reinstall service after restore"
echo "Restore complete! Service is running again on port $HOST_PORT with restored website."

View File

@ -1,30 +0,0 @@
#!/bin/bash
# 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.
source "$(dirname "$0")/_common.sh"
check_required_env_vars "CONTAINER_NAME" "HOST_PORT" "LOCAL_DATA_FOLDER"
[ -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,21 +0,0 @@
#!/bin/bash
# STATUS SCRIPT
# The status script is OPTIONAL.
# It is used to return the status of the service (0 is healthy, 1 is unhealthy).
# It is called with the path to the server specific env file as an argument.
# This is an example of a status script that checks if the service is running.
source "$(dirname "$0")/_common.sh"
check_required_env_vars "CONTAINER_NAME"
# check if the service is running
_is_container_running $CONTAINER_NAME || die "Service is not running - did not find container $CONTAINER_NAME."
# check if the service is healthy
# curl -s -X GET http://localhost:${HOST_PORT}/health | grep -q "OK" \
# || die "Service is not healthy - did not get OK response from /health endpoint."
echo "Service is healthy"
exit 0

View File

@ -1,13 +0,0 @@
#!/bin/bash
# STOP SCRIPT
# The stop script is required for all templates.
# It is used to stop the service on the server.
# It is called with the path to the server specific env file as an argument.
source "$(dirname "$0")/_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,19 +0,0 @@
#!/bin/bash
# UNINSTALL SCRIPT
# The uninstall script is required for all templates.
# It is used to uninstall the service from the server.
# It is called with the path to the server specific env file as an argument.
source "$(dirname "$0")/_common.sh"
check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG"
_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 folder ${LOCAL_DATA_FOLDER} still in place."

View File

@ -1,147 +0,0 @@
#!/bin/bash
# COMMON FUNCTIONS
# JDE
# 2025-04-25
# This file is not required if you write your own template.
# Print error message and exit with code 1
# Usage: die "error message"
die() {
echo -e "\033[91mError: $1\033[0m"
exit 1
}
grey_start() {
echo -e -n "\033[90m"
}
grey_end() {
echo -e -n "\033[0m"
}
create_and_start_container() {
if [ -z "$1" ] || [ -z "$2" ]; then
die "Template error: create_and_start_container <run_cmd> <container_name>"
fi
local run_cmd="$1"
local container_name="$2"
if _is_container_exists $container_name; then
_is_container_running $container_name && return 0
_start_container $container_name
else
grey_start
$run_cmd
grey_end
fi
if ! _is_container_running $container_name; then
die "Container ${container_name} failed to start"
fi
ID=$(_get_container_id $container_name)
echo "Container ${container_name} is running with ID ${ID}"
}
function create_folder() {
local folder="$1"
if [ -d "$folder" ]; then
return 0
fi
if ! mkdir -p "$folder"; then
die "Failed to create folder: $folder"
fi
chmod 777 "$folder"
echo "Folder created: $folder"
}
# Check if docker is installed
_check_docker_installed() {
if ! command -v docker &> /dev/null; then
echo "Docker is not installed"
return 1
fi
# check if docker daemon is running
if ! docker info &> /dev/null; then
echo "Docker daemon is not running"
return 1
fi
# check if user has permission to run docker
if ! docker run --rm hello-world &> /dev/null; then
echo "User does not have permission to run docker"
return 1
fi
return 0
}
# Check if a container exists
_is_container_exists() {
if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# Check if a container is running
_is_container_running() {
if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
return 0
}
# get contianer ID
_get_container_id() {
docker ps --format "{{.ID}}" --filter "name=$1"
}
# get container status
_get_container_status() {
docker ps --format "{{.Status}}" --filter "name=$1"
}
# start container that exists
_start_container() {
_is_container_exists $1 || return 1
_is_container_running $1 && return 0
docker start $1
}
# stop container that exists
_stop_container() {
_is_container_running $1 || return 0;
docker stop $1
}
# remove container that exists
_remove_container() {
_stop_container $1
_is_container_exists $1 || return 0;
docker rm $1
}
# get container logs
_get_container_logs() {
if ! _is_container_exists $1; then
echo "Container $1 does not exist"
return 1
fi
docker logs $1
}
check_required_env_vars() {
local required_vars=("$@")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
die "Required environment variable $var is not set in your service.env file"
fi
done
}

View File

@ -1,11 +0,0 @@
# Service settings
CONTAINER_NAME=watchtower
# Interval in seconds between checks.
# Default is 1800 seconds (30 minutes).
INTERVAL=1800
# Image settings
IMAGE_REGISTRY="docker.io"
IMAGE_REPO="containrrr/watchtower"
IMAGE_TAG="latest"

View File

@ -1,5 +0,0 @@
{
"auths": {
}
}

View File

@ -1,9 +0,0 @@
# Template to use - always required!
TEMPLATE=watchtower
# Service settings specific to this server
# (can also override anything in the _basic.env file in the template to make it specific to this server)
# Interval in seconds between checks.
# Default is 1800 seconds (30 minutes).
INTERVAL=1800

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