From 093417905351776184bacb49762f3c46682ed895 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 24 May 2025 11:15:23 +1200 Subject: [PATCH] ... --- source/agent-remote/_allservicesstatus.sh | 2 - source/src/commands/backupdata.cpp | 4 +- source/src/commands/create-service.cpp | 245 ++++++++---- source/src/commands/destroy.cpp | 2 +- source/src/commands/install.cpp | 8 +- source/src/commands/restoredata.cpp | 6 +- source/src/commands/shared_commands.cpp | 15 +- source/src/commands/shared_commands.hpp | 9 +- source/src/commands/uninstall.cpp | 2 +- source/src/servers.cpp | 101 ++--- source/src/servers.hpp | 37 +- source/src/services.cpp | 433 ++++++++++++---------- source/src/services.hpp | 5 + source/src/templates.cpp | 12 +- source/src/templates.hpp | 3 + source/src/utils/directories.cpp | 1 - source/src/utils/directories.hpp | 4 - 17 files changed, 523 insertions(+), 366 deletions(-) diff --git a/source/agent-remote/_allservicesstatus.sh b/source/agent-remote/_allservicesstatus.sh index 5895129..cef786f 100755 --- a/source/agent-remote/_allservicesstatus.sh +++ b/source/agent-remote/_allservicesstatus.sh @@ -23,7 +23,6 @@ SCRIPT_DIR="$(dirname "$0")" # // |-- service.env (actual service config) # // |-- .template_info.env # // |-- template - # // |-- _default.env # // |-- (script files) # // |-- config # // |-- service.env (default service config) @@ -66,7 +65,6 @@ function run_command() { CURRENT_OUTPUT=$( set -a - load_dotenv "${service_path}/template/_default.env" load_dotenv "${service_path}/config/service.env" load_dotenv "${service_path}/config/.template_info.env" diff --git a/source/src/commands/backupdata.cpp b/source/src/commands/backupdata.cpp index de581f7..970eb90 100644 --- a/source/src/commands/backupdata.cpp +++ b/source/src/commands/backupdata.cpp @@ -74,7 +74,7 @@ namespace dropshell return true; // nothing to back up. } - std::string user = server_env.get_user_for_service(server, service); + 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); @@ -135,7 +135,7 @@ namespace dropshell } // 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)) + 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; diff --git a/source/src/commands/create-service.cpp b/source/src/commands/create-service.cpp index f112e2d..fd152af 100644 --- a/source/src/commands/create-service.cpp +++ b/source/src/commands/create-service.cpp @@ -67,96 +67,177 @@ namespace dropshell namespace shared_commands { - - bool print_readme(const template_info &tinfo, std::string server, std::string service) - { - std::vector 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) + bool print_readme(const template_info &tinfo, std::string server, std::string service) { - if (std::filesystem::exists(readme_path / variant)) + std::vector 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) { - readme_path = readme_path / variant; - break; + if (std::filesystem::exists(readme_path / variant)) + { + readme_path = readme_path / variant; + break; + } } - } - if (!std::filesystem::exists(readme_path)) - return false; + if (!std::filesystem::exists(readme_path)) + return false; - std::map 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(); + std::map 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) - { - if (server_name.empty() || template_name.empty() || service_name.empty()) - 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); - - 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; + 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; + + server_config 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 = service_dir + "/.template_info.env"; + std::ofstream template_info_env_file_out(template_info_env_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 service.env file" << std::endl; + { // edit the service.env file to set the SSH_USER. + std::string template_service_env_file = tinfo.local_template_path() / "config" / "service.env"; + ASSERT(std::filesystem::exists(template_service_env_file), "Template service env file not found: " + template_service_env_file); + std::ifstream template_service_env_file_in(template_service_env_file); + std::ofstream service_env_file_out(service_dir + "/service.env"); + 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; } - return true; - } } // namespace shared_commands diff --git a/source/src/commands/destroy.cpp b/source/src/commands/destroy.cpp index 2548be4..7406fa2 100644 --- a/source/src/commands/destroy.cpp +++ b/source/src/commands/destroy.cpp @@ -63,7 +63,7 @@ namespace dropshell if (!SIvalid(service_info)) error << "Invalid service: " << service << std::endl; - std::string user = server_env.get_user_for_service(server, service); + std::string user = server_env.get_user_for_service(service); if (server_env.check_remote_dir_exists(remotepath(server, user).service(service), user)) { // run the nuke script on the remote server if it exists. diff --git a/source/src/commands/install.cpp b/source/src/commands/install.cpp index 9dc883b..3b1d059 100644 --- a/source/src/commands/install.cpp +++ b/source/src/commands/install.cpp @@ -88,7 +88,7 @@ namespace dropshell } // Create service directory - std::string user = server_env.get_user_for_service(server, service); + std::string user = server_env.get_user_for_service(service); std::string remote_service_path = remotepath(server,user).service(service); 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)) @@ -109,7 +109,7 @@ namespace dropshell 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)) + server_env, false, service_info.user)) { std::cerr << "Failed to copy template files using rsync" << std::endl; return false; @@ -119,7 +119,7 @@ namespace dropshell 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)) + server_env, false, service_info.user)) { std::cerr << "Failed to copy service files using rsync" << std::endl; return false; @@ -273,7 +273,7 @@ namespace dropshell // 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); + 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. diff --git a/source/src/commands/restoredata.cpp b/source/src/commands/restoredata.cpp index 0f3ff52..4847c4f 100644 --- a/source/src/commands/restoredata.cpp +++ b/source/src/commands/restoredata.cpp @@ -183,7 +183,7 @@ namespace dropshell { // create the new service info << "3) Creating new service..." << std::endl; - if (!shared_commands::create_service(server, service_info.template_name, service)) + if (!shared_commands::create_service(server, service_info.template_name, service, service_info.user)) return 1; } @@ -196,14 +196,14 @@ namespace dropshell { // restore service from backup info << "5) Restoring service data from backup..." << std::endl; - std::string user = server_env.get_user_for_service(server, service); + 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)) + 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; diff --git a/source/src/commands/shared_commands.cpp b/source/src/commands/shared_commands.cpp index 1aff7bd..fcf6a72 100644 --- a/source/src/commands/shared_commands.cpp +++ b/source/src/commands/shared_commands.cpp @@ -55,13 +55,14 @@ namespace dropshell const std::string &local_path, const std::string &remote_path, const server_config &server_env, - bool silent) + 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(server_env.get_SSH_UNPRIVILEGED_USER() + "@" + server_env.get_SSH_HOST() + ":" + + quote(user + "@" + server_env.get_SSH_HOST() + ":" + remote_path + "/"); return execute_local_command("", rsync_cmd, {}, nullptr, (silent ? cMode::Silent : cMode::Defaults)); } @@ -222,7 +223,7 @@ namespace dropshell return HealthStatus::ERROR; } - std::string user = env.get_user_for_service(server, service); + std::string user = env.get_user_for_service(service); if (!env.check_remote_dir_exists(remotepath(server,user).service(service), user)) { @@ -308,7 +309,7 @@ namespace dropshell // ------------------------------------------------------------------------------------------------ // scp_file_to_remote : SHARED COMMAND // ------------------------------------------------------------------------------------------------ - bool scp_file_to_remote(const server_config &server_env, const std::string &local_path, const std::string &remote_path, bool silent) + bool scp_file_to_remote(const server_config &server_env, const std::string &local_path, const std::string &remote_path, bool silent, std::string user) { if (!server_env.is_valid()) { @@ -316,14 +317,14 @@ namespace dropshell 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) + " " + server_env.get_SSH_UNPRIVILEGED_USER() + "@" + server_env.get_SSH_HOST() + ":" + quote(remote_path) + (silent ? " > /dev/null 2>&1" : ""); + 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 server_config &server_env, const std::string &remote_path, const std::string &local_path, bool silent) + bool scp_file_from_remote(const server_config &server_env, const std::string &remote_path, const std::string &local_path, bool silent, std::string user) { if (!server_env.is_valid()) { @@ -331,7 +332,7 @@ namespace dropshell 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() + " " + server_env.get_SSH_UNPRIVILEGED_USER() + "@" + server_env.get_SSH_HOST() + ":" + quote(remote_path) + " " + quote(local_path) + (silent ? " > /dev/null 2>&1" : ""); + 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)); } diff --git a/source/src/commands/shared_commands.hpp b/source/src/commands/shared_commands.hpp index 0a39cc8..0854e60 100644 --- a/source/src/commands/shared_commands.hpp +++ b/source/src/commands/shared_commands.hpp @@ -44,7 +44,8 @@ namespace dropshell const std::string &local_path, const std::string &remote_path, const server_config &server_env, - bool silent); + bool silent, + std::string user); std::string get_arch(); @@ -80,8 +81,8 @@ namespace dropshell std::string mDatetime; }; - bool scp_file_to_remote(const server_config &server_env, const std::string &local_path, const std::string &remote_path, bool silent); - bool scp_file_from_remote(const server_config &server_env, const std::string &remote_path, const std::string &local_path, bool silent); + bool scp_file_to_remote(const server_config &server_env, const std::string &local_path, const std::string &remote_path, bool silent, std::string user); + bool scp_file_from_remote(const server_config &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 server_config &server_env, const std::string& service); @@ -96,7 +97,7 @@ namespace dropshell bool install_service(const server_config &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); + bool create_service(const std::string &server_name, const std::string &template_name, const std::string &service_name, std::string user_override=""); } // namespace shared_commands } // namespace dropshell diff --git a/source/src/commands/uninstall.cpp b/source/src/commands/uninstall.cpp index b2add40..822e11c 100644 --- a/source/src/commands/uninstall.cpp +++ b/source/src/commands/uninstall.cpp @@ -48,7 +48,7 @@ namespace dropshell std::string server = server_env.get_server_name(); maketitle("Uninstalling " + service + " on " + server); - std::string user = server_env.get_user_for_service(server, service); + 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)) diff --git a/source/src/servers.cpp b/source/src/servers.cpp index 116c2fa..3ad9b37 100644 --- a/source/src/servers.cpp +++ b/source/src/servers.cpp @@ -49,7 +49,7 @@ namespace dropshell return; } - // get the variables from the json + // get the variables from the json, converting everything to strings. for (const auto &var : server_env_json.items()) { std::string value; @@ -111,35 +111,19 @@ namespace dropshell } } - bool server_config::create_server_json_file(const std::string &server_env_path, const std::string &SSH_HOST, const std::string &SSH_PORT, const std::vector &users) + std::string server_config::get_SSH_HOST() const { - nlohmann::json server_env_json; - server_env_json["SSH_HOST"] = SSH_HOST; - server_env_json["SSH_PORT"] = std::stoi(SSH_PORT); + return get_variable("SSH_HOST"); + } - // Create users array - nlohmann::json users_array = nlohmann::json::array(); - for (const auto &user : users) - { - nlohmann::json user_json; - user_json["USER"] = user.user; - user_json["DIR"] = user.dir; - users_array.push_back(user_json); - } - server_env_json["SSH_USERS"] = users_array; + std::string server_config::get_SSH_PORT() const + { + return get_variable("SSH_PORT"); + } - 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::vector server_config::get_users() const + { + return mUsers; } std::string server_config::get_user_dir(const std::string &user) const @@ -154,22 +138,24 @@ namespace dropshell return ""; } - std::string server_config::get_user_for_service(const std::string &server, const std::string &service) + std::string server_config::get_server_name() const + { + return mServerName; + } + + std::string server_config::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) { auto services_info = get_server_services_info(server); - if (std::find_if(services_info.begin(), services_info.end(), + auto it = std::find_if(services_info.begin(), services_info.end(), [&service](const LocalServiceInfo &si) - { return si.service_name == service; }) != services_info.end()) - { - // found a service with matching name. - 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()) - { - return it->user; - } - } + { return si.service_name == service; }); + if (it != services_info.end() && SIvalid(*it)) + return it->user; return ""; } @@ -184,6 +170,31 @@ namespace dropshell return sSSHInfo(get_SSH_HOST(), it->user, get_SSH_PORT(), get_server_name(), it->dir); } + bool server_config::hasRootUser() const + { + auto it = std::find_if(mUsers.begin(), mUsers.end(),[](const UserConfig &u) + { return u.user == "root"; }); + return it != mUsers.end(); + } + + bool server_config::hasDocker() const + { + return get_variable("HAS_DOCKER") == "true"; + } + + bool server_config::hasRootDocker() const + { + return get_variable("DOCKER_ROOTLESS") == "false"; + } + + bool server_config::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 server_config::check_remote_dir_exists(const std::string &dir_path, std::string user) const { sCommand scommand("", "test -d " + quote(dir_path), {}); @@ -255,7 +266,7 @@ namespace dropshell bool silent, std::map extra_env_vars) const { - std::string user = get_user_for_service(mServerName, service_name); + 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; @@ -278,7 +289,7 @@ namespace dropshell bool silent, std::map extra_env_vars) const { - std::string user = get_user_for_service(mServerName, service_name); + 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; @@ -305,7 +316,7 @@ namespace dropshell if (command.empty()) return std::nullopt; - std::string user = get_user_for_service(mServerName, service_name); + 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"; @@ -336,9 +347,9 @@ namespace dropshell } bool run_as_root = runas == "root"; - if (run_as_root && !get_ALLOW_ROOT_SERVICES()) + if (run_as_root && !hasRootUser()) { - error << "Error: The service " << service_name << " is set to run as root, but the server environment does not allow root services." << std::endl; + error << "Error: The service " << service_name << " is set to run as root on the remote server, but the server environment does not allow root services." << std::endl; return std::nullopt; } diff --git a/source/src/servers.hpp b/source/src/servers.hpp index 3952380..cd207c3 100644 --- a/source/src/servers.hpp +++ b/source/src/servers.hpp @@ -37,30 +37,33 @@ namespace dropshell public: server_config(const std::string &server_name); - static bool create_server_json_file( - const std::string &server_env_path, - const std::string &SSH_HOST, - const std::string &SSH_PORT, - const std::vector &users); + bool is_valid() const { return mValid; } - std::string get_variable(const std::string &name) const; // trivial getters. const std::map &get_variables() const { return mVariables; } - std::string get_SSH_HOST() const { return get_variable("SSH_HOST"); } - std::string get_SSH_PORT() const { return get_variable("SSH_PORT"); } - std::string get_SSH_UNPRIVILEGED_USER() const { return get_variable("SSH_UNPRIVILEGED_USER"); } - bool get_ALLOW_ROOT_SERVICES() const { return get_variable("ALLOW_ROOT_SERVICES") == "true"; } - std::vector get_users() const { return mUsers; } - std::string get_user_dir(const std::string &user) const; - bool is_valid() const { return mValid; } - std::string get_server_name() const { return mServerName; } + std::string get_variable(const std::string &name) const; - static std::string get_user_for_service(const std::string &server, const std::string &service); + // ------------------------------------------------------------------------------------------------ + // getters + // ------------------------------------------------------------------------------------------------ + std::string get_SSH_HOST() const; + std::string get_SSH_PORT() const; + std::vector 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; @@ -84,8 +87,12 @@ namespace dropshell bool mValid; }; // class server_config + + std::vector 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 &commands); diff --git a/source/src/services.cpp b/source/src/services.cpp index 85547ed..2f82929 100644 --- a/source/src/services.cpp +++ b/source/src/services.cpp @@ -4,8 +4,6 @@ #include "templates.hpp" #include "config.hpp" #include "utils/utils.hpp" -#include "servers.hpp" -#include "servers.hpp" #include "assert.hpp" #include @@ -13,215 +11,264 @@ namespace fs = std::filesystem; -namespace dropshell { +namespace dropshell +{ - bool SIvalid(const LocalServiceInfo& service_info) { - return !service_info.service_name.empty() && - !service_info.template_name.empty() && - !service_info.local_service_path.empty() && +#pragma 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 get_server_services_info(const std::string& server_name) { - std::vector services; - - if (server_name.empty()) - return services; - - std::vector local_server_definition_paths = gConfig().get_local_server_definition_paths(); - if (local_server_definition_paths.empty()) { - std::cerr << "Error: No local server definition paths found" << std::endl; - std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl; - return services; - } - - for (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; -} - - -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(); - - 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)) + std::vector get_server_services_info(const std::string &server_name) { - std::cerr << "Error: Service directory not found: " << service.local_service_path << std::endl; - return LocalServiceInfo(); - } + std::vector services; - // now set the template name and path. - std::map 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()) { - std::cerr << "Error: TEMPLATE variable not defined in service " << service_name << " on server " << server_name << std::endl; - return LocalServiceInfo(); - } - service.template_name = it->second; + if (server_name.empty()) + return services; - template_info tinfo = gTemplateManager().get_template_info(service.template_name); - if (!tinfo.is_set()) { - std::cerr << "Error: Template '" << service.template_name << "' not found" << std::endl; - return LocalServiceInfo(); - } + std::vector local_server_definition_paths = gConfig().get_local_server_definition_paths(); + if (local_server_definition_paths.empty()) + { + std::cerr << "Error: No local server definition paths found" << std::endl; + std::cerr << "Run 'dropshell edit' to configure DropShell" << std::endl; + return services; + } - // find the template path - service.local_template_path = tinfo.local_template_path(); - - return service; -} - -std::set get_used_commands(const std::string &server_name, const std::string &service_name) -{ - std::set 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()) { - std::cerr << "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 list_backups(const std::string &server_name, const std::string &service_name) -{ - std::set 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()) { - std::cerr << "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) + 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)) { - backups.insert(entry.path().filename().string()); - } + 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; } - return backups; -} - - -bool get_all_service_env_vars(const std::string &server_name, const std::string &service_name, std::map & all_env_vars) -{ - all_env_vars.clear(); - - if (localpath::service(server_name, service_name).empty() || !fs::exists(localpath::service(server_name, service_name))) + bool get_bool_variable(const std::map &variables, const std::string &variable_name) { - std::cerr << "Error: Service not found: " << service_name << " on server " << server_name << std::endl; - return false; - } - - server_config server_info(server_name); - if (server_info.get_SSH_HOST().empty()) - std::cerr << "Error: Server " << server_name << " not found - ssh_host empty, so HOST_NAME not set" << std::endl; - - std::string user = server_config::get_user_for_service(server_name, service_name); - - // add in some handy variables. - // if we change these, we also need to update agent/_allservicesstatus.sh - all_env_vars["CONFIG_PATH"] = remotepath(server_name,user).service_config(service_name); - all_env_vars["SERVER"] = server_name; - all_env_vars["SERVICE"] = service_name; - all_env_vars["AGENT_PATH"] = remotepath(server_name,user).agent(); - all_env_vars["HOST_NAME"] = server_info.get_SSH_HOST(); - all_env_vars["DOCKER_CLI_HINTS"] = "false"; // turn off docker junk. - all_env_vars["SSH_USER"] = user; - - // 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 env_vars; - envmanager env_manager(file); - env_manager.load(); - env_manager.get_all_variables(env_vars); - all_env_vars.merge(env_vars); + auto it = variables.find(variable_name); + if (it == variables.end()) + { + error << "Variable " << variable_name << " not found in the service .template_info.env file" << std::endl; + return false; } - else - warning << "Expected environment file not found: " << file << std::endl; - }; - - // Load environment files - load_env_file(localfile::service_env(server_name, service_name)); - load_env_file(localfile::template_info_env(server_name, service_name)); - - // determine template name. - auto it = all_env_vars.find("TEMPLATE"); - if (it == all_env_vars.end()) { - 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 service.env file and the .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()) { - std::cerr << "Error: Template '" << it->second << "' not found" << std::endl; - return false; + return it->second == "true"; } - std::string default_env_file = tinfo.local_template_path()/"_default.env"; - if (!fs::exists(default_env_file)) { - std::cerr << "Error: Template default env file '" << default_env_file << "' not found" << std::endl; - return false; + 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(); + + 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)) + { + std::cerr << "Error: Service directory not found: " << service.local_service_path << std::endl; + return LocalServiceInfo(); + } + + // now set the template name and path. + std::map 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 << "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 << "TEMPLATE_HASH variable not defined in service " << service_name << " on server " << server_name << std::endl; + return LocalServiceInfo(); + } + uint64_t service_template_hash = std::stoull(it->second); + service.service_template_hash_match = (service_template_hash == tinfo.hash()); + } + + return service; } - load_env_file(default_env_file); - return true; -} + std::set get_used_commands(const std::string &server_name, const std::string &service_name) + { + std::set 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()) + { + std::cerr << "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 list_backups(const std::string &server_name, const std::string &service_name) + { + std::set 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()) + { + std::cerr << "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 &all_env_vars) + { + all_env_vars.clear(); + + if (localpath::service(server_name, service_name).empty() || !fs::exists(localpath::service(server_name, service_name))) + { + std::cerr << "Error: Service not found: " << service_name << " 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 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; + }; + + // 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 int he service.env file." << std::endl; + info << "Please check " << localfile::service_env(server_name, service_name) << std::endl; + return false; + } + + // add in some handy variables. + // if we change these, we also need to update agent/_allservicesstatus.sh + all_env_vars["CONFIG_PATH"] = remotepath(server_name, user).service_config(service_name); + all_env_vars["SERVER"] = server_name; + all_env_vars["SERVICE"] = service_name; + all_env_vars["AGENT_PATH"] = remotepath(server_name, user).agent(); + all_env_vars["DOCKER_CLI_HINTS"] = "false"; // turn off docker junk. + + // 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 service.env file and the .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()) + { + std::cerr << "Error: Template '" << it->second << "' not found" << std::endl; + return false; + } + + return true; + } } // namespace dropshell diff --git a/source/src/services.hpp b/source/src/services.hpp index 3817a51..659a824 100644 --- a/source/src/services.hpp +++ b/source/src/services.hpp @@ -14,6 +14,10 @@ namespace dropshell { 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,6 +32,7 @@ namespace dropshell { // list all backups for a given service (across all servers) std::set list_backups(const std::string& server_name, const std::string& service_name); + } // namespace dropshell #endif diff --git a/source/src/templates.cpp b/source/src/templates.cpp index 4ffb122..3a2a07f 100644 --- a/source/src/templates.cpp +++ b/source/src/templates.cpp @@ -13,6 +13,7 @@ #include "utils/utils.hpp" #include "templates.hpp" #include "config.hpp" + #include "utils/hash.hpp" namespace dropshell { @@ -307,7 +308,6 @@ std::vector required_files = { "config/service.env", "config/.template_info.env", - "_default.env", "install.sh", "uninstall.sh" }; @@ -376,8 +376,16 @@ 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()) + 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 diff --git a/source/src/templates.hpp b/source/src/templates.hpp index 7b2bbb4..9509ec0 100644 --- a/source/src/templates.hpp +++ b/source/src/templates.hpp @@ -26,12 +26,15 @@ class template_info { 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 { diff --git a/source/src/utils/directories.cpp b/source/src/utils/directories.cpp index fc65fab..6e3294c 100644 --- a/source/src/utils/directories.cpp +++ b/source/src/utils/directories.cpp @@ -154,7 +154,6 @@ namespace localpath { // |-- config // |-- service.env (actual service config) // |-- template - // |-- _default.env // |-- (script files) // |-- config // |-- service.env (default service config) diff --git a/source/src/utils/directories.hpp b/source/src/utils/directories.hpp index d03a282..12a9456 100644 --- a/source/src/utils/directories.hpp +++ b/source/src/utils/directories.hpp @@ -30,13 +30,10 @@ namespace dropshell { // | |-- .json // | |-- // | |-- (...script files...) - // | |-- _default.env // | |-- config // | |-- service.env // | |-- .template_info.env // | |-- (...other service config files...) - // |-- remote_versions - // | |-- server_name-service_name.json // server_definition_path // |-- @@ -92,7 +89,6 @@ namespace dropshell { // |-- service.env (actual service config) // |-- .template_info.env // |-- template - // |-- _default.env // |-- (script files) // |-- config // |-- service.env (default service config)