From 50bd2e2446df98451d412de6aeab0e7c30830371 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 21 Apr 2025 14:54:33 +1200 Subject: [PATCH] backups --- src/dropshell-completion.bash | 10 +- src/main.cpp | 29 +++++- src/server_service.cpp | 136 ++++++++++++++++++++++---- src/server_service.hpp | 7 ++ templates/squashkiwi/_common.sh | 74 ++------------ templates/squashkiwi/_dockerhelper.sh | 81 +++++++++++++++ templates/squashkiwi/backup.sh | 32 +++--- templates/squashkiwi/example.env | 1 - templates/squashkiwi/install.sh | 10 +- templates/squashkiwi/start.sh | 3 +- templates/squashkiwi/status.sh | 11 +-- templates/squashkiwi/stop.sh | 3 +- templates/squashkiwi/update.sh | 6 +- 13 files changed, 288 insertions(+), 115 deletions(-) create mode 100644 templates/squashkiwi/_dockerhelper.sh diff --git a/src/dropshell-completion.bash b/src/dropshell-completion.bash index e324c2d..e1f4c81 100755 --- a/src/dropshell-completion.bash +++ b/src/dropshell-completion.bash @@ -7,7 +7,7 @@ _dropshell_completions() { prev="${COMP_WORDS[COMP_CWORD-1]}" # List of main commands - opts="help version status servers templates autocomplete_list_servers autocomplete_list_services run install" + opts="help version status servers templates autocomplete_list_servers autocomplete_list_services run install backup" # If we're completing the first argument, show all commands if [[ ${COMP_CWORD} -eq 1 ]] ; then @@ -37,15 +37,15 @@ _dropshell_completions() { COMPREPLY=( $(compgen -W "${servers[*]}" -- ${cur}) ) return 0 ;; - run|install) - # First argument after run/install is server name + run|install|backup) + # First argument after run/install/backup is server name local servers=($(dropshell autocomplete_list_servers)) COMPREPLY=( $(compgen -W "${servers[*]}" -- ${cur}) ) return 0 ;; *) - # Handle completion for service names and commands after run/install - if [[ ${COMP_CWORD} -ge 2 ]] && [[ "${COMP_WORDS[1]}" == "run" || "${COMP_WORDS[1]}" == "install" ]]; then + # Handle completion for service names and commands after run/install/backup + if [[ ${COMP_CWORD} -ge 2 ]] && [[ "${COMP_WORDS[1]}" == "run" || "${COMP_WORDS[1]}" == "install" || "${COMP_WORDS[1]}" == "backup" ]]; then if [[ ${COMP_CWORD} -eq 3 ]]; then # Second argument is service name local server_name="${COMP_WORDS[2]}" diff --git a/src/main.cpp b/src/main.cpp index 6f7187b..95fad95 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,8 +24,10 @@ void print_help() { std::cout << " servers List configured servers" << std::endl; std::cout << " servers NAME Show details for specific server" << std::endl; std::cout << " templates List available templates" << std::endl; - std::cout << " install SERVER SERVICE Install a service on a server" << std::endl; - std::cout << " run SERVER SERVICE COMMAND Run a command on a specific service" << std::endl; + std::cout << std::endl; + std::cout << " install SERVER SERVICE (Re)install a service on a server." << std::endl; + std::cout << " backup SERVER SERVICE Backup a service on a server." << std::endl; + std::cout << " run SERVER SERVICE COMMAND Run a command on a specific service" << std::endl; std::cout << std::endl; std::cout << "Examples:" << std::endl; std::cout << " dropshell servers" << std::endl; @@ -157,7 +159,28 @@ int main(int argc, char* argv[]) { } if (!service.run_command(command)) { - std::cerr << "Error: Failed to run command" << std::endl; + std::cerr << command +" failed." << std::endl; + return 1; + } + return 0; + } + + if (cmd == "backup") { + if (argc < 4) { + std::cerr << "Error: backup command requires server name and service name" << std::endl; + return 1; + } + std::string server_name = argv[2]; + std::string service_name = argv[3]; + + dropshell::server_service service; + if (!service.init(server_name, service_name)) { + std::cerr << "Error: Failed to initialize service" << std::endl; + return 1; + } + + if (!service.backup()) { + std::cerr << "Backup failed." << std::endl; return 1; } return 0; diff --git a/src/server_service.cpp b/src/server_service.cpp index 5c38e75..69cad8d 100644 --- a/src/server_service.cpp +++ b/src/server_service.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include namespace fs = boost::filesystem; @@ -106,12 +108,20 @@ bool server_service::install() { return false; } - // Copy template files - std::string scp_cmd = "scp -P " + m_server_env->get_SSH_PORT() + " -r " + - info.path + "/* " + - m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + - service_dir + "/template/"; - if (system(scp_cmd.c_str()) != 0) { + // Check if rsync is installed on remote host + std::string check_rsync_cmd = ssh_cmd.str() + "'which rsync > /dev/null 2>&1'"; + if (system(check_rsync_cmd.c_str()) != 0) { + std::cerr << "Error: rsync is not installed on the remote host" << std::endl; + return false; + } + + // Copy template files, preserving the directory structure and file permissions + std::cout << "Copying template files from " << info.path << " to " << service_dir << "/template/" << std::endl; + std::string rsync_cmd = "rsync --delete -zrpc -e 'ssh -p " + m_server_env->get_SSH_PORT() + "' " + + info.path + "/ " + + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + + service_dir + "/template/"; + if (system(rsync_cmd.c_str()) != 0) { std::cerr << "Error: Failed to copy template files" << std::endl; return false; } @@ -123,18 +133,18 @@ bool server_service::install() { return false; } fs::path service_env = fs::path(user_dir) / "servers" / m_server_name / (m_service_name + ".env"); - scp_cmd = "scp -P " + m_server_env->get_SSH_PORT() + " " + - service_env.string() + " " + - m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + - service_dir + "/" + m_service_name + ".env"; + std::string scp_cmd = "scp -P " + m_server_env->get_SSH_PORT() + " " + + service_env.string() + " " + + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + + service_dir + "/" + m_service_name + ".env"; if (system(scp_cmd.c_str()) != 0) { std::cerr << "Error: Failed to copy service environment file" << std::endl; return false; } // Run install script - std::string install_cmd = ssh_cmd.str() + "'cd " + service_dir + " && ./template/install.sh " + - m_service_name + ".env'"; + std::string install_cmd = ssh_cmd.str() + "'cd " + service_dir + "/template && /bin/bash install.sh " + + service_dir + "/" + m_service_name + ".env'"; if (system(install_cmd.c_str()) != 0) { std::cerr << "Error: Failed to run install script" << std::endl; return false; @@ -155,6 +165,10 @@ bool server_service::run_command(const std::string& command) { ssh_cmd << "ssh -p " << m_server_env->get_SSH_PORT() << " " << m_server_env->get_SSH_USER() << "@" << m_server_env->get_SSH_HOST() << " "; + std::string script_dir = service_dir + "/template"; + std::string script_path_and_command = script_dir + "/" + command + ".sh"; + std::string env_path = service_dir + "/" + m_service_name + ".env"; + // Check if service directory exists std::string check_dir_cmd = ssh_cmd.str() + "'test -d " + service_dir + "'"; if (system(check_dir_cmd.c_str()) != 0) { @@ -163,28 +177,116 @@ bool server_service::run_command(const std::string& command) { } // Check if command script exists - std::string check_script_cmd = ssh_cmd.str() + "'test -f " + service_dir + "/" + command + ".sh'"; + std::string check_script_cmd = ssh_cmd.str() + "'test -f " + script_path_and_command + "'"; if (system(check_script_cmd.c_str()) != 0) { std::cerr << "Error: Command script '" << command << ".sh' not found" << std::endl; return false; } // Check if env file exists - std::string check_env_cmd = ssh_cmd.str() + "'test -f " + service_dir + "/" + m_service_name + ".env'"; + std::string check_env_cmd = ssh_cmd.str() + "'test -f " + env_path + "'"; if (system(check_env_cmd.c_str()) != 0) { std::cerr << "Error: Service environment file not found on server" << std::endl; return false; } // Run the command - std::string run_cmd = ssh_cmd.str() + "'cd " + service_dir + " && ./" + command + ".sh " + - m_service_name + ".env'"; + std::string run_cmd = ssh_cmd.str() + "'cd " + script_dir + + " && /bin/bash " + script_path_and_command + " "+ env_path + "'"; if (system(run_cmd.c_str()) != 0) { - std::cerr << "Error: Failed to run command" << std::endl; + std::cerr << "Command returned error code: " << script_path_and_command << std::endl; return false; } return true; } +bool server_service::backup() { + if (!m_server_env) { + std::cerr << "Error: Server service not initialized" << std::endl; + return false; + } + + // Check if service directory exists + std::string service_dir = m_server_env->get_DROPSHELL_DIR() + "/" + m_service_name; + std::stringstream ssh_cmd; + ssh_cmd << "ssh -p " << m_server_env->get_SSH_PORT() << " " + << m_server_env->get_SSH_USER() << "@" << m_server_env->get_SSH_HOST() << " "; + + std::string script_dir = service_dir + "/template"; + std::string script_path = script_dir + "/backup.sh"; + std::string env_path = service_dir + "/" + m_service_name + ".env"; + + // Check if service directory exists + std::string check_dir_cmd = ssh_cmd.str() + "'test -d " + service_dir + "'"; + if (system(check_dir_cmd.c_str()) != 0) { + std::cerr << "Error: Service directory not found on server - has it been installed?" << std::endl; + return false; + } + + // Check if backup script exists + std::string check_script_cmd = ssh_cmd.str() + "'test -f " + script_path + "'"; + if (system(check_script_cmd.c_str()) != 0) { + std::cerr << "Error: Backup script not found" << std::endl; + return false; + } + + // Check if env file exists + std::string check_env_cmd = ssh_cmd.str() + "'test -f " + env_path + "'"; + if (system(check_env_cmd.c_str()) != 0) { + std::cerr << "Error: Service environment file not found on server" << std::endl; + return false; + } + + // Create backups directory on server if it doesn't exist + std::string server_backups_dir = m_server_env->get_DROPSHELL_DIR() + "/backups"; + std::string mkdir_cmd = ssh_cmd.str() + "'mkdir -p " + server_backups_dir + "'"; + if (system(mkdir_cmd.c_str()) != 0) { + std::cerr << "Error: Failed to create backups directory on server" << std::endl; + return false; + } + + // Create backups directory locally if it doesn't exist + std::string user_dir; + if (!get_user_directory(user_dir)) { + std::cerr << "Error: User directory not set" << std::endl; + return false; + } + fs::path local_backups_dir = fs::path(user_dir) / "backups"; + if (!fs::exists(local_backups_dir)) { + fs::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"); + + // Construct backup filename + std::string backup_filename = m_server_name + "-" + m_service_name + "-" + datetime.str() + ".tgz"; + std::string server_backup_path = server_backups_dir + "/" + backup_filename; + std::string local_backup_path = (local_backups_dir / backup_filename).string(); + + // Run backup script + std::string backup_cmd = ssh_cmd.str() + "'cd " + script_dir + + " && /bin/bash backup.sh " + env_path + " " + server_backup_path + "'"; + if (system(backup_cmd.c_str()) != 0) { + std::cerr << "Error: Backup script failed" << std::endl; + return false; + } + + // Copy backup file from server to local + std::string scp_cmd = "scp -P " + m_server_env->get_SSH_PORT() + " " + + m_server_env->get_SSH_USER() + "@" + m_server_env->get_SSH_HOST() + ":" + + server_backup_path + " " + local_backup_path; + if (system(scp_cmd.c_str()) != 0) { + std::cerr << "Error: Failed to copy backup file from server" << std::endl; + return false; + } + + std::cout << "Backup created successfully: " << local_backup_path << std::endl; + return true; +} + } // namespace dropshell \ No newline at end of file diff --git a/src/server_service.hpp b/src/server_service.hpp index 1f9d416..9211a10 100644 --- a/src/server_service.hpp +++ b/src/server_service.hpp @@ -37,6 +37,13 @@ class server_service { // checking that the {service_name}.env file exists in the service directory. bool run_command(const std::string& command); + // 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 + bool backup(); + private: std::string m_server_name; std::string m_service_name; diff --git a/templates/squashkiwi/_common.sh b/templates/squashkiwi/_common.sh index 7c9150b..8e68f77 100755 --- a/templates/squashkiwi/_common.sh +++ b/templates/squashkiwi/_common.sh @@ -1,5 +1,7 @@ #!/bin/bash +source _dockerhelper.sh + # Print error message and exit with code 1 # Usage: die "error message" die() { @@ -44,47 +46,11 @@ grey_end() { echo -e -n "\033[0m" } -# Test if Docker is installed and working on the local machine -# Returns 0 if Docker is working, 1 if there are any issues -test_docker() { - echo "Testing Docker on local machine..." - - # Test Docker installation - if ! command -v docker >/dev/null 2>&1; then - die "Docker is not installed on this machine" - fi - - # Test Docker daemon is running - if ! docker info >/dev/null 2>&1; then - die "Docker daemon is not running" - fi - - # Test Docker can run containers - if ! docker run --rm hello-world >/dev/null 2>&1; then - die "Docker cannot run containers" - fi - - echo "Docker is working correctly on local machine" - return 0 -} - - -start_container() { - # Check if container exists and is stopped - echo "Checking container status..." - if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - die "Container ${CONTAINER_NAME} is already running" - else - echo "Starting existing container ${CONTAINER_NAME}..." - grey_start - if ! docker start "${CONTAINER_NAME}"; then - die "Failed to start container ${CONTAINER_NAME}" - fi - grey_end - fi +create_and_start_container() { + if _is_container_exists $CONTAINER_NAME; then + _is_container_running $CONTAINER_NAME && return 0 + _start_container $CONTAINER_NAME else - echo "Creating and starting new container ${CONTAINER_NAME}..." grey_start docker run -d \ --restart unless-stopped \ @@ -95,33 +61,11 @@ start_container() { grey_end fi - # check if the container is running - if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - die "Container ${CONTAINER_NAME} is not running" + if ! _is_container_running $CONTAINER_NAME; then + die "Container ${CONTAINER_NAME} failed to start" fi - # print the container id - ID=$(docker ps --format '{{.ID}}' --filter "name=^${CONTAINER_NAME}$") + ID=$(_get_container_id $CONTAINER_NAME) echo "Container ${CONTAINER_NAME} is running with ID ${ID}" - - return 0 -} - -stop_container() { - # Check if container is running - echo "Checking if container is running..." - if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "Stopping container ${CONTAINER_NAME}..." - grey_start - if ! docker stop "${CONTAINER_NAME}"; then - die "Failed to stop container ${CONTAINER_NAME}" - fi - grey_end - echo "Container ${CONTAINER_NAME} stopped successfully" - return 0 - else - echo "Container ${CONTAINER_NAME} is not running" - return 1 - fi } diff --git a/templates/squashkiwi/_dockerhelper.sh b/templates/squashkiwi/_dockerhelper.sh new file mode 100644 index 0000000..f1ecce3 --- /dev/null +++ b/templates/squashkiwi/_dockerhelper.sh @@ -0,0 +1,81 @@ +# Routines for interacting with docker + + +# Check if docker is installed +_check_docker_installed() { + if ! command -v docker &> /dev/null; then + echo "Docker is not installed" + return 1 + fi + + # check if docker daemon is running + if ! docker info &> /dev/null; then + echo "Docker daemon is not running" + return 1 + fi + + # check if user has permission to run docker + if ! docker run --rm hello-world &> /dev/null; then + echo "User does not have permission to run docker" + return 1 + fi + + return 0 +} + +# Check if a container exists +_is_container_exists() { + if ! docker ps -a --format "{{.Names}}" | grep -q "^$1$"; then + return 1 + fi + return 0 +} + +# Check if a container is running +_is_container_running() { + if ! docker ps --format "{{.Names}}" | grep -q "^$1$"; then + return 1 + fi + return 0 +} + +# get contianer ID +_get_container_id() { + docker ps --format "{{.ID}}" --filter "name=$1" +} + +# get container status +_get_container_status() { + docker ps --format "{{.Status}}" --filter "name=$1" +} + +# start container that exists +_start_container() { + _is_container_exists $1 || return 1 + _is_container_running $1 && return 0 + docker start $1 +} + +# stop container that exists +_stop_container() { + _is_container_running $1 || return 0; + docker stop $1 +} + +# remove container that exists +_remove_container() { + _stop_container $1 + _is_container_exists $1 || return 0; + docker rm $1 +} + +# get container logs +_get_container_logs() { + if ! _is_container_exists $1; then + echo "Container $1 does not exist" + return 1 + fi + + docker logs $1 +} + diff --git a/templates/squashkiwi/backup.sh b/templates/squashkiwi/backup.sh index d668ff8..748abe7 100755 --- a/templates/squashkiwi/backup.sh +++ b/templates/squashkiwi/backup.sh @@ -1,24 +1,34 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables load_env "$1" || die "Failed to load environment variables" -# set the backup file name to be the current date and time -BACKUP_FILE="$BACKUP_FOLDER/backup-squashkiwi-$(date +%Y-%m-%d_%H-%M-%S).tar.gz" - -if [ -f "$BACKUP_FILE" ]; then - echo "Backup file $BACKUP_FILE already exists" - exit 1 +# Get backup file path from second argument +BACKUP_FILE="$2" +if [ -z "$BACKUP_FILE" ]; then + die "Backup file path not provided" fi -stop_container +# Check if backup file already exists +if [ -f "$BACKUP_FILE" ]; then + die "Backup file $BACKUP_FILE already exists" +fi -# create the backup file, using relative paths. -tar zcvf "$BACKUP_FILE" -C "$DATA_FOLDER" . || die "Failed to create backup" +# Stop container before backup +_stop_container "$CONTAINER_NAME" -start_container +# Create backup of data folder +echo "Creating backup of $DATA_FOLDER..." +if ! tar zcvf "$BACKUP_FILE" -C "$DATA_FOLDER" .; then + _start_container "$CONTAINER_NAME" + die "Failed to create backup" +fi -echo "Backup created in $BACKUP_FILE" +# Start container after backup +_start_container "$CONTAINER_NAME" + +echo "Backup created successfully: $BACKUP_FILE" diff --git a/templates/squashkiwi/example.env b/templates/squashkiwi/example.env index 9170268..b4cc999 100644 --- a/templates/squashkiwi/example.env +++ b/templates/squashkiwi/example.env @@ -9,7 +9,6 @@ HOST_PORT=80 # Deployment settings DEPLOY_FOLDER="${HOME}/sk/deploy" DATA_FOLDER="${HOME}/sk/data" -BACKUP_FOLDER="${HOME}/sk/backups" CONTAINER_NAME="squashkiwi" # Image settings diff --git a/templates/squashkiwi/install.sh b/templates/squashkiwi/install.sh index 68524f2..415f06f 100755 --- a/templates/squashkiwi/install.sh +++ b/templates/squashkiwi/install.sh @@ -1,19 +1,18 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables load_env "$1" || exit 1 # Test Docker -if ! test_docker; then +if ! _check_docker_installed; then echo "Docker test failed, aborting installation..." exit 1 fi -# Rest of your install script here - function create_folder() { local folder="$1" if ! mkdir -p "$folder"; then @@ -34,3 +33,8 @@ if ! docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG"; then fi echo "Successfully pulled the docker image from the registry" +# start the container +_stop_container $CONTAINER_NAME +create_and_start_container || die "Failed to start container ${CONTAINER_NAME}" + +echo "Installation complete" diff --git a/templates/squashkiwi/start.sh b/templates/squashkiwi/start.sh index 151cf07..a29144e 100755 --- a/templates/squashkiwi/start.sh +++ b/templates/squashkiwi/start.sh @@ -1,12 +1,13 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables load_env "$1" || die "Failed to load environment variables" -start_container || die "Failed to start container ${CONTAINER_NAME}" +create_and_start_container || die "Failed to start container ${CONTAINER_NAME}" grey_start docker logs --tail 20 "${CONTAINER_NAME}" diff --git a/templates/squashkiwi/status.sh b/templates/squashkiwi/status.sh index b3f5bd9..8c52ded 100644 --- a/templates/squashkiwi/status.sh +++ b/templates/squashkiwi/status.sh @@ -1,24 +1,23 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables load_env "$1" || exit 1 # check if the service is running -if ! docker ps | grep -q "$CONTAINER_NAME"; then - echo "Service is not running" +if ! _is_container_running $CONTAINER_NAME; then + echo "Service is not running - did not find container $CONTAINER_NAME." exit 1 fi -echo "Service is running" - # curl -s -X GET http://localhost:8080/health | grep -q "OK" if ! curl -s -X GET http://localhost:${HOST_PORT}/health | grep -q "OK"; then - echo "Service is not healthy" + echo "Service is not healthy - did not get OK response from /health endpoint." exit 1 fi echo "Service is healthy" -return 0 +exit 0 diff --git a/templates/squashkiwi/stop.sh b/templates/squashkiwi/stop.sh index 53d8164..4bca4fe 100755 --- a/templates/squashkiwi/stop.sh +++ b/templates/squashkiwi/stop.sh @@ -1,12 +1,13 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables load_env "$1" || die "Failed to load environment variables" -stop_container || die "Failed to stop container ${CONTAINER_NAME}" +_stop_container $CONTAINER_NAME || die "Failed to stop container ${CONTAINER_NAME}" grey_start docker logs --tail 20 "${CONTAINER_NAME}" diff --git a/templates/squashkiwi/update.sh b/templates/squashkiwi/update.sh index 2903dfa..af27199 100755 --- a/templates/squashkiwi/update.sh +++ b/templates/squashkiwi/update.sh @@ -1,6 +1,7 @@ #!/bin/bash # Source common functions +source "$(dirname "$0")/_dockerhelper.sh" source "$(dirname "$0")/_common.sh" # Load environment variables @@ -14,7 +15,7 @@ fi echo "Successfully pulled the docker image from the registry" # stop the old container -stop_container +_stop_container ${CONTAINER_NAME} # remove the old container grey_start @@ -26,5 +27,6 @@ grey_end echo "Successfully removed the old container" # start the new container -start_container +create_and_start_container || die "Failed to start container ${CONTAINER_NAME}" +