diff --git a/src/CoreServer.cpp b/src/CoreServer.cpp index 8f5e7a2e5..2e9650ae9 100644 --- a/src/CoreServer.cpp +++ b/src/CoreServer.cpp @@ -203,6 +203,70 @@ void CoreServer::_setup_routes(const PrometheusConfig &prom_config) res.set_content(j.dump(), "text/json"); } }); + _svr.Get(fmt::format("/api/v1/taps/({})", AbstractModule::MODULE_ID_REGEX).c_str(), [&](const httplib::Request &req, httplib::Response &res) { + json j = json::object(); + auto name = req.matches[1]; + if (!_registry->tap_manager()->module_exists(name)) { + res.status = 404; + j["error"] = "tap does not exists"; + res.set_content(j.dump(), "text/json"); + return; + } + try { + auto [tap, lock] = _registry->tap_manager()->module_get_locked(name); + tap->info_json(j[name]); + res.set_content(j.dump(), "text/json"); + } catch (const std::exception &e) { + res.status = 500; + j["error"] = e.what(); + res.set_content(j.dump(), "text/json"); + } + }); + _svr.Post(R"(/api/v1/taps)", [&]([[maybe_unused]] const httplib::Request &req, httplib::Response &res) { + json j = json::object(); + if (!req.has_header("Content-Type")) { + res.status = 400; + j["error"] = "must include Content-Type header"; + res.set_content(j.dump(), "text/json"); + return; + } + auto content_type = req.get_header_value("Content-Type"); + if (content_type != "application/x-yaml" && content_type != "application/json") { + res.status = 400; + j["error"] = "Content-Type not supported"; + res.set_content(j.dump(), "text/json"); + return; + } + try { + auto taps = _registry->tap_manager()->load_from_str(req.body); + for (auto &mod : taps) { + mod->info_json(j[mod->name()]); + } + res.set_content(j.dump(), "text/json"); + } catch (const std::exception &e) { + res.status = 500; + j["error"] = e.what(); + res.set_content(j.dump(), "text/json"); + } + }); + _svr.Delete(fmt::format("/api/v1/taps/({})", AbstractModule::MODULE_ID_REGEX).c_str(), [&](const httplib::Request &req, httplib::Response &res) { + json j = json::object(); + auto name = req.matches[1]; + if (!_registry->tap_manager()->module_exists(name)) { + res.status = 404; + j["error"] = "tap does not exists"; + res.set_content(j.dump(), "text/json"); + return; + } + try { + _registry->tap_manager()->remove_tap(name); + res.set_content(j.dump(), "text/json"); + } catch (const std::exception &e) { + res.status = 500; + j["error"] = e.what(); + res.set_content(j.dump(), "text/json"); + } + }); // Policies _svr.Get(R"(/api/v1/policies)", [&]([[maybe_unused]] const httplib::Request &req, httplib::Response &res) { json j = json::object(); @@ -375,5 +439,4 @@ void CoreServer::_setup_routes(const PrometheusConfig &prom_config) } }); } - } \ No newline at end of file diff --git a/src/Taps.cpp b/src/Taps.cpp index 2f42ac354..9fbfb290c 100644 --- a/src/Taps.cpp +++ b/src/Taps.cpp @@ -12,12 +12,34 @@ namespace visor { +std::vector TapManager::load_from_str(const std::string &str) +{ + if (str.empty()) { + throw TapException("empty data"); + } + + YAML::Node node = YAML::Load(str); + + if (!node.IsMap() || !node["visor"]) { + throw TapException("invalid schema"); + } + if (!node["version"] || !node["version"].IsScalar() || node["version"].as() != "1.0") { + throw TapException("missing or unsupported version"); + } + if (node["visor"]["taps"] && node["visor"]["taps"].IsMap()) { + return load(node["visor"]["taps"], true); + } else { + throw TapException("no taps found in schema"); + } +} + // needs to be thread safe and transactional: any errors mean resources get cleaned up with no side effects -void TapManager::load(const YAML::Node &tap_yaml, bool strict) +std::vector TapManager::load(const YAML::Node &tap_yaml, bool strict) { assert(tap_yaml.IsMap()); assert(spdlog::get("visor")); + std::vector result; for (YAML::const_iterator it = tap_yaml.begin(); it != tap_yaml.end(); ++it) { if (!it->first.IsScalar()) { throw TapException("expecting tap identifier"); @@ -60,9 +82,21 @@ void TapManager::load(const YAML::Node &tap_yaml, bool strict) // will throw if it already exists. nothing else to clean up module_add(std::move(tap_module)); - + result.push_back(tap_module.get()); spdlog::get("visor")->info("tap [{}]: loaded, type {}", tap_name, input_type); } + + return result; +} + +void TapManager::remove_tap(const std::string &name) +{ + std::unique_lock lock(_map_mutex); + if (_map.count(name) == 0) { + throw ModuleException(name, fmt::format("module name '{}' does not exist", name)); + } + //TODO: add logic to remove policies that uses the specific deleted TAP + _map.erase(name); } std::string Tap::get_input_name(const Configurable &config, const Configurable &filter) diff --git a/src/Taps.h b/src/Taps.h index 3a57e8eeb..00ca8af66 100644 --- a/src/Taps.h +++ b/src/Taps.h @@ -83,7 +83,9 @@ class TapManager : public AbstractManager { } - void load(const YAML::Node &tap_yaml, bool strict); + std::vector load_from_str(const std::string &str); + std::vector load(const YAML::Node &tap_yaml, bool strict); + void remove_tap(const std::string &name); }; } \ No newline at end of file diff --git a/src/tests/test_policies.cpp b/src/tests/test_policies.cpp index 35e54d6e3..103d7898c 100644 --- a/src/tests/test_policies.cpp +++ b/src/tests/test_policies.cpp @@ -750,6 +750,24 @@ TEST_CASE("Policies", "[policies]") REQUIRE_THROWS_WITH(registry.policy_manager()->load(config_file["visor"]["policies"]), "expecting policy configuration map"); } + SECTION("Bad Config: empty data") + { + CoreRegistry registry; + REQUIRE_THROWS_WITH(registry.policy_manager()->load_from_str(""), "empty data"); + } + + SECTION("Bad Config: invalid schema") + { + CoreRegistry registry; + REQUIRE_THROWS_WITH(registry.policy_manager()->load_from_str("invalid: schema"), "invalid schema"); + } + + SECTION("Bad Config: missing version") + { + CoreRegistry registry; + REQUIRE_THROWS_WITH(registry.policy_manager()->load_from_str(policies_config_bad1), "missing or unsupported version"); + } + SECTION("Bad Config: invalid tap") { CoreRegistry registry; @@ -772,10 +790,9 @@ TEST_CASE("Policies", "[policies]") { CoreRegistry registry; registry.start(nullptr); - YAML::Node config_file = YAML::Load(policies_config_bad4); - REQUIRE_NOTHROW(registry.tap_manager()->load(config_file["visor"]["taps"], true)); - REQUIRE_THROWS_WITH(registry.policy_manager()->load(config_file["visor"]["policies"]), "policy [default_view] failed to start: mock error on start"); + REQUIRE_NOTHROW(registry.tap_manager()->load_from_str(policies_config_bad4)); + REQUIRE_THROWS_WITH(registry.policy_manager()->load_from_str(policies_config_bad4), "policy [default_view] failed to start: mock error on start"); } SECTION("Bad Config: mis-matched input_type on tap") diff --git a/src/tests/test_taps.cpp b/src/tests/test_taps.cpp index 5d28f0b00..582378153 100644 --- a/src/tests/test_taps.cpp +++ b/src/tests/test_taps.cpp @@ -45,6 +45,26 @@ version: "1.0" iface: en7 )"; +auto tap_config_bad_version = R"( +version: "2.0" + +visor: + taps: + wired: + input_type: nonexistent + config: + iface: en7 +)"; + +auto tap_config_bad_no_tap = R"( +version: "1.0" + +visor: + policies: + default: + input_type: nonexistent +)"; + TEST_CASE("Taps", "[taps]") { @@ -65,6 +85,34 @@ TEST_CASE("Taps", "[taps]") CHECK(tap->config_get("boolean") == true); } + SECTION("Good config, test remove tap and add again") + { + CoreRegistry registry; + registry.start(nullptr); + YAML::Node config_file = YAML::Load(tap_config); + + CHECK(config_file["visor"]["taps"]); + CHECK(config_file["visor"]["taps"].IsMap()); + CHECK_NOTHROW(registry.tap_manager()->load(config_file["visor"]["taps"], true)); + + auto [tap, lock] = registry.tap_manager()->module_get_locked("wired"); + CHECK(tap->name() == "wired"); + CHECK(tap->config_get("iface") == "en7"); + CHECK(tap->config_get("number") == 123); + CHECK(tap->config_get("boolean") == true); + lock.unlock(); + + REQUIRE_NOTHROW(registry.tap_manager()->remove_tap("wired")); + REQUIRE_NOTHROW(registry.tap_manager()->remove_tap("wireless")); + + CHECK_NOTHROW(registry.tap_manager()->load(config_file["visor"]["taps"], true)); + auto [new_tap, new_lock] = registry.tap_manager()->module_get_locked("wired"); + CHECK(new_tap->name() == "wired"); + CHECK(new_tap->config_get("iface") == "en7"); + CHECK(new_tap->config_get("number") == 123); + CHECK(new_tap->config_get("boolean") == true); + } + SECTION("Duplicate") { CoreRegistry registry; @@ -86,6 +134,34 @@ TEST_CASE("Taps", "[taps]") CHECK_THROWS(registry.tap_manager()->load(config_file["visor"]["taps"], true)); } + SECTION("Bad Config: empty data") + { + CoreRegistry registry; + registry.start(nullptr); + REQUIRE_THROWS_WITH(registry.tap_manager()->load_from_str(""), "empty data"); + } + + SECTION("Bad Config: invalid schema") + { + CoreRegistry registry; + registry.start(nullptr); + REQUIRE_THROWS_WITH(registry.tap_manager()->load_from_str("invalid: schema"), "invalid schema"); + } + + SECTION("Bad Config: invalid version") + { + CoreRegistry registry; + registry.start(nullptr); + REQUIRE_THROWS_WITH(registry.tap_manager()->load_from_str(tap_config_bad_version), "missing or unsupported version"); + } + + SECTION("Bad Config: no taps") + { + CoreRegistry registry; + registry.start(nullptr); + REQUIRE_THROWS_WITH(registry.tap_manager()->load_from_str(tap_config_bad_no_tap), "no taps found in schema"); + } + SECTION("Json validation") { CoreRegistry registry;