ds
Some checks failed
Dropshell Test / Build_and_Test (push) Failing after 12s

This commit is contained in:
Your Name 2025-05-25 18:14:15 +12:00
parent f45d9a33ed
commit d71ba38754
10 changed files with 10878 additions and 149 deletions

View File

@ -5,15 +5,23 @@ A system management tool for server operations, written in C++.
## Installation ## Installation
``` ```
curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/install.sh | sudo bash curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/install.sh | bash
``` ```
This installs as dropshell, with a symlink ds if the ds command does not already exist. 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.
## Installation of Agent ## Remote Server Setup
Install the Agent on each server you wish to manage. Supports amd64 (x86_64) and arm64 (aarch64) architectures. ### 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 curl -fsSL https://gitea.jde.nz/public/dropshell/releases/download/latest/server_autosetup.sh | sudo bash
``` ```
@ -26,7 +34,21 @@ Manual steps:
1. Test ssh'ing into the server. 1. Test ssh'ing into the server.
## Install Services ### Configure and Use Remote Server
Set up a server and install a service: #### Add to local dropshell configuration, and install remote agent
1. `ds create-server SERVERNAME` 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

@ -3,12 +3,6 @@ set -e
# download and install dropshell # download and install dropshell
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run this script as root (use sudo)"
exit 1
fi
# 1. Determine architecture # 1. Determine architecture
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -30,21 +24,26 @@ trap 'rm -rf "$TMPDIR"' EXIT
URL="https://gitea.jde.nz/public/dropshell/releases/download/latest/$BIN" URL="https://gitea.jde.nz/public/dropshell/releases/download/latest/$BIN"
echo "Downloading $BIN from $URL..." echo "Downloading $BIN from $URL..."
curl -fsSL -o "$TMPDIR/dropshell" "$URL" TARGET_PATH="${HOME}/.local/bin/dropshell"
if [ ! -f "$TMPDIR/dropshell" ]; then mkdir -p "${HOME}/.local/bin"
curl -fsSL -o "$TARGET_PATH" "$URL"
if [ ! -f "$TARGET_PATH" ]; then
echo "Failed to download dropshell" >&2 echo "Failed to download dropshell" >&2
exit 1 exit 1
fi fi
chmod +x "$TMPDIR/dropshell" chmod +x "$TARGET_PATH"
cp "$TMPDIR/dropshell" /usr/local/bin/dropshell if [ ! -f "${HOME}/.local/bin/ds" ]; then
if [ -f /usr/local/bin/ds ]; then ln -s "$TARGET_PATH" "${HOME}/.local/bin/ds"
rm -f /usr/local/bin/ds
fi fi
ln -s /usr/local/bin/dropshell /usr/local/bin/ds
rm -rf "$TMPDIR"
echo "dropshell installed successfully to /usr/local/bin/dropshell" echo "dropshell installed successfully to $TARGET_PATH"
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

@ -97,81 +97,32 @@ FetchContent_Declare(
) )
FetchContent_MakeAvailable(cpptrace) FetchContent_MakeAvailable(cpptrace)
# Add cpp-httplib
FetchContent_Declare(
cpp-httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.14.1
)
FetchContent_MakeAvailable(cpp-httplib)
# 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 # Link libraries
target_link_libraries(dropshell PRIVATE target_link_libraries(dropshell PRIVATE
libassert::assert libassert::assert
cpptrace::cpptrace cpptrace::cpptrace
httplib::httplib
nlohmann_json::nlohmann_json
) )
# Install targets # Install targets
install(TARGETS dropshell install(TARGETS dropshell
RUNTIME DESTINATION bin RUNTIME DESTINATION $ENV{HOME}/.local/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,5 +1,7 @@
#!/bin/bash #!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Exit on error # Exit on error
set -e set -e
@ -53,24 +55,6 @@ if ! command -v make &> /dev/null; then
exit 1 exit 1
fi 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 # Configure with CMake
print_status "Configuring with CMake..." print_status "Configuring with CMake..."
cmake .. -DCMAKE_BUILD_TYPE=Debug cmake .. -DCMAKE_BUILD_TYPE=Debug
@ -83,21 +67,14 @@ make -j"$JOBS"
# Check if build was successful # Check if build was successful
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
print_status "Build successful!" print_status "Build successful!"
print_status "Binary location: $(pwd)/dropshell"
else else
print_error "Build failed!" print_error "Build failed!"
exit 1 exit 1
fi fi
print_status "Auto-installing dropshell..." print_status "Auto-installing dropshell locally..."
sudo make install mkdir -p "${HOME}/.local/bin"
if [ $? -eq 0 ]; then cp "$SCRIPT_DIR/build/dropshell" "${HOME}/.local/bin/dropshell"
print_status "Installation successful!"
else
print_error "Installation failed!"
exit 1
fi
# Return to original directory # Return to original directory
cd .. cd ..

View File

@ -18,6 +18,7 @@
#include <filesystem> #include <filesystem>
#include <libassert/assert.hpp> #include <libassert/assert.hpp>
#include "servers.hpp" #include "servers.hpp"
#include <sys/stat.h>
namespace dropshell namespace dropshell
{ {
@ -176,49 +177,107 @@ namespace dropshell
return trim(result); 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() int update_dropshell()
{ {
maketitle("Updating dropshell on this computer..."); maketitle("Updating dropshell on this computer...");
configure_localbin();
configure_autocomplete();
// determine path to this executable // determine path to this executable
std::filesystem::path dropshell_path = std::filesystem::canonical("/proc/self/exe"); std::filesystem::path exe_path = std::filesystem::canonical("/proc/self/exe");
std::filesystem::path parent_path = dropshell_path.parent_path(); std::filesystem::path parent_path = exe_path.parent_path();
// determine the architecture of the system // determine the architecture of the system
std::string arch = shared_commands::get_arch(); std::string arch = shared_commands::get_arch();
std::string url = "https://gitea.jde.nz/public/dropshell/releases/download/latest/dropshell." + arch; std::string url = "https://gitea.jde.nz/public/dropshell/releases/download/latest/dropshell." + arch;
// download new version, preserve permissions and ownership // check that the user that owns the exe is the current user this process is running as.
std::string bash_script; struct stat st;
bash_script += "docker run --rm -v " + parent_path.string() + ":/target"; if (stat(exe_path.c_str(), &st) != 0) {
bash_script += " gitea.jde.nz/public/debian-curl:latest"; error << "Failed to stat dropshell executable: " << strerror(errno) << std::endl;
bash_script += " sh -c \""; return -1;
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 + "'"; uid_t current_uid = getuid();
int rval = system(cmd.c_str()); if (st.st_uid != current_uid) {
if (rval != 0) 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; error << "Failed to download new version of dropshell." << std::endl;
return -1; return -1;
} }
// make executable
chmod(temp_file.c_str(), 0755);
// check if the new version is the same as the old version // check if the new version is the same as the old version
uint64_t new_hash = hash_file(parent_path / "dropshell_temp"); uint64_t new_hash = hash_file(temp_file);
uint64_t old_hash = hash_file(parent_path / "dropshell"); uint64_t old_hash = hash_file(exe_path);
if (new_hash == old_hash) if (new_hash == old_hash)
{ {
info << "Confirmed dropshell is the latest version." << std::endl; info << "Confirmed dropshell is the latest version." << std::endl;
return 0; return 0;
} }
std::string runvercmd = (parent_path / "dropshell").string() + " version"; std::string runvercmd = exe_path.string() + " version";
std::string currentver = _exec(runvercmd.c_str()); std::string currentver = _exec(runvercmd.c_str());
runvercmd = (parent_path / "dropshell_temp").string() + " version"; runvercmd = temp_file.string() + " version";
std::string newver = _exec(runvercmd.c_str()); std::string newver = _exec(runvercmd.c_str());
if (currentver >= newver) if (currentver >= newver)
@ -228,18 +287,12 @@ namespace dropshell
return 0; return 0;
} }
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);
std::string bash_script_2 = "docker run --rm -v " + parent_path.string() + ":/target gitea.jde.nz/public/debian-curl:latest " + // remove the old version.
"sh -c \"mv /target/dropshell_temp /target/dropshell\""; std::filesystem::remove(exe_path.parent_path() / "dropshell.old");
rval = system(bash_script_2.c_str());
if (rval != 0)
{
error << "Failed to install new version of dropshell." << std::endl;
return -1;
}
info << "Successfully updated " << dropshell_path << " to the latest " << arch << " version." << std::endl;
// execute the new version // execute the new version
execlp("bash", "bash", "-c", (parent_path / "dropshell").c_str(), "install", (char *)nullptr); execlp("bash", "bash", "-c", (parent_path / "dropshell").c_str(), "install", (char *)nullptr);
@ -316,8 +369,6 @@ namespace dropshell
if (rval != 0) if (rval != 0)
return rval; return rval;
return 0;
rval = install_local_agent(); rval = install_local_agent();
if (rval != 0) if (rval != 0)
return rval; return rval;

View File

@ -107,6 +107,22 @@ namespace dropshell
return mPath; 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 // get_all_services_status : SHARED COMMAND
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------

View File

@ -1,6 +1,8 @@
#ifndef SHARED_COMMANDS_HPP #ifndef SHARED_COMMANDS_HPP
#define SHARED_COMMANDS_HPP #define SHARED_COMMANDS_HPP
#include <filesystem>
#include "servers.hpp" #include "servers.hpp"
#include "command_registry.hpp" #include "command_registry.hpp"
#include "servers.hpp" #include "servers.hpp"
@ -40,6 +42,16 @@ namespace dropshell
std::string mUser; 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( bool rsync_tree_to_remote(
const std::string &local_path, const std::string &local_path,
const std::string &remote_path, const std::string &remote_path,

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

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,8 @@
#include "utils.hpp" #include "utils.hpp"
#include "httplib.hpp"
#include "json.hpp"
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <fstream> #include <fstream>
@ -10,6 +14,7 @@
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#include <cctype> #include <cctype>
#include <sstream>
namespace dropshell { namespace dropshell {
@ -445,4 +450,183 @@ std::string tolower(const std::string& str) {
return result; 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)
{
std::string first_line = segment.substr(0, segment.find("\n"));
// look backwards until we get a non-empty line.
size_t last_line_pos = segment.rfind("\n");
while (last_line_pos != std::string::npos) {
std::string last_line = segment.substr(last_line_pos + 1);
if (!trim(last_line).empty()) {
break;
}
last_line_pos = segment.rfind("\n", last_line_pos - 1);
}
std::string last_line = segment.substr(last_line_pos + 1);
// Read the entire file into memory
std::ifstream input_file(filepath);
if (!input_file.is_open()) {
std::cerr << "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();
// 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], first_line)) {
// Found potential start, look for end
for (size_t j = i + 1; j < file_lines.size(); j++) {
if (match_line(file_lines[j], last_line)) {
// Found matching block, replace it
file_lines.erase(file_lines.begin() + i, file_lines.begin() + j + 1);
// Split segment into lines and insert them
std::vector<std::string> segment_lines;
std::istringstream segment_stream(segment);
while (std::getline(segment_stream, line)) {
segment_lines.push_back(line);
}
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) {
std::istringstream segment_stream(segment);
while (std::getline(segment_stream, line)) {
file_lines.push_back(line);
}
}
// Write back to file
std::ofstream output_file(filepath);
if (!output_file.is_open()) {
std::cerr << "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();
return true;
}
} // namespace dropshell } // namespace dropshell

View File

@ -5,6 +5,7 @@
#include <map> #include <map>
#include "output.hpp" #include "output.hpp"
#include "json.hpp"
namespace dropshell { namespace dropshell {
@ -61,4 +62,11 @@ std::string get_line_wrap(std::string & src, int maxchars);
std::string tolower(const std::string& str); 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);
} // namespace dropshell } // namespace dropshell