From 707e973130c08eb4281732d8600e1b27080efd01 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 May 2025 23:47:23 +1200 Subject: [PATCH] ./ --- make_createagent.sh | 22 +- src/utils/createagent.cpp | 575 ++++++++++++++++++++++++++++++++++++++ src/utils/createagent.hpp | 10 + 3 files changed, 600 insertions(+), 7 deletions(-) mode change 100644 => 100755 make_createagent.sh create mode 100644 src/utils/createagent.cpp create mode 100644 src/utils/createagent.hpp diff --git a/make_createagent.sh b/make_createagent.sh old mode 100644 new mode 100755 index 8d7eef8..0457b7f --- a/make_createagent.sh +++ b/make_createagent.sh @@ -13,6 +13,8 @@ cat < "$SCRIPT_DIR/src/utils/createagent.hpp" #ifndef CREATEAGENT_HPP #define CREATEAGENT_HPP +#include + namespace dropshell { int create_agent(const std::string &server_name); } @@ -24,19 +26,25 @@ EOF # heredoc to create the createagent.cpp file. cat < "$SCRIPT_DIR/src/utils/createagent.cpp" #include "createagent.hpp" +#include "utils.hpp" +#include "directories.hpp" +#include "execute.hpp" +#include "server_env_manager.hpp" +#include +#include +#include namespace dropshell { - void load_agent_file_contents(std::vector &agent_file_contents); - - struct AgentFile { std::string filename; std::string content; }; - int remote_write_file(const SSH_INFO &ssh_info, AgentFile &file); + + int load_agent_file_contents(std::vector &agent_files); + int remote_write_file(const sSSHInfo &ssh_info, const AgentFile &file); int create_agent(const std::string &server_name) { // create the sh file on the remote server. @@ -48,7 +56,7 @@ namespace dropshell { // create the agent directory. std::string agent_dir = remotepath::agent(server_name); - remote_create_directory(server_env.get_SSH_INFO(), agent_dir); + execute_ssh_command(server_env.get_SSH_INFO(), sCommand("","mkdir -p " + agent_dir,{}), cMode::Silent); std::vector agent_files; load_agent_file_contents(agent_files); @@ -62,10 +70,10 @@ namespace dropshell { return 0; } - int remote_write_file(const SSH_INFO &ssh_info, AgentFile &file) + int remote_write_file(const sSSHInfo &ssh_info, const AgentFile &file) { std::string generate_command = "cat <<'GENERATE_EOF' > " + file.filename + "\n" + file.content + "\nGENERATE_EOF\n"; // use heredoc to write the file. - return execute_ssh_command(ssh_info, sCommand(generate_command), cMode::Defaults); + return execute_ssh_command(ssh_info, sCommand("",generate_command,{}), cMode::Defaults); } // auto generated load_agent_file_contents function goes here, followed by closing } for namespace dropshell. diff --git a/src/utils/createagent.cpp b/src/utils/createagent.cpp new file mode 100644 index 0000000..0571b3a --- /dev/null +++ b/src/utils/createagent.cpp @@ -0,0 +1,575 @@ +#include "createagent.hpp" +#include "utils.hpp" +#include "directories.hpp" +#include "execute.hpp" +#include "server_env_manager.hpp" + +#include +#include +#include + +namespace dropshell { + + struct AgentFile { + std::string filename; + std::string content; + }; + + + int load_agent_file_contents(std::vector &agent_files); + int remote_write_file(const sSSHInfo &ssh_info, const AgentFile &file); + + int create_agent(const std::string &server_name) { + // create the sh file on the remote server. + server_env_manager server_env(server_name); + if (!server_env.is_valid()) { + std::cerr << "Invalid server environment for " << server_name << std::endl; + return 1; + } + + // create the agent directory. + std::string agent_dir = remotepath::agent(server_name); + execute_ssh_command(server_env.get_SSH_INFO(), sCommand("","mkdir -p " + agent_dir,{}), cMode::Silent); + + std::vector agent_files; + load_agent_file_contents(agent_files); + + // create the sh file on the remote server. + for (const auto &file : agent_files) { + // write out the file contents in the file. + remote_write_file(server_env.get_SSH_INFO(), file); + } + + return 0; + } + + int remote_write_file(const sSSHInfo &ssh_info, const AgentFile &file) + { + std::string generate_command = "cat <<'GENERATE_EOF' > " + file.filename + "\n" + file.content + "\nGENERATE_EOF\n"; // use heredoc to write the file. + return execute_ssh_command(ssh_info, sCommand("",generate_command,{}), cMode::Defaults); + } + + // auto generated load_agent_file_contents function goes here, followed by closing } for namespace dropshell. + int load_agent_file_contents(std::vector &agent_files) + { + agent_files={ + AgentFile { "_common.sh", R"AGENTFILE( +# COMMON FUNCTIONS +# JDE +# 2025-05-03 + +# This file is available TO ***ALL*** templates, as ${AGENT_PATH}/_common.sh + +# ---------------------------------------------------------------------------------------------------------- + +# summary of functions: +# _die "message" : Prints an error message in red and exits with status code 1. +# _grey_start : Switches terminal output color to grey. +# _grey_end : Resets terminal output color from grey. +# _create_and_start_container "" : Creates/starts a container, verifying it runs. +# _create_folder : Creates a directory if it doesn't exist (chmod 777). +# _check_docker_installed : Checks if Docker is installed, running, and user has permission. Returns 1 on failure. +# _is_container_exists : Checks if a container (any state) exists. Returns 1 if not found. +# _is_container_running : Checks if a container is currently running. Returns 1 if not running. +# _get_container_id : Prints the ID of the named container. +# _get_container_status : Prints the status string of the named container. +# _start_container : Starts an existing, stopped container. +# _stop_container : Stops a running container. +# _remove_container : Stops (if needed) and removes a container. +# _get_container_logs : Prints the logs for a container. +# _check_required_env_vars "VAR1" ... : Checks if listed environment variables are set; calls _die() if any are missing. +# _root_remove_tree : Removes a path using a root Docker container (for permissions). + +# ---------------------------------------------------------------------------------------------------------- + +# Prints an error message in red and exits with status code 1. +_die() { + echo -e "\033[91mError: $1\033[0m" + exit 1 +} + +# Switches terminal output color to grey. +_grey_start() { + echo -e -n "\033[90m" +} + +# Resets terminal output color from grey. +_grey_end() { + echo -e -n "\033[0m" +} + +# Creates/starts a container, verifying it runs. +_create_and_start_container() { + if [ -z "$1" ] || [ -z "$2" ]; then + _die "Template error: create_and_start_container " + fi + + local run_cmd="$1" + local container_name="$2" + + if _is_container_exists $container_name; then + _is_container_running $container_name && return 0 + _start_container $container_name + else + _grey_start + $run_cmd + _grey_end + fi + + if ! _is_container_running $container_name; then + _die "Container ${container_name} failed to start" + fi + + ID=$(_get_container_id $container_name) + echo "Container ${container_name} is running with ID ${ID}" +} + +# Creates a directory if it doesn't exist (chmod 777). +_create_folder() { + local folder="$1" + if [ -d "$folder" ]; then + return 0 + fi + if ! mkdir -p "$folder"; then + _die "Failed to create folder: $folder" + fi + chmod 777 "$folder" + echo "Folder created: $folder" +} + +# Checks if Docker is installed, running, and user has permission. Returns 1 on failure. +_check_docker_installed() { + if ! command -v docker &> /dev/null; then + echo "Docker is not installed" + return 1 + fi + + # check if docker daemon is running + if ! docker info &> /dev/null; then + echo "Docker daemon is not running" + return 1 + fi + + # check if user has permission to run docker + if ! docker run --rm hello-world &> /dev/null; then + echo "User does not have permission to run docker" + return 1 + fi + + return 0 +} + +# Checks if a container (any state) exists. Returns 1 if not found. +_is_container_exists() { + if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then + return 1 + fi + return 0 +} + +# Checks if a container is currently running. Returns 1 if not running. +_is_container_running() { + if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then + return 1 + fi + return 0 +} + +# Prints the ID of the named container. +_get_container_id() { + docker ps --format "{{.ID}}" --filter "name=$1" +} + +# Prints the status string of the named container. +_get_container_status() { + docker ps --format "{{.Status}}" --filter "name=$1" +} + +# Starts an existing, stopped container. +_start_container() { + _is_container_exists $1 || return 1 + _is_container_running $1 && return 0 + docker start $1 +} + +# Stops a running container. +_stop_container() { + _is_container_running $1 || return 0; + docker stop $1 +} + +# Stops (if needed) and removes a container. +_remove_container() { + _stop_container $1 + _is_container_exists $1 || return 0; + docker rm $1 +} + +# Prints the logs for a container. +_get_container_logs() { + if ! _is_container_exists $1; then + echo "Container $1 does not exist" + return 1 + fi + + docker logs $1 +} + +# Checks if listed environment variables are set; calls _die() if any are missing. +_check_required_env_vars() { + local required_vars=("$@") + for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + _die "Required environment variable $var is not set" + fi + done +} + +# Removes a path using a root Docker container (for permissions). +_root_remove_tree() { + local to_remove="$1" + parent=$(dirname "$to_remove") + abs_parent=$(realpath "$parent") + child=$(basename "$to_remove") + docker run --rm -v "$abs_parent":/data alpine rm -rf "/data/$child" +} + + +# Load autocommands +source "${AGENT_PATH}/_autocommands.sh")AGENTFILE" }, + AgentFile { "_nuke_other.sh", R"AGENTFILE( +#!/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" +)AGENTFILE" }, + AgentFile { "_allservicesstatus.sh", R"AGENTFILE( +#!/bin/bash + +# This script checks ALL services on the server and returns a status for each. + +# Return format is simple ENV with the following format: +# SERVICE_NAME_HEALTH=healthy|unhealthy|unknown +# SERVICE_NAME_PORTS=port1,port2,port3 + +# 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) + +CURRENT_OUTPUT="" +CURRENT_EXIT_CODE=0 + +load_dotenv(){ + local file_path=$1 + if [ -f "${file_path}" ]; then + source "${file_path}" + fi +} + +function run_command() { + local service_path=$1 + local command=$2 + local capture_output=${3:-false} # default to false if not specified + + # check if the command is a file + if [ ! -f "${service_path}/template/${command}.sh" ]; then + return; + fi + + # 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 + + # update the main variables. + export CONFIG_PATH="${service_path}/config" + # SERVER is correct + export SERVICE="${SERVICE_NAME}" + + if [ "$capture_output" = "true" ]; then + # Capture and return output + bash "${service_path}/template/${command}.sh" 2>&1 + else + # Run silently and return exit code + bash "${service_path}/template/${command}.sh" > /dev/null 2>&1 + fi + ) + CURRENT_EXIT_CODE=$? +} + +function command_exists() { + local service_path=$1 + local command=$2 + if [ ! -f "${service_path}/template/${command}.sh" ]; then + return 1 + fi + return 0 +} + +# Get all services on the server +SERVICES_PATH=$(realpath "${SCRIPT_DIR}/../../../") + +# Get all service names +SERVICE_NAMES=$(ls "${SERVICES_PATH}") + +# Iterate over all service names +for SERVICE_NAME in ${SERVICE_NAMES}; do + + SERVICE_PATH=$(realpath "${SERVICES_PATH}/${SERVICE_NAME}") + + #-------------------------------- + # Get the service health + if ! command_exists "${SERVICE_PATH}" "status"; then + SERVICE_HEALTH="unknown" + else + run_command "${SERVICE_PATH}" "status" "false" + if [ "${CURRENT_EXIT_CODE}" -eq 0 ]; then + SERVICE_HEALTH="healthy" + else + SERVICE_HEALTH="unhealthy" + fi + fi + + #-------------------------------- + # Get the service ports + if ! command_exists "${SERVICE_PATH}" "ports"; then + SERVICE_PORTS="" + else + run_command "${SERVICE_PATH}" "ports" "true" + SERVICE_PORTS="${CURRENT_OUTPUT}" + fi + + #-------------------------------- + # return the health and ports + echo "${SERVICE_NAME}_HEALTH=${SERVICE_HEALTH}" + echo "${SERVICE_NAME}_PORTS=${SERVICE_PORTS}" +done +)AGENTFILE" }, + AgentFile { "_autocommands.sh", R"AGENTFILE( +#!/bin/bash + +# This script contains the common code for the autocommands. + +MYID=$(id -u) +MYGRP=$(id -g) + +_autocommandrun_volume() { + local command="$1" + local volume_name="$2" + local backup_folder="$3" + + case "$command" in + create) + echo "Creating volume ${volume_name}" + docker volume create ${volume_name} + ;; + nuke) + echo "Nuking volume ${volume_name}" + docker volume rm ${volume_name} + ;; + backup) + echo "Backing up volume ${volume_name}" + docker run --rm -v ${volume_name}:/volume -v ${backup_folder}:/backup debian bash -c "tar -czvf /backup/backup.tgz -C /volume . && chown -R $MYID:$MYGRP /backup" + ;; + restore) + echo "Restoring volume ${volume_name}" + docker volume rm ${volume_name} + docker volume create ${volume_name} + docker run --rm -v ${volume_name}:/volume -v ${backup_folder}:/backup debian bash -c "tar -xzvf /backup/backup.tgz -C /volume --strip-components=1" + ;; + esac +} + +_autocommandrun_path() { + local command="$1" + local path="$2" + local backup_folder="$3" + + case "$command" in + create) + echo "Creating path ${path}" + mkdir -p ${path} + ;; + nuke) + echo "Nuking path ${path}" + local path_parent=$(dirname ${path}) + local path_child=$(basename ${path}) + if [ -d "${path_parent}/${path_child}" ]; then + docker run --rm -v ${path_parent}:/volume debian bash -c "rm -rf /volume/${path_child}" || echo "Failed to nuke path ${path}" + else + echo "Path ${path} does not exist - nothing to nuke" + fi + ;; + backup) + echo "Backing up path ${path}" + if [ -d "${path}" ]; then + docker run --rm -v ${path}:/path -v ${backup_folder}:/backup debian bash -c "tar -czvf /backup/backup.tgz -C /path . && chown -R $MYID:$MYGRP /backup" + else + echo "Path ${path} does not exist - nothing to backup" + fi + ;; + restore) + echo "Restoring path ${path}" + tar -xzvf ${backup_folder}/backup.tgz -C ${path} --strip-components=1 + ;; + esac +} + +_autocommandrun_file() { + local command="$1" + local filepath="$2" + local backup_folder="$3" + + case "$command" in + create) + ;; + nuke) + rm -f ${filepath} + ;; + backup) + echo "Backing up file ${filepath}" + local file_parent=$(dirname ${filepath}) + local file_name=$(basename ${filepath}) + if [ -f "${file_parent}/${file_name}" ]; then + docker run --rm -v ${file_parent}:/volume -v ${backup_folder}:/backup debian bash -c "cp /volume/${file_name} /backup/${file_name} && chown -R $MYID:$MYGRP /backup" + else + echo "File ${filepath} does not exist - nothing to backup" + fi + ;; + restore) + echo "Restoring file ${filepath}" + local file_name=$(basename ${filepath}) + cp ${backup_folder}/${file_name} ${filepath} + ;; + esac +} + +_autocommandparse() { + # first argument is the command + # if the command is backup or restore, then the last two arguments are the backup file and the temporary path + # all other arguments are of form: + # key=value + # where key can be one of volume, path or file. + # value is the path or volume name. + + # we iterate over the key=value arguments, and for each we call: + # autorun + + local command="$1" + shift + + local backup_temp_path="$1" + shift + + echo "autocommandparse: command=$command backup_temp_path=$backup_temp_path" + + # Extract the backup file and temp path (last two arguments) + local args=("$@") + local arg_count=${#args[@]} + + # Process all key=value pairs + for ((i=0; i<$arg_count; i++)); do + local pair="${args[$i]}" + + # Skip if not in key=value format + if [[ "$pair" != *"="* ]]; then + continue + fi + + local key="${pair%%=*}" + local value="${pair#*=}" + + # create backup folder unique to key/value. + local bfolder=$(echo "${key}_${value}" | tr -cd '[:alnum:]_-') + local targetpath="${backup_temp_path}/${bfolder}" + mkdir -p ${targetpath} + + # Key must be one of volume, path or file + case "$key" in + volume) + _autocommandrun_volume "$command" "$value" "$targetpath" + ;; + path) + _autocommandrun_path "$command" "$value" "$targetpath" + ;; + file) + _autocommandrun_file "$command" "$value" "$targetpath" + ;; + *) + _die "Unknown key $key passed to auto${command}. We only support volume, path and file." + ;; + esac + done +} + + +autocreate() { + _autocommandparse create none "$@" +} + + +autonuke() { + _autocommandparse nuke none "$@" +} + +autobackup() { + _check_required_env_vars "BACKUP_FILE" "TEMP_DIR" + BACKUP_TEMP_PATH="$TEMP_DIR/backup" + + + mkdir -p "$BACKUP_TEMP_PATH" + echo "_autocommandparse [backup] [$BACKUP_TEMP_PATH] [$@]" + _autocommandparse backup "$BACKUP_TEMP_PATH" "$@" + + tar zcvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" . +} + +autorestore() { + _check_required_env_vars "BACKUP_FILE" "TEMP_DIR" + BACKUP_TEMP_PATH="$TEMP_DIR/restore" + + echo "_autocommandparse [restore] [$BACKUP_TEMP_PATH] [$@]" + + mkdir -p "$BACKUP_TEMP_PATH" + tar zxvf "$BACKUP_FILE" -C "$BACKUP_TEMP_PATH" --strip-components=1 + + _autocommandparse restore "$BACKUP_TEMP_PATH" "$@" +} +)AGENTFILE" }, +}; return 0;} // namespace dropshell diff --git a/src/utils/createagent.hpp b/src/utils/createagent.hpp new file mode 100644 index 0000000..e6f4f05 --- /dev/null +++ b/src/utils/createagent.hpp @@ -0,0 +1,10 @@ +#ifndef CREATEAGENT_HPP +#define CREATEAGENT_HPP + +#include + +namespace dropshell { + int create_agent(const std::string &server_name); +} + +#endif // CREATEAGENT_HPP