Compare commits

...

129 Commits

Author SHA1 Message Date
Your Name
9d4e5f76ce 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m12s
2025-06-15 21:37:21 +12:00
Your Name
366f5c2d0e 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m57s
2025-06-02 00:44:23 +12:00
Your Name
0b0f3df59c 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m20s
2025-06-01 23:38:58 +12:00
Your Name
f48302c05e 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-06-01 23:37:34 +12:00
Your Name
7f341699c1 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m52s
2025-06-01 23:34:31 +12:00
Your Name
18c53acd71 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m21s
2025-06-01 23:16:42 +12:00
Your Name
eb632c010c 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m27s
2025-06-01 23:06:14 +12:00
Your Name
964e8598b1 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m27s
2025-06-01 18:33:37 +12:00
Your Name
a5cf9313e9 dropshell release 2025.0601.1821
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-06-01 18:21:32 +12:00
Your Name
ab73a47751 dropshell release 2025.0601.1754
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m31s
2025-06-01 17:54:13 +12:00
Your Name
1da7dc7951 dropshell release 2025.0601.1752
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 16m46s
2025-06-01 17:53:06 +12:00
Your Name
49d61f0da0 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 16m26s
2025-06-01 15:57:52 +12:00
Your Name
27c0abcb9f 'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-06-01 15:38:57 +12:00
Your Name
483ee4e3ef :-'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 13m16s
2025-05-30 00:14:24 +12:00
Your Name
f7294e01e4 :-'Generic Commit'
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m40s
2025-05-28 20:40:24 +12:00
Your Name
c836b26657 dropshell release 2025.0527.2201
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m44s
2025-05-27 22:01:51 +12:00
Your Name
7bf624589f tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m46s
2025-05-26 23:39:40 +12:00
Your Name
a5e339a358 tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m37s
2025-05-26 23:26:37 +12:00
Your Name
029823a6b4 tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m52s
2025-05-26 23:22:38 +12:00
Your Name
f79abd346e Tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m2s
2025-05-26 23:19:10 +12:00
Your Name
940c2a12a1 dropshell release 2025.0526.2310
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m58s
2025-05-26 23:12:28 +12:00
Your Name
6ac651d4f0 Tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m40s
2025-05-26 22:49:37 +12:00
Your Name
84fd96e74e dropshell release 2025.0526.2234
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m47s
2025-05-26 22:34:52 +12:00
Your Name
8eb652b84e dropshell release 2025.0526.2224
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 3m0s
2025-05-26 22:25:03 +12:00
Your Name
c8c0c3289c dropshell release 2025.0526.2223
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-26 22:23:35 +12:00
Your Name
3dc82c682c dropshell release 2025.0526.2221
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-26 22:21:39 +12:00
Your Name
4035538ba5 GPT4.5 is having a go
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m59s
2025-05-26 00:13:32 +12:00
Your Name
8ec4976cc0 Playing with static still
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m30s
2025-05-25 23:59:50 +12:00
Your Name
1b35f74bfe Seems to be fully statically built now.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m15s
2025-05-25 23:47:12 +12:00
Your Name
08794e6480 LOL Zig
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m43s
2025-05-25 23:04:39 +12:00
Your Name
3b51a511a6 zlib not static
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m28s
2025-05-25 22:40:46 +12:00
Your Name
e1be3dff8d Tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m17s
2025-05-25 22:32:56 +12:00
Your Name
9375acafa9 Ninja and ccache - faster builds!
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m28s
2025-05-25 22:18:58 +12:00
Your Name
f45baa8362 Craziness with multiarch
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m18s
2025-05-25 22:12:14 +12:00
Your Name
d3ceb3f4be Add vscode stuff.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m28s
2025-05-25 19:46:14 +12:00
Your Name
27a2d25fb2 Fixed.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m33s
2025-05-25 19:44:10 +12:00
Your Name
e7558be416 Not better.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m29s
2025-05-25 19:31:43 +12:00
Your Name
8f06fc31ae Debugging
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 2m10s
2025-05-25 19:17:51 +12:00
Your Name
1502d6e3d2 Fix test.yaml
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 1m26s
2025-05-25 18:16:19 +12:00
Your Name
d71ba38754 ds
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s
2025-05-25 18:14:15 +12:00
Your Name
f45d9a33ed yaml
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s
2025-05-25 10:10:07 +12:00
Your Name
73f85769a2 yaml
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-25 10:02:45 +12:00
Your Name
9a141685de Update test.
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s
2025-05-24 21:27:02 +12:00
Your Name
9c94510213 .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s
2025-05-24 20:44:21 +12:00
Your Name
bcc78859fc dropshell release 2025.0524.2033
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 11s
2025-05-24 20:33:55 +12:00
Your Name
a5243a7e79 Install is broken - putting the wrong template on.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-24 20:05:00 +12:00
Your Name
ddc57173cb .
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 11s
2025-05-24 19:27:48 +12:00
Your Name
343e597d84 Working on nuke
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-24 17:16:19 +12:00
Your Name
60907e5e02 List tidy
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 11s
2025-05-24 17:00:43 +12:00
Your Name
b3398582ca dropshell release 2025.0524.1314
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s
2025-05-24 13:14:51 +12:00
Your Name
763293c7d0 dropshell release 2025.0524.1149
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 17s
2025-05-24 11:49:24 +12:00
Your Name
0934179053 ...
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-24 11:15:23 +12:00
Your Name
e1c631dc72 Fix list a bit
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-23 23:07:26 +12:00
Your Name
b07704612b .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-23 22:24:28 +12:00
Your Name
462d215d5c Big refactor
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-23 22:06:37 +12:00
Your Name
048345c636 Broken 2025-05-23 21:41:33 +12:00
Your Name
94f77994f0 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 21:38:40 +12:00
Your Name
b3a57f13dc dropshell release 2025.0521.2125
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 21:25:39 +12:00
Your Name
270d6ef792 Tidying
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 20:51:05 +12:00
Your Name
9063edb45f create-server
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 19:30:10 +12:00
Your Name
fc6b310b89 dropshell release 2025.0521.1908
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 19:08:48 +12:00
Your Name
7a710b525f dropshell release 2025.0521.1906
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-21 19:06:50 +12:00
Your Name
1b16741288 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 23:09:43 +12:00
Your Name
d8236a58df dropshell release 2025.0519.1727
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 17:28:01 +12:00
j842
625de98890 Fix generated files in build, so they don't have to exist when the build starts.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 11:24:42 +12:00
j842
7c1b51a93c Don't include autogen in commits
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 11:19:47 +12:00
j842
1be7af571f Remove autogen files. 2025-05-19 11:18:36 +12:00
j842
1439ec2f79 Add dehydrate to build prereq's.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 11:16:57 +12:00
Your Name
6c3c35bf89 dropshell release 2025.0519.0021
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-19 00:21:34 +12:00
Your Name
548ffea6f9 dropshell release 2025.0518.2308
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 23:08:44 +12:00
Your Name
434a2bc6da dropshell release 2025.0518.2245
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 22:45:25 +12:00
Your Name
cf8738aee9 dropshell release 2025.0518.2236
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 22:37:00 +12:00
Your Name
d80820db15 Back to issue from pre-refactor!
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 20:23:15 +12:00
Your Name
fb6974b51a Working on backup/restore.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 20:19:47 +12:00
Your Name
263edd9b50 dropshell release 2025.0518.1558
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 15:58:29 +12:00
Your Name
e45afe460b Backups seem to be working
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
.
2025-05-18 15:43:19 +12:00
Your Name
630a9fd19a Add backupdata
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 15:41:51 +12:00
Your Name
f89d90c12b dropshell release 2025.0518.1451
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 14:51:53 +12:00
Your Name
5883c62c54 Tidy output
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 14:29:51 +12:00
Your Name
b278e81533 info for table print
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 14:03:12 +12:00
Your Name
6c99b429b9 dropshell release 2025.0518.1356
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 13:56:52 +12:00
Your Name
2b446f80a3 dropshell release 2025.0518.1355
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 13:55:39 +12:00
Your Name
314a5fe96a Trying new approach 2025-05-18 13:07:09 +12:00
Your Name
828171c977 dropshell release 2025.0518.1300
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 13:00:35 +12:00
Your Name
668cef5a05 . 2025-05-18 12:40:26 +12:00
Your Name
e7c6d38273 dropshell release 2025.0518.1218
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 12:18:43 +12:00
Your Name
d0152c3ec7 dropshell release 2025.0518.1150
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 11:50:23 +12:00
Your Name
ebb101e381 dropshell release 2025.0518.1145
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-18 11:45:37 +12:00
Your Name
dc2f694ebe .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 23:12:43 +12:00
Your Name
038d08e638 dropshell release 2025.0517.2231
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 22:31:29 +12:00
Your Name
27f86e95e7 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 22:24:25 +12:00
Your Name
891f0d023f Self-test.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 20:41:17 +12:00
Your Name
91f706ffcd dropshell release 2025.0517.2027
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 20:27:46 +12:00
Your Name
0e1ac9ddd8 dropshell release DEV
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 20:21:31 +12:00
Your Name
e5aaa57259 dropshell release DEV
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 20:10:55 +12:00
Your Name
cf42ce5304 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 19:54:07 +12:00
Your Name
1d3bb634f0 Refact0r
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 19:40:51 +12:00
Your Name
203068048d FIx nuke
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 18:55:14 +12:00
Your Name
5bf93dc954 Wrench
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 18:17:25 +12:00
Your Name
583bb18676 .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 17:22:23 +12:00
Your Name
4c0bca4def Nice message.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:40:53 +12:00
Your Name
4efccf7793 Fix install check
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:37:51 +12:00
Your Name
f362c1699b variables now correctly passed through to commands run from _allservicestatus.sh
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:33:35 +12:00
Your Name
4147d4b97f .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:24:38 +12:00
Your Name
399fe1d549 Aligning the different parts with the new directory and file structure.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:16:25 +12:00
Your Name
67da992326 Fixing allservicestatus
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 12:00:13 +12:00
Your Name
82af6a6af7 Working!
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 11:56:09 +12:00
Your Name
985153377f ...
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 11:12:33 +12:00
Your Name
93e563948f Shift things around
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 10:18:25 +12:00
Your Name
9eb9707c2e Can now create local agent files.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-17 10:12:57 +12:00
Your Name
49b1475ffd Remote create not working yet.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-15 00:01:26 +12:00
Your Name
a68e31eb6b .
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:58:57 +12:00
Your Name
5be6a3e038 ..
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:57:30 +12:00
Your Name
707e973130 ./
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:47:23 +12:00
Your Name
2cc00244c6 Experimental?
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:34:10 +12:00
Your Name
22e37b212a Installs bb64 correctly.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:12:18 +12:00
Your Name
ba866494cd fix perms
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 23:01:56 +12:00
Your Name
d8d73058de README
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:36:21 +12:00
Your Name
b396441271 readme
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:32:56 +12:00
Your Name
2397c665a5 dropshell release 2025.0514.2229
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:29:26 +12:00
Your Name
a828100878 dropshell release 2025.0514.2226
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:26:45 +12:00
Your Name
fcc517a115 dropshell release 2025.0514.2225
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:25:20 +12:00
Your Name
47dcfca5f2 dropshell release 2025.0514.2222
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-14 22:22:15 +12:00
Your Name
283b88effc Working on install.
Some checks failed
Dropshell Test / Build_and_Test (push) Has been cancelled
2025-05-13 22:26:24 +12:00
Your Name
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
Your Name
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
Your Name
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
Your Name
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
109 changed files with 17638 additions and 30312 deletions

View File

@ -1,5 +1,5 @@
SSH_ADDRESS=localhost
SSH_PORT=22
SSH_USER=$(whoami)
SSH_UNPRIVILEGED_USER=$(whoami)

View File

@ -12,12 +12,28 @@ jobs:
sudo apt-get install -y openssh-server
- name: Check out repository code
uses: actions/checkout@v4
- name: Build
- name: Install build dependencies
run: |
cd ${{ gitea.workspace }}/docker
./compile.sh
cd source
./install_build_prerequisites.sh
- name: Build Native
run: |
cd source
./build_native.sh
- name: Test
run: |
cd ${{ gitea.workspace }}/docker/output
./dropshell_x86_64 list
./dropshell_x86_64 help
cd source
./test.sh
- name: Build Production
run: |
cd source
./build_production.sh
- name: Test
run: |
cd source
./test.sh
- name: Publish
run: |
cd source
./publish.sh

2
.gitignore vendored
View File

@ -7,6 +7,7 @@ out/
bin/
lib/
output/
autogen/
# Compiled Object files
*.o
@ -38,7 +39,6 @@ cmake_install.cmake
Makefile
# IDE specific files
.vscode/
.idea/
*.swp
*.swo

17
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"${workspaceFolder}/source/build/src/autogen"
],
"defines": [],
"compilerPath": "/usr/bin/g++",
"cStandard": "c23",
"cppStandard": "c++23",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}

103
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,103 @@
{
"files.associations": {
"*.inja": "jinja-html",
"*.ipp": "cpp",
"random": "cpp",
"ostream": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"csetjmp": "cpp",
"csignal": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"any": "cpp",
"array": "cpp",
"atomic": "cpp",
"strstream": "cpp",
"bit": "cpp",
"*.tcc": "cpp",
"bitset": "cpp",
"cfenv": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"cinttypes": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"complex": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"cstdint": "cpp",
"deque": "cpp",
"forward_list": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"expected": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"ratio": "cpp",
"regex": "cpp",
"source_location": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"numbers": "cpp",
"ranges": "cpp",
"scoped_allocator": "cpp",
"semaphore": "cpp",
"shared_mutex": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"typeindex": "cpp",
"typeinfo": "cpp",
"valarray": "cpp",
"variant": "cpp",
"format": "cpp",
"stdfloat": "cpp",
"__nullptr": "cpp",
"__node_handle": "cpp",
"__split_buffer": "cpp",
"filesystem": "cpp",
"queue": "cpp",
"stack": "cpp",
"__bit_reference": "cpp",
"__functional_base": "cpp",
"__memory": "cpp",
"locale": "cpp",
"stacktrace": "cpp",
"__locale": "cpp"
}
}

View File

@ -1,135 +0,0 @@
cmake_minimum_required(VERSION 3.10)
project(dropshell VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Set default build type to Release if not specified
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build (Debug or Release)" FORCE)
endif()
# Configure build-specific compiler flags
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")
# Configure version information
string(TIMESTAMP CURRENT_YEAR "%Y")
string(TIMESTAMP CURRENT_MONTH "%m")
string(TIMESTAMP CURRENT_DAY "%d")
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
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/src/version.hpp.in"
"${CMAKE_CURRENT_BINARY_DIR}/src/version.hpp"
@ONLY
)
# Set CMAKE_MODULE_PATH to include our custom find modules
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
# Auto-detect source files
file(GLOB_RECURSE SOURCES "src/*.cpp")
file(GLOB_RECURSE HEADERS "src/*.hpp")
# Add executable
add_executable(dropshell ${SOURCES})
# Set include directories
# build dir goes first so that we can use the generated version.hpp
target_include_directories(dropshell PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src>
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
${CMAKE_CURRENT_SOURCE_DIR}/src/contrib
${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
)
# Install targets
install(TARGETS dropshell
RUNTIME DESTINATION bin
)
# Create symbolic link 'ds' pointing to 'dropshell'
install(CODE "
message(STATUS \"Checking if 'ds' command already exists...\")
execute_process(
COMMAND which ds
RESULT_VARIABLE DS_NOT_EXISTS
OUTPUT_QUIET
ERROR_QUIET
)
if(DS_NOT_EXISTS)
message(STATUS \"Command 'ds' does not exist. Creating symlink.\")
execute_process(
COMMAND ${CMAKE_COMMAND} -E create_symlink
\${CMAKE_INSTALL_PREFIX}/bin/dropshell
\${CMAKE_INSTALL_PREFIX}/bin/ds
)
else()
message(STATUS \"Command 'ds' already exists. Skipping symlink creation.\")
endif()
")
# Install completion script
install(FILES src/dropshell-completion.bash
DESTINATION /etc/bash_completion.d
RENAME dropshell
)
# Create a symlink for the completion script to work with 'ds' command
install(CODE "
# First check if 'ds' command exists after our installation
execute_process(
COMMAND which ds
RESULT_VARIABLE DS_NOT_EXISTS
OUTPUT_VARIABLE DS_PATH
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Only proceed if 'ds' exists
if(NOT DS_NOT_EXISTS)
# Check if 'ds' is a symlink pointing to dropshell
execute_process(
COMMAND readlink -f \${DS_PATH}
RESULT_VARIABLE READLINK_FAILED
OUTPUT_VARIABLE REAL_PATH
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Get the path to our dropshell binary
set(DROPSHELL_PATH \${CMAKE_INSTALL_PREFIX}/bin/dropshell)
# Check if the real path is our dropshell binary
if(NOT READLINK_FAILED AND \"\${REAL_PATH}\" STREQUAL \"\${DROPSHELL_PATH}\")
message(STATUS \"Command 'ds' exists and points to dropshell. Creating completion script symlink.\")
execute_process(
COMMAND ${CMAKE_COMMAND} -E create_symlink
/etc/bash_completion.d/dropshell
/etc/bash_completion.d/ds
)
else()
message(STATUS \"Command 'ds' exists but doesn't point to dropshell. Skipping completion symlink.\")
endif()
else()
message(STATUS \"Command 'ds' not found. Skipping completion symlink.\")
endif()
")

View File

@ -1,3 +1,54 @@
# 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
```
This installs as dropshell for the local user, with a symbolic link ds.
You'll need to run:
```
~/.local/bin/dropshell edit
~/.local/bin/dropshell install
source ~/.bashrc
```
to configure dropshell and install the local components.
## Remote Server Setup
### Initial setup
Auto setup script which creates a dropshell user, and includes installing docker if not already present:
```
curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/server_autosetup.sh | sudo bash
```
Manual steps:
1. `apt install curl wget jq`
1. `curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh`
1. `useradd -m dropshell && usermod -aG docker dropshell && chsh -s /bin/bash dropshell`
1. Put appropriate ssh keys in `/home/dropshell/.ssh/authorized_keys`
1. Test ssh'ing into the server.
### Configure and Use Remote Server
#### Add to local dropshell configuration, and install remote agent
Back on the dropshell host:
1. `dropshell create-server SERVERNAME`
1. `dropshell edit SERVERNAME`
1. `dropshell install SERVERNAME`
#### Install Services
Create and install a service
1. `ds template list` -- see what templates are available to install.
1. `ds create-service SERVERNAME SERVICENAME TEMPLATE`
1. `ds edit SERVERNAME SERVICENAME`
1. Edit other config files if needed.
1. `ds install SERVERNAME SERVICENAME`
1. `ds list`
The service should now be seen to be running.

View File

@ -1,25 +0,0 @@
#!/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"

117
build.sh
View File

@ -1,117 +0,0 @@
#!/bin/bash
# Exit on error
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Parse command line arguments
AUTO_INSTALL=false
for arg in "$@"; do
case $arg in
--auto-install)
AUTO_INSTALL=true
;;
esac
done
# 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 build directory exists, if not create it
if [ ! -d "build" ]; then
print_status "Creating build directory..."
mkdir build
fi
# Enter build directory
cd build
# Check if CMake is installed
if ! command -v cmake &> /dev/null; then
print_error "CMake is not installed. Please install CMake first."
exit 1
fi
# Check if make is installed
if ! command -v make &> /dev/null; then
print_error "Make is not installed. Please install Make first."
exit 1
fi
# Check if pkg-config is installed
if ! command -v pkg-config &> /dev/null; then
print_error "pkg-config is not installed. Please install pkg-config first."
print_warning "On Ubuntu/Debian: sudo apt-get install pkg-config"
print_warning "On Fedora: sudo dnf install pkg-config"
print_warning "On Arch: sudo pacman -S pkg-config"
exit 1
fi
# Check if ncurses is installed
if ! pkg-config --exists ncurses; then
print_error "ncurses is not installed. Please install ncurses first."
print_warning "On Ubuntu/Debian: sudo apt-get install libncurses-dev"
print_warning "On Fedora: sudo dnf install ncurses-devel"
print_warning "On Arch: sudo pacman -S ncurses"
exit 1
fi
# Configure with CMake
print_status "Configuring with CMake..."
cmake .. -DCMAKE_BUILD_TYPE=Debug
#cmake .. -DCMAKE_BUILD_TYPE=Release
# Build the project
print_status "Building project..."
make -j$(nproc)
# Check if build was successful
if [ $? -eq 0 ]; then
print_status "Build successful!"
print_status "Binary location: $(pwd)/dropshell"
else
print_error "Build failed!"
exit 1
fi
# Check if user wants to install
if [ $AUTO_INSTALL = true ]; then
print_status "Auto-installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
else
print_status "Installing dropshell..."
sudo make install
if [ $? -eq 0 ]; then
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
fi
# Return to original directory
cd ..
print_status "Build process completed!"

22
dropshell-install.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
# download and install dropshell
# 1. Determine architecture
# -----------------------------------------------------------------------------
ARCH=$(uname -m)
TARGET_PATH="${HOME}/.local/bin/dropshell"
[ ! -f "${TARGET_PATH}" ] || rm -f "${TARGET_PATH}"
mkdir -p "$(dirname "${TARGET_PATH}")"
curl -L -s -o "${TARGET_PATH}" "https://getbin.xyz/dropshell.${ARCH}" || die "Failed to download dropshell for ${ARCH}"
chmod +x "${TARGET_PATH}"
echo "dropshell installed successfully to $TARGET_PATH"
echo " "
echo "Please:"
echo "1. run '${TARGET_PATH} edit' to edit the configuration."
echo "2. run '${TARGET_PATH} install' to install dropshell components on this computer."
echo "3. run 'source ~/.bashrc' to add to your path and autocomplete for the current shell."

View File

@ -1,4 +1,6 @@
# can you make this script run in bash, but fall back to sh if bash is not installed?
#!/bin/bash
# set up a remote server for use with dropshell.
# check if we are running as root
if [ "$(id -u)" -ne 0 ]; then
@ -90,18 +92,6 @@ 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

@ -1,40 +0,0 @@
#!/bin/bash
set -e
# download and install dropshell
# 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"
docker run --rm -v "$TMPDIR:/tmp" -v /usr/local/bin:/target alpine sh -c "cp /tmp/dropshell /target/dropshell && ln -s /target/dropshell /target/ds"
rm "$TMPDIR/dropshell"
echo "dropshell installed successfully to /usr/local/bin/dropshell"

View File

@ -1,150 +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"
;;
*)
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,25 +0,0 @@
#!/bin/bash
# build amd64 and arm64 versions of dropshell, to:
# build/dropshell.amd64
# build/dropshell.arm64
set -e
# Build for amd64
echo "Building for amd64..."
cmake -B build_amd64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-march=x86-64" .
cmake --build build_amd64 --target dropshell --config Release
mkdir -p build
cp build_amd64/dropshell build/dropshell.amd64
# Build for arm64
echo "Building for arm64..."
cmake -B build_arm64 -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-march=armv8-a" -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ .
cmake --build build_arm64 --target dropshell --config Release
mkdir -p build
cp build_arm64/dropshell build/dropshell.arm64
echo "Builds complete:"
ls -lh build/dropshell.*

View File

@ -1,95 +0,0 @@
#!/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."

133
source/CMakeLists.txt Normal file
View File

@ -0,0 +1,133 @@
cmake_minimum_required(VERSION 3.10)
project(dropshell VERSION 1.0.0 LANGUAGES CXX)
# Force static linking globally
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE)
set(CMAKE_POSITION_INDEPENDENT_CODE OFF)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static")
set(ZLIB_USE_STATIC_LIBS "ON")
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_C_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Set default build type to Release if not specified
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build (Debug or Release)" FORCE)
endif()
# Configure build-specific compiler flags
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DNDEBUG")
# Configure version information
string(TIMESTAMP CURRENT_YEAR "%Y")
string(TIMESTAMP CURRENT_MONTH "%m")
string(TIMESTAMP CURRENT_DAY "%d")
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
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/src/version.hpp.in"
"${CMAKE_CURRENT_BINARY_DIR}/src/autogen/version.hpp"
@ONLY
)
# Set CMAKE_MODULE_PATH to include our custom find modules
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
# Auto-detect source files
file(GLOB_RECURSE SOURCES "src/*.cpp")
file(GLOB_RECURSE HEADERS "src/*.hpp")
# Add custom target to run cmake_prebuild.sh at the start of the build process
add_custom_target(run_prebuild_script ALL
COMMAND ${CMAKE_COMMAND} -E echo "Running cmake_prebuild.sh..."
COMMAND ${CMAKE_COMMAND} -E env bash ${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
# Add executable
add_executable(dropshell ${SOURCES})
add_dependencies(dropshell run_prebuild_script)
# Mark the generated files as GENERATED so CMake knows they'll be created during build
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/src/autogen/_agent-remote.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/autogen/_agent-local.cpp
PROPERTIES GENERATED TRUE
)
# Explicitly add the generated agent files, as they might not be in the source directory when globbed at the start.
target_sources(dropshell PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/autogen/_agent-remote.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/autogen/_agent-local.cpp
)
# Set include directories
# build dir goes first so that we can use the generated version.hpp
target_include_directories(dropshell PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/src/autogen>
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
${CMAKE_CURRENT_SOURCE_DIR}/src/contrib
${CMAKE_CURRENT_SOURCE_DIR}/src/commands
${CMAKE_CURRENT_SOURCE_DIR}/src/autogen
)
if(WIN32)
add_custom_command(
TARGET dropshell POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE_DIR:dropshell>
)
endif()
# Configure libassert
include(FetchContent)
FetchContent_Declare(
libassert
GIT_REPOSITORY https://github.com/jeremy-rifkin/libassert.git
GIT_TAG v2.1.5
)
FetchContent_MakeAvailable(libassert)
# Add cpptrace
FetchContent_Declare(
cpptrace
GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git
GIT_TAG v0.8.3
)
FetchContent_MakeAvailable(cpptrace)
# Add nlohmann/json
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(nlohmann_json)
# Link libraries
target_link_libraries(dropshell PRIVATE
libassert::assert
cpptrace::cpptrace
nlohmann_json::nlohmann_json
)
# Set static linking flags
set_target_properties(dropshell PROPERTIES
LINK_FLAGS "-static"
)
# Install targets
install(TARGETS dropshell
RUNTIME DESTINATION $ENV{HOME}/.local/bin
)

65
source/Dockerfile Normal file
View File

@ -0,0 +1,65 @@
FROM --platform=$BUILDPLATFORM alpine:latest AS builder
# Add build arguments for platform
ARG TARGETPLATFORM
ARG BUILDPLATFORM
# Install build dependencies
RUN apk add --no-cache \
build-base \
cmake \
git \
musl-dev \
curl \
bash \
musl \
g++ \
ninja \
linux-headers
# Install cross-compilation tools for ARM64
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
apk add --no-cache \
crossbuild-essential-arm64 \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu; \
fi
# Set working directory
WORKDIR /build
# Copy source files
COPY . .
# Configure and build
RUN mkdir -p build_static
# Set up cross-compilation environment for ARM64
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
export CC=aarch64-linux-gnu-gcc \
export CXX=aarch64-linux-gnu-g++ \
export CMAKE_TOOLCHAIN_FILE=/build/toolchain.cmake; \
fi
# Create toolchain file for ARM64
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
echo "set(CMAKE_SYSTEM_NAME Linux)" > toolchain.cmake && \
echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake && \
echo "set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)" >> toolchain.cmake && \
echo "set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)" >> toolchain.cmake && \
echo "set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)" >> toolchain.cmake && \
echo "set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)" >> toolchain.cmake && \
echo "set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)" >> toolchain.cmake; \
fi
RUN cmake -G Ninja -B build_static -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXE_LINKER_FLAGS="-static" \
-DCMAKE_FIND_LIBRARY_SUFFIXES=".a" \
-DBUILD_SHARED_LIBS=OFF \
${CMAKE_TOOLCHAIN_FILE:+-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE}
RUN cmake --build build_static
# Final stage that only contains the binary
FROM scratch AS dropshell
COPY --from=builder /build/build_static/dropshell /dropshell

View File

@ -0,0 +1,63 @@
#!/bin/bash
# install the dropshell host agent on this computer.
# (not for remote servers)
SCRIPT_DIR=$(dirname "$0")
echo "Installing dropshell host agent on this computer..."
# Prints an error message in red and exits with status code 1.
_die() {
echo -e "Error: $1"
exit 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
}
function install_bb64() {
# check curl installed
if ! command -v curl &> /dev/null; then
_die "Curl is not installed. Curl is required for agent installation."
fi
curl -fsSL "https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh" | bash -s -- "$AGENT_LOCAL_PATH" "$(id -u $USER):$(id -g $USER)"
# test result code from curl
if [ $? -ne 0 ]; then
_die "Failed to install bb64. Curl returned non-zero exit code."
fi
# test if bb64 is installed
"$AGENT_LOCAL_PATH/bb64" -v
if [ $? -ne 0 ]; then
_die "bb64 did not install correctly."
fi
echo "bb64 installed successfully."
return 0;
}
#-------------------------------------------------------------------------
set -a
AGENT_LOCAL_PATH="$SCRIPT_DIR"
set +a
_check_required_env_vars "AGENT_LOCAL_PATH"
echo "Installing host agent into $AGENT_LOCAL_PATH"
install_bb64

View File

@ -0,0 +1 @@
TODO!

View File

@ -9,19 +9,28 @@
# Get all services on the server
SCRIPT_DIR="$(dirname "$0")"
# // DROPSHELL_DIR
# // |-- backups
# // |-- services
# // |-- service name
# // |-- config <-- this is passed as argument to all scripts
# // |-- service.env
# // |-- template
# // |-- (script files)
# // |-- shared
# // |-- _allservicesstatus.sh
# // |-- config
# // |-- service.env
# // |-- (other config files for specific server&service)
# //------------------------------------------------------------------------------------------------
# // remote paths
# // DROPSHELL_DIR
# // |-- backups
# // |-- temp_files
# // |-- agent
# // | |-- bb64
# // | |-- (other agent files, including _allservicesstatus.sh)
# // |-- services
# // |-- service name
# // |-- config
# // |-- service.env (actual service config)
# // |-- .template_info.env
# // |-- template
# // |-- (script files)
# // |-- config
# // |-- service.env (default service config)
# // |-- .template_info.env
# // |-- (other config files for specific server&service)
# Get all services on the server
SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../services/")
CURRENT_OUTPUT=""
CURRENT_EXIT_CODE=0
@ -29,10 +38,20 @@ CURRENT_EXIT_CODE=0
load_dotenv(){
local file_path=$1
if [ -f "${file_path}" ]; then
# shellcheck source=/dev/null
source "${file_path}"
fi
}
_check_required_env_vars_allservicesstatus() {
local required_vars=("$@")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
_die "Required environment variable $var is not set"
fi
done
}
function run_command() {
local service_path=$1
local command=$2
@ -46,15 +65,19 @@ function run_command() {
# run the command in a subshell to prevent environment changes
CURRENT_OUTPUT=$(
set -a
load_dotenv "${service_path}/template/_default.env"
load_dotenv "${service_path}/config/service.env"
set +a
load_dotenv "${service_path}/config/.template_info.env"
# update the main variables.
export CONFIG_PATH="${service_path}/config"
# SERVER is correct
export SERVICE="${SERVICE_NAME}"
export DOCKER_CLI_HINTS=false
set +a
_check_required_env_vars_allservicesstatus "CONFIG_PATH" "SERVER" "SERVICE" "AGENT_PATH" "HOST_NAME" "TEMPLATE"
if [ "$capture_output" = "true" ]; then
# Capture and return output
bash "${service_path}/template/${command}.sh" 2>&1
@ -75,8 +98,11 @@ function command_exists() {
return 0
}
# Get all services on the server
SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../../../")
if [ ! -d "${SERVICES_PATH}" ]; then
echo "Services path does not exist: ${SERVICES_PATH}"
exit 0
fi
# Get all service names
SERVICE_NAMES=$(ls "${SERVICES_PATH}")

View File

@ -0,0 +1,90 @@
#!/bin/bash
# This script is used to install the dropshell agent on a remote server.
SCRIPT_DIR=$(dirname "$0")
set -a
AGENT_PATH="$SCRIPT_DIR"
set +a
if [ -f "$SCRIPT_DIR/common.sh" ]; then
source "$SCRIPT_DIR/common.sh"
else
echo "Error: common.sh not found in $SCRIPT_DIR"
exit 1
fi
_check_required_env_vars "AGENT_PATH"
function install_bb64() {
if ! curl -fsSL "https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh" | \
bash -s -- "$AGENT_PATH" "$(id -u "$USER"):$(id -g "$USER")"; then
_die "Failed to install bb64. Curl returned non-zero exit code."
fi
# test if bb64 is installed
if ! VER=$("$AGENT_PATH/bb64" -v); then
_die "bb64 did not install correctly."
fi
echo "bb64 v$VER installed."
return 0;
}
#-------------------------------------------------------------------------
# Check pre-requisites
# Check if curl is installed
if ! command -v curl &> /dev/null; then
_die "Curl is not installed. Curl is required for agent installation."
fi
# check docker installation
if ! command -v docker &> /dev/null; then
echo "Docker is not installed. Docker is required for agent installation."
exit 1
fi
# check rsync installation
if ! command -v rsync &> /dev/null; then
echo "Rsync is not installed. Rsync is required for agent installation."
exit 1
fi
#-------------------------------------------------------------------------
echo "Installing dropshell agent..."
install_bb64
#-------------------------------------------------------------------------
# confirm we're in a good state.
required_files=(
"$AGENT_PATH/bb64"
"$AGENT_PATH/_allservicesstatus.sh"
"$AGENT_PATH/common.sh"
"$AGENT_PATH/datacommands.sh"
)
# check if all files exist
for file in "${required_files[@]}"; do
if [ ! -f "$file" ]; then
_die "$file does not exist. Please check the installation."
fi
done
#-------------------------------------------------------------------------
echo "Completed dropshell agent installation."
#-------------------------------------------------------------------------
exit 0

View File

@ -28,20 +28,10 @@
# Prints an error message in red and exits with status code 1.
_die() {
echo -e "\033[91mError: $1\033[0m"
echo -e "Error: $1"
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
@ -51,20 +41,18 @@ _create_and_start_container() {
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
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
if ! _is_container_running "$container_name"; then
_die "Container ${container_name} failed to start"
fi
ID=$(_get_container_id $container_name)
ID=$(_get_container_id "$container_name")
echo "Container ${container_name} is running with ID ${ID}"
}
@ -105,6 +93,7 @@ _check_docker_installed() {
# Checks if a container (any state) exists. Returns 1 if not found.
_is_container_exists() {
[ -n "${1:-}" ] || { echo "_is_container_exists: Container name is empty" >&2; return 1; }
if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
@ -113,6 +102,7 @@ _is_container_exists() {
# Checks if a container is currently running. Returns 1 if not running.
_is_container_running() {
[ -n "${1:-}" ] || { echo "_is_container_running: Container name is empty" >&2; return 1; }
if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then
return 1
fi
@ -131,39 +121,39 @@ _get_container_status() {
# Starts an existing, stopped container.
_start_container() {
_is_container_exists $1 || return 1
_is_container_running $1 && return 0
docker start $1
_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
_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
_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
if ! _is_container_exists "$1"; then
echo "Container $1 does not exist"
return 1
fi
docker logs $1
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
if [ -z "${!var:-}" ]; then
_die "Required environment variable $var is not set"
fi
done
@ -180,4 +170,4 @@ _root_remove_tree() {
# Load autocommands
source "${AGENT_PATH}/_autocommands.sh"
source "${AGENT_PATH}/datacommands.sh"

View File

@ -12,22 +12,26 @@ _autocommandrun_volume() {
case "$command" in
create)
if docker volume ls | grep -q "${volume_name}"; then
echo "Volume ${volume_name} already exists - leaving unchanged"
return
fi
echo "Creating volume ${volume_name}"
docker volume create ${volume_name}
docker volume create "${volume_name}"
;;
nuke)
echo "Nuking volume ${volume_name}"
docker volume rm ${volume_name}
destroy)
echo "Destroying 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"
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"
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
}
@ -39,30 +43,42 @@ _autocommandrun_path() {
case "$command" in
create)
if [ -d "${path}" ]; then
echo "Path ${path} already exists - unchanged"
return
fi
echo "Creating path ${path}"
mkdir -p ${path}
mkdir -p "${path}"
;;
nuke)
echo "Nuking path ${path}"
local path_parent=$(dirname ${path})
local path_child=$(basename ${path})
destroy)
echo "Destroying path ${path}"
local path_parent;
path_parent=$(dirname "${path}")
local path_child;
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}"
docker run --rm -v "${path_parent}":/volume debian bash -c "rm -rfv /volume/${path_child}" || echo "Failed to destroy path ${path}"
else
echo "Path ${path} does not exist - nothing to nuke"
echo "Path ${path} does not exist - nothing to destroy"
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"
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
if [ ! -f "${backup_folder}/backup.tgz" ]; then
echo "Backup file ${backup_folder}/backup.tgz does not exist - nothing to restore"
else
echo "Clearing existing data in path ${path}"
docker run --rm -v "${path}":/path debian bash -c "rm -rfv /path/{*,.*}"
echo "Restoring path ${path} from backup file ${backup_folder}/backup.tgz"
tar -xzvf "${backup_folder}/backup.tgz" -C "${path}" --strip-components=1
fi
;;
esac
}
@ -74,24 +90,36 @@ _autocommandrun_file() {
case "$command" in
create)
local file_parent;
file_parent=$(dirname "${filepath}")
local file_name;
file_name=$(basename "${filepath}")
if [ ! -d "${file_parent}" ]; then
echo "Parent directory ${file_parent} of ${file_name} does not exist - creating"
mkdir -p "${file_parent}"
fi
;;
nuke)
rm -f ${filepath}
destroy)
rm -f "${filepath}"
;;
backup)
echo "Backing up file ${filepath}"
local file_parent=$(dirname ${filepath})
local file_name=$(basename ${filepath})
local file_parent;
file_parent=$(dirname "${filepath}")
local file_name;
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"
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}
local file_name;
file_name=$(basename "${filepath}")
rm -f "${filepath}" || return_die "Unable to remove existing file ${filepath}, restore failed."
cp "${backup_folder}/${file_name}" "${filepath}" || return_die "Unable to copy file ${backup_folder}/${file_name} to ${filepath}, restore failed."
;;
esac
}
@ -132,9 +160,10 @@ _autocommandparse() {
local value="${pair#*=}"
# create backup folder unique to key/value.
local bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-')
local bfolder;
bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-')
local targetpath="${backup_temp_path}/${bfolder}"
mkdir -p ${targetpath}
mkdir -p "${targetpath}"
# Key must be one of volume, path or file
case "$key" in
@ -155,32 +184,32 @@ _autocommandparse() {
}
autocreate() {
datacreate() {
_autocommandparse create none "$@"
}
autonuke() {
_autocommandparse nuke none "$@"
datadestroy() {
_autocommandparse destroy none "$@"
}
autobackup() {
databackup() {
_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] [$@]"
echo "_autocommandparse [backup] [$BACKUP_TEMP_PATH]" "$@"
_autocommandparse backup "$BACKUP_TEMP_PATH" "$@"
tar zcvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" .
}
autorestore() {
datarestore() {
_check_required_env_vars "BACKUP_FILE" "TEMP_DIR"
BACKUP_TEMP_PATH="$TEMP_DIR/restore"
echo "_autocommandparse [restore] [$BACKUP_TEMP_PATH] [$@]"
echo "_autocommandparse [restore] [$BACKUP_TEMP_PATH]" "$@"
mkdir -p "$BACKUP_TEMP_PATH"
tar zxvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" --strip-components=1

53
source/build_native.sh Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
OUTPUT_DIR=${SCRIPT_DIR}/output
INSTALL_DIR=${HOME}/.local/bin
mkdir -p "${OUTPUT_DIR}"
# Exit on error
set -euo pipefail
ARCH=$(uname -m)
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
echo "Unsupported architecture: $ARCH"
exit 1
fi
function build_native() {
local BUILDDIR=${SCRIPT_DIR}/build/native
local PREVDIR=$PWD
local JOBS;
JOBS=$(nproc) # Set JOBS to the number of available CPU cores
mkdir -p "${BUILDDIR}"
cd "${SCRIPT_DIR}" || exit 1
CC="${HOME}/.musl-cross/${ARCH}-linux-musl-native/bin/${ARCH}-linux-musl-gcc"
CXX="${HOME}/.musl-cross/${ARCH}-linux-musl-native/bin/${ARCH}-linux-musl-g++"
cmake -B "${BUILDDIR}" -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_LINKER=mold \
-DCMAKE_C_COMPILER="${CC}" \
-DCMAKE_CXX_COMPILER="${CXX}"
cd "${BUILDDIR}" || exit 1
ninja -j"$JOBS"
#upx ${BUILDDIR}/dropshell
cp "${BUILDDIR}/dropshell" "${OUTPUT_DIR}/dropshell.${ARCH}"
cd "${PREVDIR}" || exit 1
}
build_native
echo "Auto-installing dropshell locally..."
mkdir -p "${INSTALL_DIR}"
cp "${OUTPUT_DIR}/dropshell.${ARCH}" "${INSTALL_DIR}/dropshell"
echo "Build process completed!"

37
source/build_production.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create output directory
mkdir -p "${SCRIPT_DIR}/output"
PREV_DIR=$(pwd)
cd "${SCRIPT_DIR}"
trap 'cd "${PREV_DIR}"' EXIT
function build_arch() {
local arch=$1
if [ ! -f "${HOME}/.musl-cross/${arch}-linux-musl-cross/bin/${arch}-linux-musl-c++" ]; then
echo "Musl cross compiler for ${arch} not found. Please run install_build_prerequisites.sh first."
exit 1
fi
CMAKE_BUILD_TYPE=Release
CC="${HOME}/.musl-cross/${arch}-linux-musl-cross/bin/${arch}-linux-musl-gcc"
CXX="${HOME}/.musl-cross/${arch}-linux-musl-cross/bin/${arch}-linux-musl-g++"
BUILDDIR="${SCRIPT_DIR}/build/${arch}"
mkdir -p "${BUILDDIR}"
cmake -B "${BUILDDIR}" -G Ninja -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" -DCMAKE_C_COMPILER="${CC}" -DCMAKE_CXX_COMPILER="${CXX}"
cmake --build "${BUILDDIR}"
upx "${BUILDDIR}/dropshell"
cp "${BUILDDIR}/dropshell" "${SCRIPT_DIR}/output/dropshell.${arch}"
}
build_arch x86_64
build_arch aarch64
echo "Static binaries have been created:"
ls -la output

24
source/cmake_prebuild.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -e
# CMake pre-build script.
# Runs before the build process.
# This script creates two files:
# src/utils/createagent.hpp
# src/utils/createagent.cpp
#
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# check if dehydrate is installed
if ! command -v dehydrate &> /dev/null; then
echo "dehydrate could not be found - installing"
curl -fsSL https://gitea.jde.nz/public/dehydrate/releases/download/latest/install.sh | bash
else
# ensure we have latest dehydrate.
dehydrate -u
fi
mkdir -p "${SCRIPT_DIR}/src/autogen"
dehydrate "${SCRIPT_DIR}/agent-remote" "${SCRIPT_DIR}/src/autogen"
dehydrate "${SCRIPT_DIR}/agent-local" "${SCRIPT_DIR}/src/autogen"

View File

@ -0,0 +1,165 @@
#!/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"
#----------------------------------------------------------------------------------------------------------
# INSTALL PREREQUISITE PACKAGES
#----------------------------------------------------------------------------------------------------------
# Define packages based on distribution
case $OS in
"Ubuntu"|"Debian GNU/Linux")
# Common packages for both Ubuntu and Debian
PACKAGES="bash cmake make g++ devscripts debhelper build-essential upx musl-tools wget tar ccache ninja-build"
INSTALLCMD="apt-get install -y"
UPDATECMD="apt-get update"
;;
"Alpine Linux")
PACKAGES="bash build-base cmake git nlohmann-json wget tar curl ninja mold nodejs npm"
INSTALLCMD="apk add --no-cache"
UPDATECMD="apk update"
;;
*)
print_error "Unsupported distribution: $OS"
exit 1
;;
esac
# Function to check if a package is installed
is_package_installed() {
if [ "$OS" = "Alpine Linux" ]; then
# Use apk info <pkg> and check exit status
apk info "$1" >/dev/null 2>&1
return $?
else
dpkg -l "$1" 2>/dev/null | grep -q "^ii"
fi
}
UPDATED=false
# Install missing packages
print_status "Checking and installing required packages..."
for pkg in $PACKAGES; do
if ! is_package_installed "$pkg"; then
if [ "$UPDATED" = false ]; then
print_status "Updating package lists..."
$UPDATECMD
UPDATED=true
fi
print_status "Installing $pkg..."
$INSTALLCMD "$pkg"
if [ $? -ne 0 ]; then
print_error "Failed to install $pkg"
exit 1
fi
else
print_status "$pkg is already installed"
fi
done
# ----------------------------------------------------------------------------------------------------------
# MUSL CROSS COMPILERS
# ----------------------------------------------------------------------------------------------------------
# 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"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
function install_musl_cross() {
local TOOLCHAIN="$1"
local MUSL_CC_URL="https://musl.cc"
if [ ! -d "$INSTALL_DIR/$TOOLCHAIN" ]; then
echo "Downloading $TOOLCHAIN musl cross toolchain..."
wget -nc -O "$TMPDIR/$TOOLCHAIN.tgz" "$MUSL_CC_URL/$TOOLCHAIN.tgz"
tar -C "$INSTALL_DIR" -xvf "$TMPDIR/$TOOLCHAIN.tgz"
fi
}
function check_path() {
if [ -n "$SUDO_USER" ] && [ "$SUDO_USER" != "root" ]; then
local BASHRC="$USER_HOME/.bashrc"
local TOOLCHAIN="$1"
local MUSL_PATH="$INSTALL_DIR/$TOOLCHAIN/bin"
if ! echo "$PATH" | grep -q "$MUSL_PATH"; then
echo "Adding $MUSL_PATH to PATH in $BASHRC"
PATH_LINE="export PATH=\"$MUSL_PATH:\$PATH\""
if ! grep -Fxq "$PATH_LINE" "$BASHRC"; then
echo "" >> "$BASHRC"
echo "# Add musl cross compilers to PATH for dropshell" >> "$BASHRC"
echo "$PATH_LINE" >> "$BASHRC"
echo "Added musl cross compilers to $BASHRC"
echo "You should run 'source ~/.bashrc' to update your PATH"
else
echo "You should run 'source ~/.bashrc' to update your PATH"
fi
fi
fi
}
TOOLCHAIN_LIST=(
"aarch64-linux-musl-cross"
"x86_64-linux-musl-cross"
"x86_64-linux-musl-native"
)
for TOOLCHAIN in "${TOOLCHAIN_LIST[@]}"; do
install_musl_cross "$TOOLCHAIN"
check_path "$TOOLCHAIN"
done
# Clean up
rm -rf "$TMPDIR"
# ----------------------------------------------------------------------------------------------------------
# COMPLETE
# ----------------------------------------------------------------------------------------------------------
print_status "All dependencies installed successfully!"
print_status "You can now run ./build.sh to build the project"

57
source/publish.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/bash
set -e
# directory of this script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "Script directory: $SCRIPT_DIR"
# Check for GITEA_TOKEN_DEPLOY or GITEA_TOKEN
TOKEN="${GITEA_TOKEN_DEPLOY:-${GITEA_TOKEN}}"
[ -z "$TOKEN" ] && { echo "Neither GITEA_TOKEN_DEPLOY nor GITEA_TOKEN environment variable set!" >&2; exit 1; }
OLD_PWD="$PWD"
cd "$SCRIPT_DIR" || exit 1
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR" && cd "$OLD_PWD"' EXIT
ARCH=$(uname -m)
TAG=$("$SCRIPT_DIR/output/dropshell.${ARCH}" --version)
[ -z "$TAG" ] && echo "Failed to get version from dropshell.${ARCH}" >&2 && exit 1
echo "Publishing dropshell version $TAG"
function die() {
echo "$@" >&2
exit 1
}
# Function to find file in specified locations
find_file() {
local filename="$1"
shift # remove filename from args
local locations=("$@") # grab the rest of the args as locations
for loc in "${locations[@]}"; do
if [ -f "$loc/$filename" ]; then
echo "$loc/$filename"
return 0 # Found the file, return success
fi
done
echo "" # Return empty string if not found
return 1
}
curl -L -s -o "${TEMP_DIR}/sos" "https://getbin.xyz/sos" || die "Failed to download sos"
chmod +x "${TEMP_DIR}/sos"
# Upload binaries and install.sh
for FILE in dropshell.x86_64 dropshell.aarch64 dropshell-install.sh dropshell-server-autosetup.sh; do
# Pass the locations directly to the find_file function
filetoupload=$(find_file "$FILE" "output" "../" ".")
[ -z "$filetoupload" ] && die "File $FILE not found in expected locations!"
"${TEMP_DIR}/sos" upload getbin.xyz "$filetoupload" "$FILE:latest" "$FILE:TAG"
done
echo "Published dropshell $TAG to getbin.xyz"

View File

@ -5,7 +5,7 @@
#include "services.hpp"
#include "servers.hpp"
#include <assert.hpp>
#include <libassert/assert.hpp>
#include <algorithm>
#include <iostream>
@ -14,9 +14,7 @@ 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"};
const std::set<std::string> system_commands_hidden = {"_allservicesstatus"};
void merge_commands(std::set<std::string> &commands, const std::set<std::string> &new_commands)
{
@ -59,7 +57,7 @@ bool autocomplete(const std::vector<std::string> &args)
{
auto servers = dropshell::get_configured_servers();
for (const auto& server : servers)
std::cout << server.name << std::endl;
std::cout << server.get_server_name() << std::endl;
return true;
}

View File

@ -0,0 +1,172 @@
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/output.hpp"
#include <libassert/assert.hpp>
#include "utils/utils.hpp"
#include "command_registry.hpp"
#include "config.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "servers.hpp"
#include "templates.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
namespace dropshell
{
int backupdata_handler(const CommandContext &ctx);
static std::vector<std::string> backupdata_name_list = {"backupdata", "bd", "backup", "bup"};
// Static registration
struct BackupDataCommandRegister
{
BackupDataCommandRegister()
{
CommandRegistry::instance().register_command({backupdata_name_list,
backupdata_handler,
shared_commands::std_autocomplete_allowall,
false, // hidden
true, // requires_config
true, // requires_install
2, // min_args (after command)
2, // max_args (after command)
"backupdata SERVER SERVICE",
"Backup data for a service on a server.",
// heredoc
R"(
backupdata SERVER SERVICE Backup data for a service on a server.
backupdata SERVER all Backup data for all services on a server.
Note: This command will not create any data or configuration.
It will simply backup the data on the remote server, saving it to a local file.
Restore the data with restore.
)"});
}
} backupdata_command_register;
namespace shared_commands
{
bool backupdata_service(const ServerConfig &server_env, const std::string &service)
{
ASSERT(server_env.is_valid(), "Invalid server environment for " + server_env.get_server_name());
std::string server = server_env.get_server_name();
LocalServiceInfo sinfo = get_service_info(server, service);
if (!SIvalid(sinfo))
{
error << "Service " << service << " is not valid" << std::endl;
return false;
}
const std::string command = "backup";
if (!gTemplateManager().template_command_exists(sinfo.template_name, command))
{
info << service << " has no data to backup" << std::endl;
debug << "(no backup script for " << sinfo.template_name << ")" << std::endl;
return true; // nothing to back up.
}
std::string user = server_env.get_user_for_service(service);
// Check if basic installed stuff is in place.
std::string remote_service_template_path = remotepath(server, user).service_template(service);
std::string remote_command_script_file = remote_service_template_path + "/" + command + ".sh";
std::string remote_service_config_path = remotepath(server, user).service_config(service);
if (!server_env.check_remote_items_exist({remotepath(server, user).service(service),
remote_command_script_file,
remotefile(server, user).service_env(service)}, user))
{
error << "Required service directories not found on remote server" << std::endl;
info << "Is the service installed?" << std::endl;
return false;
}
// Create backups directory on server if it doesn't exist
std::string remote_backups_dir = remotepath(server, user).backups();
debug << "Remote backups directory on " << server << ": " << remote_backups_dir << std::endl;
std::string mkdir_cmd = "mkdir -p " + quote(remote_backups_dir);
if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Defaults))
{
error << "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();
if (local_backups_dir.empty())
{
error << "Local backups directory not found" << std::endl;
info << "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
shared_commands::cBackupFileName backup_filename_construction(server, service, sinfo.template_name);
if (!backup_filename_construction.is_valid())
{
error << "Invalid backup filename" << std::endl;
return false;
}
// Construct backup filename
std::string backup_filename = backup_filename_construction.get_filename();
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
shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user);
if (!server_env.run_remote_template_command(service, command, {}, false, {{"BACKUP_FILE", remote_backup_file_path}, {"TEMP_DIR", remote_temp_folder.path()}}))
{
error << "Backup script failed on remote server: " << remote_backup_file_path << std::endl;
return false;
}
// Copy backup file from server to local
if (!shared_commands::scp_file_from_remote(server_env, remote_backup_file_path, local_backup_file_path, false, sinfo.user))
{
error << "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
info << "Backup created successfully. Restore with:" << std::endl;
info << " dropshell restore " << server << " " << service << " " << backup_filename << std::endl;
return true;
}
} // namespace shared_commands
int backupdata_handler(const CommandContext &ctx)
{
ASSERT(ctx.args.size() == 2, "Invalid number of arguments");
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
if (service == "all")
{
// backup all services on the server
maketitle("Backing up data for all services on " + server);
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const LocalServiceInfo &si : services)
okay &= shared_commands::backupdata_service(server, si.service_name);
return okay ? 0 : 1;
}
return shared_commands::backupdata_service(server, service) ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,44 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/output.hpp"
#include <libassert/assert.hpp>
namespace dropshell {
void colours_autocomplete(const CommandContext& ctx) {}
int colours_handler(const CommandContext& ctx)
{
info << "Colours:" << std::endl;
debug << "Debug Example: The quick brown fox jumps over the lazy dog." << std::endl;
info << "Info Example: The quick brown fox jumps over the lazy dog." << std::endl;
warning << "Warning Example: The quick brown fox jumps over the lazy dog." << std::endl;
error << "Error Example: The quick brown fox jumps over the lazy dog." << std::endl;
return 0;
}
static std::vector<std::string> colours_name_list={"colours","c","--colours","-c"};
// Static registration
struct ColoursCommandRegister {
ColoursCommandRegister() {
CommandRegistry::instance().register_command({
colours_name_list,
colours_handler,
colours_autocomplete,
true, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
0, // max_args (after command)
"colours",
"Show the colours used by dropshell.",
// heredoc
R"(
Show the colours used by dropshell.
)"
});
}
} colours_command_register;
} // namespace dropshell

View File

@ -20,6 +20,15 @@ void CommandRegistry::register_command(const CommandInfo& info) {
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();
// go deep now.
for (const auto& cmd : all_commands_) {
if (cmd->names.size() > 0) {
for (const auto& altname : cmd->names) {
if (name == altname) return cmd.get();
}
}
}
return nullptr;
}

View File

@ -0,0 +1,63 @@
#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 <libassert/assert.hpp>
namespace dropshell {
void create_server_autocomplete(const CommandContext& ctx);
int create_server_handler(const CommandContext& ctx);
static std::vector<std::string> create_server_name_list={"create-server"};
// Static registration
struct CreateServerCommandRegister {
CreateServerCommandRegister() {
CommandRegistry::instance().register_command({
create_server_name_list,
create_server_handler,
create_server_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
1, // max_args (after command)
"create-server [SERVER]",
"Create a new server entry on this host.",
// heredoc
R"(
Create a new server entry on this host.
Note you will need to use ds install SERVER to prepare the service for use.
create-server SERVER
)"
});
}
} create_server_command_register;
void create_server_autocomplete(const CommandContext& ctx) {
return; // can't autocomplete as it's a new server!
}
int create_server_handler(const CommandContext& ctx) {
// create a new server entry on this host
if (ctx.args.size() == 0) {
error << "No server name provided" << std::endl;
return 1;
}
bool ok = create_server(ctx.args[0]);
return ok ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,278 @@
#include "command_registry.hpp"
#include "directories.hpp"
#include "shared_commands.hpp"
#include "templates.hpp"
#include <libassert/assert.hpp>
#include "utils/utils.hpp"
#include "services.hpp"
#include <fstream>
namespace dropshell
{
int create_service_handler(const CommandContext &ctx);
void create_service_autocomplete(const CommandContext &ctx);
static std::vector<std::string> create_service_name_list = {"create-service"};
// Static registration
struct CreateServiceCommandRegister
{
CreateServiceCommandRegister()
{
CommandRegistry::instance().register_command({create_service_name_list,
create_service_handler,
create_service_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
3, // min_args (after command)
3, // max_args (after command)
"create-service SERVER SERVICE TEMPLATE",
"Create a service on a server.",
// heredoc
R"(
Create a service on a server.
create-service SERVER SERVICE TEMPLATE create the given service on the given server.
)"});
}
} create_service_command_register;
int create_service_handler(const CommandContext &ctx)
{
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
std::string template_name = safearg(ctx.args, 2);
return shared_commands::create_service(server, template_name, service) ? 0 : 1;
}
void create_service_autocomplete(const CommandContext &ctx)
{
if (ctx.args.size() < 2)
shared_commands::std_autocomplete(ctx);
else
{
if (ctx.args.size() == 2)
{
std::set<std::string> templates = gTemplateManager().get_template_list();
for (const auto &template_name : templates)
rawout << template_name << std::endl;
}
}
}
namespace shared_commands
{
bool print_readme(const template_info &tinfo, std::string server, std::string service)
{
std::vector<std::string> variants_to_try = {"README.txt", "readme.txt", "ReadMe.txt", "README", "readme", "README.md", "readme.md"};
std::filesystem::path readme_path = tinfo.local_template_path();
for (const auto &variant : variants_to_try)
{
if (std::filesystem::exists(readme_path / variant))
{
readme_path = readme_path / variant;
break;
}
}
if (!std::filesystem::exists(readme_path))
return false;
std::map<std::string, std::string> all_env_vars;
get_all_service_env_vars(server, service, all_env_vars);
all_env_vars["LOCAL_CONFIG_PATH"] = localpath::service(server, service);
all_env_vars["LOCAL_TEMPLATE_PATH"] = tinfo.local_template_path().string();
info << std::endl;
std::ifstream readme_file(readme_path);
std::string line;
while (std::getline(readme_file, line))
{
rawout << substitute_provided_key_value_pairs(line, all_env_vars) << std::endl;
}
return true;
}
bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name, std::string user_override/*=""*/)
{
if (server_name.empty() || template_name.empty() || service_name.empty())
return false;
if (!legal_service_name(service_name)) {
error << "Service name contains illegal characters: " << service_name << std::endl;
return false;
}
ServerConfig server_info(server_name);
if (!server_info.is_valid())
{
error << "Server " << server_name << " is not valid" << std::endl;
return false;
}
std::string service_dir = localpath::service(server_name, service_name);
if (service_dir.empty())
{
error << "Couldn't locate server " << server_name << " in any config directory" << std::endl;
info << "Please check the server name is correct and try again" << std::endl;
info << "You can list all servers with 'dropshell servers'" << std::endl;
info << "You can create a new server with 'dropshell create-server " << server_name << "'" << std::endl;
return false;
}
if (std::filesystem::exists(service_dir))
{
error << "Service already exists: " << service_name << std::endl;
debug << "Current service path: " << service_dir << std::endl;
return false;
}
template_info tinfo = gTemplateManager().get_template_info(template_name);
if (!tinfo.is_set())
{
error << "Template '" << template_name << "' not found" << std::endl;
info << "Please check the template name is correct and try again" << std::endl;
info << "You can list all templates with 'dropshell templates'" << std::endl;
info << "You can create a new template with 'dropshell create-template " << template_name << "'" << std::endl;
return false;
}
// check template is all good.
if (!gTemplateManager().test_template(tinfo.local_template_path()))
{
error << "Template '" << template_name << "' is not valid" << std::endl;
return false;
}
// create the service directory
std::filesystem::create_directory(service_dir);
// copy the template config files to the service directory
recursive_copy(tinfo.local_template_path() / "config", service_dir);
// append TEMPLATE_HASH to the .template_info.env file
std::string template_info_env_file = localfile::template_info_env(server_name,service_name);
ASSERT(std::filesystem::exists(template_info_env_file), "Template info env file not found: " + template_info_env_file);
std::ofstream template_info_env_file_out(template_info_env_file, std::ios::app); // append to the file.
template_info_env_file_out << "TEMPLATE_HASH=" << tinfo.hash() << std::endl;
template_info_env_file_out.close();
// modify the SSH_USER to be nice.
// everything is created, so we can get the service info.
LocalServiceInfo service_info = get_service_info(server_name, service_name);
std::string sshuser = "root";
if (!user_override.empty())
sshuser = user_override;
else
if (!service_info.requires_host_root)
{ // find a non-root user.
auto users = server_info.get_users();
auto it = std::find_if(users.begin(), users.end(), [&sshuser](const UserConfig &user)
{ return user.user != "root"; });
if (it != users.end())
sshuser = it->user;
}
if (sshuser == "root" && !server_info.hasRootUser())
{
error << "Server " << server_name << " does not have a root user, but the service " << service_name << " requires it." << std::endl;
return false;
}
if (sshuser != "root" && service_info.requires_host_root)
{
error << "The service " << service_name << " requires a root user, but a non-root user was specified." << std::endl;
return false;
}
if (!server_info.hasUser(sshuser))
{
error << "User " << sshuser << "is not available on server " << server_name << std::endl;
return false;
}
info << "Setting SSH_USER to " << sshuser << " in the " << filenames::service_env << " file" << std::endl;
{ // edit the service.env file to set the SSH_USER.
std::string source_service_env = tinfo.local_template_path() / "config" / filenames::service_env;
ASSERT(std::filesystem::exists(source_service_env), "Template service env file not found: " + source_service_env);
std::ifstream template_service_env_file_in(source_service_env);
std::ofstream service_env_file_out(localfile::service_env(server_name, service_name));
std::string line;
while (std::getline(template_service_env_file_in, line))
{
if (line.find("SSH_USER") != std::string::npos)
line = "SSH_USER=" + sshuser;
service_env_file_out << line << std::endl;
}
template_service_env_file_in.close();
service_env_file_out.close();
}
// check docker.
if (service_info.requires_docker)
{
if (!server_info.hasDocker())
{
error << "Server " << server_name << " does not have docker, but the service " << service_name << " requires it." << std::endl;
return false;
}
if (service_info.requires_docker_root)
{
if (!server_info.hasRootDocker())
{
error << "Server " << server_name << " does not have a root docker, but the service " << service_name << " requires it." << std::endl;
return false;
}
}
}
info << "Service " << service_name << " created successfully" << std::endl;
if (!print_readme(tinfo, server_name, service_name))
{
info << std::endl;
info << "To complete the installation, please:" << std::endl;
info << "1. edit the service config file: dropshell edit " << server_name << " " << service_name << std::endl;
info << "2. install the remote service: dropshell install " << server_name << " " << service_name << std::endl;
}
return true;
}
bool merge_updated_service_template(const std::string &server_name, const std::string &service_name)
{
LocalServiceInfo service_info = get_service_info(server_name, service_name);
ASSERT(SIvalid(service_info), "Service info is not valid for " + service_name + " on " + server_name);
template_info tinfo = gTemplateManager().get_template_info(service_info.template_name);
ASSERT(tinfo.is_set(), "Failed to load template " + service_info.template_name);
// copy across .template_info.env file
std::string template_info_env_file = tinfo.local_template_path() / "config" / filenames::template_info_env;
std::string target_template_info_env_file = localfile::template_info_env(server_name, service_name);
ASSERT(std::filesystem::exists(template_info_env_file), "Template service env file not found: " + template_info_env_file);
std::filesystem::remove(target_template_info_env_file);
std::filesystem::copy(template_info_env_file, target_template_info_env_file);
#pragma message("TODO: merge the template info env file")
// update hash in template info env file
// append TEMPLATE_HASH to the .template_info.env file
ASSERT(std::filesystem::exists(target_template_info_env_file), "Template info env file not found: " + target_template_info_env_file);
std::ofstream template_info_env_file_out(target_template_info_env_file, std::ios::app); // append to the file.
template_info_env_file_out << "TEMPLATE_HASH=" << tinfo.hash() << std::endl;
template_info_env_file_out.close();
return true;
}
} // namespace shared_commands
} // namespace dropshell

View File

@ -0,0 +1,63 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "version.hpp"
#include <libassert/assert.hpp>
#include "templates.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
namespace dropshell {
void create_template_autocomplete(const CommandContext& ctx);
int create_template_handler(const CommandContext& ctx);
static std::vector<std::string> create_template_name_list={"create-template"};
// Static registration
struct CreateTemplateCommandRegister {
CreateTemplateCommandRegister() {
CommandRegistry::instance().register_command({
create_template_name_list,
create_template_handler,
create_template_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
1, // max_args (after command)
"create-template TEMPLATE",
"Create a new template.",
// heredoc
R"(
Create a new template.
create-template TEMPLATE
)"
});
}
} create_template_command_register;
void create_template_autocomplete(const CommandContext& ctx) {
return; // can't autocomplete as it's a new server!
}
int create_template_handler(const CommandContext& ctx) {
// create a new server entry on this host
if (ctx.args.size() == 0) {
error << "No template name provided" << std::endl;
return 1;
}
bool ok = gTemplateManager().create_template(ctx.args[0]);
return ok ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,180 @@
#include "command_registry.hpp"
#include "shared_commands.hpp"
#include "config.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/directories.hpp"
#include "servers.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include <libassert/assert.hpp>
namespace dropshell
{
int destroy_handler(const CommandContext &ctx);
static std::vector<std::string> destroy_name_list = {"destroy", "nuke", "nuke-service","erase","destroy-service"};
// Static registration
struct DestroyCommandRegister
{
DestroyCommandRegister()
{
CommandRegistry::instance().register_command({destroy_name_list,
destroy_handler,
shared_commands::std_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
2, // min_args (after command)
2, // max_args (after command)
"destroy SERVER SERVICE|all",
"Destroy a service on a server. Erases everything, both local and remote!",
// heredoc
R"(
Destroy a service.
Examples:
destroy SERVER SERVICE destroy the given service on the given server.
destroy SERVER all destroy all services on the given server.
Note: This command is destructive and will destroy all data and all configuration,
both on the dropshell host and on the remote server.
Use with caution!
)"});
}
} destroy_command_register;
namespace shared_commands
{
bool destroy_service(const std::string &server, const std::string &service)
{
ServerConfig server_env(server);
// step 1 - destroy on remote server.
if (server_env.is_valid())
{
std::string user = server_env.get_user_for_service(service); // returns empty string if no user found.
if (user.empty())
{
warning << "No user found for service " << service << " on " << server << std::endl;
for (auto sshuser : server_env.get_users())
{
if (server_env.check_remote_dir_exists(remotepath(server, sshuser.user).service(service), sshuser.user))
{
info << "Found a remote service directory here: " << remotepath(server, sshuser.user).service(service) << std::endl;
info << "Deleting it as user " << sshuser.user << std::endl;
user = sshuser.user;
break;
}
}
}
if (user.empty())
warning << "No remote service directory found for " << service << " on " << server << std::endl;
else
{ // user is not empty.
LocalServiceInfo service_info;
service_info = get_service_info(server, service);
bool service_valid = SIvalid(service_info);
if (!service_valid)
warning << "No valid service definition found for " << service << std::endl;
if (server_env.check_remote_dir_exists(remotepath(server, user).service(service), user))
{
// run the destroy script on the remote server if it exists.
// otherwise just uninstall.
if (service_valid)
{
if (gTemplateManager().template_command_exists(service_info.template_name, "destroy"))
{
info << "Running destroy script for " << service << " on " << server << std::endl;
if (!server_env.run_remote_template_command(service, "destroy", {}, false, {}))
warning << "Failed to run destroy script: " << service << std::endl;
}
else
{
info << "No destroy script found for " << service << " on " << server << std::endl;
info << "Running uninstall script instead and will clean directories." << std::endl;
if (!server_env.run_remote_template_command(service, "uninstall", {}, false, {}))
warning << "Failed to uninstall service: " << service << std::endl;
}
}
// Remove the service directory from the server, running in a docker container as root.
if (server_env.remove_remote_dir(remotepath(server, user).service(service), true, user))
{
ASSERT(!server_env.check_remote_dir_exists(remotepath(server, user).service(service), user), "Service directory still found on server after uninstall");
info << "Remote service directory removed: " << remotepath(server, user).service(service) << std::endl;
}
else
warning << "Failed to remove remote service directory" << std::endl;
}
else
warning << "No remote service directory found for " << service << " on "<< server << std::endl;
} // user is not empty.
} // server_env is valid.
else
error << "No valid local server information for server " << server << std::endl;
// step 2 - destroy the local service directory, if it exists.
std::string local_service_path = localpath::service(server, service);
if (local_service_path.empty() || !std::filesystem::exists(local_service_path))
{
warning << "No local service directory found for " << service << " on " << server << std::endl;
}
else
{
auto itemsdeleted = std::filesystem::remove_all(local_service_path);
if (itemsdeleted == 0)
error << "Failed to remove local service directory" << std::endl;
else
info << "Local service directory removed: " << local_service_path << std::endl;
}
info << "Finished destroying service " << service << " on server " << server << std::endl;
return true;
}
} // namespace shared_commands
int destroy_handler(const CommandContext &ctx)
{
ASSERT(ctx.args.size() == 2, "Usage: destroy SERVER SERVICE|all (requires 2 args - you supplied " + std::to_string(ctx.args.size()) + ")");
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);
if (service == "all")
{
int rval = 0;
// iterate through all service folders in the server directory.
std::string server_path = localpath::server(server);
if (server_path.empty())
{
error << "Server not found: " << server << std::endl;
return 1;
}
for (const auto &entry : std::filesystem::directory_iterator(server_path))
{
if (entry.is_directory() && entry.path().filename().string().find(".") != 0)
{
std::string service_name = entry.path().filename().string();
rval |= (shared_commands::destroy_service(server, service_name) ? 0 : 1);
}
}
return rval;
}
else
{
return (shared_commands::destroy_service(server, service) ? 0 : 1);
}
}
} // namespace dropshell

View File

@ -9,7 +9,7 @@
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/assert.hpp"
#include <libassert/assert.hpp>
namespace dropshell {
@ -23,7 +23,7 @@ struct EditCommandRegister {
CommandRegistry::instance().register_command({
edit_name_list,
edit_handler,
std_autocomplete,
shared_commands::std_autocomplete,
false, // hidden
false, // requires_config
false, // requires_install
@ -66,16 +66,16 @@ bool edit_file(const std::string &file_path, bool has_bb64)
// 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;
error << "Standard input is not a terminal and EDITOR environment variable is not set." << std::endl;
info << "Try setting the EDITOR environment variable (e.g., export EDITOR=nano) or run in an interactive terminal." << std::endl;
info << "You can manually edit the file at: " << file_path << std::endl;
return false;
}
std::cout << "Editing file: " << file_path << std::endl;
info << "Editing file: " << file_path << std::endl;
if (has_bb64) {
return execute_local_command(editor_cmd, nullptr, cMode::Interactive);
return execute_local_command("", editor_cmd, {}, nullptr, cMode::Interactive);
}
else {
// might not have bb64 at this early stage. Direct edit.
@ -94,16 +94,14 @@ int edit_config()
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.");
return return_die("Failed to edit config file.");
gConfig().load_config();
if (!gConfig().is_config_set())
return die("Error: Failed to load and parse edited config file!");
return return_die("Failed to load and parse edited config file!");
gConfig().save_config(true);
// make sure we have executables.
std::cout << "Successfully edited config file at " << config_file << std::endl;
return 0;
}
@ -113,28 +111,47 @@ int edit_config()
// ------------------------------------------------------------------------------------------------
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;
if (localpath::server(server_name).empty()) {
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 = localfile::server_json(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;
error << "Failed to edit server config" << std::endl;
info << "You can manually edit this file at: " << config_file << std::endl;
return 1;
}
else
std::cout << aftertext.str() << std::endl;
info << "If you have changed DROPSHELL_DIR, you should manually move the files to the new location NOW." << std::endl;
info << "You can ssh in to the remote server with: dropshell ssh "<<server_name<< std::endl;
info << "Once moved, reinstall all services with: dropshell install " << server_name << std::endl;
return 0;
}
void list_directory(std::string dir, std::string msg)
{
bool first=true;
std::vector<std::string> directories;
for (const auto &file : std::filesystem::directory_iterator(dir))
{
if (first)
{
if (!msg.empty())
info << msg << std::endl;
first=false;
}
if (std::filesystem::is_directory(file.path()))
directories.push_back(file.path());
else
info << " " << file.path() << std::endl;
}
for (const auto &dir : directories)
list_directory(dir, "");
}
// ------------------------------------------------------------------------------------------------
// edit service config
// ------------------------------------------------------------------------------------------------
@ -143,12 +160,19 @@ 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;
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;
info << "Successfully edited service config file at " << config_file << std::endl;
std::string service_dir = localpath::service(server, service);
list_directory(service_dir, "You may wish to edit the other files in " + service_dir);
info << "Then to apply your changes, run:" << std::endl;
info << " dropshell uninstall " + server + " " + service << std::endl;
info << " dropshell install " + server + " " + service << std::endl;
return 0;
}
@ -172,7 +196,7 @@ int edit_handler(const CommandContext& ctx) {
return 0;
}
std::cout << "Edit handler called with " << ctx.args.size() << " args\n";
info << "Edit handler called with " << ctx.args.size() << " args\n";
return -1;
}

View File

@ -0,0 +1,83 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "version.hpp"
#include "hash.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include <libassert/assert.hpp>
namespace dropshell {
void hash_autocomplete(const CommandContext& ctx);
int hash_handler(const CommandContext& ctx);
static std::vector<std::string> hash_name_list={"hash"};
// Static registration
struct HashCommandRegister {
HashCommandRegister() {
CommandRegistry::instance().register_command({
hash_name_list,
hash_handler,
hash_autocomplete,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
1, // max_args (after command)
"hash [FILE|DIRECTORY]",
"Hash a file or directory.",
// heredoc
R"(
Hash a file or directory recursively.
)"
});
}
} hash_command_register;
void hash_autocomplete(const CommandContext& ctx) {
if (ctx.args.size() == 0) {
// list all files and directories in the current directory
for (const auto& entry : std::filesystem::directory_iterator(".")) {
rawout << entry.path().string() << std::endl;
}
}
return;
}
int hash_handler(const CommandContext& ctx) {
std::filesystem::path path = safearg(ctx.args, 0);
if (path.empty())
path=std::filesystem::current_path();
if (!std::filesystem::exists(path))
{
error << "Does not exist: " << path.string() << std::endl;
return 1;
}
if (std::filesystem::is_directory(path))
{
// hash the directory recursively
uint64_t hash = hash_directory_recursive(path.string());
std::cout << hash << std::endl;
}
else
{
// hash the file
uint64_t hash = hash_file(path.string());
std::cout << hash << std::endl;
}
return 0;
}
} // namespace dropshell

View File

@ -0,0 +1,73 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "servers.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,
shared_commands::std_autocomplete_allowall,
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
// ------------------------------------------------------------------------------------------------
int health_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{
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 = shared_commands::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 << shared_commands::healthtick(server, service) << " " << service << " (" << service_info.template_name << ")" << std::endl << std::flush;
}
return 0;
}
} // namespace dropshell

View File

@ -0,0 +1,159 @@
#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 <libassert/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() == 0) {
// list all commands
for (const auto& cmd : CommandRegistry::instance().list_primary_commands(false)) {
rawout << cmd << std::endl;
}
}
return;
}
void show_command(const std::string& cmd) {
// get console width
int width = get_console_width() - 6; // 5 for [INF] + 1 for space
int firstcol = 34;
int secondcol = width - firstcol - 3;
const auto& cmd_info = CommandRegistry::instance().find_command(cmd);
if (!cmd_info)
{
error << "Unknown command: " << cmd << std::endl;
return;
}
if (cmd_info->help_usage.length() < width-secondcol)
{
std::string remaining_description = cmd_info->help_description;
info << " " << left_align(cmd_info->help_usage, firstcol) << get_line_wrap(remaining_description, secondcol);
while (!remaining_description.empty())
info << " " << left_align(" ",firstcol) << get_line_wrap(remaining_description, secondcol-1);
}
else
{
info << " " << cmd_info->help_usage << std::endl;
std::string remaining_description = cmd_info->help_description;
info << " " << left_align(" ",firstcol) << get_line_wrap(remaining_description, secondcol);
while (!remaining_description.empty())
info << " " << left_align(" ",firstcol) << get_line_wrap(remaining_description, secondcol-1);
}
}
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)
{
error << "Unknown command: " << cmd << std::endl;
return 1;
}
info << "Command " << cmd << " usage:" << std::endl;
info << " ";
info << left_align(cmd_info->help_usage, 32);
info << cmd_info->help_description << std::endl;
info << std::endl;
info << " Equivalent names: ";
bool first = true;
for (const auto& name : cmd_info->names) {
if (!first) info << ", ";
info << name;
first = false;
}
info << std::endl;
info << 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);
info << std::endl;
info << "A tool for managing remote servers, by " << AUTHOR << std::endl;
info << std::endl;
info << "dropshell ..." << std::endl;
show_command("help");
show_command("edit");
if (gConfig().is_config_set())
{
// show more!
show_command("list");
info << std::endl;
show_command("install");
show_command("uninstall");
show_command("destroy");
info << std::endl;
show_command("start");
show_command("stop");
info << std::endl;
show_command("ssh");
info << std::endl;
show_command("create-server");
show_command("create-service");
}
return 0;
}
} // namespace dropshell

View File

@ -0,0 +1,457 @@
#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 "autogen/_agent-local.hpp"
#include "autogen/_agent-remote.hpp"
#include "services.hpp"
#include "utils/output.hpp"
#include <fstream>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include <libassert/assert.hpp>
#include "servers.hpp"
#include <sys/stat.h>
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,
shared_commands::std_autocomplete_allowall,
false, // hidden
false, // requires_config
false, // requires_install
0, // min_args (after command)
2, // max_args (after command)
"install [SERVER] [SERVICE|all]",
"Install/reinstall host and remote servers, or service(s). Safe 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, and on all servers.
install SERVER (re)install dropshell agent on the particular given server.
install SERVER [SERVICE|all] (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;
namespace shared_commands
{
// ------------------------------------------------------------------------------------------------
// install service over ssh : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool install_service(const ServerConfig &server_env, const std::string &service)
{
std::string server = server_env.get_server_name();
LocalServiceInfo service_info = get_service_info(server_env.get_server_name(), service);
if (!SIvalid(service_info))
{
error << "Failed to install - service information not valid." << std::endl;
return false;
}
if (!server_env.is_valid())
return false; // should never hit this.
std::string user = service_info.user;
std::string remote_service_path = remotepath(server,user).service(service);
ASSERT(!remote_service_path.empty(), "Install_Service: Remote service path is empty for " + service + " on " + server);
ASSERT(!user.empty(), "Install_Service: User is empty for " + service + " on " + server);
if (server_env.check_remote_dir_exists(remote_service_path, user))
{ // uninstall the old service before we update the config or template!
info << "Service " << service << " is already installed on " << server << std::endl;
shared_commands::uninstall_service(server_env, service);
}
if (!service_info.service_template_hash_match)
{
warning << "Service " << service << " is using an old template. Updating. " << std::endl;
if (!merge_updated_service_template(server_env.get_server_name(), service))
{
error << "Failed to merge updated service template. " << std::endl;
return false;
}
service_info = get_service_info(server_env.get_server_name(), service);
if (!SIvalid(service_info) || !service_info.service_template_hash_match)
{
error << "Merged updated service template, but it is still not valid. " << std::endl;
return false;
}
}
maketitle("Installing " + service + " (" + service_info.template_name + ") on " + server);
// Check if template exists
template_info tinfo = gTemplateManager().get_template_info(service_info.template_name);
if (!tinfo.is_set())
return false;
if (!tinfo.template_valid())
{
std::cerr << "Template is not valid: " << service_info.template_name << std::endl;
return false;
}
// Create service directory
std::string mkdir_cmd = "mkdir -p " + quote(remote_service_path);
if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent))
{
std::cerr << "Failed to create service directory " << remote_service_path << std::endl;
return false;
}
// Copy template files
debug << "Copying: [LOCAL] " << tinfo.local_template_path() << std::endl
<< std::string(8, ' ') << "[REMOTE] " << remotepath(server,user).service_template(service) << "/" << std::endl;
if (!shared_commands::rsync_tree_to_remote(tinfo.local_template_path().string(), remotepath(server,user).service_template(service),
server_env, false, service_info.user))
{
std::cerr << "Failed to copy template files using rsync" << std::endl;
return false;
}
// Copy service files
debug << "Copying: [LOCAL] " << localpath::service(server, service) << std::endl
<< std::string(8, ' ') << "[REMOTE] " << remotepath(server,user).service_config(service) << std::endl;
if (!shared_commands::rsync_tree_to_remote(localpath::service(server, service), remotepath(server,user).service_config(service),
server_env, false, service_info.user))
{
std::cerr << "Failed to copy service files using rsync" << std::endl;
return false;
}
// Run install script
{
info << "Running " << service_info.template_name << " install script on " << server << "..." << std::endl;
shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user);
if (!server_env.run_remote_template_command(service, "install", {}, false, {{"TEMP_DIR", remote_temp_folder.path()}}))
{
error << "Failed to run install script on " << server << std::endl;
return false;
}
}
// print health tick
info << "Health: " << shared_commands::healthtick(server, service) << std::endl;
return true;
}
} // namespace shared_commands
// ------------------------------------------------------------------------------------------------
// update_dropshell
// ------------------------------------------------------------------------------------------------
std::string _exec(const char *cmd)
{
char buffer[128];
std::string result = "";
FILE *pipe = popen(cmd, "r");
if (!pipe)
{
throw std::runtime_error("popen() failed!");
}
while (!feof(pipe))
{
if (fgets(buffer, 128, pipe) != nullptr)
result += buffer;
}
pclose(pipe);
return trim(result);
}
int configure_autocomplete()
{
debug << "Ensuring dropshell autocomplete is registered in ~/.bashrc..." << std::endl;
std::filesystem::path bashrc = localpath::current_user_home() +"/.bashrc";
std::string autocomplete_script = R"(
#---DROPSHELL AUTOCOMPLETE START---
_dropshell_completions() {
local cur
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
# call dropshell to get the list of possiblities for the current argument. Supply all previous arguments.
local completions=($(dropshell autocomplete "${COMP_WORDS[@]:1:${COMP_CWORD}-1}"))
COMPREPLY=( $(compgen -W "${completions[*]}" -- ${cur}) )
return 0
}
# Register the completion function
complete -F _dropshell_completions dropshell
complete -F _dropshell_completions ds
#---DROPSHELL AUTOCOMPLETE END---
)";
file_replace_or_add_segment(bashrc.string(), autocomplete_script);
return 0;
}
int configure_localbin()
{
debug << "Ensuring ~/.local/bin is in the ~/.bashrc path..." << std::endl;
std::filesystem::path bashrc = localpath::current_user_home() +"/.bashrc";
std::filesystem::path localbin = localpath::current_user_home() + "/.local/bin";
std::filesystem::create_directories(localbin);
// check if already in path
const char* env_p = std::getenv("PATH");
if (env_p) {
std::string path_str = env_p;
if (path_str.find(localbin.string()) == std::string::npos) {
std::string pathstr="#---DROPSHELL PATH START---\nexport PATH=\""+localbin.string()+":$PATH\"\n#---DROPSHELL PATH END---\n";
file_replace_or_add_segment(bashrc.string(), pathstr);
}
}
return 0;
}
int update_dropshell()
{
maketitle("Updating dropshell on this computer...");
configure_localbin();
configure_autocomplete();
// determine path to this executable
std::filesystem::path exe_path = std::filesystem::canonical("/proc/self/exe");
std::filesystem::path parent_path = exe_path.parent_path();
// determine the architecture of the system
std::string arch = shared_commands::get_arch();
std::string url = "https://gitea.jde.nz/public/dropshell/releases/download/latest/dropshell." + arch;
// check that the user that owns the exe is the current user this process is running as.
struct stat st;
if (stat(exe_path.c_str(), &st) != 0) {
error << "Failed to stat dropshell executable: " << strerror(errno) << std::endl;
return -1;
}
uid_t current_uid = getuid();
if (st.st_uid != current_uid) {
warning << "Current user does not own the dropshell executable. Please run as the owner to update." << std::endl;
return -1;
}
shared_commands::cLocalTempFolder local_temp_folder;
std::filesystem::path temp_file = local_temp_folder.path() / "dropshell";
bool download_okay = download_file(url, temp_file);
if (!download_okay)
{
error << "Failed to download new version of dropshell." << std::endl;
return -1;
}
// make executable
chmod(temp_file.c_str(), 0755);
// check if the new version is the same as the old version
uint64_t new_hash = hash_file(temp_file);
uint64_t old_hash = hash_file(exe_path);
if (new_hash == old_hash)
{
info << "Confirmed dropshell is the latest version." << std::endl;
return 0;
}
std::string runvercmd = exe_path.string() + " version";
std::string currentver = _exec(runvercmd.c_str());
runvercmd = temp_file.string() + " version";
std::string newver = _exec(runvercmd.c_str());
if (currentver >= newver)
{
info << "Current dropshell version: " << currentver << ", published version: " << newver << std::endl;
info << "Release version is not newer, no update needed." << std::endl;
return 0;
}
// move the new version to the old version.
std::filesystem::rename(exe_path, exe_path.parent_path() / "dropshell.old");
std::filesystem::rename(temp_file, exe_path);
// remove the old version.
std::filesystem::remove(exe_path.parent_path() / "dropshell.old");
// execute the new version
execlp("bash", "bash", "-c", (exe_path.parent_path() / "dropshell").string() + "install", (char *)nullptr);
error << "Failed to execute new version of dropshell." << std::endl;
return -1;
}
int install_local_agent()
{
maketitle("Installing dropshell agent on this computer...");
// clear out old cruft.
std::filesystem::remove_all(localpath::agent_local());
std::filesystem::remove_all(localpath::agent_remote());
// recreate the directories.
localpath::create_directories();
// populate the agent-local directory.
recreate_agent_local::recreate_tree(localpath::agent_local());
// run the local agent installer.
execute_local_command(localpath::agent_local(), "agent-install.sh",{}, nullptr, cMode::Defaults | cMode::NoBB64);
// populate the agent-remote directory.
info << "Creating local files to copy to remote agents..." << std::endl;
recreate_agent_remote::recreate_tree(localpath::agent_remote());
return 0;
}
int install_server(const ServerConfig &server)
{
// install the dropshell agent on the given server.
maketitle("Installing dropshell agent on " + server.get_server_name(), sColour::INFO);
for (const auto &user : server.get_users())
{
info << "Installing agent for user " << user.user << " on " << server.get_server_name() << std::endl;
std::string agent_path = remotepath(server.get_server_name(),user.user).agent();
ASSERT(agent_path == user.dir+"/agent", "Remote agent path does not match user directory for "+user.user+"@" + server.get_server_name() + " : " + agent_path + " != " + user.dir);
ASSERT(!agent_path.empty(), "Agent path is empty for " + user.user + "@" + server.get_server_name());
// now create the agent.
// copy across from the local agent files.
info << "Copying local agent files to remote server... " << std::flush;
shared_commands::rsync_tree_to_remote(localpath::agent_remote(), agent_path, server, false, user.user);
info << "done." << std::endl;
// run the agent installer. Can't use BB64 yet, as we're installing it on the remote server.
bool okay = execute_ssh_command(server.get_SSH_INFO(user.user), sCommand(agent_path, "agent-install.sh",{}), cMode::Defaults | cMode::NoBB64, nullptr);
if (!okay)
{
error << "Failed to install remote agent on " << server.get_server_name() << std::endl;
return 1;
}
info << "Installation on " << server.get_server_name() << " complete." << std::endl;
}
return 0;
}
// ------------------------------------------------------------------------------------------------
// install_host
// ------------------------------------------------------------------------------------------------
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;
// install the dropshell agent on all servers.
std::vector<ServerConfig> servers = get_configured_servers();
for (const auto &server : servers)
{
rval = install_server(server);
if (rval != 0)
return rval;
}
std::cout << "Installation complete." << std::endl;
return 0;
}
// ------------------------------------------------------------------------------------------------
// install command implementation
// ------------------------------------------------------------------------------------------------
int install_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{ // install host
return install_host();
}
if (!gConfig().is_config_set())
{
error << "Dropshell is not configured. Please run 'dropshell edit' to configure it." << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
if (ctx.args.size() == 1)
{ // install server
return install_server(server);
}
// install service(s)
if (!server_exists(server))
{
error << "Server " << server << " does not exist." << std::endl;
info << "Create it with: dropshell create-server " << server << std::endl;
return 1;
}
ServerConfig server_env(server);
ASSERT(server_env.is_valid(), "Invalid server environment for " + server);
if (safearg(ctx.args, 1) == "all")
{
// install all services on the server
maketitle("Installing all services on " + server);
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &lsi : services)
{
if (!shared_commands::install_service(server_env, lsi.service_name))
okay = false;
}
return okay ? 0 : 1;
}
else
{ // install the specific service.
std::string service = safearg(ctx.args, 1);
return shared_commands::install_service(server_env, service) ? 0 : 1;
}
}
} // namespace dropshell

View File

@ -0,0 +1,224 @@
#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 "servers.hpp"
#include "services.hpp"
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include <libassert/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,
shared_commands::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;
}
debug << "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()) {
error << "No servers found" << std::endl;
info << "Please run 'dropshell edit' to set up dropshell." << std::endl;
info << "Then run 'dropshell create-server' to create a server." << std::endl;
return;
}
tableprint tp("All DropShell Servers");
tp.add_row({"Name", "Address", "User", "Health", "Ports"});
typedef std::map<std::string, shared_commands::ServiceStatus> tServiceStatusMap;
std::vector<tServiceStatusMap> service_status_maps;
typedef struct {dropshell::ServerConfig server; dropshell::UserConfig user;} server_user_pair;
std::vector<server_user_pair> server_user_pairs;
for (const auto& server : servers)
for (const auto& user : server.get_users())
server_user_pairs.push_back({server, user});
// mutex for the tableprint
std::mutex tp_mutex;
info << "Checking "<<server_user_pairs.size() << " agents: " << std::flush;
int checked = 0;
transwarp::parallel exec{server_user_pairs.size()};
auto task = transwarp::for_each(exec, server_user_pairs.begin(), server_user_pairs.end(), [&](const server_user_pair& sup) {
ServerConfig server_env(sup.server.get_server_name());
if (!server_env.is_valid())
{
error << "Invalid server environment for " << sup.server.get_server_name() << std::endl;
return;
}
std::string serviceticks = "";
std::string ports_used_str = "";
std::set<int> ports_used;
std::map<std::string, shared_commands::ServiceStatus> status = shared_commands::get_all_services_status(sup.server.get_server_name(),sup.user.user);
for (const auto& [service_name, service_status] : status) {
ports_used.insert(service_status.ports.begin(), service_status.ports.end());
serviceticks += shared_commands::HealthStatus2String(service_status.health) + " ";
}
for (const auto& port : ports_used)
ports_used_str += std::to_string(port) + " ";
// critical section
{
std::lock_guard<std::mutex> lock(tp_mutex);
tp.add_row({sup.server.get_server_name(), sup.server.get_SSH_HOST(), sup.user.user, serviceticks, ports_used_str});
++checked;
// print out a tick character for each server checked.
info << checked << "" << std::flush;
}
});
task->wait();
info << std::endl << std::endl;
tp.sort({0,2});
tp.print();
}
void show_server_details(const std::string& server_name) {
ServerConfig env(server_name);
if (!env.is_valid()) {
error << "Invalid server environment file: " << server_name << std::endl;
return;
}
//---------------------
// Check if server is reachable via SSH
ASSERT(env.get_users().size() > 0, "No users found for server " + server_name);
sSSHInfo sshinfo = env.get_SSH_INFO(env.get_users()[0].user);
ASSERT(sshinfo.valid(), "Invalid SSH info for server " + server_name);
info << std::endl << "Server Status:" << std::endl;
info << std::string(40, '-') << std::endl;
// Try to connect to the server
std::string cmd = "ssh -o ConnectTimeout=5 " + sshinfo.get_user() + "@" + sshinfo.get_host() + " -p " + sshinfo.get_port() + " 'true' 2>/dev/null";
int result = system(cmd.c_str());
if (result == 0) {
info << "Status: Online" << std::endl;
} else {
warning << "Status: Offline" << std::endl;
}
info << 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()) {
if (key == "SSH_USERS")
{
int i=1;
for (const auto& user : env.get_users())
{
tp.add_row({"USERS -> USER[" + std::to_string(i) + "]", user.user});
tp.add_row({"USERS -> DIR[" + std::to_string(i) + "]", user.dir});
i++;
}
}
else
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", "Template","Ports"});
std::map<std::string, shared_commands::ServiceStatus> status = shared_commands::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 = shared_commands::HealthStatus2String(service_status.health);
std::string ports_str = "";
for (const auto& port : service_status.ports)
ports_str += std::to_string(port) + " ";
std::string template_name = get_service_info(server_name,service_name).template_name;
if (template_name.empty())
template_name = "Unknown";
tp.add_row({healthy, service_name, template_name, ports_str});
} // end of for (const auto& service : services)
tp.print();
} // end of list services
} // end of show_server_details
} // namespace dropshell

View File

@ -0,0 +1,277 @@
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <sstream>
#include <filesystem>
#include "utils/output.hpp"
#include <libassert/assert.hpp>
#include "utils/utils.hpp"
#include "command_registry.hpp"
#include "config.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "servers.hpp"
#include "templates.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
namespace dropshell
{
int restoredata_handler(const CommandContext &ctx);
void restoredata_autocomplete(const CommandContext &ctx);
static std::vector<std::string> restoredata_name_list = {"restoredata", "rd", "restore", "rest"};
// Static registration
struct RestoreDataCommandRegister
{
RestoreDataCommandRegister()
{
CommandRegistry::instance().register_command({restoredata_name_list,
restoredata_handler,
restoredata_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
3, // min_args (after command)
3, // max_args (after command)
"restoredata SERVER SERVICE BACKUP_FILE|latest",
"Restore data for a service on a server, overwriting the existing data.",
// heredoc
R"(
restoredata SERVER SERVICE BACKUP_FILE Restore data to a service on a server. Destructive.
restoredata SERVER SERVICE latest Restore the latest backup for the given service.
Note: This command will not create any service configuration, you need
to have a valid service installed first.
The backup file must be in the local backups directory.
WARNING: This will permanently overwrite the service's data on the remote server!
)"});
}
} restoredata_command_register;
std::vector<shared_commands::cBackupFileName> get_backup_files(const std::string &server, const std::string &match_service = "", const std::string &match_template_name = "")
{
std::string local_backups_dir = localpath::backups();
if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir))
{
error << "Local backups directory not found: " << local_backups_dir << std::endl;
return {};
}
std::vector<shared_commands::cBackupFileName> backups;
for (const auto &entry : std::filesystem::directory_iterator(local_backups_dir))
{
if (!entry.is_regular_file())
continue;
std::string filename = entry.path().filename().string();
shared_commands::cBackupFileName backup_details(filename);
if (backup_details.is_valid())
if (match_service.empty() || backup_details.get_service() == match_service)
if (match_template_name.empty() || backup_details.get_template_name() == match_template_name)
backups.push_back(backup_details);
}
// sort backups by datetime
std::sort(backups.begin(), backups.end(), [](const shared_commands::cBackupFileName &a, const shared_commands::cBackupFileName &b)
{ return a.get_datetime() > b.get_datetime(); });
return backups;
}
int restoredata_handler(const CommandContext &ctx)
{
ASSERT(ctx.args.size() == 3, "Invalid number of arguments");
std::string server = ctx.args[0];
std::string service = ctx.args[1];
std::string backup_arg = ctx.args[2];
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return 1;
}
LocalServiceInfo service_info = get_service_info(server, service);
if (!SIvalid(service_info))
{
error << "Service " << service << " is not valid" << std::endl;
return 1;
}
if (!gTemplateManager().template_command_exists(service_info.template_name, "backup") ||
!gTemplateManager().template_command_exists(service_info.template_name, "restore"))
{
info << service << " has no data to restore" << std::endl;
debug << "(no backup or restore script for " << service_info.template_name << ")" << std::endl;
return 0; // nothing to back up.
}
std::optional<shared_commands::cBackupFileName> backup_details;
if (backup_arg == "latest")
{ // special case.
std::vector<shared_commands::cBackupFileName> backups = get_backup_files(server, service, service_info.template_name); // this service only (and also match template in case something changed there!).
if (backups.empty())
{
error << "No backups found for " << server << "/" << service << std::endl;
debug << "Template also has to match with the service template: " << service_info.template_name << std::endl;
return 1;
}
backup_details = backups[0];
} else {
backup_details = shared_commands::cBackupFileName(backup_arg);
if (!backup_details->is_valid())
{
error << "Invalid backup file: " << backup_arg << std::endl;
return 1;
}
}
ASSERT(backup_details.has_value() && backup_details->is_valid(), "Invalid backup file.");
debug << "Backup details: " << std::endl;
debug << " Backup filename: " << backup_details->get_filename() << std::endl;
debug << " Backup template: " << backup_details->get_template_name() << std::endl;
debug << " Backup taken from server: " << backup_details->get_server() << std::endl;
debug << " Backup taken from service: " << backup_details->get_service() << std::endl;
debug << " " << std::endl;
debug << "Restoring to:" << std::endl;
debug << " Server: " << server << std::endl;
debug << " Service: " << service << std::endl;
std::string local_backups_dir = localpath::backups();
if (local_backups_dir.empty() || !std::filesystem::exists(local_backups_dir))
{
error << "Local backups directory not found: " << local_backups_dir << std::endl;
return 1;
}
std::string local_backup_file_path = (std::filesystem::path(local_backups_dir) / backup_details->get_filename()).string();
if (!std::filesystem::exists(local_backup_file_path))
{
error << "Backup file not found at " << local_backup_file_path << std::endl;
return 1;
}
if (backup_details->get_template_name() != service_info.template_name)
{
error << "Backup template does not match service template. Can't restore." << std::endl;
info << "Backup template: " << backup_details->get_template_name() << std::endl;
info << "Service template: " << service_info.template_name << std::endl;
return 1;
}
warning << "*** ALL DATA FOR " << server << "/" << service << " WILL BE OVERWRITTEN! ***" << std::endl;
// run the restore script
info << "OK, here goes..." << std::endl;
{ // backup existing service
info << "1) Backing up old service... " << std::endl;
if (!shared_commands::backupdata_service(server, service))
{
error << "Backup failed, restore aborted." << std::endl;
info << "You can try using dropshell install " << server << " " << service << " to install the service afresh." << std::endl;
info << "Otherwise, stop the service, create and initialise a new one, then restore to that." << std::endl;
return 1;
}
info << "Backup complete." << std::endl;
}
{ // Destroy the old service
info << "2) Destroying old service..." << std::endl;
if (!shared_commands::destroy_service(server, service))
return 1;
}
{ // create the new service
info << "3) Creating new service..." << std::endl;
if (!shared_commands::create_service(server, service_info.template_name, service, service_info.user))
return 1;
}
{ // installing fresh service
info << "4) Install of fresh service..." << std::endl;
ServerConfig server_env(server);
if (!shared_commands::install_service(server_env, service))
return 1;
}
{ // restore service from backup
info << "5) Restoring service data from backup..." << std::endl;
std::string user = server_env.get_user_for_service(service);
std::string remote_backups_dir = remotepath(server, user).backups();
std::string remote_backup_file_path = remote_backups_dir + "/" + backup_details->get_filename();
debug << "Copying backup file from local to server: " << local_backup_file_path << " -> " << remote_backup_file_path << std::endl;
// Copy backup file from local to server
if (!shared_commands::scp_file_to_remote(server_env, local_backup_file_path, remote_backup_file_path, false, service_info.user))
{
error << "Failed to copy backup file from local to server" << std::endl;
return 1;
}
shared_commands::cRemoteTempFolder remote_temp_folder(server_env,user);
debug << "Running restore script on server: " << server << std::endl;
debug << " BACKUP_FILE: " << remote_backup_file_path << std::endl;
debug << " TEMP_DIR: " << remote_temp_folder.path() << std::endl;
server_env.run_remote_template_command(service, "restore", {}, false, {{"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
{ // healthcheck the service
info << "5) Healthchecking service..." << std::endl;
std::string green_tick = "\033[32m✓\033[0m";
std::string red_cross = "\033[31m✗\033[0m";
bool healthy = (server_env.run_remote_template_command(service, "status", {}, false, {}));
info << (healthy ? green_tick : red_cross) << " Service is " << (healthy ? "healthy" : "NOT healthy") << std::endl;
}
return 0;
}
void restoredata_autocomplete(const CommandContext &ctx)
{
shared_commands::std_autocomplete(ctx);
if (ctx.args.size() == 2) // next arg is the backup file
{
std::string server = ctx.args[0];
std::string service = ctx.args[1];
LocalServiceInfo service_info = get_service_info(server, service);
if (!SIvalid(service_info))
{
error << "Service " << service << " is not valid" << std::endl;
return;
}
std::string template_name = service_info.template_name;
std::vector<shared_commands::cBackupFileName> backups = get_backup_files(server, "", template_name); // any service, but must match template.
// print most recent backup for each {host,service} pair
std::map<std::string, std::string> unique_backups;
for (const auto &backup : backups)
{
std::string key = backup.get_server() + "-" + backup.get_service();
if (unique_backups.find(key) == unique_backups.end())
unique_backups[key] = backup.get_filename();
}
for (const auto &[key, value] : unique_backups)
rawout << value << std::endl;
}
}
} // namespace dropshell

View File

@ -0,0 +1,357 @@
#include "shared_commands.hpp"
#include <libassert/assert.hpp>
#include "utils/utils.hpp"
#include "servers.hpp"
#include "directories.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/output.hpp"
namespace dropshell
{
namespace shared_commands
{
// ------------------------------------------------------------------------------------------------
// std_autocomplete : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
void std_autocomplete(const CommandContext &ctx)
{
if (ctx.args.size() == 0)
{ // just the command, no args yet.
// list servers
std::vector<ServerConfig> servers = get_configured_servers();
for (const auto &server : servers)
{
rawout << server.get_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)
{
rawout << service.service_name << std::endl;
}
}
}
// ------------------------------------------------------------------------------------------------
// std_autocomplete_allowall : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
void std_autocomplete_allowall(const CommandContext &ctx)
{
std_autocomplete(ctx);
if (ctx.args.size() == 1)
rawout << "all" << std::endl;
}
// ------------------------------------------------------------------------------------------------
// rsync_tree_to_remote : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool rsync_tree_to_remote(
const std::string &local_path,
const std::string &remote_path,
const ServerConfig &server_env,
bool silent,
std::string user)
{
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(user + "@" + server_env.get_SSH_HOST() + ":" +
remote_path + "/");
return execute_local_command("", rsync_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults));
}
// ------------------------------------------------------------------------------------------------
// get_arch : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
std::string get_arch()
{
// determine the architecture of the system
std::string arch;
#ifdef __aarch64__
arch = "aarch64";
#elif __x86_64__
arch = "x86_64";
#endif
return arch;
}
// ------------------------------------------------------------------------------------------------
// cRemoteTempFolder : SHARED CLASS
// ------------------------------------------------------------------------------------------------
cRemoteTempFolder::cRemoteTempFolder(const ServerConfig &server_env, std::string user) :
mServerEnv(server_env), mUser(user)
{
std::string p = remotepath(server_env.get_server_name(),user).temp_files() + "/" + random_alphanumeric_string(10);
std::string mkdir_cmd = "mkdir -p " + quote(p);
if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent))
error << "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(mUser), sCommand("", rm_cmd, {}), cMode::Silent);
}
std::string cRemoteTempFolder::path() const
{
return mPath;
}
cLocalTempFolder::cLocalTempFolder()
{
mPath = std::filesystem::temp_directory_path() / random_alphanumeric_string(10);
std::filesystem::create_directories(mPath);
}
cLocalTempFolder::~cLocalTempFolder()
{
std::filesystem::remove_all(mPath);
}
std::filesystem::path cLocalTempFolder::path() const
{
return mPath;
}
// ------------------------------------------------------------------------------------------------
// get_all_services_status : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
std::map<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env)
{
std::map<std::string, ServiceStatus> status;
for (const auto& user : server_env.get_users()) {
status.merge(get_all_services_status(server_env, user.user));
}
return status;
}
std::map<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env, std::string user)
{
std::map<std::string, ServiceStatus> status;
std::string server_name = server_env.get_server_name();
std::string output;
std::string agentpath = remotepath(server_name,user).agent();
if (!execute_ssh_command(server_env.get_SSH_INFO(user),
sCommand(agentpath, "./_allservicesstatus.sh", {{"HOST_NAME", server_name}, {"SERVER", server_name}, {"AGENT_PATH", agentpath}}),
cMode::Silent,
&output))
return status;
std::stringstream ss(output);
std::string line;
while (std::getline(ss, line))
{
std::string key, value;
std::size_t pos = line.find("=");
if (pos != std::string::npos)
{
key = dequote(trim(line.substr(0, pos)));
value = dequote(trim(line.substr(pos + 1)));
// decode key, it's of format SERVICENAME_[HEALTH|PORTS]
std::string service_name = key.substr(0, key.find_last_of("_"));
std::string status_type = key.substr(key.find_last_of("_") + 1);
if (status_type == "HEALTH")
{ // healthy|unhealthy|unknown
if (value == "healthy")
status[service_name].health = HealthStatus::HEALTHY;
else if (value == "unhealthy")
status[service_name].health = HealthStatus::UNHEALTHY;
else if (value == "unknown")
status[service_name].health = HealthStatus::UNKNOWN;
else
status[service_name].health = HealthStatus::ERROR;
}
else if (status_type == "PORTS")
{ // port1,port2,port3
std::vector<std::string> ports = string2multi(value);
for (const auto &port : ports)
{
if (port != "unknown")
status[service_name].ports.push_back(str2int(port));
}
}
}
}
return status;
}
// ------------------------------------------------------------------------------------------------
// healthtick : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
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;
}
// ------------------------------------------------------------------------------------------------
// HealthStatus2String : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
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:";
}
// ------------------------------------------------------------------------------------------------
// is_healthy : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
HealthStatus is_healthy(const std::string &server, const std::string &service)
{
ServerConfig env(server);
if (!env.is_valid())
{
error << "Server service not initialized" << std::endl;
return HealthStatus::ERROR;
}
std::string user = env.get_user_for_service(service);
if (!env.check_remote_dir_exists(remotepath(server,user).service(service), user))
{
return HealthStatus::NOTINSTALLED;
}
std::string script_path = remotepath(server,user).service_template(service) + "/status.sh";
if (!env.check_remote_file_exists(script_path, user))
{
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;
}
// ------------------------------------------------------------------------------------------------
// healthmark : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
std::string healthmark(const std::string &server, const std::string &service)
{
HealthStatus status = is_healthy(server, service);
return HealthStatus2String(status);
}
// ------------------------------------------------------------------------------------------------
// cBackupFileName : SHARED CLASS
// ------------------------------------------------------------------------------------------------
cBackupFileName::cBackupFileName(const std::string &server, const std::string &service, const std::string &template_name)
{
mServer = server;
mService = service;
mTemplateName = template_name;
// 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");
mDatetime = datetime.str();
}
cBackupFileName::cBackupFileName(const std::string &filename)
{
// Parse the filename according to the format:
// server + magic_string() + template_name + magic_string() + service + magic_string() + datetime + ".tgz"
std::string name = filename;
if (name.size() > 4 && name.substr(name.size() - 4) == ".tgz")
name = name.substr(0, name.size() - 4);
std::string sep = magic_string();
size_t first = name.find(sep);
size_t second = name.find(sep, first + sep.size());
size_t third = name.find(sep, second + sep.size());
if (first == std::string::npos || second == std::string::npos || third == std::string::npos)
{
mServer = mService = mTemplateName = mDatetime = "";
return;
}
mServer = name.substr(0, first);
mTemplateName = name.substr(first + sep.size(), second - (first + sep.size()));
mService = name.substr(second + sep.size(), third - (second + sep.size()));
mDatetime = name.substr(third + sep.size());
}
std::string cBackupFileName::get_filename() const
{
return mServer + magic_string() + mTemplateName + magic_string() + mService + magic_string() + mDatetime + ".tgz";
}
std::string cBackupFileName::get_server() const { return mServer; }
std::string cBackupFileName::get_service() const { return mService; }
std::string cBackupFileName::get_template_name() const { return mTemplateName; }
std::string cBackupFileName::get_datetime() const { return mDatetime; }
bool cBackupFileName::is_valid() const
{
// All fields must be non-empty, and none may contain the magic string
return !mServer.empty() && !mService.empty() && !mTemplateName.empty() && !mDatetime.empty() &&
!has_magic_string(mServer) && !has_magic_string(mService) && !has_magic_string(mTemplateName);
}
// ------------------------------------------------------------------------------------------------
// scp_file_to_remote : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool scp_file_to_remote(const ServerConfig &server_env, const std::string &local_path, const std::string &remote_path, bool silent, std::string user)
{
if (!server_env.is_valid())
{
error << "Invalid server environment" << std::endl;
return false;
}
ASSERT(!remote_path.empty() && !local_path.empty(), "Remote or local path not specified. Can't scp.");
std::string scp_cmd = "scp -P " + server_env.get_SSH_PORT() + " " + quote(local_path) + " " + user + "@" + server_env.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : "");
return execute_local_command("", scp_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults));
}
// ------------------------------------------------------------------------------------------------
// scp_file_from_remote : SHARED COMMAND
// ------------------------------------------------------------------------------------------------
bool scp_file_from_remote(const ServerConfig &server_env, const std::string &remote_path, const std::string &local_path, bool silent, std::string user)
{
if (!server_env.is_valid())
{
error << "Invalid server environment" << std::endl;
return false;
}
ASSERT(!remote_path.empty() && !local_path.empty(), "Remote or local path not specified. Can't scp.");
std::string scp_cmd = "scp -P " + server_env.get_SSH_PORT() + " " + user + "@" + server_env.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::Defaults));
}
} // namespace shared_commands
} // namespace dropshell

View File

@ -0,0 +1,117 @@
#ifndef SHARED_COMMANDS_HPP
#define SHARED_COMMANDS_HPP
#include <filesystem>
#include "servers.hpp"
#include "command_registry.hpp"
#include "servers.hpp"
namespace dropshell
{
namespace shared_commands
{
typedef enum HealthStatus
{
HEALTHY,
UNHEALTHY,
NOTINSTALLED,
ERROR,
UNKNOWN
} HealthStatus;
typedef struct ServiceStatus
{
HealthStatus health;
std::vector<int> ports;
} ServiceStatus;
// expose routines used by multiple commands.
class cRemoteTempFolder
{
public:
cRemoteTempFolder(const ServerConfig &server_env, std::string user); // 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 ServerConfig &mServerEnv;
std::string mUser;
};
class cLocalTempFolder
{
public:
cLocalTempFolder(); // create a temp folder on the local machine
~cLocalTempFolder(); // delete the temp folder on the local machine
std::filesystem::path path() const; // get the path to the temp folder on the local machine
private:
std::filesystem::path mPath;
};
bool rsync_tree_to_remote(
const std::string &local_path,
const std::string &remote_path,
const ServerConfig &server_env,
bool silent,
std::string user);
std::string get_arch();
std::map<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env);
std::map<std::string, ServiceStatus> get_all_services_status(const ServerConfig & server_env, std::string user);
std::string healthtick(const std::string &server, const std::string &service);
std::string HealthStatus2String(HealthStatus status);
HealthStatus is_healthy(const std::string &server, const std::string &service);
std::string healthmark(const std::string &server, const std::string &service);
void std_autocomplete(const CommandContext &ctx);
void std_autocomplete_allowall(const CommandContext &ctx);
class cBackupFileName
{
public:
cBackupFileName(const std::string &server, const std::string &service, const std::string &template_name);
cBackupFileName(const std::string &filename);
std::string get_filename() const;
std::string get_server() const;
std::string get_service() const;
std::string get_template_name() const;
std::string get_datetime() const;
bool is_valid() const;
private:
std::string mServer;
std::string mService;
std::string mTemplateName;
std::string mDatetime;
};
bool scp_file_to_remote(const ServerConfig &server_env, const std::string &local_path, const std::string &remote_path, bool silent, std::string user);
bool scp_file_from_remote(const ServerConfig &server_env, const std::string &remote_path, const std::string &local_path, bool silent, std::string user);
// defined in backupdata.cpp, used by restoredata.cpp.
bool backupdata_service(const ServerConfig &server_env, const std::string& service);
// defined in uninstall.cpp
bool uninstall_service(const ServerConfig &server_env, const std::string &service);
// defined in destroy.cpp
bool destroy_service(const std::string &server, const std::string &service);
// defined in install.cpp
bool install_service(const ServerConfig &server_env, const std::string &service);
// defined in create-service.cpp
bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name, std::string user_override="");
bool merge_updated_service_template(const std::string &server_name, const std::string &service_name);
} // namespace shared_commands
} // namespace dropshell
#endif

138
source/src/commands/ssh.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 "servers.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "templates.hpp"
#include <libassert/assert.hpp>
namespace dropshell
{
int ssh_handler(const CommandContext &ctx);
static std::vector<std::string> ssh_name_list = {"ssh"};
// Static registration
struct SSHCommandRegister
{
SSHCommandRegister()
{
CommandRegistry::instance().register_command({ssh_name_list,
ssh_handler,
shared_commands::std_autocomplete,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
2, // max_args (after command)
"ssh SERVER",
"SSH into a server, or into a docker container for a service.",
R"(
ssh SERVER SERVICE SSH into a docker container for a service.
ssh SERVER SSH into a server.
ssh USER@SERVER SSH into a server as a specific user.
)"});
}
} ssh_command_register;
bool ssh_into_server(const std::string &server, std::string user)
{
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return false;
}
execute_ssh_command(server_env.get_SSH_INFO(user), sCommand(remotepath(server, user).DROPSHELL_DIR(), "ls --color && bash", {}), cMode::Interactive);
return true;
}
bool ssh_into_service(const std::string &server, const std::string &service)
{
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return false;
}
if (!legal_service_name(service))
{
error << "Service name contains illegal characters: " << service << std::endl;
return false;
}
LocalServiceInfo sinfo = get_service_info(server, service);
if (!SIvalid(sinfo))
{
error << "Service " << service << " is not valid" << std::endl;
return false;
}
if (!gTemplateManager().has_template(sinfo.template_name))
{
error << "Template " << sinfo.template_name << " is not valid" << std::endl;
return false;
}
if (!gTemplateManager().template_command_exists(sinfo.template_name, "ssh"))
{
error << "Template " << sinfo.template_name << " does not have an ssh command" << std::endl;
return false;
}
server_env.run_remote_template_command(service, "ssh", {}, false, {}); // explicitly supports interactive ssh!
return true;
}
int ssh_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{
error << "Server name is required" << std::endl;
return 1;
}
// ssh into the server
if (ctx.args.size() < 2)
{
std::string arg1 = safearg(ctx.args, 0);
std::string server, user;
// parse either user@server or server
if (arg1.find("@") != std::string::npos)
{
user = arg1.substr(0, arg1.find("@"));
server = arg1.substr(arg1.find("@") + 1);
}
else
{
server = arg1;
// get the first user from the server.env file, and ssh in as that user.
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return 1;
}
ASSERT(server_env.get_users().size() > 0, "Server " + server + " has no users");
user = server_env.get_users()[0].user;
}
return ssh_into_server(server, user) ? 0 : 1;
}
else
{ // ssh into a service on the server.
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
return ssh_into_service(server, service) ? 0 : 1;
}
}
} // namespace dropshell

View File

@ -0,0 +1,98 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "servers.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/output.hpp"
namespace dropshell
{
int start_handler(const CommandContext &ctx);
static std::vector<std::string> start_name_list = {"start", "start-service"};
// Static registration
struct StartCommandRegister
{
StartCommandRegister()
{
CommandRegistry::instance().register_command({start_name_list,
start_handler,
shared_commands::std_autocomplete_allowall,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
2, // max_args (after command)
"start SERVER SERVICE|all",
"Start a service or all services on a server.",
R"(
start SERVER SERVICE Starts the given service on the given server.
start SERVER all Starts all services on the given server.
Note: This command will not create any data or configuration.
It will simply start the service on the remote server.
Stop the service with stop, or uninstall with uninstall.
)"});
}
} start_command_register;
bool start_service(const std::string &server, const std::string &service)
{
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return false;
}
if (!legal_service_name(service))
{
error << "Service name contains illegal characters: " << service << std::endl;
return false;
}
// run the start script.
bool started = server_env.run_remote_template_command(service, "start", {}, false, {});
if (started)
{
info << "Service " << service << " on server " << server << " started." << std::endl;
return true;
}
error << "Failed to start service " << service << " on server " << server << std::endl;
return false;
}
int start_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 2)
{
error << "Server name and service name are both required" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
if (service == "all")
{
// install all services on the server
maketitle("Stopping all services on " + server);
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &service : services)
okay &= start_service(server, service.service_name);
return okay ? 0 : 1;
}
// start the specific service.
return start_service(server, service) ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,98 @@
#include "command_registry.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/directories.hpp"
#include "shared_commands.hpp"
#include "servers.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/output.hpp"
namespace dropshell
{
int stop_handler(const CommandContext &ctx);
static std::vector<std::string> stop_name_list = {"stop", "stop-service"};
// Static registration
struct StopCommandRegister
{
StopCommandRegister()
{
CommandRegistry::instance().register_command({stop_name_list,
stop_handler,
shared_commands::std_autocomplete_allowall,
false, // hidden
true, // requires_config
true, // requires_install
1, // min_args (after command)
2, // max_args (after command)
"stop SERVER SERVICE|all",
"Stop a service or all services on a server.",
R"(
stop SERVER SERVICE Stops the given service on the given server.
stop SERVER all Stops all services on the given server.
Note: This command will not destroy any data or configuration.
It will simply stop the service on the remote server.
Restart the service with start, or update and start it with install.
)"});
}
} stop_command_register;
bool stop_service(const std::string &server, const std::string &service)
{
ServerConfig server_env(server);
if (!server_env.is_valid())
{
error << "Server " << server << " is not valid" << std::endl;
return false;
}
if (!legal_service_name(service))
{
error << "Service name contains illegal characters: " << service << std::endl;
return false;
}
// run the stop script.
bool stopped = server_env.run_remote_template_command(service, "stop", {}, false, {});
if (stopped)
{
info << "Service " << service << " on server " << server << " stopped." << std::endl;
return true;
}
error << "Failed to stop service " << service << " on server " << server << std::endl;
return false;
}
int stop_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 2)
{
error << "Server name and service name are both required" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
std::string service = safearg(ctx.args, 1);
if (service == "all")
{
// install all services on the server
maketitle("Stopping all services on " + server);
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &service : services)
okay &= stop_service(server, service.service_name);
return okay ? 0 : 1;
}
// stop the specific service.
return stop_service(server, service) ? 0 : 1;
}
} // namespace dropshell

View File

@ -0,0 +1,106 @@
#include "command_registry.hpp"
#include "directories.hpp"
#include "shared_commands.hpp"
#include "templates.hpp"
#include <libassert/assert.hpp>
#include "utils/utils.hpp"
#include "services.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,
shared_commands::std_autocomplete_allowall,
false, // hidden
true, // requires_config
true, // requires_install
2, // min_args (after command)
2, // max_args (after command)
"uninstall SERVER SERVICE|all",
"Uninstall a service on a server. Does not remove configuration or user data.",
// heredoc
R"(
Uninstall a service, leaving all configuration and data intact.
uninstall SERVER SERVICE Uninstall the given service on the given server.
uninstall SERVER all Uninstall all services on the given server.
Update and reinstall the service with install, or delete all configuration and data with destroy.
)"});
}
} uninstall_command_register;
namespace shared_commands {
bool uninstall_service(const ServerConfig & server_env, const std::string &service)
{
ASSERT(server_env.is_valid(), "Invalid server environment for " + server_env.get_server_name());
std::string server = server_env.get_server_name();
maketitle("Uninstalling " + service + " on " + server);
std::string user = server_env.get_user_for_service(service);
// 2. Check if service directory exists on server
if (!server_env.check_remote_dir_exists(remotepath(server, user).service(service), user))
{
error << "Service is not installed: " << service << std::endl;
return true; // Nothing to uninstall
}
// 3. Run uninstall script if it exists
std::string uninstall_script = remotepath(server, user).service_template(service) + "/uninstall.sh";
if (!server_env.run_remote_template_command(service, "uninstall", {}, false, {}))
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(server, user).service(service), false, user))
{
ASSERT(!server_env.check_remote_dir_exists(remotepath(server, user).service(service), user), "Service directory still found on server after uninstall");
info << "Removed remote service directory " << remotepath(server, user).service(service) << std::endl;
}
else
warning << "Failed to remove remote service directory" << std::endl;
info << "Completed service " << service << " uninstall on " << server << std::endl;
return true;
}
} // namespace shared_commands
int uninstall_handler(const CommandContext &ctx)
{
if (ctx.args.size() < 1)
{
error << "uninstall requires a server and a service (or all)" << std::endl;
return 1;
}
std::string server = safearg(ctx.args, 0);
if (safearg(ctx.args, 1) == "all")
{
// uninstall all services on the server
bool okay = true;
std::vector<LocalServiceInfo> services = get_server_services_info(server);
for (const auto &service : services)
{
if (!shared_commands::uninstall_service(server, service.service_name))
okay = false;
}
return okay ? 0 : 1;
}
std::string service = safearg(ctx.args, 1);
return shared_commands::uninstall_service(server, service) ? 0 : 1;
}
} // namespace dropshell

View File

@ -3,9 +3,11 @@
#include <fstream>
#include "config.hpp"
#include "utils/utils.hpp"
#include "utils/json.hpp"
#include <filesystem>
#include "utils/execute.hpp"
#include "output.hpp"
namespace dropshell {
@ -35,7 +37,7 @@ bool config::load_config() { // load json config file.
}
catch (nlohmann::json::parse_error& ex)
{
std::cerr << "Error: Failed to parse config file: " << ex.what() << std::endl;
error << "Failed to parse config file: " << ex.what() << std::endl;
return false;
}
@ -43,6 +45,15 @@ bool config::load_config() { // load json config file.
return true;
}
void _append(std::vector<std::string> & a, const std::vector<std::string> & b) {
if (b.empty())
return;
if (a.empty())
a = b;
else
a.insert(std::end(a), std::begin(b), std::end(b));
}
bool config::save_config(bool create_aux_directories)
{
std::string config_path = localfile::dropshell_json();
@ -59,38 +70,30 @@ bool config::save_config(bool create_aux_directories)
{
std::string homedir = localpath::current_user_home();
std::string dropshell_base = homedir + "/.dropshell";
mConfig["tempfiles"] = dropshell_base + "/tmp";
mConfig["backups"] = dropshell_base + "/backups";
mConfig["executables"] = dropshell_base + "/executables";
mConfig["template_cache"] = dropshell_base + "/template_cache";
mConfig["template_registry_URLs"] = {
"https://templates.dropshell.app"
mConfig["server_definition_paths"] = {
dropshell_base + "/servers"
};
mConfig["template_local_paths"] = {
dropshell_base + "/local_templates"
};
mConfig["server_definition_paths"] = {
dropshell_base + "/servers"
mConfig["template_registry_URLs"] = {
"https://templates.dropshell.app"
};
mConfig["template_upload_token"] = "SECRETTOKEN";
mConfig["backups_path"] = {
dropshell_base + "/backups"
};
mConfig["template_upload_registry_url"] = "https://templates.dropshell.app";
mConfig["template_upload_registry_token"] = "SECRETTOKEN";
}
config_file << mConfig.dump(4);
config_file.close();
if (create_aux_directories) {
std::vector<std::filesystem::path> paths = {
get_local_template_cache_path(),
get_local_backup_path(),
get_local_tempfiles_path(),
get_local_executables_path()
};
for (auto & p : get_local_server_definition_paths())
paths.push_back(p);
std::vector<std::string> paths;
_append(paths, get_local_template_paths());
_append(paths, get_local_server_definition_paths());
for (auto & p : paths)
if (!std::filesystem::exists(p))
{
@ -99,17 +102,11 @@ bool config::save_config(bool create_aux_directories)
}
}
// also make sure the executables are in the path.
std::string executables_path = get_local_executables_path();
// download bb64.
std::string cmd = "cd " + executables_path + " && 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 " << executables_path << std::endl;
else
std::cerr << "Failed to download bb64 to " << executables_path << std::endl;
debug << "Config paths: " << std::endl;
for (auto [key,value] : mConfig.items()) {
debug << " " << key << ": " << value << std::endl;
}
return true;
}
@ -120,36 +117,20 @@ bool config::is_config_set() const
bool config::is_agent_installed()
{
return std::filesystem::exists(localpath::agent() + "/bb64");
return std::filesystem::exists(localfile::bb64());
}
std::string config::get_local_tempfiles_path() {
return mConfig["tempfiles"];
}
std::string config::get_local_backup_path() {
return mConfig["backups"];
}
std::string config::get_local_template_cache_path() {
return mConfig["template_cache"];
}
std::string config::get_local_executables_path()
{
return mConfig["executables"];
}
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);
std::vector<tRegistryEntry> config::get_template_registry_urls() {
nlohmann::json template_registries = mConfig["template_registries"];
std::vector<tRegistryEntry> registries;
for (auto &registry : template_registries) {
if (registry.is_object() && !registry.empty())
registries.push_back(tRegistryEntry(registry));
}
return urls;
return registries;
}
std::vector<std::string> config::get_template_local_paths()
std::vector<std::string> config::get_local_template_paths()
{
nlohmann::json template_local_paths = mConfig["template_local_paths"];
std::vector<std::string> paths;
@ -162,23 +143,67 @@ std::vector<std::string> config::get_template_local_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_server_create_path()
{
std::vector<std::string> paths = get_local_server_definition_paths();
if (paths.empty())
return "";
return paths[0];
}
std::string config::get_template_upload_registry_token() {
return mConfig["template_upload_registry_token"];
std::string config::get_template_create_path()
{
std::vector<std::string> paths = get_local_template_paths();
if (paths.empty())
return "";
return paths[0];
}
std::string config::get_backups_path()
{
nlohmann::json backups_path = mConfig["backups_path"];
if (backups_path.empty())
return "";
if (backups_path.is_string())
return backups_path;
warning << "backups_path is not a string: " << backups_path << std::endl;
return "";
}
dropshell::tRegistryEntry::tRegistryEntry(nlohmann::json json)
{
valid = false;
if (json.is_object() && !json.empty()) {
for (auto &[key, value] : json.items()) {
if (value.is_string() && !value.empty())
switch (switchhash(key.c_str())) {
case switchhash("name"):
name = value;
break;
case switchhash("url"):
url = value;
break;
case switchhash("token"):
token = value;
break;
default:
break;
}
}
valid = (!url.empty()&&!name.empty()); // token can be empty.
}
}
tRegistryEntry::~tRegistryEntry()
{
}
} // namespace dropshell

51
source/src/config.hpp Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include <string>
#include <vector>
#define JSON_INLINE_ALL
#include <nlohmann/json.hpp>
namespace dropshell {
class tRegistryEntry {
public:
tRegistryEntry(nlohmann::json json);
~tRegistryEntry();
public:
std::string name;
std::string url;
std::string token;
bool valid;
};
class config {
public:
config();
~config();
bool load_config();
bool save_config(bool create_aux_directories);
bool is_config_set() const;
static bool is_agent_installed();
std::vector<tRegistryEntry> get_template_registry_urls();
std::vector<std::string> get_local_template_paths();
std::vector<std::string> get_local_server_definition_paths();
std::string get_server_create_path();
std::string get_template_create_path();
std::string get_backups_path();
private:
nlohmann::json mConfig;
bool mIsConfigSet;
};
config & gConfig();
} // namespace dropshell

10509
source/src/contrib/httplib.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,8 @@ _dropshell_completions() {
cur="${COMP_WORDS[COMP_CWORD]}"
# call dropshell to get the list of possiblities for the current argument. Supply all previous arguments.
local completions=($(dropshell autocomplete "${COMP_WORDS[@]:1:${COMP_CWORD}-1}"))
COMPREPLY=( $(compgen -W "${completions[*]}" -- ${cur}) )
mapfile -t completions < <(dropshell autocomplete "${COMP_WORDS[@]:1:${COMP_CWORD}-1}")
mapfile -t COMPREPLY < <(compgen -W "${completions[*]}" -- "$cur")
return 0
}

147
source/src/main.cpp Normal file
View File

@ -0,0 +1,147 @@
#include "version.hpp"
#include "config.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/directories.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "autocomplete.hpp"
#include "utils/hash.hpp"
#include "command_registry.hpp"
#include "output.hpp"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
#include <chrono>
#include <libassert/assert.hpp>
#include <sstream>
#include <algorithm>
namespace dropshell {
extern const std::string VERSION;
extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
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* cmdinfo = CommandRegistry::instance().find_command(ctx.command);
if (!cmdinfo) {
error << "Unknown command: " << ctx.command << std::endl;
return 1;
}
if (cmdinfo->requires_config && !gConfig().is_config_set()) {
error << "Valid dropshell configuration required for command: " << ctx.command << std::endl;
info << "Please run 'dropshell edit' to set up the dropshell configuration." << std::endl;
return 1;
}
if (cmdinfo->requires_install && !gConfig().is_agent_installed()) {
error << "Dropshell agent not installed for command: " << ctx.command << std::endl;
info << "Please run 'dropshell install' to install the local dropshell agent." << std::endl;
return 1;
}
int arg_count = ctx.args.size();
if (arg_count < cmdinfo->min_args || (cmdinfo->max_args != -1 && arg_count > cmdinfo->max_args)) {
error << "Invalid number of arguments for command: " << ctx.command << std::endl;
debug << "(" << ctx.args.size() << " args provided, " << ctx.command << " requires " << (cmdinfo->min_args) << " to " << (cmdinfo->max_args) << " args)" << std::endl;
info << "Usage: " << std::endl;
info << " ";
info << left_align(cmdinfo->help_usage,30);
info << cmdinfo->help_description << std::endl;
return 1;
}
return cmdinfo->handler(ctx);
}
catch (const std::exception& e) {
error << "Uncaught Exception: " << e.what() << std::endl;
return 1;
}
}
// ------------------------------------------------------------------------------------------------
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_and_services.server_name = arg2;
if (arg3.empty()) {
server_and_services.servicelist = get_server_services_info(arg2);
} else {
server_and_services.servicelist.push_back(get_service_info(arg2, arg3));
}
return true;
}
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;
};
#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; \
} \
}
} // namespace dropshell
int main(int argc, char* argv[]) {
return dropshell::main(argc, argv);
}

483
source/src/servers.cpp Normal file
View File

@ -0,0 +1,483 @@
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "servers.hpp"
#include "services.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "utils/execute.hpp"
#include "output.hpp"
#include <libassert/assert.hpp>
#include "config.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
{
ServerConfig::ServerConfig(const std::string &server_name) : mValid(false), mServerName(server_name)
{
if (server_name.empty())
return;
std::string server_json_path = localfile::server_json(server_name);
// Check if file exists
if (!std::filesystem::exists(server_json_path))
{
std::cerr << "Server environment file not found: " + server_json_path << " for server " << server_name << std::endl;
return;
}
try
{
// Use envmanager to handle the environment file
nlohmann::json server_env_json = nlohmann::json::parse(std::ifstream(server_json_path));
if (server_env_json.empty())
{
error << "Failed to parse server environment file at "<< server_json_path << std::endl;
info << "The returned json was empty." << std::endl;
return;
}
// get the variables from the json, converting everything to strings.
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_PORT", "SSH_USERS"})
{
if (mVariables.find(var) == mVariables.end())
{
// Print the variables identified in the file
info << "Variables identified in the file:" << std::endl;
for (const auto &v : mVariables)
{
info << " " << v.first << std::endl;
}
throw std::runtime_error("Missing required variable: " + std::string(var));
}
}
// Parse users array
if (!server_env_json.contains("SSH_USERS") || !server_env_json["SSH_USERS"].is_array())
{
error << "SSH_USERS array not found or invalid in server configuration" << std::endl;
return;
}
for (const auto &user_json : server_env_json["SSH_USERS"])
{
UserConfig user;
user.user = user_json["USER"].get<std::string>();
user.dir = user_json["DIR"].get<std::string>();
mUsers.push_back(user);
}
if (mUsers.empty())
{
error << "No users defined in server configuration " << server_json_path << std::endl;
return;
}
mValid = true;
}
catch (const std::exception &e)
{
error << "Failed to parse " << server_json_path << std::endl;
error << "Exception: " << e.what() << std::endl;
mValid = false;
}
}
std::string ServerConfig::get_SSH_HOST() const
{
return get_variable("SSH_HOST");
}
std::string ServerConfig::get_SSH_PORT() const
{
return get_variable("SSH_PORT");
}
std::vector<UserConfig> ServerConfig::get_users() const
{
return mUsers;
}
std::string ServerConfig::get_user_dir(const std::string &user) const
{
for (const auto &u : mUsers)
{
if (u.user == user)
{
return u.dir;
}
}
return "";
}
std::string ServerConfig::get_server_name() const
{
return mServerName;
}
std::string ServerConfig::get_user_for_service(const std::string &service) const
{
return dropshell::get_user_for_service(mServerName, service);
}
std::string get_user_for_service(const std::string &server, const std::string &service)
{
if (!legal_service_name(service))
{
error << "Service name contains illegal characters: " + service << std::endl;
return "";
}
auto services_info = get_server_services_info(server);
auto it = std::find_if(services_info.begin(), services_info.end(),
[&service](const LocalServiceInfo &si)
{ return si.service_name == service; });
if (it != services_info.end() && SIvalid(*it))
return it->user;
debug << "Couldn't find user for service \"" << service << "\" on server \"" << server << "\"" << std::endl;
return "";
}
sSSHInfo ServerConfig::get_SSH_INFO(std::string user) const
{
ASSERT(!user.empty(), "User is empty, cannot get SSH info.");
// Find user in mUsers vector
auto it = std::find_if(mUsers.begin(), mUsers.end(),
[&user](const UserConfig &u)
{ return u.user == user; });
ASSERT(it != mUsers.end(), ("User " + user + " not found in server environment."));
return sSSHInfo(get_SSH_HOST(), it->user, get_SSH_PORT(), get_server_name(), it->dir);
}
bool ServerConfig::hasRootUser() const
{
auto it = std::find_if(mUsers.begin(), mUsers.end(),[](const UserConfig &u)
{ return u.user == "root"; });
return it != mUsers.end();
}
bool ServerConfig::hasDocker() const
{
return get_variable("HAS_DOCKER") == "true";
}
bool ServerConfig::hasRootDocker() const
{
return get_variable("DOCKER_ROOTLESS") == "false";
}
bool ServerConfig::hasUser(const std::string &user) const
{
auto it = std::find_if(mUsers.begin(), mUsers.end(),
[&user](const UserConfig &u)
{ return u.user == user; });
return it != mUsers.end();
}
bool ServerConfig::check_remote_dir_exists(const std::string &dir_path, std::string user) const
{
if (user.empty())
{
debug << "Can't check remote directory exists for " << dir_path << " as user is empty" << std::endl;
return false;
}
sCommand scommand("", "test -d " + quote(dir_path), {});
return execute_ssh_command(get_SSH_INFO(user), scommand, cMode::Silent);
}
bool ServerConfig::check_remote_file_exists(const std::string &file_path, std::string user) const
{
if (user.empty())
{
debug << "Can't check remote file exists for " << file_path << " as user is empty" << std::endl;
return false;
}
sCommand scommand("", "test -f " + quote(file_path), {});
return execute_ssh_command(get_SSH_INFO(user), scommand, cMode::Silent);
}
bool ServerConfig::check_remote_items_exist(const std::vector<std::string> &file_paths, std::string user) const
{
if (user.empty())
{
debug << "Can't check remote items exist as user is empty" << std::endl;
return false;
}
// convert file_paths to a single string, separated by spaces
std::string file_paths_str;
std::string file_names_str;
for (const auto &file_path : file_paths)
{
file_paths_str += quote(file_path) + " ";
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", {});
sSSHInfo sshinfo = get_SSH_INFO(user);
bool okay = execute_ssh_command(sshinfo, scommand, cMode::Silent);
if (!okay)
{
error << "Required items not found on remote server: " << file_names_str << std::endl;
return false;
}
return true;
}
bool ServerConfig::remove_remote_dir(
const std::string &dir_path, bool silent, std::string user) const
{
std::filesystem::path path(dir_path);
std::filesystem::path parent_path = path.parent_path();
std::string target_dir = path.filename().string();
if (parent_path.empty())
parent_path = "/";
if (target_dir.empty())
return false;
if (!silent)
std::cout << "Removing remote directory " << target_dir << " in " << parent_path << " on " << mServerName << std::endl;
std::string remote_cmd =
"docker run --rm -v " + quote(parent_path.string()) + ":/parent " +
" alpine rm -rf \"/parent/" + target_dir + "\"";
// if (!silent)
// std::cout << "Running command: " << remote_cmd << std::endl;
sCommand scommand("", remote_cmd, {});
cMode mode = (silent ? cMode::Silent : cMode::Defaults);
sSSHInfo sshinfo = get_SSH_INFO(user);
return execute_ssh_command(sshinfo, scommand, mode);
}
bool ServerConfig::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 user = get_user_for_service(service_name);
auto scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
if (!scommand.has_value())
return false;
// add the extra env vars to the command
for (const auto &[key, value] : extra_env_vars)
scommand->add_env_var(key, value);
if (scommand->get_command_to_run().empty())
return false;
cMode mode = (command == "ssh") ? (cMode::Interactive) : (silent ? cMode::Silent : cMode::Defaults);
return execute_ssh_command(get_SSH_INFO(user), scommand.value(), mode);
}
bool ServerConfig::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
{
std::string user = get_user_for_service(service_name);
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);
return execute_ssh_command(get_SSH_INFO(user), scommand.value(), cMode::Defaults, &output);
}
std::string ServerConfig::get_variable(const std::string &name) const
{
auto it = mVariables.find(name);
if (it == mVariables.end())
{
return "";
}
return it->second;
}
std::optional<sCommand> ServerConfig::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, const std::vector<std::string> args, const bool silent) const
{
if (command.empty())
return std::nullopt;
std::string user = get_user_for_service(service_name);
std::string remote_service_template_path = remotepath(mServerName, user).service_template(service_name);
std::string script_path = remote_service_template_path + "/" + command + ".sh";
std::map<std::string, std::string> env_vars;
if (!get_all_service_env_vars(mServerName, service_name, env_vars))
{
error << "Failed to get all service env vars for " << service_name << std::endl;
return std::nullopt;
}
env_vars["HOST_NAME"] = get_SSH_HOST();
std::string argstr = "";
for (const auto &arg : args)
{
argstr += " " + quote(dequote(trim(arg)));
}
sCommand sc(
remote_service_template_path,
quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""),
env_vars);
if (sc.empty())
{
error << "Failed to construct command for " << service_name << " " << command << std::endl;
return std::nullopt;
}
return sc;
}
std::vector<ServerConfig> get_configured_servers()
{
std::vector<ServerConfig> servers;
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return servers;
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;
ServerConfig env(server_name);
if (!env.is_valid())
{
error << "Invalid server environment file: " << entry.path().string() << std::endl;
continue;
}
servers.push_back(env);
}
}
}
}
return servers;
}
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())
{
error << "Server name already exists: " << server_name << std::endl;
info << "Current server path: " << server_existing_dir << std::endl;
return false;
}
// 2. create a new directory in the user config directory
auto lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty() || lsdp[0].empty())
{
error << "Local server definition path not found" << std::endl;
info << "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
std::string user = getenv("USER");
std::string server_env_path = server_dir + "/" + filenames::server_json;
std::ofstream server_env_file(server_env_path);
server_env_file << "{" << std::endl;
server_env_file << " \"SSH_HOST\": \"" << server_name << "\"," << std::endl;
server_env_file << " \"SSH_PORT\": " << 22 << "," << std::endl;
server_env_file << " \"SSH_USERS\": [" << std::endl;
server_env_file << " {" << std::endl;
server_env_file << " \"USER\": \"" << user << "\"," << std::endl;
server_env_file << " \"DIR\": \"" << "/home/" + user << "/.dropshell\"" << std::endl;
server_env_file << " }" << std::endl;
server_env_file << " ]," << std::endl;
server_env_file << " \"HAS_DOCKER\": \"true\"," << std::endl;
server_env_file << " \"DOCKER_ROOTLESS\": \"false\"" << std::endl;
server_env_file << "}" << std::endl;
server_env_file.close();
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) install the server: dropshell install " << server_name << std::endl;
std::cout << std::endl;
return true;
}
void get_all_used_commands(std::set<std::string> &commands)
{
std::vector<ServerConfig> servers = get_configured_servers();
for (const auto &server : servers)
{
auto services = get_server_services_info(server.get_server_name());
for (const auto &service : services)
commands.merge(get_used_commands(server.get_server_name(), service.service_name));
}
}
bool server_exists(const std::string &server_name)
{
std::string server_existing_dir = localpath::server(server_name);
if (server_existing_dir.empty())
return false;
if (std::filesystem::exists(server_existing_dir));
return true;
return false;
}
} // namespace dropshell

104
source/src/servers.hpp Normal file
View File

@ -0,0 +1,104 @@
// server_env.hpp
//
// read the server.env file and provide a class to access the variables
#ifndef __SERVER_ENV_HPP
#define __SERVER_ENV_HPP
#include <string>
#include <map>
#include <memory>
#include <vector>
#include <set>
#include <optional>
#include "utils/execute.hpp"
namespace dropshell
{
struct UserConfig
{
std::string user;
std::string dir;
};
// ------------------------------------------------------------------------------------------------
// reads path / server.env and provides a class to access the variables.
// each env file is required to have the following variables:
// SSH_HOST
// SSH_UNPRIVILEGED_USER
// SSH_PORT
// the following replacements are made in the values:
// ${USER} -> the username of the user running dropshell
class ServerConfig
{
public:
ServerConfig(const std::string &server_name);
bool is_valid() const { return mValid; }
// trivial getters.
const std::map<std::string, std::string> &get_variables() const { return mVariables; }
std::string get_variable(const std::string &name) const;
// ------------------------------------------------------------------------------------------------
// getters
// ------------------------------------------------------------------------------------------------
std::string get_SSH_HOST() const;
std::string get_SSH_PORT() const;
std::vector<UserConfig> get_users() const;
std::string get_user_dir(const std::string &user) const;
std::string get_server_name() const;
std::string get_user_for_service(const std::string &service) const;
sSSHInfo get_SSH_INFO(std::string user) const;
// server capabilities
bool hasRootUser() const;
bool hasDocker() const;
bool hasRootDocker() const;
// helper functions
bool hasUser(const std::string &user) const;
public:
bool check_remote_dir_exists(const std::string &dir_path, std::string user) const;
bool check_remote_file_exists(const std::string &file_path, std::string user) const;
bool check_remote_items_exist(const std::vector<std::string> &file_paths, std::string user) const;
bool remove_remote_dir(const std::string &dir_path, bool silent, std::string user) const;
bool run_remote_template_command(const std::string &service_name, const std::string &command,
std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const;
bool run_remote_template_command_and_capture_output(const std::string &service_name, const std::string &command,
std::vector<std::string> args, std::string &output, bool silent, std::map<std::string, std::string> extra_env_vars) const;
private:
std::optional<sCommand> construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, const std::vector<std::string> args, const bool silent) const;
private:
std::string mServerName;
std::map<std::string, std::string> mVariables;
std::vector<UserConfig> mUsers;
bool mValid;
}; // class ServerConfig
std::vector<ServerConfig> get_configured_servers();
std::string get_user_for_service(const std::string &server, const std::string &service);
bool create_server(const std::string &server_name);
void get_all_used_commands(std::set<std::string> &commands);
bool server_exists(const std::string &server_name);
} // namespace dropshell
#endif // __SERVER_ENV_HPP

View File

@ -0,0 +1,117 @@
// #include <iostream>
// #include <fstream>
// #include <sstream>
// #include <cstdlib>
// #include <chrono>
// #include <iomanip>
// #include <filesystem>
// #include <unistd.h>
// #include <libassert/assert.hpp>
// #include "config.hpp"
// #include "servers.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 {
// 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)
// {
// if (server_name.empty() || service_name.empty())
// return;
// // Initialize server environment
// if (!mServerEnv.is_valid())
// 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();
// }
// // ------------------------------------------------------------------------------------------------
// // Run a command on the service.
// // ------------------------------------------------------------------------------------------------
// 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 = gTemplateManager().get_template_info(mServiceInfo.template_name);
// if (!tinfo.is_set()) {
// std::cerr << "Error: Template '" << mServiceInfo.template_name << "' not found" << std::endl;
// return false;
// }
// 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_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 == "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, false, env_vars);
// }
// } // namespace dropshell

282
source/src/services.cpp Normal file
View File

@ -0,0 +1,282 @@
#include "services.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "templates.hpp"
#include "config.hpp"
#include "utils/utils.hpp"
#include <libassert/assert.hpp>
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
namespace dropshell
{
#pragma message("TODO : Smart test that the service is fully valid.")
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() &&
!service_info.user.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_server_definition_paths = gConfig().get_local_server_definition_paths();
if (local_server_definition_paths.empty())
{
error << "No local server definition paths found" << std::endl;
info << "Run 'dropshell edit' to configure DropShell" << std::endl;
return services;
}
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))
{
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
warning << "Failed to get service info for " << dirname << " on server " << server_name << std::endl;
}
} // end of for
}
return services;
}
bool get_bool_variable(const std::map<std::string, std::string> &variables, const std::string &variable_name)
{
auto it = variables.find(variable_name);
if (it == variables.end())
{
error << "Variable " << variable_name << " not found in the service " << filenames::template_info_env << std::endl;
return false;
}
return it->second == "true";
}
LocalServiceInfo get_service_info(const std::string &server_name, const std::string &service_name)
{
LocalServiceInfo service;
if (server_name.empty() || service_name.empty())
return LocalServiceInfo();
if (!legal_service_name(service_name))
return LocalServiceInfo();
service.service_name = service_name;
service.local_service_path = localpath::service(server_name, service_name);
if (service.local_service_path.empty())
return LocalServiceInfo();
// check the service directory exists.
if (!fs::exists(service.local_service_path))
{
warning << "Service directory not found: " << service.local_service_path << std::endl;
return LocalServiceInfo();
}
// now set the template name and path.
std::map<std::string, std::string> variables;
if (!get_all_service_env_vars(server_name, service_name, variables))
return LocalServiceInfo();
{ // confirm TEMPLATE is defined.
auto it = variables.find("TEMPLATE");
if (it == variables.end())
{
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 = gTemplateManager().get_template_info(service.template_name);
if (!tinfo.is_set())
{
error << "Template specified '" << service.template_name << "' could not be found" << std::endl;
return LocalServiceInfo();
}
// find the template path
service.local_template_path = tinfo.local_template_path();
{ // set the user.
auto it = variables.find("SSH_USER");
if (it == variables.end())
{
error << "SSH_USER variable not defined in service " << service_name << " on server " << server_name << std::endl;
return LocalServiceInfo();
}
service.user = it->second;
}
// set the host root and docker requirements.
service.requires_host_root = get_bool_variable(variables, "REQUIRES_HOST_ROOT");
service.requires_docker = get_bool_variable(variables, "REQUIRES_DOCKER");
service.requires_docker_root = get_bool_variable(variables, "REQUIRES_DOCKER_ROOT");
{ // determine if the service template hash matches the template hash.
auto it = variables.find("TEMPLATE_HASH");
if (it == variables.end())
error << "Variable TEMPLATE_HASH not found in the service " << filenames::template_info_env << std::endl;
else
{
uint64_t service_template_hash = std::stoull(it->second);
service.service_template_hash_match = (service_template_hash == tinfo.hash());
//debug << "Service template hash: " << service_template_hash << " == " << tinfo.hash() << std::endl;
}
}
return service;
}
std::set<std::string> get_used_commands(const std::string &server_name, const std::string &service_name)
{
std::set<std::string> commands;
if (server_name.empty() || service_name.empty())
return commands;
auto service_info = get_service_info(server_name, service_name);
if (service_info.local_template_path.empty())
{
error << "Service not found: " << service_name << std::endl;
return commands;
}
// iterate over all files in the template path, and add the command name to the set.
// commands are .sh files that don't begin with _
for (const auto &entry : fs::directory_iterator(service_info.local_template_path))
{
if (fs::is_regular_file(entry) && entry.path().extension() == ".sh" && (entry.path().filename().string().rfind("_", 0) != 0))
commands.insert(entry.path().stem().string());
}
return commands;
}
std::set<std::string> list_backups(const std::string &server_name, const std::string &service_name)
{
std::set<std::string> backups;
if (server_name.empty() || service_name.empty())
return backups;
// need to find the template for the service.
auto service_info = get_service_info(server_name, service_name);
if (service_info.local_template_path.empty())
{
error << "Service not found: " << service_name << std::endl;
return backups;
}
std::string backups_dir = localpath::backups();
if (backups_dir.empty())
return backups;
if (fs::exists(backups_dir))
{
for (const auto &entry : fs::directory_iterator(backups_dir))
{
if (fs::is_regular_file(entry) && entry.path().extension() == ".tgz")
if (entry.path().filename().string().find(service_info.template_name) != std::string::npos)
{
backups.insert(entry.path().filename().string());
}
}
}
return backups;
}
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)))
{
error << "Service not found: " << service_name << " on server " << server_name << std::endl;
return false;
}
// 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
warning << "Expected environment file not found: " << file << std::endl;
};
// add in some simple variables first, as others below may depend on/use these in bash.
// if we change these, we also need to update agent/_allservicesstatus.sh
all_env_vars["SERVER"] = server_name;
all_env_vars["SERVICE"] = service_name;
all_env_vars["DOCKER_CLI_HINTS"] = "false"; // turn off docker junk.
// Load environment files
load_env_file(localfile::service_env(server_name, service_name));
load_env_file(localfile::template_info_env(server_name, service_name));
std::string user = all_env_vars["SSH_USER"];
if (user.empty())
{
error << "SSH_USER variable not defined in service " << service_name << " on server " << server_name << std::endl;
info << "This variable definition is always required, and usually set in the "<<filenames::service_env << " file." << std::endl;
info << "Please check " << localfile::service_env(server_name, service_name) << std::endl;
return false;
}
// more additional, these depend on others above.
all_env_vars["CONFIG_PATH"] = remotepath(server_name, user).service_config(service_name);
all_env_vars["AGENT_PATH"] = remotepath(server_name, user).agent();
// determine template name.
auto it = all_env_vars.find("TEMPLATE");
if (it == all_env_vars.end())
{
error << "TEMPLATE variable not defined in service " << service_name << " on server " << server_name << std::endl;
info << "The TEMPLATE variable is required to determine the template name." << std::endl;
info << "Please check the " << filenames::service_env << " file and the "<< filenames::template_info_env << " file in:" << std::endl;
info << " " << 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())
{
error << "Template '" << it->second << "' not found" << std::endl;
return false;
}
return true;
}
} // namespace dropshell

View File

@ -13,6 +13,11 @@ namespace dropshell {
std::string template_name;
std::string local_service_path;
std::string local_template_path;
std::string user;
bool requires_host_root;
bool requires_docker;
bool requires_docker_root;
bool service_template_hash_match;
};
bool SIvalid(const LocalServiceInfo& service_info);
@ -28,7 +33,6 @@ namespace dropshell {
// 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);
bool create_service(const std::string& server_name, const std::string& template_name, const std::string& service_name, bool silent=false);
} // namespace dropshell
#endif

View File

@ -6,13 +6,14 @@
#include <algorithm>
#include <iomanip>
#include <map>
#include "utils/assert.hpp"
#include <libassert/assert.hpp>
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "templates.hpp"
#include "config.hpp"
#include "utils/hash.hpp"
namespace dropshell {
@ -162,7 +163,7 @@
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;
error << "Template '" << template_name << "' not found" << std::endl;
return false;
}
return source->template_command_exists(template_name, command);
@ -170,25 +171,30 @@
bool template_manager::create_template(const std::string &template_name) const
{
if (!legal_service_name(template_name)) {
error << "Template name contains illegal characters: " << template_name << std::endl;
return false;
}
// 1. Create a new directory in the user templates directory
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;
error << "No local server definition paths found" << std::endl;
info << "Run 'dropshell edit' to configure DropShell" << std::endl;
return false;
}
auto info = get_template_info(template_name);
if (info.is_set()) {
std::cerr << "Error: Template '" << template_name << "' already exists at " << info.locationID() << std::endl;
auto tinfo = get_template_info(template_name);
if (tinfo.is_set()) {
error << "Template '" << template_name << "' already exists at " << tinfo.locationID() << std::endl;
return false;
}
auto local_template_paths = gConfig().get_template_local_paths();
auto local_template_paths = gConfig().get_local_template_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;
error << "No local template paths found" << std::endl;
info << "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;
@ -199,7 +205,7 @@
// 2. Copy the example template from the system templates directory
auto example_info = gTemplateManager().get_template_info("example-nginx");
if (!example_info.is_set()) {
std::cerr << "Error: Example template not found" << std::endl;
error << "Example template not found" << std::endl;
return false;
}
std::string example_template_path = example_info.local_template_path();
@ -219,9 +225,9 @@
// 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;
std::string service_env_path = new_template_path + "/config/.template_info.env";
std::string service_env_path = new_template_path + "/config/" + filenames::template_info_env;
if (!replace_line_in_file(service_env_path, search_string, replacement_line)) {
std::cerr << "Error: Failed to replace TEMPLATE= line in the .template_info.env file" << std::endl;
error << "Failed to replace TEMPLATE= line in the " << filenames::template_info_env <<" file" << std::endl;
return false;
}
@ -252,7 +258,7 @@
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();
auto local_template_paths = gConfig().get_local_template_paths();
if (local_template_paths.empty())
return;
for (const auto& path : local_template_paths)
@ -277,7 +283,7 @@
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;
error << path << " file not found in template - REQUIRED." << template_name << std::endl;
return false;
}
return true;
@ -296,28 +302,42 @@
bool template_manager::test_template(const std::string &template_path)
{
if (template_path.empty())
return false;
if (!std::filesystem::exists(template_path))
return false;
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",
"config/" + filenames::service_env,
"config/" + filenames::template_info_env,
"install.sh",
"uninstall.sh",
"nuke.sh"
"uninstall.sh"
};
for (const auto& file : required_files) {
if (!required_file(template_path + "/" + file, template_name))
return false;
// check if file is executable, if it ends in .sh
std::string suffix=".sh";
if (file.find(suffix) == file.size() - suffix.size())
{
std::filesystem::path path = template_path + "/" + file;
auto perms = std::filesystem::status(path).permissions();
if ((perms & std::filesystem::perms::owner_exec) == std::filesystem::perms::none)
error << file << " is not executable" << std::endl;
}
}
// ------------------------------------------------------------
// 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"
"config/" + filenames::service_env,
"config/" + filenames::template_info_env
};
for (const auto& file : env_files) {
{ // load service.env from the service on this machine.
@ -332,18 +352,18 @@
// 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;
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;
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;
error << "TEMPLATE variable is wrong in " << template_path << std::endl;
return false;
}
@ -356,4 +376,21 @@
return instance;
}
template_info::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),
mTemplateValid(template_manager::test_template(local_template_path.string())),
mIsSet(!template_name.empty() && !location_id.empty() && !local_template_path.empty()),\
mHash(0)
{
if (!std::filesystem::exists(local_template_path))
{
error << "Template path does not exist: " << local_template_path << std::endl;
return;
}
mHash = hash_directory_recursive(local_template_path);
}
} // namespace dropshell

View File

@ -4,7 +4,10 @@
#include <memory>
#include <set>
#include "utils/json.hpp"
#include "config.hpp"
#define JSON_INLINE_ALL
#include <nlohmann/json.hpp>
namespace dropshell {
@ -17,17 +20,22 @@ typedef enum template_source_type {
class template_info {
public:
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) {}
template_info(const std::string& template_name, const std::string& location_id, const std::filesystem::path& local_template_path);
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; }
bool is_set() const { return mIsSet; }
std::string name() const { return mTemplateName; }
std::string locationID() const { return mLocationID; }
std::filesystem::path local_template_path() const { return mTemplateLocalPath; }
bool template_valid() const { return mTemplateValid; }
uint64_t hash() const { return mHash; }
private:
std::string mTemplateName;
std::string mLocationID;
std::filesystem::path mTemplateLocalPath; // source or cache.
bool mTemplateValid;
bool mIsSet;
uint64_t mHash;
};
class template_source_interface {
@ -43,7 +51,7 @@ class template_source_interface {
class template_source_registry : public template_source_interface {
public:
template_source_registry(std::string URL) : mURL(URL) {}
template_source_registry(tRegistryEntry registry) : mRegistry(registry) {}
~template_source_registry() {}
@ -52,11 +60,11 @@ class template_source_registry : public template_source_interface {
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; }
std::string get_description() { return "Registry: " + mRegistry.name + " (" + mRegistry.url + ")"; }
private:
std::filesystem::path get_cache_dir();
private:
std::string mURL;
tRegistryEntry mRegistry;
std::vector<nlohmann::json> mTemplates; // cached list.
};

View File

@ -0,0 +1,237 @@
#include "directories.hpp"
#include "config.hpp"
#include "servers.hpp"
#include "output.hpp"
#include <iostream>
#include <string>
#include <filesystem>
namespace fs = std::filesystem;
namespace dropshell
{
namespace localfile
{
std::string dropshell_json()
{
return localpath::dropshell_dir() + "/" + filenames::dropshell_json;
}
std::string server_json(const std::string &server_name)
{
std::string serverpath = localpath::server(server_name);
return (serverpath.empty() ? "" : (fs::path(serverpath) / filenames::server_json).string());
}
std::string service_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) / filenames::service_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) / filenames::template_info_env).string());
}
std::string template_example()
{
return localpath::agent_local() + "/template_example";
}
std::string bb64()
{
return localpath::agent_local() + "/bb64";
}
} // namespace localfile
// ------------------------------------------------------------------------------------------
namespace localpath
{
std::string dropshell_dir()
{
return current_user_home() + "/.dropshell";
}
std::string server(const std::string &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() || service_name.empty()) ? "" : (serverpath + "/" + service_name));
}
std::string agent_local()
{
return dropshell_dir() + "/agent-local";
}
std::string agent_remote()
{
return dropshell_dir() + "/agent-remote";
}
std::string current_user_home()
{
char *homedir = std::getenv("HOME");
if (homedir)
{
std::filesystem::path homedir_path(homedir);
return fs::canonical(homedir_path).string();
}
warning << "Couldn't determine user directory" << std::endl;
return std::string();
}
std::string backups()
{
if (!gConfig().is_config_set())
return "";
return gConfig().get_backups_path();
}
std::string temp_files()
{
return dropshell_dir() + "/temp_files";
}
std::string template_cache()
{
return dropshell_dir() + "/template_cache";
}
bool create_directories()
{
std::vector<std::filesystem::path> paths = {
dropshell_dir(),
agent_local(),
agent_remote(),
template_cache(),
backups(),
temp_files()};
for (auto &p : gConfig().get_local_server_definition_paths())
paths.push_back(p);
for (auto &p : paths)
if (!p.empty() && !std::filesystem::exists(p))
{
info << "Creating directory: " << p << std::endl;
std::filesystem::create_directories(p);
}
return false;
}
} // namespace localpath
//------------------------------------------------------------------------------------------------
// remote paths
// DROPSHELL_DIR
// |-- backups
// |-- temp_files
// |-- agent
// | |-- bb64
// | |-- (other agent files, including _allservicesstatus.sh)
// |-- services
// |-- service name
// |-- config
// |-- service.env (actual service config)
// |-- template
// |-- (script files)
// |-- config
// |-- service.env (default service config)
// |-- (other config files for specific server&service)
remotefile::remotefile(const std::string &server_name, const std::string &user) : mServer_name(server_name), mUser(user) {}
std::string remotefile::service_env(const std::string &service_name) const
{
return remotepath(mServer_name, mUser).service_config(service_name) + "/" + filenames::service_env;
}
remotepath::remotepath(const std::string &server_name, const std::string &user) : mServer_name(server_name), mUser(user) {}
std::string remotepath::DROPSHELL_DIR() const
{
try
{
return ServerConfig(mServer_name).get_user_dir(mUser);
} catch (const std::exception &e)
{
error << "Failed to get remote dropshell directory for " << mServer_name << "@" << mUser << std::endl;
error << "Exception: " << e.what() << std::endl;
return "";
}
}
std::string remotepath::services() const
{
std::string dsp = DROPSHELL_DIR();
return (dsp.empty() ? "" : (dsp + "/services"));
}
std::string remotepath::service(const std::string &service_name) const
{
std::string services_path = services();
return (services_path.empty() ? "" : (services_path + "/" + service_name));
}
std::string remotepath::service_config(const std::string &service_name) const
{
std::string service_path = service(service_name);
return (service_path.empty() ? "" : (service_path + "/config"));
}
std::string remotepath::service_template(const std::string &service_name) const
{
std::string service_path = service(service_name);
return (service_path.empty() ? "" : (service_path + "/template"));
}
std::string remotepath::backups() const
{
std::string dsp = DROPSHELL_DIR();
return (dsp.empty() ? "" : (dsp + "/backups"));
}
std::string remotepath::temp_files() const
{
std::string dsp = DROPSHELL_DIR();
return (dsp.empty() ? "" : (dsp + "/temp_files"));
}
std::string remotepath::agent() const
{
std::string dsp = DROPSHELL_DIR();
return (dsp.empty() ? "" : (dsp + "/agent"));
}
// ------------------------------------------------------------------------------------------
// Utility functions
std::string get_parent(const std::filesystem::path path)
{
if (path.empty())
return std::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

@ -0,0 +1,133 @@
#ifndef DIRECTORIES_HPP
#define DIRECTORIES_HPP
#include <string>
#include <filesystem>
namespace dropshell {
// all functions return empty string on failure
//------------------------------------------------------------------------------------------------
// local user config directories
// ~/.dropshell
// |-- dropshell.json
// |-- agent-local
// |-- agent-install.sh
// |-- bb64 (only used locally, as it's for the local machine's architecture!)
// |-- template_example
// |-- agent-remote
// |-- (remote agent files, including _allservicesstatus.sh)
// |-- temp_files
// |-- template_cache
// |-- templates
// | |-- <template_name>.json
// | |-- <template_name>
// | |-- (...script files...)
// | |-- config
// | |-- service.env
// | |-- .template_info.env
// | |-- (...other service config files...)
// backups_path
// |-- katie-_-squashkiwi-_-squashkiwi-test-_-2025-04-28_21-23-59.tgz
// server_definition_path
// |-- <server_name>
// |-- server.json
// |-- services
// |-- <service_name>
// |-- service.env
// |-- .template_info.env
// |-- (...other config files for specific server&service...)
namespace filenames {
static const std::string template_info_env = ".template_info.env";
static const std::string service_env = "service.env";
static const std::string readme = "README.txt";
static const std::string server_json = "server.json";
static const std::string dropshell_json = "dropshell.json";
} // namespace filenames.
namespace localfile {
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 template_info_env(const std::string &server_name, const std::string &service_name);
std::string template_example();
std::string bb64();
} // namespace localfile
namespace localpath {
std::string dropshell_dir();
std::string server(const std::string &server_name);
std::string service(const std::string &server_name, const std::string &service_name);
std::string agent_local();
std::string agent_remote();
std::string current_user_home();
std::string backups();
std::string temp_files();
std::string template_cache();
bool create_directories();
} // namespace local
//------------------------------------------------------------------------------------------------
// remote paths
// DROPSHELL_DIR
// |-- backups
// |-- temp_files
// |-- agent
// | |-- bb64
// | |-- (other agent files, including _allservicesstatus.sh)
// |-- services
// |-- service name
// |-- config
// |-- service.env (actual service config)
// |-- .template_info.env
// |-- template
// |-- (script files)
// |-- config
// |-- service.env (default service config)
// |-- .template_info.env
// |-- (other config files for specific server&service)
class remotefile {
public:
remotefile(const std::string &server_name, const std::string &user);
std::string service_env(const std::string &service_name) const;
private:
std::string mServer_name;
std::string mUser;
};
class remotepath {
public:
remotepath(const std::string &server_name, const std::string &user);
std::string DROPSHELL_DIR() const;
std::string services() const;
std::string service(const std::string &service_name) const;
std::string service_config(const std::string &service_name) const;
std::string service_template(const std::string &service_name) const;
std::string backups() const;
std::string temp_files() const;
std::string agent() const;
private:
std::string mServer_name;
std::string mUser;
};
//------------------------------------------------------------------------------------------------
// utility functions
std::string get_parent(const std::filesystem::path path);
std::string get_child(const std::filesystem::path path);
} // namespace dropshell
#endif // DIRECTORIES_HPP

View File

@ -0,0 +1,251 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include <iostream>
#include <string>
#include <cstdlib>
#include <sstream>
#include <libassert/assert.hpp>
#include "execute.hpp"
#include "utils/utils.hpp"
#include "utils/b64ed.hpp"
#include "config.hpp"
#include "utils/directories.hpp"
#include "utils/output.hpp"
namespace dropshell
{
bool EXITSTATUSCHECK(int ret)
{
return (ret != -1 && WIFEXITED(ret) && (WEXITSTATUS(ret) == 0)); // ret is -1 if the command failed to execute.
}
// ----------------------------------------------------------------------------------------------------------
// execute_local_command_interactive
// ----------------------------------------------------------------------------------------------------------
bool execute_local_command_interactive(const sCommand &command)
{
if (command.get_command_to_run().empty())
return false;
std::string full_command = command.construct_cmd(localfile::bb64()); // 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);
}
}
// ----------------------------------------------------------------------------------------------------------
// execute_local_command
// ----------------------------------------------------------------------------------------------------------
class fancypinter
{
public:
fancypinter(sColour startColour) : startColour_(startColour), currentColour_(startColour) {}
void print_chunk(std::string chunk)
{
if (chunk.empty())
return;
if (newline_)
{
// sniff the mode... if the string starts with warning or warning: then set mode to WARNING. etc.
if (chunk.find("warning") == 0)
currentColour_ = sColour::WARNING;
else if (chunk.find("error") == 0)
currentColour_ = sColour::ERROR;
else if (chunk.find("debug") == 0)
currentColour_ = sColour::DEBUG;
else if (chunk.find("info") == 0)
currentColour_ = sColour::INFO;
else
currentColour_ = startColour_;
}
colourstream(currentColour_) << chunk;
newline_ = (chunk[chunk.size() - 1] == '\n');
}
void print(const std::string &buffer)
{
size_t start = 0;
while (start < buffer.size())
{
size_t newline_pos = buffer.find('\n', start);
if (newline_pos == std::string::npos)
{
if (start < buffer.size())
{
print_chunk(buffer.substr(start));
}
break;
}
print_chunk(buffer.substr(start, newline_pos - start + 1)); // include the newline
start = newline_pos + 1;
}
}
private:
bool newline_ = true;
sColour startColour_;
sColour currentColour_;
};
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(output == nullptr, "Interactive mode and an output string cannot be used together");
return execute_local_command_interactive(command);
}
if (command.get_command_to_run().empty())
return false;
bool silent = hasFlag(mode, cMode::Silent);
std::string full_cmd;
if (!hasFlag(mode, cMode::NoBB64))
full_cmd = command.construct_cmd(localfile::bb64());
else
full_cmd = command.construct_cmd("");
if (output != nullptr)
full_cmd += " 2>&1"; // capture both stdout and stderr
FILE *pipe = popen(full_cmd.c_str(), "r");
if (!pipe)
{
return false;
}
char buffer[128];
fancypinter fancyprint(sColour::DEBUG);
while (fgets(buffer, sizeof(buffer), pipe) != nullptr)
{
if (output != nullptr)
(*output) += buffer;
if (!silent)
fancyprint.print(buffer);
}
int ret = pclose(pipe);
return EXITSTATUSCHECK(ret);
}
// ----------------------------------------------------------------------------------------------------------
// execute_ssh_command
// ----------------------------------------------------------------------------------------------------------
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;
std::stringstream ssh_cmd;
ssh_cmd << "ssh -p " << ssh_info.get_port() << " " << (hasFlag(mode, cMode::Interactive) ? "-tt " : "")
<< ssh_info.get_user() << "@" << ssh_info.get_host();
std::string remote_bb64_path;
if (!hasFlag(mode, cMode::NoBB64))
remote_bb64_path = remotepath(ssh_info.get_server_ID(), ssh_info.get_user()).agent() + "/bb64";
bool rval = execute_local_command(
"", // local directory to run in
ssh_cmd.str() + " " + remote_command.construct_cmd(remote_bb64_path), // local command to run
{}, // environment variables
output, // output string
mode // mode
);
if (!rval && !hasFlag(mode, cMode::Silent))
{
error << "Failed to execute ssh command:" << std::endl;
debug << ssh_cmd.str() + " " + remote_command.construct_cmd(remote_bb64_path) << std::endl;
}
return rval;
}
// ----------------------------------------------------------------------------------------------------------
// makesafecmd
// ----------------------------------------------------------------------------------------------------------
std::string sCommand::makesafecmd(std::string bb64path, const std::string &command) const
{
if (command.empty())
return "";
std::string encoded = base64_encode(dequote(trim(command)));
std::string commandstr = bb64path + " " + encoded;
return commandstr;
}
// ----------------------------------------------------------------------------------------------------------
// construct_cmd
// ----------------------------------------------------------------------------------------------------------
std::string sCommand::construct_cmd(std::string bb64path) const
{
if (mCmd.empty())
return "";
// need to construct to change directory and set environment variables
std::string cmdstr;
if (!bb64path.empty())
{
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;
cmdstr = makesafecmd(bb64path, cmdstr);
}
else
{ // raw! bootstrapping only.
ASSERT(mVars.empty(), "Bootstrapping command must not have environment variables");
if (!mDir.empty())
cmdstr += mDir + "/" + mCmd;
else
cmdstr += mCmd;
}
return cmdstr;
}
bool sSSHInfo::valid() const
{
if (host.empty() || user.empty() || port.empty() || server_ID.empty() || user_dir.empty())
return false;
if (atoi(port.c_str()) == 0)
return false;
return true;
}
} // namespace dropshell

View File

@ -10,10 +10,10 @@ class sCommand;
// mode bitset
enum class cMode {
None = 0,
Defaults = 0,
Interactive = 1,
Silent = 2,
CaptureOutput = 4
NoBB64 = 4
};
inline cMode operator&(cMode lhs, cMode rhs) {return static_cast<cMode>(static_cast<int>(lhs) & static_cast<int>(rhs));}
@ -24,16 +24,28 @@ 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;
class sSSHInfo {
public:
sSSHInfo(std::string host, std::string user, std::string port, std::string server_ID, std::string user_dir) :
host(host), user(user), port(port), server_ID(server_ID), user_dir(user_dir) {}
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);
std::string get_host() const { return host; }
std::string get_user() const { return user; }
std::string get_port() const { return port; }
std::string get_server_ID() const { return server_ID; }
std::string get_user_dir() const { return user_dir; }
bool valid() const;
private:
std::string host;
std::string user;
std::string port;
std::string server_ID; // dropshell name for server.
std::string user_dir; // dropshell directory for the user.
};
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::Defaults);
bool execute_ssh_command(const sSSHInfo & ssh_info, const sCommand & remote_command, cMode mode = cMode::Defaults, std::string * output = nullptr);
// ------------------------------------------------------------------------------------------------
@ -53,10 +65,10 @@ class sCommand {
bool empty() const { return mCmd.empty(); }
std::string construct_cmd(std::string agent_path) const;
std::string construct_cmd(std::string bb64path) const;
private:
std::string makesafecmd(std::string agent_path, const std::string& command) const;
std::string makesafecmd(std::string bb64path, const std::string& command) const;
private:
std::string mDir;

View File

@ -3,6 +3,8 @@
#define XXH_INLINE_ALL
#include "contrib/xxhash.hpp"
#include "output.hpp"
#include <fstream>
#include <filesystem>
#include <iostream>
@ -13,7 +15,7 @@ uint64_t hash_file(const std::string &path) {
// Create hash state
XXH64_state_t* const state = XXH64_createState();
if (state == nullptr) {
std::cerr << "Failed to create hash state" << std::endl;
error << "Failed to create hash state" << std::endl;
return 0;
}
@ -24,7 +26,7 @@ uint64_t hash_file(const std::string &path) {
// Open file
std::ifstream file(path, std::ios::binary);
if (!file.is_open()) {
std::cerr << "Failed to open file: " << path << std::endl;
error << "Failed to open file: " << path << std::endl;
XXH64_freeState(state);
return 0;
}
@ -34,7 +36,7 @@ uint64_t hash_file(const std::string &path) {
char buffer[buffer_size];
while (file.read(buffer, buffer_size)) {
if (XXH64_update(state, buffer, file.gcount()) == XXH_ERROR) {
std::cerr << "Failed to update hash" << std::endl;
error << "Failed to update hash" << std::endl;
XXH64_freeState(state);
return 0;
}
@ -43,7 +45,7 @@ uint64_t hash_file(const std::string &path) {
// Handle any remaining bytes
if (file.gcount() > 0) {
if (XXH64_update(state, buffer, file.gcount()) == XXH_ERROR) {
std::cerr << "Failed to update hash" << std::endl;
error << "Failed to update hash" << std::endl;
XXH64_freeState(state);
return 0;
}
@ -59,14 +61,14 @@ uint64_t hash_directory_recursive(const std::string &path) {
// Create hash state
XXH64_state_t* const state = XXH64_createState();
if (state == nullptr) {
std::cerr << "Failed to create hash state" << std::endl;
error << "Failed to create hash state" << std::endl;
return 0;
}
// Initialize state with seed 0
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;
error << "Failed to reset hash state" << std::endl;
XXH64_freeState(state);
return 0;
}
@ -81,7 +83,7 @@ uint64_t hash_directory_recursive(const std::string &path) {
}
}
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Filesystem error: " << e.what() << std::endl;
error << "Filesystem error: " << e.what() << std::endl;
XXH64_freeState(state);
return 0;
}
@ -94,7 +96,7 @@ uint64_t hash_directory_recursive(const std::string &path) {
uint64_t hash_path(const std::string &path) {
if (!std::filesystem::exists(path)) {
std::cerr << "Path does not exist: " << path << std::endl;
error << "Path does not exist: " << path << std::endl;
return 0;
}
@ -103,28 +105,28 @@ uint64_t hash_path(const std::string &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;
error << "Path is neither a file nor a directory: " << path << std::endl;
return 0;
}
}
void hash_demo(const std::string & path)
{
std::cout << "Hashing path: " << path << std::endl;
info << "Hashing path: " << path << std::endl;
auto start = std::chrono::high_resolution_clock::now();
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;
info << "Hash: " << hash << " (took " << duration.count() << "ms)" << std::endl;
}
int hash_demo_raw(const std::string & path)
{
if (!std::filesystem::exists(path)) {
std::cout << 0 <<std::endl; return 1;
info << 0 <<std::endl; return 1;
}
XXH64_hash_t hash = hash_path(path);
std::cout << hash << std::endl;
info << hash << std::endl;
return 0;
}

133
source/src/utils/output.cpp Normal file
View File

@ -0,0 +1,133 @@
#include "output.hpp"
#include <iostream>
#include <mutex>
namespace dropshell
{
// Mutex to synchronize output
std::mutex output_mutex;
// ANSI colour codes
constexpr const char *GREY = "\033[90m";
constexpr const char *RESET = "\033[0m";
constexpr const char *DEBUG_COLOUR = "\033[36m"; // Cyan
// constexpr const char *INFO_COLOUR = "\033[32m"; // Green
constexpr const char *INFO_COLOUR = "\033[37m"; // White
constexpr const char *WARNING_COLOUR = "\033[33m"; // Yellow
constexpr const char *ERROR_COLOUR = "\033[31m"; // Red
// Tag and colour for each stream
struct StreamInfo
{
const char *tag;
const char *colour;
};
const StreamInfo stream_infos[] = {
{"[DBG]", DEBUG_COLOUR},
{"[INF]", INFO_COLOUR},
{"[WRN]", WARNING_COLOUR},
{"[ERR]", ERROR_COLOUR}};
// Custom streambuf to prefix and colour each line
class PrefixStreambuf : public std::streambuf
{
public:
PrefixStreambuf(std::ostream &dest, const char *tag, const char *colour)
: dest_(dest), tag_(tag), colour_(colour), at_line_start_(true) {}
protected:
int overflow(int c) override
{
std::lock_guard<std::mutex> lock(output_mutex);
if (c == EOF)
return !EOF;
if (at_line_start_) // && c != '\n')
{
dest_ << GREY << tag_ << RESET << ' ' << colour_;
at_line_start_ = false;
}
dest_.put(static_cast<char>(c));
if (c == '\n')
{
dest_ << RESET;
at_line_start_ = true;
}
return c;
}
private:
std::ostream &dest_;
const char *tag_;
const char *colour_;
bool at_line_start_;
};
PrefixStreambuf debug_buf(std::clog, stream_infos[0].tag, stream_infos[0].colour);
PrefixStreambuf info_buf(std::clog, stream_infos[1].tag, stream_infos[1].colour);
PrefixStreambuf warning_buf(std::clog, stream_infos[2].tag, stream_infos[2].colour);
PrefixStreambuf error_buf(std::cerr, stream_infos[3].tag, stream_infos[3].colour);
std::ostream debug_stream(&debug_buf);
std::ostream info_stream(&info_buf);
std::ostream warning_stream(&warning_buf);
std::ostream error_stream(&error_buf);
std::ostream &debug = debug_stream;
std::ostream &info = info_stream;
std::ostream &warning = warning_stream;
std::ostream &error = error_stream;
std::ostream &rawout = std::cout;
std::ostream &rawerr = std::cerr;
std::ostream &colourstream(sColour colour)
{
switch (colour)
{
case sColour::DEBUG:
return debug_stream;
case sColour::INFO:
return info_stream;
case sColour::WARNING:
return warning_stream;
case sColour::ERROR:
return error_stream;
default:
return info_stream;
}
}
void SetColour(sColour colour, std::ostream &os)
{
switch (colour)
{
case sColour::RESET:
os << RESET;
break;
case sColour::DEBUG:
os << DEBUG_COLOUR;
break;
case sColour::INFO:
os << INFO_COLOUR;
break;
case sColour::WARNING:
os << WARNING_COLOUR;
break;
case sColour::ERROR:
os << ERROR_COLOUR;
break;
}
}
SwitchColour::SwitchColour(sColour colour, std::ostream &os) : os_(os), colour_(colour)
{
SetColour(colour_, os_);
}
SwitchColour::~SwitchColour()
{
SetColour(sColour::RESET, os_);
}
} // namespace dropshell

View File

@ -0,0 +1,87 @@
#ifndef OUTPUT_HPP
#define OUTPUT_HPP
#include <iostream>
#include <string>
#include <vector>
#include <ostream>
namespace dropshell {
/*
output.hpp and output.cpp - simple output helpers.
Defines ostreams:
debug, info, warning, error.
These ostreams can be used with C++23 print and println, e.g.
std::println(debug, "funny variable: {}={}","my_var",my_var);
Also defines a few helper functions:
PrintDebug(const std::string& msg); // equivalent to std::println(debug, msg);
PrintInfo(const std::string& msg); // equivalent to std::println(info, msg);
PrintWarning(const std::string& msg); // equivalent to std::println(warning, msg);
PrintError(const std::string& msg); // equivalent to std::println(error, msg);
Output for these streams for each line is formatted as:
[DBG] <message>
[INF] <message>
[WRN] <message>
[ERR] <message>
The output is coloured, and the tag is printed in grey.
In addition, when not using any of the above, helper routines for coloring the output of cout and cerr are provided.
SetColour(std::ostream& os, sColour colour);
Where sColour is an enum:
enum class sColour {
RESET,
DEBUG,
INFO,
WARNING,
ERROR
};
*/
// Output streams for different log levels
extern std::ostream& debug;
extern std::ostream& info;
extern std::ostream& warning;
extern std::ostream& error;
extern std::ostream& rawout;
extern std::ostream& rawerr;
// Enum for colours
enum class sColour {
RESET,
DEBUG,
INFO,
WARNING,
ERROR
};
std::ostream& colourstream(sColour colour);
// Set colour for a stream
void SetColour(sColour colour, std::ostream& os = std::cerr);
class SwitchColour
{
public:
SwitchColour(sColour colour, std::ostream& os = std::cerr);
~SwitchColour();
private:
std::ostream& os_;
sColour colour_;
};
} // namespace dropshell
#endif // OUTPUT_HPP

View File

@ -9,6 +9,8 @@
#include <iostream>
#include <map>
#include "output.hpp"
enum kTextColors {
kTextColor_Default,
kTextColor_Red,
@ -32,7 +34,7 @@ const std::map<std::string, coloredText> kReplacements = {
{":tick:", {"+", kTextColor_Green}},
{":cross:", {"x", kTextColor_Red}},
{":warning:", {"!", kTextColor_Yellow}},
{":info:", {"i", kTextColor_Blue}},
{":dropshell::info:", {"i", kTextColor_Blue}},
{":check:", {"+", kTextColor_Green}},
{":x:", {"x", kTextColor_Red}},
{":error:", {"!", kTextColor_Red}},
@ -115,12 +117,12 @@ std::string width_print_centered(std::string str,int width, std::string rowcolor
oss << rowcolor << std::string(lpad, ' ') << process_cell(str, rowcolor) <<
std::string(rpad, ' ') << "\033[0m";
// std::cout << "str = "<<str <<std::endl;
// std::cout << "width = "<<width <<std::endl;
// std::cout << "padding = "<<padding <<std::endl;
// std::cout << "get_visible_length(str) = "<<get_visible_length(str) <<std::endl;
// std::cout << "get_codepoints(str) = "<<get_codepoints(str) <<std::endl;
// std::cout << "oss.str() = ["<<oss.str() <<"]"<<std::endl;
// dropshell::info << "str = "<<str <<std::endl;
// dropshell::info << "width = "<<width <<std::endl;
// dropshell::info << "padding = "<<padding <<std::endl;
// dropshell::info << "get_visible_length(str) = "<<get_visible_length(str) <<std::endl;
// dropshell::info << "get_codepoints(str) = "<<get_codepoints(str) <<std::endl;
// dropshell::info << "oss.str() = ["<<oss.str() <<"]"<<std::endl;
return oss.str();
}
@ -145,6 +147,27 @@ void tableprint::set_title(const std::string title) {
this->title = title;
}
// gives the columns to sort by, starting at 0.
void tableprint::sort(std::vector<int> sort_columns)
{
// Skip header row and sort remaining rows
if (rows.size() <= 1) return; // Only header or empty table
// Create a custom comparator that compares rows based on the specified columns
auto comparator = [this, &sort_columns](const std::vector<std::string>& a, const std::vector<std::string>& b) {
for (int col : sort_columns) {
if (col >= 0 && col < a.size() && col < b.size()) {
int cmp = a[col].compare(b[col]);
if (cmp != 0) return cmp < 0;
}
}
return false; // Equal rows maintain original order
};
// Sort rows starting from index 1 (after header)
std::sort(rows.begin() + 1, rows.end(), comparator);
}
void tableprint::add_row(const std::vector<std::string>& row) {
std::vector<std::string> trimmed_row;
for (const auto& cell : row) {
@ -195,98 +218,105 @@ void tableprint::print() {
// Print title if it exists
if (!title.empty()) {
std::cout << "\033[90m"; // Dark grey color for borders
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
std::cout << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) std::cout << "-";
dropshell::info << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) dropshell::info << "-";
}
std::cout << "+" << std::endl;
dropshell::info << "+" << std::endl;
std::cout << "|"; // White color for title
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "|"; // White color for title
size_t title_width = 0;
for (size_t width : col_widths) {
title_width += width + 2; // +2 for padding
}
title_width += col_widths.size() - 1; // Add space for vertical borders
std::cout << width_print_centered(title,title_width,"\033[1;37m");
std::cout << "\033[90m|" << std::endl;
dropshell::info << width_print_centered(title,title_width,"\033[1;37m");
dropshell::info << "\033[90m|" << std::endl;
// Use └─┴─┘ for bottom of title box to connect with table
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
std::cout << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) std::cout << "-";
dropshell::info << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) dropshell::info << "-";
}
std::cout << "+" << std::endl;
dropshell::info << "+" << std::endl;
} else {
// Print top border if no title
std::cout << "\033[90m"; // Dark grey color for borders
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
std::cout << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) std::cout << "+";
dropshell::info << std::string(col_widths[i] + 2, '-');
if (i < rows[0].size() - 1) dropshell::info << "+";
}
std::cout << "+" << std::endl;
dropshell::info << "+" << std::endl;
}
// Print header
std::cout << "|";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "|";
for (size_t i = 0; i < rows[0].size(); ++i) {
std::cout << width_print_centered(rows[0][i],col_widths[i]+2,"\033[1;36m");
dropshell::info << width_print_centered(rows[0][i],col_widths[i]+2,"\033[1;36m");
if (i < rows[0].size() - 1) {
std::cout << "\033[90m|\033[1;36m"; // Border color then back to cyan
dropshell::info << "\033[90m|\033[1;36m"; // Border color then back to cyan
} else {
std::cout << "\033[90m|"; // Just border color for last column
dropshell::info << "\033[90m|"; // Just border color for last column
}
}
std::cout << std::endl;
dropshell::info << std::endl;
// Print header separator
if (!mCompact) {
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
for (size_t j = 0; j < col_widths[i] + 2; ++j) {
std::cout << "-";
dropshell::info << "-";
}
if (i < rows[0].size() - 1) std::cout << "+";
if (i < rows[0].size() - 1) dropshell::info << "+";
}
std::cout << "+" << std::endl;
dropshell::info << "+" << std::endl;
}
// Print rows
for (size_t row_idx = 1; row_idx < rows.size(); ++row_idx) {
const auto& row = rows[row_idx];
std::cout << "|";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "|";
for (size_t i = 0; i < row.size(); ++i) {
// Set the appropriate color for the row
std::string rowcolor = (row_idx % 2 == 1) ? "\033[38;5;142m" : "\033[38;5;250m";
std::cout << width_print_left(row[i],col_widths[i]+2,rowcolor);
std::cout << "\033[90m" << "|";
dropshell::info << width_print_left(row[i],col_widths[i]+2,rowcolor);
dropshell::info << "\033[90m" << "|";
}
std::cout << std::endl;
dropshell::info << std::endl;
// Print row separator if not the last row
if (row_idx < rows.size() - 1 && !mCompact) {
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
for (size_t j = 0; j < col_widths[i] + 2; ++j) {
std::cout << "-";
dropshell::info << "-";
}
if (i < rows[0].size() - 1) std::cout << "+";
if (i < rows[0].size() - 1) dropshell::info << "+";
}
std::cout << "+" << std::endl;
dropshell::info << "+" << std::endl;
}
}
// Print bottom border
std::cout << "+";
dropshell::info << "\033[90m"; // Dark grey color for borders
dropshell::info << "+";
for (size_t i = 0; i < rows[0].size(); ++i) {
for (size_t j = 0; j < col_widths[i] + 2; ++j) {
std::cout << "-";
dropshell::info << "-";
}
if (i < rows[0].size() - 1) std::cout << "+";
if (i < rows[0].size() - 1) dropshell::info << "+";
}
std::cout << "+" << "\033[0m" << std::endl; // Reset color
dropshell::info << "+" << "\033[0m" << std::endl; // Reset color
}

View File

@ -16,6 +16,7 @@ class tableprint {
void add_row(const std::vector<std::string>& row);
void print();
void set_title(const std::string title);
void sort(std::vector<int> sort_columns);
private:
std::vector<std::vector<std::string>> rows;
std::string title;

673
source/src/utils/utils.cpp Normal file
View File

@ -0,0 +1,673 @@
#include "utils.hpp"
#include "httplib.hpp"
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <filesystem>
#include <regex>
#include <random>
#include <sys/ioctl.h>
#include <unistd.h>
#include <cctype>
#include <sstream>
namespace dropshell {
std::string magic_string() {
return "-_-";
}
bool has_magic_string(std::string name)
{
return name.find(magic_string()) != std::string::npos;
}
void maketitle(const std::string& title, sColour colour) {
colourstream(colour) << std::string(title.length() + 4, '-') << std::endl;
colourstream(colour) << "| " << title << " |" << std::endl;
colourstream(colour) << std::string(title.length() + 4, '-') << std::endl;
}
bool replace_line_in_file(const std::string& file_path, const std::string& search_string, const std::string& replacement_line) {
std::ifstream input_file(file_path);
std::vector<std::string> file_lines;
std::string line;
if (!input_file.is_open()) {
error << "Unable to open file: " << file_path << std::endl;
return false;
}
while (std::getline(input_file, line)) {
if (line.find(search_string) != std::string::npos)
file_lines.push_back(replacement_line);
else
file_lines.push_back(line);
}
input_file.close();
std::ofstream output_file(file_path);
if (!output_file.is_open())
{
error << "Unable to open file: " << file_path << std::endl;
return false;
}
for (const auto& modified_line : file_lines)
output_file << modified_line << "\n";
output_file.close();
return true;
}
std::string trim(std::string str) {
// Trim leading whitespace
str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
// Trim trailing whitespace
str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), str.end());
return str;
}
std::string dequote(std::string str)
{
if (str.length() < 2)
return str;
if (str.front() == '"' && str.back() == '"') {
return str.substr(1, str.length() - 2);
}
return str;
}
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;
for (const auto& value : values) {
// remove any " contained in the string value, if present
result += dequote(trim(value)) + ",";
}
if (!result.empty())
result.pop_back(); // Remove the last comma
return result;
}
std::vector<std::string> string2multi(std::string values)
{
std::vector<std::string> result;
values = dequote(trim(values));
// Return values separated by commas, but ignore commas within quotes
bool inside_quotes = false;
std::string current_item;
for (char c : values) {
if (c == '"') {
inside_quotes = !inside_quotes;
} else if (c == ',' && !inside_quotes) {
if (!current_item.empty()) {
std::string final = dequote(trim(current_item));
if (!final.empty())
result.push_back(final);
current_item.clear();
}
} else {
current_item += c;
}
}
// Add the last item if not empty
if (!current_item.empty()) {
std::string final = dequote(trim(current_item));
if (!final.empty())
result.push_back(final);
}
return result;
}
int str2int(const std::string &str)
{
try {
return std::stoi(str);
} catch (const std::exception& e) {
error << "Invalid integer string: [" << str << "]" << std::endl;
return 0;
}
}
void recursive_copy(const std::string & source, const std::string & destination) {
try {
if (std::filesystem::is_directory(source)) {
if (!std::filesystem::exists(destination)) {
std::filesystem::create_directory(destination);
}
for (const auto& entry : std::filesystem::directory_iterator(source)) {
recursive_copy(entry.path(), destination / entry.path().filename());
}
} else if (std::filesystem::is_regular_file(source)) {
std::filesystem::copy(source, destination, std::filesystem::copy_options::overwrite_existing);
}
} catch (const std::filesystem::filesystem_error& ex) {
std::cerr << "Error copying " << source << " to " << destination << ": " << ex.what() << std::endl;
}
}
void ensure_directories_exist(std::vector<std::string> directories)
{
for (const auto& directory : directories) {
if (!std::filesystem::exists(directory)) {
std::filesystem::create_directories(directory);
}
}
}
//https://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/
void constructLps(const std::string &pat, std::vector<int> &lps) {
// len stores the length of longest prefix which
// is also a suffix for the previous index
int len = 0;
// lps[0] is always 0
lps[0] = 0;
int i = 1;
while (i < pat.length()) {
// If characters match, increment the size of lps
if (pat[i] == pat[len]) {
len++;
lps[i] = len;
i++;
}
// If there is a mismatch
else {
if (len != 0) {
// Update len to the previous lps value
// to avoid reduntant comparisons
len = lps[len - 1];
}
else {
// If no matching prefix found, set lps[i] to 0
lps[i] = 0;
i++;
}
}
}
}
std::vector<int> search(const std::string &pat, const std::string &txt) {
int n = txt.length();
int m = pat.length();
std::vector<int> lps(m);
std::vector<int> res;
constructLps(pat, lps);
// Pointers i and j, for traversing
// the text and pattern
int i = 0;
int j = 0;
while (i < n) {
// If characters match, move both pointers forward
if (txt[i] == pat[j]) {
i++;
j++;
// If the entire pattern is matched
// store the start index in result
if (j == m) {
res.push_back(i - j);
// Use LPS of previous index to
// skip unnecessary comparisons
j = lps[j - 1];
}
}
// If there is a mismatch
else {
// Use lps value of previous index
// to avoid redundant comparisons
if (j != 0)
j = lps[j - 1];
else
i++;
}
}
return res;
}
int count_substring(const std::string &substring, const std::string &text) {
std::vector<int> positions = search(substring, text);
return positions.size();
}
std::vector<std::string> split(const std::string& str, const std::string& delimiter) {
std::vector<std::string> tokens;
size_t start = 0;
size_t end = 0;
while ((end = str.find(delimiter, start)) != std::string::npos) {
tokens.push_back(str.substr(start, end - start));
start = end + delimiter.length();
}
// Add the last token
tokens.push_back(str.substr(start));
return tokens;
}
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 return_die(const std::string & msg) {
error << "Fatal error:" << std::endl;
error << 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];
}
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, ' ');
}
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 substitute_provided_key_value_pairs(std::string str, const std::map<std::string, std::string> &env_vars)
{
// 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 environment variables map
auto it = env_vars.find(var_name);
std::string value = (it != env_vars.end()) ? it->second : "";
result = result.replace(match.position(), match.length(), value);
}
return result;
}
int get_console_width()
{
struct winsize w;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
return w.ws_col;
}
// Fallback to a reasonable default if we can't get the width
return 80;
}
std::string remove_return(std::string str)
{
str.erase(std::remove(str.begin(), str.end(), '\n'), str.end());
return str;
}
std::string get_line_wrap(std::string &src, int maxchars)
{
if (src.empty())
return "";
if (src.length() <= maxchars)
{
std::string out = src;
src.erase();
return remove_return(out) + '\n';
}
// find last whitespace up to but not more than maxchars
size_t grab_to=maxchars;
size_t lastreturn = src.rfind('\n', maxchars);
size_t lastspace = src.rfind(' ', maxchars);
if (lastreturn != std::string::npos)
grab_to = lastreturn;
else if (lastspace != std::string::npos)
grab_to = lastspace;
std::string out = src.substr(0, grab_to);
src = src.substr(grab_to + 1);
return remove_return(out) + '\n';
}
std::string tolower(const std::string& str) {
if (str.empty()) return str;
std::string result;
result.reserve(str.size()); // Pre-allocate space for efficiency
for (unsigned char c : str) {
result.push_back(std::tolower(c));
}
return result;
}
// Common utility function to make HTTP requests
struct HttpResult {
bool success;
int status;
std::string body;
std::string error;
};
HttpResult make_http_request(const std::string& url) {
try {
// Parse the URL to get host and path
std::string host;
std::string path;
size_t protocol_end = url.find("://");
if (protocol_end != std::string::npos) {
size_t host_start = protocol_end + 3;
size_t path_start = url.find('/', host_start);
if (path_start != std::string::npos) {
host = url.substr(host_start, path_start - host_start);
path = url.substr(path_start);
} else {
host = url.substr(host_start);
path = "/";
}
} else {
return {false, 0, "", "Invalid URL format"};
}
// Create HTTP client
httplib::Client cli(host);
cli.set_connection_timeout(10); // 10 second timeout
// Make GET request
auto res = cli.Get(path);
if (!res) {
return {false, 0, "", "Failed to connect to server"};
}
if (res->status != 200) {
return {false, res->status, "", "HTTP request failed with status " + std::to_string(res->status)};
}
return {true, res->status, res->body, ""};
} catch (const std::exception& e) {
return {false, 0, "", std::string("Exception: ") + e.what()};
}
}
bool download_file(const std::string &url, const std::string &destination) {
auto result = make_http_request(url);
if (!result.success) {
warning << "Failed to download file from URL: " << url << std::endl;
return false;
}
try {
std::ofstream out_file(destination, std::ios::binary);
if (!out_file) {
warning << "Failed to open file for writing: " << destination << std::endl;
return false;
}
out_file.write(result.body.c_str(), result.body.size());
out_file.close();
return true;
} catch (const std::exception& e) {
warning << "Failed to download file from URL: " << url << std::endl;
warning << "Exception: " << e.what() << std::endl;
return false;
}
}
nlohmann::json get_json_from_url(const std::string &url) {
auto result = make_http_request(url);
if (!result.success) {
warning << "Failed to get JSON from URL: " << url << std::endl;
return nlohmann::json();
}
try {
return nlohmann::json::parse(result.body);
} catch (const nlohmann::json::parse_error& e) {
warning << "Failed to parse JSON from URL: " << url << std::endl;
warning << "JSON: " << result.body << std::endl;
return nlohmann::json();
}
}
std::string get_string_from_url(const std::string &url) {
auto result = make_http_request(url);
if (!result.success) {
warning << "Failed to get string from URL: " << url << std::endl;
return std::string();
}
return result.body;
}
bool match_line(const std::string &line, const std::string &pattern) {
return trim(line) == trim(pattern);
}
// replace or append a block of text to a file, matching first and last lines if replacing.
// edits file in-place.
bool file_replace_or_add_segment(std::string filepath, std::string segment)
{
// Create a backup of the original file
std::string backup_path = filepath + ".bak";
try {
std::filesystem::copy_file(filepath, backup_path, std::filesystem::copy_options::overwrite_existing);
} catch (const std::exception& e) {
std::cerr << "Error creating backup file: " << e.what() << std::endl;
return false;
}
// Handle empty segment
if (segment.empty()) {
error << "Empty segment provided" << std::endl;
return false;
}
// split the segment into lines
std::vector<std::string> segment_lines = split(segment, "\n");
// remove empty lines
segment_lines.erase(std::remove_if(segment_lines.begin(), segment_lines.end(), [](const std::string& line) {
return trim(line).empty();
}), segment_lines.end());
// remove any lines that are just whitespace
segment_lines.erase(std::remove_if(segment_lines.begin(), segment_lines.end(), [](const std::string& line) { return trim(line).empty(); }), segment_lines.end());
// check that the segment has at least two lines
if (segment_lines.size() < 2) {
error << "Segment must contain at least two non-empty lines" << std::endl;
return false;
}
// Read the entire file into memory
std::ifstream input_file(filepath);
if (!input_file.is_open()) {
error << "Unable to open file: " << filepath << std::endl;
return false;
}
std::vector<std::string> file_lines;
std::string line;
while (std::getline(input_file, line)) {
file_lines.push_back(line);
}
input_file.close();
// Store original file size for verification
size_t original_size = file_lines.size();
if (original_size == 0) {
warning << "File is empty" << std::endl;
}
// Try to find the matching block
bool found_match = false;
for (size_t i = 0; i < file_lines.size(); i++) {
if (match_line(file_lines[i], segment_lines[0])) {
// Found potential start, look for end
for (size_t j = i + 1; j < file_lines.size(); j++) {
if (match_line(file_lines[j], segment_lines[segment_lines.size() - 1])) {
// Found matching block, replace it
file_lines.erase(file_lines.begin() + i, file_lines.begin() + j + 1);
file_lines.insert(file_lines.begin() + i, segment_lines.begin(), segment_lines.end());
found_match = true;
break;
}
}
if (found_match) break;
}
}
// If no match found, append the segment
if (!found_match) {
file_lines.insert(file_lines.end(), segment_lines.begin(), segment_lines.end());
}
// Write back to file
std::ofstream output_file(filepath);
if (!output_file.is_open()) {
error << "Unable to open file for writing: " << filepath << std::endl;
return false;
}
for (const auto& line : file_lines) {
output_file << line << "\n";
}
output_file.close();
// If everything succeeded, remove the backup
try {
std::filesystem::remove(backup_path);
} catch (const std::exception& e) {
warning << "Could not remove backup file: " << e.what() << std::endl;
}
return true;
}
bool legal_service_name(const std::string &service_name) {
static bool initialized = false;
static bool legal_chars[256] = {false}; // Initialize all to false
// One-time initialization
if (!initialized) {
// Set true for valid characters
for (unsigned char c : "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"._-") {
legal_chars[c] = true;
}
initialized = true;
}
return std::all_of(service_name.begin(), service_name.end(),
[](unsigned char c) { return legal_chars[c]; });
}
} // namespace dropshell

View File

@ -2,6 +2,10 @@
#include <string>
#include <vector>
#include <map>
#include "output.hpp"
#include <nlohmann/json.hpp>
namespace dropshell {
@ -10,10 +14,12 @@ namespace dropshell {
*
* @param title The title string to display
*/
void maketitle(const std::string& title);
void maketitle(const std::string& title, sColour colour=sColour::INFO);
bool replace_line_in_file(const std::string& file_path, const std::string& search_string, const std::string& replacement_line);
std::string magic_string();
bool has_magic_string(std::string name);
// utility functions
std::string trim(std::string str);
@ -37,20 +43,39 @@ 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);
int return_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);
std::string replace_with_environment_variables_like_bash(std::string str);
std::string substitute_provided_key_value_pairs(std::string str, const std::map<std::string, std::string> & env_vars);
int get_console_width();
std::string get_line_wrap(std::string & src, int maxchars);
std::string tolower(const std::string& str);
bool download_file(const std::string& url, const std::string& destination);
nlohmann::json get_json_from_url(const std::string& url);
std::string get_string_from_url(const std::string& url);
// replace or append a block of text to a file, matching first and last lines if replacing.
bool file_replace_or_add_segment(std::string filepath, std::string segment);
constexpr unsigned int switchhash(const char *s, int off = 0)
{
return !s[off] ? 5381 : (switchhash(s, off + 1) * 33) ^ s[off];
}
bool legal_service_name(const std::string & service_name);
} // namespace dropshell

View File

@ -1,5 +1,13 @@
#pragma once
/*
version.hpp is automatically generated by the build system, from version.hpp.in.
DO NOT EDIT VERSION.HPP!
*/
#include <string>
namespace dropshell {

12
source/test.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ARCH=$(uname -m)
PREV_DIR=$(pwd)
trap 'cd "$PREV_DIR"' EXIT
"$SCRIPT_DIR/output/dropshell.${ARCH}" hash "${SCRIPT_DIR}/test.sh"
"$SCRIPT_DIR/output/dropshell.${ARCH}" help

View File

@ -1,138 +0,0 @@
#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

View File

@ -1,178 +0,0 @@
#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

View File

@ -1,299 +0,0 @@
#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_host()
{
// update dropshell.
// install the local dropshell agent.
return update_dropshell();
}
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

View File

@ -1,184 +0,0 @@
#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

View File

@ -1,106 +0,0 @@
#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

@ -1,31 +0,0 @@
#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

@ -1,36 +0,0 @@
#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

View File

@ -1,108 +0,0 @@
#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

View File

@ -1,39 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include "utils/json.hpp"
namespace dropshell {
class config {
public:
config();
~config();
bool load_config();
bool save_config(bool create_aux_directories);
bool is_config_set() const;
static bool is_agent_installed();
std::string get_local_tempfiles_path();
std::string get_local_backup_path();
std::string get_local_template_cache_path();
std::string get_local_executables_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();
std::string get_template_upload_registry_url();
std::string get_template_upload_registry_token();
private:
nlohmann::json mConfig;
bool mIsConfigSet;
};
config & gConfig();
} // namespace dropshell

View File

@ -1,262 +0,0 @@
#include "version.hpp"
#include "config.hpp"
#include "service_runner.hpp"
#include "services.hpp"
#include "servers.hpp"
#include "utils/directories.hpp"
#include "templates.hpp"
#include "utils/utils.hpp"
#include "autocomplete.hpp"
#include "utils/hash.hpp"
#include "command_registry.hpp"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
#include <chrono>
#include <assert.hpp>
#include <sstream>
#include <algorithm>
namespace dropshell {
extern const std::string VERSION;
extern const std::string RELEASE_DATE;
extern const std::string AUTHOR;
extern const std::string LICENSE;
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);
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}
// ------------------------------------------------------------------------------------------------
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_and_services.server_name = arg2;
if (arg3.empty()) {
server_and_services.servicelist = get_server_services_info(arg2);
} else {
server_and_services.servicelist.push_back(get_service_info(arg2, arg3));
}
return true;
}
void printversion() {
maketitle("DropShell version " + VERSION);
std::cout << "Release date: " << RELEASE_DATE << std::endl;
std::cout << "Author: " << AUTHOR << std::endl;
std::cout << "License: " << LICENSE << std::endl;
}
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;
};
#define BOOLEXIT(CMD_LIST, RUNCMD) { \
if (command_match(CMD_LIST, argc, argv)) { \
return (RUNCMD) ? 0 : 1; \
} \
}
#define HAPPYEXIT(CMD_LIST, RUNCMD) { \
if (command_match(CMD_LIST, argc, argv)) { \
RUNCMD; \
return 0; \
} \
}
int old_main(int argc, char* argv[]) {
HAPPYEXIT("hash", hash_demo_raw(safearg(argc,argv,2)))
HAPPYEXIT("version", printversion())
BOOLEXIT("test-template", gTemplateManager().test_template(safearg(argc,argv,2)))
ASSERT(safearg(argc,argv,1) != "assert", "Hello! Here is an assert.");
try {
// silently attempt to load the config file and templates.
gConfig().load_config();
if (gConfig().is_config_set())
gTemplateManager().load_sources();
std::string cmd = argv[1];
// ------------------------------------------------------------
// from here we require the config file to be loaded.
if (!gConfig().is_config_set())
return die("Please run 'dropshell edit' to set up the dropshell configuration.");
const std::vector<std::string> & server_definition_paths = gConfig().get_local_server_definition_paths();
if (server_definition_paths.size()>1) { // only show if there are multiple.
std::cout << "Server definition paths: ";
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");
return (gTemplateManager().create_template(argv[2])) ? 0 : 1;
}
if (cmd == "create-server") {
if (argc < 3) return die("Error: create-server requires a server name");
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");
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");
service_runner::interactive_ssh(argv[2], "bash");
return 0;
}
// handle running a command.
std::set<std::string> commands;
get_all_used_commands(commands);
autocomplete::merge_commands(commands, autocomplete::service_commands_require_config); // handled by service_runner, but not in template_shell_commands.
if (commands.count(cmd)) {
std::set<std::string> safe_commands = {"nuke", "fullnuke"};
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);
}
}
// success!
return 0;
}
// Unknown command
std::cerr << "Error: Unknown command '" << cmd << "'" << std::endl;
std::cerr << "Valid commands: ";
for (const auto& command : commands) {
if (!command.empty() && command[0]!='_')
std::cerr << command << " ";
}
std::cerr << std::endl;
return 1;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}
} // namespace dropshell
int main(int argc, char* argv[]) {
return dropshell::main(argc, argv);
}

View File

@ -1,231 +0,0 @@
#include "server_env_manager.hpp"
#include "utils/directories.hpp"
#include "utils/utils.hpp"
#include "services.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), mServerName(server_name) {
if (server_name.empty())
return;
// Construct the full path to server.env
std::string server_env_path = localfile::server_json(server_name);
// Check if file exists
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
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 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 (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 : mVariables) {
std::cout << " " << v.first << std::endl;
}
throw std::runtime_error("Missing required variable: " + std::string(var));
}
}
mValid = true;
} catch (const std::exception& e) {
std::cerr << "Failed to parse server environment file: " + std::string(e.what()) << std::endl;
}
}
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 {
auto it = mVariables.find(name);
if (it == mVariables.end()) {
return "";
}
return it->second;
}
std::optional<sCommand> server_env_manager::construct_standard_template_run_cmd(const std::string &service_name, const std::string &command, const std::vector<std::string> args, const bool silent) const
{
if (command.empty())
return 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;
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 sc(
remote_service_template_path,
quote(script_path) + argstr + (silent ? " > /dev/null 2>&1" : ""),
env_vars
);
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(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(get_SSH_INFO(), scommand, cMode::Silent);
}
bool server_env_manager::check_remote_items_exist(const std::vector<std::string> &file_paths) const
{
// convert file_paths to a single string, separated by spaces
std::string file_paths_str;
std::string file_names_str;
for (const auto& file_path : file_paths) {
file_paths_str += quote(file_path) + " ";
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",{});
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;
}
return true;
}
bool server_env_manager::remove_remote_dir(const std::string &dir_path, bool silent) const
{
std::filesystem::path path(dir_path);
std::filesystem::path parent_path = path.parent_path();
std::string target_dir = path.filename().string();
if (parent_path.empty())
parent_path="/";
if (target_dir.empty())
return false;
if (!silent)
std::cout << "Removing remote directory " << target_dir << " in " << parent_path << " on " << mServerName << std::endl;
std::string remote_cmd =
"docker run --rm -v " + quote(parent_path.string()) + ":/parent " +
" alpine rm -rf \"/parent/" + target_dir + "\"";
if (!silent)
std::cout << "Running command: " << remote_cmd << std::endl;
sCommand scommand("", remote_cmd,{});
cMode mode = (silent ? cMode::Silent : cMode::None);
return execute_ssh_command(get_SSH_INFO(), scommand, mode);
}
bool server_env_manager::run_remote_template_command(const std::string &service_name, const std::string &command, std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const
{
auto scommand = construct_standard_template_run_cmd(service_name, command, args, silent);
if (!scommand.has_value())
return false;
// add the extra env vars to the command
for (const auto& [key, value] : extra_env_vars)
scommand->add_env_var(key, value);
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
} // namespace dropshell

View File

@ -1,77 +0,0 @@
// server_env.hpp
//
// read the server.env file and provide a class to access the variables
#ifndef __SERVER_ENV_HPP
#define __SERVER_ENV_HPP
#include <string>
#include <map>
#include <memory>
#include <vector>
#include "utils/execute.hpp"
#include <optional>
namespace dropshell {
class server_env_manager;
// ------------------------------------------------------------------------------------------------
// reads path / server.env and provides a class to access the variables.
// each env file is required to have the following variables:
// SSH_HOST
// SSH_USER
// SSH_PORT
// the following replacements are made in the values:
// ${USER} -> the username of the user running dropshell
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 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:
bool check_remote_dir_exists(const std::string &dir_path) const;
bool check_remote_file_exists(const std::string& file_path) const;
bool check_remote_items_exist(const std::vector<std::string>& file_paths) const;
bool remove_remote_dir(const std::string &dir_path, bool silent) const;
bool run_remote_template_command(const std::string& service_name, const std::string& command,
std::vector<std::string> args, bool silent, std::map<std::string, std::string> extra_env_vars) const;
bool run_remote_template_command_and_capture_output(const std::string& service_name, const std::string& command,
std::vector<std::string> args, std::string & output, bool silent, std::map<std::string, std::string> extra_env_vars) const;
private:
std::optional<sCommand> construct_standard_template_run_cmd(const std::string& service_name, const std::string& command, const std::vector<std::string> args, const bool silent) const;
private:
std::string mServerName;
std::map<std::string, std::string> mVariables;
bool mValid;
};
} // namespace dropshell
#endif // __SERVER_ENV_HPP

View File

@ -1,129 +0,0 @@
#include "servers.hpp"
#include "server_env_manager.hpp"
#include "service_runner.hpp"
#include "utils/tableprint.hpp"
#include "utils/envmanager.hpp"
#include "utils/directories.hpp"
#include "services.hpp"
#include "config.hpp"
#include "templates.hpp"
#include "contrib/transwarp.hpp"
#include <iostream>
#include <fstream>
#include <iomanip>
#include <filesystem>
namespace dropshell {
std::vector<ServerInfo> get_configured_servers() {
std::vector<ServerInfo> servers;
std::vector<std::string> lsdp = gConfig().get_local_server_definition_paths();
if (lsdp.empty())
return servers;
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;
continue;
}
servers.push_back({
server_name,
env.get_SSH_HOST(),
env.get_SSH_USER(),
env.get_SSH_PORT()
});
}
}
}
}
return 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();
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()});
}
}
return ServerInfo();
}
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 false;
}
// 2. create a new directory in the user config directory
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
std::string user = getenv("USER");
std::string server_env_path = server_dir + "/server.env";
std::ofstream server_env_file(server_env_path);
server_env_file << "SSH_HOST=" << server_name << std::endl;
server_env_file << "SSH_USER=" << user << std::endl;
server_env_file << "SSH_PORT=" << 22 << std::endl;
server_env_file << std::endl;
server_env_file << "DROPSHELL_DIR=/home/"+user+"/.dropshell" << std::endl;
server_env_file.close();
// 4. add dropshell-agent service to server
create_service(server_name, "dropshell-agent", "dropshell-agent", true); // silently create service.
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)
{
std::vector<ServerInfo> servers = get_configured_servers();
for (const auto& server : servers)
{
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));
}
}
} // namespace dropshell

View File

@ -1,29 +0,0 @@
#ifndef SERVERS_HPP
#define SERVERS_HPP
#include <string>
#include <vector>
#include "service_runner.hpp" // for ServiceStatus
namespace dropshell {
// Server information structure
struct ServerInfo {
std::string name;
std::string ssh_host;
std::string ssh_user;
std::string ssh_port;
};
std::vector<ServerInfo> get_configured_servers();
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);
} // namespace dropshell
#endif // SERVERS_HPP

View File

@ -1,551 +0,0 @@
#include <iostream>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <chrono>
#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)
{
if (server_name.empty() || service_name.empty())
return;
// Initialize server environment
if (!mServerEnv.is_valid())
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::nuke(bool silent)
{
maketitle("Nuking " + mService + " (" + mServiceInfo.template_name + ") on " + mServer);
if (!mServerEnv.is_valid()) return false; // should never hit this.
std::string remote_service_path = remotepath::service(mServer, mService);
bool okay = mServerEnv.run_remote_template_command("dropshell-agent", "_nuke_other", {mService, remote_service_path}, silent, {});
if (!okay)
{
std::cerr << "Warning: Nuke script failed" << std::endl;
return false;
}
std::cout << "Service " << mService << " successfully nuked from " << mServer << std::endl;
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;
}
return true;
}
bool service_runner::fullnuke()
{
if (!nuke(true))
{
std::cerr << "Warning: Nuke script failed, aborting fullnuke!" << std::endl;
return false;
}
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;
}
return true;
}
// ------------------------------------------------------------------------------------------------
// Run a command on the service.
// ------------------------------------------------------------------------------------------------
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 = gTemplateManager().get_template_info(mServiceInfo.template_name);
if (!tinfo.is_set()) {
std::cerr << "Error: Template '" << mServiceInfo.template_name << "' not found" << std::endl;
return false;
}
if (command == "fullnuke")
return fullnuke();
if (command == "nuke")
{
std::cout << "Nuking " << mService << " (" << mServiceInfo.template_name << ") on " << mServer << std::endl;
return nuke();
}
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_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 == "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, false, env_vars);
}
std::map<std::string, ServiceStatus> service_runner::get_all_services_status(std::string server_name)
{
std::map<std::string, ServiceStatus> status;
std::string command = "_allservicesstatus";
std::string service_name = "dropshell-agent";
if (!gTemplateManager().template_command_exists(service_name, "shared/"+command))
{
std::cerr << "Error: " << service_name << " does not contain the " << command << " script" << std::endl;
return status;
}
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment" << std::endl;
return status;
}
std::string output;
if (!env.run_remote_template_command_and_capture_output(service_name, "shared/"+command, {}, output, true, {}))
return status;
std::stringstream ss(output);
std::string line;
while (std::getline(ss, line)) {
std::string key, value;
std::size_t pos = line.find("=");
if (pos != std::string::npos) {
key = dequote(trim(line.substr(0, pos)));
value = dequote(trim(line.substr(pos + 1)));
// decode key, it's of format SERVICENAME_[HEALTH|PORTS]
std::string service_name = key.substr(0, key.find_last_of("_"));
std::string status_type = key.substr(key.find_last_of("_") + 1);
if (status_type == "HEALTH") { // healthy|unhealthy|unknown
if (value == "healthy")
status[service_name].health = HealthStatus::HEALTHY;
else if (value == "unhealthy")
status[service_name].health = HealthStatus::UNHEALTHY;
else if (value == "unknown")
status[service_name].health = HealthStatus::UNKNOWN;
else
status[service_name].health = HealthStatus::ERROR;
} else if (status_type == "PORTS") { // port1,port2,port3
std::vector<std::string> ports = string2multi(value);
for (const auto& port : ports) {
if (port!="unknown")
status[service_name].ports.push_back(str2int(port));
}
}
}
}
return status;
}
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 false;
}
server_env_manager env(server_name);
if (!env.is_valid()) {
std::cerr << "Error: Invalid server environment file: " << server_name << std::endl;
return false;
}
sCommand scommand("", "bash",{});
return execute_ssh_command(env.get_SSH_INFO(), scommand, cMode::Interactive);
}
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 false;
}
std::vector<std::string> args; // not passed through yet.
return mServerEnv.run_remote_template_command(mService, "ssh", args, false, {});
}
bool service_runner::scp_file_to_remote(const std::string &local_path, const std::string &remote_path, bool silent)
{
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 local_backups_dir = gConfig().get_local_backup_path();
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

@ -1,102 +0,0 @@
// server_service.hpp
//
// manage a service on a server
//
#ifndef SERVICE_RUNNER_HPP
#define SERVICE_RUNNER_HPP
#include <string>
#include <vector>
#include <memory>
#include "server_env_manager.hpp"
#include "services.hpp"
#include "utils/utils.hpp"
#include "utils/hash.hpp"
namespace dropshell {
typedef enum HealthStatus {
HEALTHY,
UNHEALTHY,
NOTINSTALLED,
ERROR,
UNKNOWN
} HealthStatus;
typedef struct ServiceStatus {
HealthStatus health;
std::vector<int> ports;
} ServiceStatus;
class service_runner {
public:
service_runner(const std::string& server_name, const std::string& service_name);
bool isValid() const { return mValid; }
// run a command over ssh, using the credentials from server.env (via server_env.hpp)
// first check that the command corresponds to a valid .sh file in the service directory
// then run the command, passing the {service_name}.env file as an argument
// do a lot of checks, such as:
// checking that we can ssh to the server.
// checking whether the service directory exists on the server.
// 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, 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
// 2. return the output of the status.sh script
HealthStatus is_healthy();
std::string healthtick();
std::string healthmark();
public:
// backup and restore
bool backup(bool silent=false);
bool restore(std::string backup_file, bool silent=false);
// 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
bool interactive_ssh_service();
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;
LocalServiceInfo mServiceInfo;
std::string mService;
bool mValid;
// Helper methods
public:
};
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
#endif // SERVICE_RUNNER_HPP

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