From 65f592be91b45cc1ff642c106f0f45f99689ed6d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 24 Aug 2017 10:37:28 -0700 Subject: [PATCH] Revert "Revert PCP-769" --- CMakeLists.txt | 2 +- README.md | 5 + acceptance/lib/pxp-agent/task_helper.rb | 14 +- acceptance/tests/tasks/run_echo.rb | 4 +- acceptance/tests/tasks/run_ps1.rb | 8 +- acceptance/tests/tasks/run_puppet.rb | 9 +- acceptance/tests/tasks/run_ruby.rb | 9 +- lib/CMakeLists.txt | 1 + lib/inc/pxp-agent/modules/task.hpp | 10 + lib/src/configuration.cc | 41 +++- lib/src/modules/task.cc | 186 +++++++++++++++- lib/src/request_processor.cc | 4 + .../unparseable.bat | 0 .../init | 0 .../printer.bat | 0 .../error.bat | 0 .../printer | 0 .../unparseable | 0 .../error | 0 .../init.bat | 0 lib/tests/unit/configuration_test.cc | 52 ++++- lib/tests/unit/modules/task_test.cc | 200 +++++++++++++++--- locales/pxp-agent.pot | 38 ++++ 23 files changed, 516 insertions(+), 67 deletions(-) rename lib/tests/resources/{tasks => tasks-cache/0d75633b5dd4b153496b4593e9d94e69265d2a812579f724ba0b4422b0bfb836}/unparseable.bat (100%) mode change 100644 => 100755 rename lib/tests/resources/{tasks => tasks-cache/15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13}/init (100%) rename lib/tests/resources/{tasks => tasks-cache/1c616ed98f54880444d0c49036cdf930120457c20e7a9a204db750f2d6162999}/printer.bat (100%) mode change 100644 => 100755 rename lib/tests/resources/{tasks => tasks-cache/554f86a33add88c371c2bbb79839c9adfd3d420dc5f405a07e97fab54efbe1ba}/error.bat (100%) mode change 100644 => 100755 rename lib/tests/resources/{tasks => tasks-cache/936e85a9b7f1e7b4b593c9f051a36105ed36f7fb8dcff67ff23a3a9af2abe962}/printer (100%) rename lib/tests/resources/{tasks => tasks-cache/d2795e0a1b66ca75be9e2be25c2a61fdbab9efc641f8e480f5ab1b348112701d}/unparseable (100%) rename lib/tests/resources/{tasks => tasks-cache/d5b8819b51ecd53b32de74c09def0e71f617076bc8e4f75e1eac99b8f77a6c70}/error (100%) rename lib/tests/resources/{tasks => tasks-cache/e1c10f8c709f06f4327ac6a07a918e297a039a24a788fabf4e2ebc31d16e8dc3}/init.bat (100%) mode change 100644 => 100755 diff --git a/CMakeLists.txt b/CMakeLists.txt index a90e0ee0..5230d584 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,7 @@ if ("${INSTALL_IS_SYSTEM_DIR}" STREQUAL "-1") endif() # Find libraries -set(LEATHERMAN_COMPONENTS locale nowide catch logging rapidjson json_container util file_util execution) +set(LEATHERMAN_COMPONENTS locale nowide catch logging rapidjson json_container util file_util execution curl) if (WIN32) list(APPEND LEATHERMAN_COMPONENTS windows) diff --git a/README.md b/README.md index 43cd3cee..01388fb6 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,11 @@ Build cmake -DCMAKE_BUILD_TYPE=Debug -DDEV_LOG_COLOR=ON .. make + NOTE: If the versions of OpenSSL and libcurl conflict with each other, curl may fail + to load SSL files. On macOS this is common when using Homebrew. Invoke cmake with the + following commands to use the Homebrew versions of OpenSSL and libcurl: + cmake -DCMAKE_PREFIX_PATH="/usr/local/opt/openssl;/usr/local/opt/curl" .. + Usage ----- diff --git a/acceptance/lib/pxp-agent/task_helper.rb b/acceptance/lib/pxp-agent/task_helper.rb index 825b198e..4f9c204f 100644 --- a/acceptance/lib/pxp-agent/task_helper.rb +++ b/acceptance/lib/pxp-agent/task_helper.rb @@ -1,9 +1,10 @@ require 'pxp-agent/test_helper.rb' require 'json' +require 'digest' # Runs a task on targets, and passes the output to the block for validation # Block should expect a map parsed from the JSON output -def run_task(broker, targets, task, filename, input = {}, max_retries = 30, query_interval = 1, &block) +def run_task(broker, targets, task, filename, sha256, input = {}, max_retries = 30, query_interval = 1, &block) target_identities = targets.map {|agent| "pcp://#{agent}/agent"} provisional_responses = @@ -14,7 +15,7 @@ def run_task(broker, targets, task, filename, input = {}, max_retries = 30, quer :input => input, :files => [{:uri => {:path => "/#{task}/tasks/#{filename}", :params => {}}, :filename => filename, - :sha256 => 'tbd'}]}) + :sha256 => sha256}]}) rescue => exception fail("Exception occurred when trying to run task '#{task}' on all agents: #{exception.message}") end @@ -33,12 +34,13 @@ def run_task(broker, targets, task, filename, input = {}, max_retries = 30, quer def create_task_on(agent, mod, task, body) if agent['platform'] =~ /win/ - tasks_path = 'C:/ProgramData/PuppetLabs/pxp-agent/tasks' + tasks_path = 'C:/ProgramData/PuppetLabs/pxp-agent/tasks-cache' else - tasks_path = '/opt/puppetlabs/pxp-agent/tasks' + tasks_path = '/opt/puppetlabs/pxp-agent/tasks-cache' end - task_path = "#{tasks_path}/#{mod}/tasks" + sha256 = Digest::SHA256.hexdigest(body+"\n") + task_path = "#{tasks_path}/#{sha256}" on agent, "mkdir -p #{task_path}" create_remote_file(agent, "#{task_path}/#{task}", body) @@ -47,4 +49,6 @@ def create_task_on(agent, mod, task, body) teardown do on agent, "rm -rf #{task_path}" end + + sha256 end diff --git a/acceptance/tests/tasks/run_echo.rb b/acceptance/tests/tasks/run_echo.rb index b694fde5..37004fdd 100644 --- a/acceptance/tests/tasks/run_echo.rb +++ b/acceptance/tests/tasks/run_echo.rb @@ -21,12 +21,12 @@ task_body = "#!/bin/sh\necho $PT_message" end - create_task_on(agent, 'echo', 'init.bat', task_body) + @sha256 = create_task_on(agent, 'echo', 'init.bat', task_body) end end step 'Run echo task on agent hosts' do - run_task(master, agents, 'echo', 'init.bat', {:message => 'hello'}) do |stdout| + run_task(master, agents, 'echo', 'init.bat', @sha256, {:message => 'hello'}) do |stdout| assert_equal('hello', stdout.strip, "Output did not contain 'hello'") end end # test step diff --git a/acceptance/tests/tasks/run_ps1.rb b/acceptance/tests/tasks/run_ps1.rb index 97d02d43..00ab6167 100644 --- a/acceptance/tests/tasks/run_ps1.rb +++ b/acceptance/tests/tasks/run_ps1.rb @@ -20,15 +20,13 @@ step 'Create powershell task on agent hosts' do windows_hosts.each do |agent| - create_task_on(agent, 'echo', 'init.ps1', <<-EOF) -foreach ($i in $input) { Write-Output $i } -Write-Output $env:PT_data -EOF + task_body = "foreach ($i in $input) { Write-Output $i }\nWrite-Output $env:PT_data"; + @sha256 = create_task_on(agent, 'echo', 'init.ps1', task_body) end end step 'Run powershell task on Windows agent hosts' do - run_task(master, windows_hosts, 'echo', 'init.ps1', {:data => [1, 2, 3]}) do |stdout| + run_task(master, windows_hosts, 'echo', 'init.ps1', @sha256, {:data => [1, 2, 3]}) do |stdout| json, data = stdout.delete("\r").split("\n") assert_equal('{"data":[1,2,3]}', json, "Output did not contain 'data'") assert_equal('[1,2,3]', data, "Output did not contain 'data'") diff --git a/acceptance/tests/tasks/run_puppet.rb b/acceptance/tests/tasks/run_puppet.rb index b72c0b5c..85e362d5 100644 --- a/acceptance/tests/tasks/run_puppet.rb +++ b/acceptance/tests/tasks/run_puppet.rb @@ -32,15 +32,14 @@ shebang = '#!/opt/puppetlabs/bin/puppet apply' end - create_task_on(agent, 'hello', 'init.pp', <<-EOF) -#{shebang} -notify { 'hello': } -EOF + task_body = "#{shebang}\nnotify { 'hello': }" + + @sha256 = create_task_on(agent, 'hello', 'init.pp', task_body) end end step 'Run puppet task on agent hosts' do - run_task(master, agents, 'hello', 'init.pp', {:data => [1, 2, 3]}) do |stdout| + run_task(master, agents, 'hello', 'init.pp', @sha256, {:data => [1, 2, 3]}) do |stdout| assert_match(/Notify\[hello\]\/message: defined 'message' as 'hello'/, stdout, "Output did not contain 'hello'") end end # test step diff --git a/acceptance/tests/tasks/run_ruby.rb b/acceptance/tests/tasks/run_ruby.rb index 2eca60d5..a0d4f2f5 100644 --- a/acceptance/tests/tasks/run_ruby.rb +++ b/acceptance/tests/tasks/run_ruby.rb @@ -15,16 +15,13 @@ step 'Create ruby task on agent hosts' do agents.each do |agent| - create_task_on(agent, 'echo', 'init.rb', <<-EOF) -#!/opt/puppetlabs/puppet/bin/ruby -puts STDIN.gets -puts ENV['PT_data'] -EOF + task_body = "#!/opt/puppetlabs/puppet/bin/ruby\nputs STDIN.gets\nputs ENV['PT_data']" + @sha256 = create_task_on(agent, 'echo', 'init.rb', task_body) end end step 'Run ruby task on agent hosts' do - run_task(master, agents, 'echo', 'init.rb', {:data => [1, 2, 3]}) do |stdout| + run_task(master, agents, 'echo', 'init.rb', @sha256, {:data => [1, 2, 3]}) do |stdout| json, data = stdout.delete("\r").split("\n") assert_equal('{"data":[1,2,3]}', json, "Output did not contain 'data'") assert_equal('[1,2,3]', data, "Output did not contain 'data'") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 13c5ffe3..73a7b824 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -9,6 +9,7 @@ include_directories( ${HORSEWHISPERER_INCLUDE_DIRS} ${INIH_INCLUDE_DIRS} ${cpp-pcp-client_INCLUDE_DIR} + ${OPENSSL_INCLUDE_DIR} ) set(LIBRARY_COMMON_SOURCES diff --git a/lib/inc/pxp-agent/modules/task.hpp b/lib/inc/pxp-agent/modules/task.hpp index f9d55455..4e109e80 100644 --- a/lib/inc/pxp-agent/modules/task.hpp +++ b/lib/inc/pxp-agent/modules/task.hpp @@ -5,6 +5,8 @@ #include #include +#include + namespace PXPAgent { namespace Modules { @@ -17,6 +19,10 @@ class Task : public PXPAgent::Module { public: Task(const boost::filesystem::path& exec_prefix, const std::string& task_cache_dir, + const std::vector& master_uris, + const std::string& ca, + const std::string& crt, + const std::string& key, const std::string& spool_dir); /// Whether or not the module supports non-blocking / asynchronous requests. @@ -38,6 +44,10 @@ class Task : public PXPAgent::Module { std::string task_cache_dir_, wrapper_executable_; + std::vector master_uris_; + + leatherman::curl::client client_; + void callBlockingAction( const ActionRequest& request, const TaskCommand &command, diff --git a/lib/src/configuration.cc b/lib/src/configuration.cc index 5e75a969..a54897bf 100644 --- a/lib/src/configuration.cc +++ b/lib/src/configuration.cc @@ -84,8 +84,7 @@ namespace lth_util = leatherman::util; } }(); - // TODO(task) switch back to tasks-cache when downloading is implemented. - static const std::string DEFAULT_TASK_CACHE_DIR { (DATA_DIR / "tasks").string() }; + static const std::string DEFAULT_TASK_CACHE_DIR { (DATA_DIR / "tasks-cache").string() }; #else static const fs::path DEFAULT_CONF_DIR { "/etc/puppetlabs/pxp-agent" }; const std::string DEFAULT_SPOOL_DIR { "/opt/puppetlabs/pxp-agent/spool" }; @@ -93,8 +92,7 @@ namespace lth_util = leatherman::util; static const std::string DEFAULT_LOG_FILE { "/var/log/puppetlabs/pxp-agent/pxp-agent.log" }; static const std::string DEFAULT_PCP_ACCESS_FILE { "/var/log/puppetlabs/pxp-agent/pcp-access.log" }; static const std::string DEFAULT_MODULES_DIR { "/opt/puppetlabs/pxp-agent/modules" }; - // TODO(task) switch back to tasks-cache when downloading is implemented. - static const std::string DEFAULT_TASK_CACHE_DIR { "/opt/puppetlabs/pxp-agent/tasks" }; + static const std::string DEFAULT_TASK_CACHE_DIR { "/opt/puppetlabs/pxp-agent/tasks-cache" }; #endif static const std::string DEFAULT_MODULES_CONF_DIR { @@ -330,7 +328,7 @@ const Configuration::Agent& Configuration::getAgentConfiguration() const HW::GetFlag("spool-dir"), HW::GetFlag("spool-dir-purge-ttl"), HW::GetFlag("modules-config-dir"), - DEFAULT_TASK_CACHE_DIR, + HW::GetFlag("task-cache-dir"), AGENT_CLIENT_TYPE, HW::GetFlag("connection-timeout") * 1000, static_cast(HW::GetFlag("association-timeout")), @@ -582,6 +580,16 @@ void Configuration::defineDefaultValues() Types::String, DEFAULT_MODULES_CONF_DIR) } }); + defaults_.insert( + Option { "task-cache-dir", + Base_ptr { new Entry( + "task-cache-dir", + "", + lth_loc::format("Tasks cache directory, default: {1}", + DEFAULT_TASK_CACHE_DIR), + Types::String, + DEFAULT_TASK_CACHE_DIR) } }); + defaults_.insert( Option { "spool-dir", Base_ptr { new Entry( @@ -917,6 +925,29 @@ void Configuration::validateAndNormalizeOtherSettings() HW::SetFlag(option, val_path.string()); } + // Create the task-cache-dir if needed and ensure that we + // have the required user rwx, and group rx permissions. + const auto task_cache_dir = HW::GetFlag("task-cache-dir"); + + if (task_cache_dir.empty()) { + throw Configuration::Error { + lth_loc::translate("task-cache-dir must be defined") }; + } + + fs::path task_cache_dir_path { lth_file::tilde_expand(task_cache_dir) }; + check_and_create_dir(task_cache_dir_path, "task-cache-dir", true); +#ifndef _WIN32 + try { + fs::permissions(task_cache_dir_path, NIX_DIR_PERMS); + } catch (const fs::filesystem_error& e) { + throw Configuration::Error { + lth_loc::format("Failed to make the task-cache-dir '{1}' user/group readable and executable, and user writable during configuration validation: {2}", + task_cache_dir_path.string(), e.what()) }; + } +#endif + + HW::SetFlag("task-cache-dir", task_cache_dir_path.string()); + // Create the spool-dir if needed and ensure we can write in it const auto spool_dir = HW::GetFlag("spool-dir"); diff --git a/lib/src/modules/task.cc b/lib/src/modules/task.cc index 98ae83d7..1ec64c65 100644 --- a/lib/src/modules/task.cc +++ b/lib/src/modules/task.cc @@ -6,21 +6,30 @@ #include #include +#include +#include +#include #include #define LEATHERMAN_LOGGING_NAMESPACE "puppetlabs.pxp_agent.modules.task" #include +#include +#include + namespace PXPAgent { namespace Modules { namespace fs = boost::filesystem; +namespace alg = boost::algorithm; +namespace boost_error = boost::system::errc; namespace lth_exec = leatherman::execution; namespace lth_file = leatherman::file_util; namespace lth_jc = leatherman::json_container; namespace lth_loc = leatherman::locale; +namespace lth_curl = leatherman::curl; static const std::string TASK_RUN_ACTION { "run" }; @@ -95,10 +104,15 @@ static const std::map> BUIL Task::Task(const fs::path& exec_prefix, const std::string& task_cache_dir, + const std::vector& master_uris, + const std::string& ca, + const std::string& crt, + const std::string& key, const std::string& spool_dir) : storage_ { spool_dir }, task_cache_dir_ { task_cache_dir }, - wrapper_executable_ { (exec_prefix / TASK_WRAPPER_EXECUTABLE).string() } + wrapper_executable_ { (exec_prefix / TASK_WRAPPER_EXECUTABLE).string() }, + master_uris_ { master_uris } { module_name = "task"; actions.push_back(TASK_RUN_ACTION); @@ -108,6 +122,10 @@ Task::Task(const fs::path& exec_prefix, input_validator_.registerSchema(input_schema); results_validator_.registerSchema(output_schema); + + client_.set_ca_cert(ca); + client_.set_client_cert(crt, key); + client_.set_supported_protocols(CURLPROTO_HTTPS); } static void addParametersToEnvironment(const lth_jc::JsonContainer &input, std::map &environment) @@ -128,11 +146,162 @@ static TaskCommand getTaskCommand(const fs::path &task_executable) return TaskCommand { task_executable.string(), { } }; } +// Converts boost filesystem errors to a task_error object. +static Module::ProcessingError toModuleProcessingError(const fs::filesystem_error& e) { + auto err_code = e.code(); + if (err_code == boost_error::no_such_file_or_directory) { + return Module::ProcessingError(lth_loc::format("No such file or directory: {1}", e.path1())); + } else if (err_code == boost_error::file_exists) { + return Module::ProcessingError(lth_loc::format("A file matching the name of the provided sha already exists")); + } else { + return Module::ProcessingError(e.what()); + } +} + +#ifndef _WIN32 +static const fs::perms NIX_TASK_FILE_PERMS = NIX_DIR_PERMS; +#endif + +// Creates the / directory for a single +// task, ensuring that its permissions are readable by +// the PXP agent owner/group (for unix OSes), writable for the PXP agent owner, +// and executable by both PXP agent owner and group. Returns the path to this directory. +// Note that the last modified time of the directory is updated, and that this routine +// will not fail if the directory already exists. +static fs::path createCacheDir(const fs::path& task_cache_dir, const std::string& sha256) { + auto cache_dir = task_cache_dir / sha256; + fs::create_directory(cache_dir); + fs::last_write_time(cache_dir, time(nullptr)); +#ifndef _WIN32 + fs::permissions(cache_dir, NIX_DIR_PERMS); +#endif + return cache_dir; +} + +// Computes the sha256 of the file denoted by path. Assumes that +// the file designated by "path" exists. +static std::string calculateSha256(const std::string& path) { + const std::streamsize CHUNK_SIZE = 2 << 14; + char buffer[CHUNK_SIZE]; + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + + auto md = EVP_sha256(); + auto mdctx = EVP_MD_CTX_create(); + EVP_DigestInit_ex(mdctx, md, nullptr); + boost::nowide::ifstream ifs(path); + while (ifs.read(buffer, CHUNK_SIZE)) { + EVP_DigestUpdate(mdctx, buffer, CHUNK_SIZE); + } + if (!ifs.eof()) { + throw Module::ProcessingError(lth_loc::format("Error while reading {1}", path)); + } + EVP_DigestUpdate(mdctx, buffer, ifs.gcount()); + EVP_DigestFinal_ex(mdctx, md_value, &md_len); + EVP_MD_CTX_destroy(mdctx); + + char md_value_ascii[2*EVP_MAX_MD_SIZE+1] = {'\0'}; + constexpr size_t hexlen = 3; + for (unsigned int i = 0; i < md_len; ++i) { + snprintf(md_value_ascii+2*i, hexlen, "%02x", md_value[i]); + } + return std::string(md_value_ascii); +} + +static std::string createUrlEndpoint(const lth_jc::JsonContainer& uri) { + std::string url = uri.get("path"); + auto params = uri.getWithDefault("params", lth_jc::JsonContainer()); + if (params.empty()) { + return url; + } + auto curl_handle = lth_curl::curl_handle(); + url += "?"; + for (auto& key : params.keys()) { + auto escaped_key = std::string( + lth_curl::curl_escaped_string(curl_handle, key)); + auto escaped_val = std::string( + lth_curl::curl_escaped_string(curl_handle, params.get(key))); + url += escaped_key + "=" + escaped_val + "&"; + } + return url; +} + +// Downloads the file at the specified url into the provided path. If something goes +// wrong during the download, a PXP-error should be returned. Note that the provided +// "file_path" argument is a temporary file, call it "tempA". Leatherman.curl during +// the download method will create another temporary file, call it "tempB", to save +// the downloaded task file's contents in chunks before renaming it to "tempA." The +// rationale behind this solution is that: +// (1) After download, we still need to check "tempA" to ensure that its sha matches +// the provided sha. So the downloaded task is not quite a "valid" task after this +// method is called; it's still temporary. +// +// (2) It somewhat simplifies error handling if multiple threads try to download +// the same task file. +// The downloaded task file's permissions will be set to rwx for user and rx for +// group for non-Windows OSes. +static void downloadTaskFile(const std::vector& master_uris, + lth_curl::client& client, + const fs::path& file_path, + const lth_jc::JsonContainer& uri) { + auto url = master_uris[0] + createUrlEndpoint(uri); + lth_curl::request req(url); + // timeout from connection after one minute, can configure + req.connection_timeout(60000); +#ifndef _WIN32 + client.download_file(req, file_path.string(), NIX_TASK_FILE_PERMS); +#else + client.download_file(req, file_path.string()); +#endif +} + +// This method does the following. If the file matching the "filename" field of the +// file_obj JSON does not exist OR if its hash does not match the sha value in the +// "sha256" field of file_obj, then: +// (1) The file is downloaded using Leatherman.curl from the first entry in +// master-uris to a temporary directory. If this download fails, a PXP error +// is thrown. +// +// (2) If the downloaded file's sha does not match the provided sha, then a PXP +// error is returned. +// +// (3) If (1) and (2) both succeed, then the downloaded file is atomically +// renamed to cache_dir/ +static fs::path updateTaskFile(const std::vector& master_uris, + lth_curl::client& client, + const fs::path& cache_dir, + const lth_jc::JsonContainer& file) { + auto filename = file.get("filename"); + auto sha256 = file.get("sha256"); + auto filepath = cache_dir / filename; + + if (fs::exists(filepath) && sha256 == calculateSha256(filepath.string())) { +#ifndef _WIN32 + fs::permissions(filepath, NIX_TASK_FILE_PERMS); +#endif + return filepath; + } + + if (master_uris.empty()) { + throw Module::ProcessingError(lth_loc::format("Cannot download task. No master-uris were provided")); + } + auto tempname = cache_dir / fs::unique_path("temp_task_%%%%-%%%%-%%%%-%%%%"); + downloadTaskFile(master_uris, client, tempname, file.get("uri")); + if (sha256 != calculateSha256(tempname.string())) { + fs::remove(tempname); + throw Module::ProcessingError(lth_loc::format("The downloaded {1}'s sha differs from the provided sha", filename)); + } + fs::rename(tempname, filepath); + return filepath; +} + // Verify (this includes checking the SHA256 checksums) that *all* task files are present // in the task cache downloading them if necessary. // Return the full path of the cached version of the first file from the list (which // is assumed to be the task executable). -static fs::path getCachedTaskFile(const std::string& task_cache_dir, +static fs::path getCachedTaskFile(const fs::path& task_cache_dir, + const std::vector& master_uris, + lth_curl::client& client, const std::vector &files) { if (files.empty()) { throw Module::ProcessingError { @@ -141,9 +310,14 @@ static fs::path getCachedTaskFile(const std::string& task_cache_dir, auto file = files[0]; LOG_DEBUG("Verifying task file based on {1}", file.toString()); - // TODO task file verification and downloading - // This is a temporary hack to make existing workflows continue to work. - return { task_cache_dir + file.get({"uri", "path"}) }; + try { + auto cache_dir = createCacheDir(task_cache_dir, file.get("sha256")); + return updateTaskFile(master_uris, client, cache_dir, file); + } catch (lth_curl::http_file_download_exception& e) { + throw Module::ProcessingError(lth_loc::format("Downloading the task file {1} failed. Reason: {2}", file.get("filename"), e.what())); + } catch (fs::filesystem_error& e) { + throw toModuleProcessingError(e); + } } void Task::callBlockingAction( @@ -251,6 +425,8 @@ ActionResponse Task::callAction(const ActionRequest& request) auto task_command = getTaskCommand( getCachedTaskFile(task_cache_dir_, + master_uris_, + client_, task_execution_params.get>("files"))); ActionResponse response { ModuleType::Internal, request }; diff --git a/lib/src/request_processor.cc b/lib/src/request_processor.cc index 726541d6..c1a17582 100644 --- a/lib/src/request_processor.cc +++ b/lib/src/request_processor.cc @@ -825,6 +825,10 @@ void RequestProcessor::loadInternalModules(const Configuration::Agent& agent_con registerModule(new Modules::Ping); registerModule(new Modules::Task(Configuration::Instance().getExecPrefix(), agent_configuration.task_cache_dir, + agent_configuration.master_uris, + agent_configuration.ca, + agent_configuration.crt, + agent_configuration.key, spool_dir_path_.string())); } diff --git a/lib/tests/resources/tasks/unparseable.bat b/lib/tests/resources/tasks-cache/0d75633b5dd4b153496b4593e9d94e69265d2a812579f724ba0b4422b0bfb836/unparseable.bat old mode 100644 new mode 100755 similarity index 100% rename from lib/tests/resources/tasks/unparseable.bat rename to lib/tests/resources/tasks-cache/0d75633b5dd4b153496b4593e9d94e69265d2a812579f724ba0b4422b0bfb836/unparseable.bat diff --git a/lib/tests/resources/tasks/init b/lib/tests/resources/tasks-cache/15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13/init similarity index 100% rename from lib/tests/resources/tasks/init rename to lib/tests/resources/tasks-cache/15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13/init diff --git a/lib/tests/resources/tasks/printer.bat b/lib/tests/resources/tasks-cache/1c616ed98f54880444d0c49036cdf930120457c20e7a9a204db750f2d6162999/printer.bat old mode 100644 new mode 100755 similarity index 100% rename from lib/tests/resources/tasks/printer.bat rename to lib/tests/resources/tasks-cache/1c616ed98f54880444d0c49036cdf930120457c20e7a9a204db750f2d6162999/printer.bat diff --git a/lib/tests/resources/tasks/error.bat b/lib/tests/resources/tasks-cache/554f86a33add88c371c2bbb79839c9adfd3d420dc5f405a07e97fab54efbe1ba/error.bat old mode 100644 new mode 100755 similarity index 100% rename from lib/tests/resources/tasks/error.bat rename to lib/tests/resources/tasks-cache/554f86a33add88c371c2bbb79839c9adfd3d420dc5f405a07e97fab54efbe1ba/error.bat diff --git a/lib/tests/resources/tasks/printer b/lib/tests/resources/tasks-cache/936e85a9b7f1e7b4b593c9f051a36105ed36f7fb8dcff67ff23a3a9af2abe962/printer similarity index 100% rename from lib/tests/resources/tasks/printer rename to lib/tests/resources/tasks-cache/936e85a9b7f1e7b4b593c9f051a36105ed36f7fb8dcff67ff23a3a9af2abe962/printer diff --git a/lib/tests/resources/tasks/unparseable b/lib/tests/resources/tasks-cache/d2795e0a1b66ca75be9e2be25c2a61fdbab9efc641f8e480f5ab1b348112701d/unparseable similarity index 100% rename from lib/tests/resources/tasks/unparseable rename to lib/tests/resources/tasks-cache/d2795e0a1b66ca75be9e2be25c2a61fdbab9efc641f8e480f5ab1b348112701d/unparseable diff --git a/lib/tests/resources/tasks/error b/lib/tests/resources/tasks-cache/d5b8819b51ecd53b32de74c09def0e71f617076bc8e4f75e1eac99b8f77a6c70/error similarity index 100% rename from lib/tests/resources/tasks/error rename to lib/tests/resources/tasks-cache/d5b8819b51ecd53b32de74c09def0e71f617076bc8e4f75e1eac99b8f77a6c70/error diff --git a/lib/tests/resources/tasks/init.bat b/lib/tests/resources/tasks-cache/e1c10f8c709f06f4327ac6a07a918e297a039a24a788fabf4e2ebc31d16e8dc3/init.bat old mode 100644 new mode 100755 similarity index 100% rename from lib/tests/resources/tasks/init.bat rename to lib/tests/resources/tasks-cache/e1c10f8c709f06f4327ac6a07a918e297a039a24a788fabf4e2ebc31d16e8dc3/init.bat diff --git a/lib/tests/unit/configuration_test.cc b/lib/tests/unit/configuration_test.cc index bbffb33f..e32998e9 100644 --- a/lib/tests/unit/configuration_test.cc +++ b/lib/tests/unit/configuration_test.cc @@ -45,6 +45,9 @@ static const std::string MODULES_CONFIG_DIR { std::string { PXP_AGENT_ROOT_PATH + "/lib/tests/resources/modules_config" }; static const std::string SPOOL_DIR { std::string { PXP_AGENT_ROOT_PATH } + "/lib/tests/resources/test_spool" }; +static const std::string TASK_CACHE_DIR { std::string { PXP_AGENT_ROOT_PATH } + + "/lib/tests/resources/test_task_cache" }; + static const char* ARGV[] = { "test-command", @@ -57,14 +60,18 @@ static const char* ARGV[] = { "--modules-dir", MODULES_DIR.data(), "--modules-config-dir", MODULES_CONFIG_DIR.data(), "--spool-dir", SPOOL_DIR.data(), + "--task-cache-dir", TASK_CACHE_DIR.data(), "--foreground=true", nullptr }; -static const int ARGC = 19; +static const int ARGC = 21; static void configureTest() { if (!fs::exists(SPOOL_DIR) && !fs::create_directories(SPOOL_DIR)) { FAIL("Failed to create the results directory"); } + if (!fs::exists(TASK_CACHE_DIR) && !fs::create_directories(TASK_CACHE_DIR)) { + FAIL("Failed to create the task cache directory"); + } if (!fs::exists(MODULES_CONFIG_DIR) && !fs::create_directories(MODULES_CONFIG_DIR)) { FAIL("Failed to create the modules configuration directory"); } @@ -78,6 +85,9 @@ static void resetTest() { if (fs::exists(SPOOL_DIR)) { fs::remove_all(SPOOL_DIR); } + if (fs::exists(TASK_CACHE_DIR)) { + fs::remove_all(TASK_CACHE_DIR); + } if (fs::exists(MODULES_CONFIG_DIR)) { fs::remove_all(MODULES_CONFIG_DIR); } @@ -190,7 +200,7 @@ TEST_CASE("Configuration::get", "[configuration]") { SECTION("return the default value if the flag was not set") { // NB: ignoring --foreground in ARGV since argc is set to 19 - Configuration::Instance().parseOptions(18, const_cast(ARGV)); + Configuration::Instance().parseOptions(ARGC - 1, const_cast(ARGV)); #ifndef _WIN32 HW::SetFlag("pidfile", SPOOL_DIR + "/test.pid"); #endif @@ -310,6 +320,32 @@ TEST_CASE("Configuration::validate", "[configuration]") { Configuration::Error); } + SECTION("it fails when --task-cache-dir is empty") { + HW::SetFlag("task-cache-dir", ""); + REQUIRE_THROWS_AS(Configuration::Instance().validate(), + Configuration::Error); + } + + SECTION("it fails when --task-cache-dir exists but is not a directory") { + HW::SetFlag("task-cache-dir", CONFIG); + REQUIRE_THROWS_AS(Configuration::Instance().validate(), + Configuration::Error); + } + + SECTION("it creates --task-cache-dir when needed and with the right permissions") { + auto test_task_cache_dir = TASK_CACHE_DIR + "/testing_creation"; + HW::SetFlag("task-cache-dir", test_task_cache_dir); + + REQUIRE_FALSE(fs::exists(test_task_cache_dir)); + REQUIRE_NOTHROW(Configuration::Instance().validate()); + REQUIRE(fs::exists(test_task_cache_dir)); +#ifndef _WIN32 + REQUIRE(fs::status(test_task_cache_dir).permissions() == 0750); +#endif + + fs::remove_all(test_task_cache_dir); + } + SECTION("it fails when --spool-dir is empty") { HW::SetFlag("spool-dir", ""); REQUIRE_THROWS_AS(Configuration::Instance().validate(), @@ -344,9 +380,10 @@ TEST_CASE("Configuration::validate multiple brokers", "[configuration]") { "--modules-dir", MODULES_DIR.data(), "--modules-config-dir", MODULES_CONFIG_DIR.data(), "--spool-dir", SPOOL_DIR.data(), + "--task-cache-dir", TASK_CACHE_DIR.data(), "--foreground=true", nullptr }; - const int altArgc = 16; + const int altArgc = 18; lth_util::scope_exit config_cleaner { resetTest }; configureTest(); @@ -382,9 +419,10 @@ TEST_CASE("Configuration::parseOptions duplicate broker-ws-uris", "[configuratio "--modules-dir", MODULES_DIR.data(), "--modules-config-dir", MODULES_CONFIG_DIR.data(), "--spool-dir", SPOOL_DIR.data(), + "--task-cache-dir", TASK_CACHE_DIR.data(), "--foreground=true", nullptr }; - const int altArgc = 16; + const int altArgc = 18; lth_util::scope_exit config_cleaner { resetTest }; configureTest(); @@ -405,9 +443,10 @@ TEST_CASE("Configuration::validate bad broker-ws-uris", "[configuration]") { "--modules-dir", MODULES_DIR.data(), "--modules-config-dir", MODULES_CONFIG_DIR.data(), "--spool-dir", SPOOL_DIR.data(), + "--task-cache-dir", TASK_CACHE_DIR.data(), "--foreground=true", nullptr }; - const int altArgc = 16; + const int altArgc = 18; lth_util::scope_exit config_cleaner { resetTest }; configureTest(); @@ -429,9 +468,10 @@ TEST_CASE("Configuration::validate bad master-uris", "[configuration]") { "--modules-dir", MODULES_DIR.data(), "--modules-config-dir", MODULES_CONFIG_DIR.data(), "--spool-dir", SPOOL_DIR.data(), + "--task-cache-dir", TASK_CACHE_DIR.data(), "--foreground=true", nullptr }; - const int altArgc = 16; + const int altArgc = 18; lth_util::scope_exit config_cleaner { resetTest }; configureTest(); diff --git a/lib/tests/unit/modules/task_test.cc b/lib/tests/unit/modules/task_test.cc index 99c05644..faf7a92f 100644 --- a/lib/tests/unit/modules/task_test.cc +++ b/lib/tests/unit/modules/task_test.cc @@ -13,6 +13,7 @@ #include #include +#include #include @@ -37,7 +38,17 @@ static const std::string SPOOL_DIR { std::string { PXP_AGENT_ROOT_PATH } + "/lib/tests/resources/test_spool" }; static const std::string TASK_CACHE_DIR { std::string { PXP_AGENT_ROOT_PATH } - + "/lib/tests/resources/tasks" }; + + "/lib/tests/resources/tasks-cache" }; + +static const std::string TEMP_TASK_CACHE_DIR { TASK_CACHE_DIR + "/temp" }; + +static const std::vector MASTER_URIS { { "https://_mock_master_" } }; + +static const std::string CA { "mock_ca" }; + +static const std::string CRT { "mock_crt" }; + +static const std::string KEY { "mock_key" }; static const std::string NON_BLOCKING_ECHO_TXT { (NON_BLOCKING_DATA_FORMAT % "\"1988\"" @@ -46,7 +57,7 @@ static const std::string NON_BLOCKING_ECHO_TXT { % "{\"task\":\"foo\",\"input\":{\"message\":\"hello\"}," "\"files\":[{\"uri\":{\"path\":\"/init" EXTENSION "\"," "\"params\":{\"environment\":\"production\"}}," - "\"sha256\":\"d5296d3bd81d1245646f3467531349120519eb771b8e7a919b5fe3d3036537cb\"," + "\"sha256\":\"15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13\"," "\"filename\":\"init\"," "\"size_bytes\":131}]}" % "false").str() }; @@ -59,12 +70,12 @@ static const PCPClient::ParsedChunks NON_BLOCKING_CONTENT { TEST_CASE("Modules::Task", "[modules]") { SECTION("can successfully instantiate") { - REQUIRE_NOTHROW(Modules::Task(PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR)); + REQUIRE_NOTHROW(Modules::Task(PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR)); } } TEST_CASE("Modules::Task::hasAction", "[modules]") { - Modules::Task mod { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; + Modules::Task mod { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; SECTION("correctly reports false") { REQUIRE(!mod.hasAction("foo")); @@ -79,6 +90,9 @@ static void configureTest() { if (!fs::exists(SPOOL_DIR) && !fs::create_directories(SPOOL_DIR)) { FAIL("Failed to create the results directory"); } + if (!fs::exists(TEMP_TASK_CACHE_DIR) && !fs::create_directories(TEMP_TASK_CACHE_DIR)) { + FAIL("Failed to create the temporary tasks cache directory"); + } Configuration::Instance().initialize( [](std::vector) { return EXIT_SUCCESS; @@ -89,6 +103,36 @@ static void resetTest() { if (fs::exists(SPOOL_DIR)) { fs::remove_all(SPOOL_DIR); } + if (fs::exists(TEMP_TASK_CACHE_DIR)) { + fs::remove_all(TEMP_TASK_CACHE_DIR); + } +} + +TEST_CASE("Modules::Task::callAction", "[modules]") { + configureTest(); + lth_util::scope_exit config_cleaner { resetTest }; + + SECTION("throws module processing error when a file system error is thrown") { + Modules::Task e_m { PXP_AGENT_BIN_PATH, TEMP_TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto existent_sha = "existent"; + boost::nowide::ofstream(TEMP_TASK_CACHE_DIR + "/" + existent_sha); + auto task_txt = (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"existent\"}]}").str(); + PCPClient::ParsedChunks task_content { + lth_jc::JsonContainer(ENVELOPE_TXT), + lth_jc::JsonContainer(task_txt), + {}, + 0 }; + ActionRequest request { RequestType::Blocking, task_content }; + + auto response = e_m.executeAction(request); + REQUIRE_FALSE(response.action_metadata.includes("results")); + REQUIRE_FALSE(response.action_metadata.get("results_are_valid")); + REQUIRE(response.action_metadata.includes("execution_error")); + REQUIRE_THAT(response.action_metadata.get("execution_error"), Catch::EndsWith("A file matching the name of the provided sha already exists")); + } } TEST_CASE("Modules::Task::callAction - non blocking", "[modules]") { @@ -96,7 +140,7 @@ TEST_CASE("Modules::Task::callAction - non blocking", "[modules]") { lth_util::scope_exit config_cleaner { resetTest }; SECTION("the pid is written to file") { - Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; + Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; ActionRequest request { RequestType::NonBlocking, NON_BLOCKING_CONTENT }; fs::path spool_path { SPOOL_DIR }; auto results_dir = (spool_path / request.transactionId()).string(); @@ -121,11 +165,19 @@ TEST_CASE("Modules::Task::executeAction", "[modules][output]") { lth_util::scope_exit config_cleaner { resetTest }; SECTION("passes input as json") { - Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; - auto echo_txt = (DATA_FORMAT % "\"0632\"" - % "\"task\"" - % "\"run\"" - % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"uri\": {\"path\": \"/init" EXTENSION "\"}}]}").str(); + Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto echo_txt = +#ifndef _WIN32 + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"15f26bdeea9186293d256db95fed616a7b823de947f4e9bd0d8d23c5ac786d13\", \"filename\": \"init\"}]}").str(); +#else + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"e1c10f8c709f06f4327ac6a07a918e297a039a24a788fabf4e2ebc31d16e8dc3\", \"filename\": \"init.bat\"}]}").str(); +#endif PCPClient::ParsedChunks echo_content { lth_jc::JsonContainer(ENVELOPE_TXT), lth_jc::JsonContainer(echo_txt), @@ -139,11 +191,19 @@ TEST_CASE("Modules::Task::executeAction", "[modules][output]") { } SECTION("passes input as env variables") { - Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; - auto echo_txt = (DATA_FORMAT % "\"0632\"" - % "\"task\"" - % "\"run\"" - % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"uri\": {\"path\": \"/printer" EXTENSION "\"}}]}").str(); + Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto echo_txt = +#ifndef _WIN32 + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"936e85a9b7f1e7b4b593c9f051a36105ed36f7fb8dcff67ff23a3a9af2abe962\", \"filename\": \"printer\"}]}").str(); +#else + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\": \"1c616ed98f54880444d0c49036cdf930120457c20e7a9a204db750f2d6162999\", \"filename\": \"printer.bat\"}]}").str(); +#endif PCPClient::ParsedChunks echo_content { lth_jc::JsonContainer(ENVELOPE_TXT), lth_jc::JsonContainer(echo_txt), @@ -157,11 +217,19 @@ TEST_CASE("Modules::Task::executeAction", "[modules][output]") { } SECTION("succeeds on non-zero exit") { - Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; - auto echo_txt = (DATA_FORMAT % "\"0632\"" - % "\"task\"" - % "\"run\"" - % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"uri\": {\"path\": \"/error" EXTENSION "\"}}]}").str(); + Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto echo_txt = +#ifndef _WIN32 + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\" : \"d5b8819b51ecd53b32de74c09def0e71f617076bc8e4f75e1eac99b8f77a6c70\", \"filename\": \"error\"}]}").str(); +#else + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"input\":{\"message\":\"hello\"}, \"files\" : [{\"sha256\" : \"554f86a33add88c371c2bbb79839c9adfd3d420dc5f405a07e97fab54efbe1ba\", \"filename\": \"error.bat\"}]}").str(); +#endif PCPClient::ParsedChunks echo_content { lth_jc::JsonContainer(ENVELOPE_TXT), lth_jc::JsonContainer(echo_txt), @@ -182,12 +250,21 @@ TEST_CASE("Modules::Task::executeAction", "[modules][output]") { } SECTION("errors on unparseable output") { - Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, SPOOL_DIR }; - auto echo_txt = (DATA_FORMAT % "\"0632\"" - % "\"task\"" - % "\"run\"" - % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " - "\"files\" : [{\"uri\": {\"path\": \"/unparseable" EXTENSION "\"}}]}").str(); + Modules::Task e_m { PXP_AGENT_BIN_PATH, TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto echo_txt = +#ifndef _WIN32 + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " + "\"files\" : [{\"sha256\": \"d2795e0a1b66ca75be9e2be25c2a61fdbab9efc641f8e480f5ab1b348112701d\", \"filename\": \"unparseable\"}]}").str(); +#else + (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " + "\"files\" : [{\"sha256\": \"0d75633b5dd4b153496b4593e9d94e69265d2a812579f724ba0b4422b0bfb836\", \"filename\": \"unparseable.bat\"}]}").str(); +#endif PCPClient::ParsedChunks echo_content { lth_jc::JsonContainer(ENVELOPE_TXT), lth_jc::JsonContainer(echo_txt), @@ -206,6 +283,75 @@ TEST_CASE("Modules::Task::executeAction", "[modules][output]") { REQUIRE(response.output.std_err == ""); REQUIRE(response.output.exitcode == 0); } -} + SECTION("errors on download when no master-uri is provided") { + Modules::Task e_m { PXP_AGENT_BIN_PATH, TEMP_TASK_CACHE_DIR, {}, CA, CRT, KEY, SPOOL_DIR }; + auto task_txt = (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " + "\"files\" : [{\"sha256\": \"some_sha\", \"filename\": \"some_file\"}]}").str(); + PCPClient::ParsedChunks task_content { + lth_jc::JsonContainer(ENVELOPE_TXT), + lth_jc::JsonContainer(task_txt), + {}, + 0 }; + ActionRequest request { RequestType::Blocking, task_content }; + auto response = e_m.executeAction(request); + + REQUIRE_FALSE(response.action_metadata.includes("results")); + REQUIRE_FALSE(response.action_metadata.get("results_are_valid")); + REQUIRE(response.action_metadata.includes("execution_error")); + REQUIRE(boost::contains(response.action_metadata.get("execution_error"), "Cannot download task. No master-uris were provided")); + } + + SECTION("errors when a download error occurs and removes the temporary file") { + Modules::Task e_m { PXP_AGENT_BIN_PATH, TEMP_TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto cache = fs::path(TEMP_TASK_CACHE_DIR) / "some_sha"; + std::string bad_path = "bad_path"; + auto task_txt = (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " + "\"files\" : [{\"uri\": { \"path\": \"bad_path\", \"params\": { \"environment\": \"production\", \"code-id\": \"1234\" }}, \"sha256\": \"some_sha\", \"filename\": \"some_file\"}]}").str(); + PCPClient::ParsedChunks task_content { + lth_jc::JsonContainer(ENVELOPE_TXT), + lth_jc::JsonContainer(task_txt), + {}, + 0 }; + ActionRequest request { RequestType::Blocking, task_content }; + auto response = e_m.executeAction(request); + + REQUIRE_FALSE(response.action_metadata.includes("results")); + REQUIRE_FALSE(response.action_metadata.get("results_are_valid")); + REQUIRE(response.action_metadata.includes("execution_error")); + REQUIRE_THAT(response.action_metadata.get("execution_error"), Catch::Contains("Downloading the task file some_file failed")); + + // Make sure temp file is removed. + REQUIRE(fs::is_empty(cache)); + } + + SECTION("creates the tasks-cache/ directory with ower/group read and write permissions") { + Modules::Task e_m { PXP_AGENT_BIN_PATH, TEMP_TASK_CACHE_DIR, MASTER_URIS, CA, CRT, KEY, SPOOL_DIR }; + auto cache = fs::path(TEMP_TASK_CACHE_DIR) / "some_other_sha"; + auto task_txt = (DATA_FORMAT % "\"0632\"" + % "\"task\"" + % "\"run\"" + % "{\"task\": \"unparseable\", \"input\":{\"message\":\"hello\"}, " + "\"files\" : [{\"sha256\": \"some_other_sha\"}]}").str(); + PCPClient::ParsedChunks task_content { + lth_jc::JsonContainer(ENVELOPE_TXT), + lth_jc::JsonContainer(task_txt), + {}, + 0 }; + ActionRequest request { RequestType::Blocking, task_content }; + e_m.executeAction(request); + + REQUIRE(fs::exists(cache)); + REQUIRE(fs::is_directory(cache)); +#ifndef _WIN32 + REQUIRE(fs::status(cache).permissions() == 0750); +#endif + } +} } // namespace PXPAgent diff --git a/locales/pxp-agent.pot b/locales/pxp-agent.pot index e05fff87..95423856 100644 --- a/locales/pxp-agent.pot +++ b/locales/pxp-agent.pot @@ -274,6 +274,10 @@ msgstr "" msgid "Module config files directory, default: {1}" msgstr "" +#: lib/src/configuration.cc +msgid "Tasks cache directory, default: {1}" +msgstr "" + #: lib/src/configuration.cc msgid "Spool action results directory, default: {1}" msgstr "" @@ -357,6 +361,16 @@ msgstr "" msgid "the {1} '{2}' is not a directory" msgstr "" +#: lib/src/configuration.cc +msgid "task-cache-dir must be defined" +msgstr "" + +#: lib/src/configuration.cc +msgid "" +"Failed to make the task-cache-dir '{1}' user/group readable and executable, " +"and user writable during configuration validation: {2}" +msgstr "" + #: lib/src/configuration.cc msgid "spool-dir must be defined" msgstr "" @@ -637,6 +651,26 @@ msgstr "" msgid "debug entry is not valid JSON" msgstr "" +#: lib/src/modules/task.cc +msgid "No such file or directory: {1}" +msgstr "" + +#: lib/src/modules/task.cc +msgid "A file matching the name of the provided sha already exists" +msgstr "" + +#: lib/src/modules/task.cc +msgid "Error while reading {1}" +msgstr "" + +#: lib/src/modules/task.cc +msgid "Cannot download task. No master-uris were provided" +msgstr "" + +#: lib/src/modules/task.cc +msgid "The downloaded {1}'s sha differs from the provided sha" +msgstr "" + #: lib/src/modules/task.cc msgid "at least one file must be specified for a task" msgstr "" @@ -646,6 +680,10 @@ msgstr "" msgid "Verifying task file based on {1}" msgstr "" +#: lib/src/modules/task.cc +msgid "Downloading the task file {1} failed. Reason: {2}" +msgstr "" + #. warning #: lib/src/modules/task.cc msgid ""