| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <variant> |
| |
| #include "net/proto2/contrib/parse_proto/parse_text_proto.h" |
| #include "gmock.h" |
| #include "gunit.h" |
| #include "absl/log/log.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/synchronization/notification.h" |
| #include "absl/time/time.h" |
| #include "absl/types/span.h" |
| #include "redfish_query_engine/http/codes.h" |
| #include "redfish_query_engine/redfish/test_mockup.h" |
| #include "redfish_query_engine/redfish/transport/grpc.h" |
| #include "redfish_query_engine/redfish/transport/grpc_tls_options.h" |
| #include "redfish_query_engine/redfish/transport/interface.h" |
| #include "grpcpp/channel.h" |
| #include "grpcpp/client_context.h" |
| #include "grpcpp/create_channel.h" |
| #include "grpcpp/security/credentials.h" |
| #include "grpcpp/support/client_callback.h" |
| #include "grpcpp/support/status.h" |
| #include "nlohmann/json.hpp" |
| #include "nlohmann/json_fwd.hpp" |
| #include "proxy.h" |
| #include "proxy_config.pb.h" |
| #include "redfish_passthrough_plugin.h" |
| #include "redfish_plugin.h" |
| #include "request_response.h" |
| #include "voyager/deferrable_priority_queue.hpp" |
| #include "voyager/voyager_telemetry.grpc.pb.h" |
| #include "voyager/voyager_telemetry.pb.h" |
| #include "thread/thread.h" |
| #include "thread/thread_options.h" |
| |
| namespace milotic { |
| |
| namespace { |
| |
| constexpr std::string_view kUdsFilename = "/tmp/test/grpc_proxy.socket"; |
| |
| using ::testing::Contains; |
| using ::testing::ElementsAre; |
| using ::testing::EqualsProto; |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::Pair; |
| using ::testing::StartsWith; |
| using ::testing::Test; |
| using ::testing::proto::Partially; |
| using ::testing::status::StatusIs; |
| |
| using ::google::protobuf::contrib::parse_proto::ParseTextProtoOrDie; |
| |
| class MockedProxyServerTest : public Test { |
| protected: |
| MockedProxyServerTest() = default; |
| |
| void SetUp() override { |
| ecclesia::StaticBufferBasedTlsOptions options; |
| int port; |
| |
| options.SetToInsecure(); |
| mockup_server_ = std::make_unique<ecclesia::TestingMockupServer>( |
| "barebones_session_auth/mockup.shar"); |
| port = std::get<ecclesia::TestingMockupServer::ConfigNetwork>( |
| mockup_server_->GetConfig()) |
| .port; |
| milotic_grpc_proxy::Plugin::RedfishPassthrough plugin_config; |
| |
| plugin_config.set_request_timeout_msec(30000); |
| plugin_config.set_connect_timeout_msec(5000); |
| plugin_config.set_dns_timeout_sec(60); |
| plugin_config.set_max_recv_speed(-1); |
| plugin_config.set_sse_low_speed_limit_bytes_per_sec(100); |
| plugin_config.set_sse_low_speed_time_sec(5); |
| |
| auto plugin = std::make_unique<RedfishPassthroughPlugin>(plugin_config); |
| std::unique_ptr<milotic::RedfishPlugin> plugins[] = {std::move(plugin)}; |
| proxy_ = std::make_unique<Proxy>( |
| milotic::Host::CreateTestHosts(absl::StrCat("localhost:", port)), |
| absl::MakeSpan(plugins), &queue_, Proxy::Resources{}, |
| ParseTextProtoOrDie(R"pb( |
| mappings: { |
| name: "match_all" |
| allow: { key: "all" } |
| resource_path: "" |
| with_subtree: true |
| } |
| )pb")); |
| |
| ASSERT_OK(proxy_->AddService( |
| milotic_grpc_proxy::RedfishV1Options::default_instance())); |
| |
| ASSERT_OK(proxy_->AddService( |
| milotic_grpc_proxy::VoyagerTelemetryOptions::default_instance())); |
| milotic_grpc_proxy::GrpcConfiguration config; |
| config.mutable_uds_endpoint()->set_path(kUdsFilename); |
| config.mutable_net_endpoint()->set_host("localhost"); |
| config.mutable_net_endpoint()->set_port(8889); |
| |
| ASSERT_OK(proxy_->ConfigGrpcAndStart( |
| config, [](const std::string& root) { return root == "/tmp"; })); |
| |
| auto transport = ecclesia::CreateGrpcRedfishTransport( |
| absl::StrCat("localhost:", 8889), {}, options.GetChannelCredentials()); |
| ASSERT_TRUE(transport.ok()); |
| client_ = std::move(*transport); |
| transport = ecclesia::CreateGrpcRedfishTransport( |
| absl::StrCat("unix://", kUdsFilename), {}, |
| options.GetChannelCredentials()); |
| ASSERT_TRUE(transport.ok()); |
| uds_client_ = std::move(*transport); |
| thread_.Start(); |
| } |
| |
| void TearDown() override { |
| queue_.Shutdown(true); |
| thread_.Join(); |
| } |
| |
| void RunQueue() { |
| while (queue_.ProcessQueue(1).ok()) { |
| } |
| } |
| |
| std::unique_ptr<ecclesia::RedfishTransport> client_; |
| std::unique_ptr<ecclesia::RedfishTransport> uds_client_; |
| std::unique_ptr<ecclesia::TestingMockupServer> mockup_server_; |
| std::unique_ptr<Proxy> proxy_; |
| voyager::DeferrablePriorityQueue queue_{1}; |
| MemberThread<MockedProxyServerTest> thread_{ |
| thread::Options().set_joinable(true), "backgroundQueue", this, |
| &MockedProxyServerTest::RunQueue}; |
| }; |
| |
| TEST_F(MockedProxyServerTest, TestPostPatchGet) { |
| absl::string_view data_post = R"json({ |
| "ChassisType": "RackMount", |
| "Name": "MyChassis" |
| })json"; |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_post = |
| client_->Post("/redfish/v1/Chassis", data_post); |
| ASSERT_TRUE(result_post.status().ok()) << result_post.status().message(); |
| EXPECT_EQ(result_post->code, |
| ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| |
| // Test Patch request |
| absl::string_view data_patch = R"json({ |
| "Name": "MyNewName" |
| })json"; |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_patch = |
| client_->Patch("/redfish/v1/Chassis/Member1", data_patch); |
| ASSERT_TRUE(result_patch.status().ok()) << result_patch.status().message(); |
| EXPECT_EQ(result_patch->code, |
| ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_get = |
| client_->Get("/redfish/v1/Chassis/Member1"); |
| LOG(WARNING) << result_get.status().message(); |
| ASSERT_TRUE(result_get.status().ok()) << result_get.status().message(); |
| ASSERT_TRUE(std::holds_alternative<nlohmann::json>(result_get->body)); |
| std::string name = std::get<nlohmann::json>(result_get->body)["Name"]; |
| EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK); |
| EXPECT_EQ(name, "MyNewName"); |
| EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK); |
| } |
| |
| TEST_F(MockedProxyServerTest, TestUdsPostPatchGet) { |
| absl::string_view data_post = R"json({ |
| "ChassisType": "RackMount", |
| "Name": "MyChassis" |
| })json"; |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_post = |
| uds_client_->Post("/redfish/v1/Chassis", data_post); |
| ASSERT_TRUE(result_post.status().ok()) << result_post.status().message(); |
| EXPECT_EQ(result_post->code, |
| ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| |
| // Test Patch request |
| absl::string_view data_patch = R"json({ |
| "Name": "MyNewName" |
| })json"; |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_patch = |
| uds_client_->Patch("/redfish/v1/Chassis/Member1", data_patch); |
| ASSERT_TRUE(result_patch.status().ok()) << result_patch.status().message(); |
| EXPECT_EQ(result_patch->code, |
| ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| |
| absl::StatusOr<ecclesia::RedfishTransport::Result> result_get = |
| uds_client_->Get("/redfish/v1/Chassis/Member1"); |
| LOG(WARNING) << result_get.status().message(); |
| ASSERT_TRUE(result_get.status().ok()) << result_get.status().message(); |
| ASSERT_TRUE(std::holds_alternative<nlohmann::json>(result_get->body)); |
| std::string name = std::get<nlohmann::json>(result_get->body)["Name"]; |
| EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK); |
| EXPECT_EQ(name, "MyNewName"); |
| EXPECT_EQ(result_get->code, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK); |
| } |
| |
| TEST_F(MockedProxyServerTest, TestVoyagerPostPatchGet) { |
| constexpr absl::string_view data_post = R"json({ |
| "ChassisType": "RackMount", |
| "Name": "MyChassis" |
| })json"; |
| |
| std::shared_ptr<grpc::Channel> channel = |
| grpc::CreateChannel(absl::StrCat("unix://", kUdsFilename), |
| grpc::InsecureChannelCredentials()); |
| std::unique_ptr<third_party_voyager::MachineTelemetry::Stub> stub( |
| third_party_voyager::MachineTelemetry::NewStub(channel)); |
| |
| third_party_voyager::SetRequest set_req; |
| set_req.set_req_id("test_post"); |
| third_party_voyager::RequestFqp* req_fqp = set_req.add_req_fqp(); |
| req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis"); |
| set_req.set_json(data_post); |
| |
| grpc::ClientContext post_context; |
| third_party_voyager::Update result; |
| ASSERT_OK(stub->Post(&post_context, set_req, &result)); |
| |
| EXPECT_EQ(result.req_id(), "test_post"); |
| EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| EXPECT_EQ(result.data_points().size(), 0); |
| |
| constexpr std::string_view data_patch = R"json({ |
| "Name": "MyNewName" |
| })json"; |
| |
| // Test Patch request |
| set_req.Clear(); |
| set_req.set_req_id("test_patch"); |
| req_fqp = set_req.add_req_fqp(); |
| req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis/{chassis_id}"); |
| (*req_fqp->mutable_fqp()->mutable_identifiers())["chassis_id"] = "Member1"; |
| set_req.set_json(data_patch); |
| |
| grpc::ClientContext patch_context; |
| result.Clear(); |
| ASSERT_OK(stub->Patch(&patch_context, set_req, &result)); |
| |
| EXPECT_EQ(result.req_id(), "test_patch"); |
| EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_NO_CONTENT); |
| EXPECT_EQ(result.data_points().size(), 0); |
| EXPECT_THAT(result.http_headers(), |
| Contains(Pair("server", StartsWith("RedfishMockup")))); |
| |
| // Test get request |
| third_party_voyager::Request req; |
| req.set_req_id("test_get"); |
| req_fqp = req.add_req_fqp(); |
| req_fqp->mutable_fqp()->set_specifier("/redfish/v1/Chassis/{chassis_id}"); |
| (*req_fqp->mutable_fqp()->mutable_identifiers())["chassis_id"] = "Member1"; |
| |
| grpc::ClientContext get_context; |
| result.Clear(); |
| ASSERT_OK(stub->Get(&get_context, req, &result)); |
| |
| EXPECT_EQ(result.req_id(), "test_get"); |
| EXPECT_EQ(result.code(), ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK); |
| EXPECT_THAT(result.http_headers(), |
| Contains(Pair("server", StartsWith("RedfishMockup")))); |
| ASSERT_EQ(result.data_points().size(), 1); |
| third_party_voyager::DataPoint expected; |
| expected.mutable_res_fqp()->CopyFrom(req_fqp->fqp()); |
| EXPECT_THAT(result.data_points(0), Partially(EqualsProto(expected))); |
| auto json = |
| nlohmann::json::parse(result.data_points(0).json(), nullptr, false); |
| ASSERT_FALSE(json.is_discarded()); |
| EXPECT_EQ(json["Name"], "MyNewName"); |
| EXPECT_EQ(json["ChassisType"], "RackMount"); |
| } |
| |
| class MockReadReactor |
| : public grpc::ClientReadReactor<third_party_voyager::Update> { |
| public: |
| MOCK_METHOD(void, OnReadInitialMetadataDone, (bool), (override)); |
| MOCK_METHOD(void, OnReadDone, (bool), (override)); |
| MOCK_METHOD(void, OnDone, (const grpc::Status&), (override)); |
| }; |
| |
| TEST_F(MockedProxyServerTest, SubscribeToRedfishEventsWorks) { |
| std::shared_ptr<grpc::Channel> channel = |
| grpc::CreateChannel(absl::StrCat("unix://", kUdsFilename), |
| grpc::InsecureChannelCredentials()); |
| std::unique_ptr<third_party_voyager::MachineTelemetry::Stub> stub( |
| third_party_voyager::MachineTelemetry::NewStub(channel)); |
| grpc::ClientContext subscribe_context; |
| third_party_voyager::Request req; |
| req.set_req_id("test_subscribe"); |
| req.add_req_fqp()->mutable_fqp()->set_specifier( |
| "/redfish/v1/EventService/Subscriptions/SSE"); |
| |
| testing::StrictMock<MockReadReactor> reactor; |
| absl::Notification initial_metadata_done; |
| EXPECT_CALL(reactor, OnReadInitialMetadataDone(true)) |
| .WillOnce(InvokeWithoutArgs( |
| [&initial_metadata_done] { initial_metadata_done.Notify(); })); |
| |
| stub->async()->Subscribe(&subscribe_context, &req, &reactor); |
| |
| third_party_voyager::Update response; |
| reactor.StartRead(&response); |
| reactor.StartCall(); |
| |
| // Make sure the request is being processed before we send the event |
| ASSERT_TRUE( |
| initial_metadata_done.WaitForNotificationWithTimeout(absl::Seconds(10))); |
| |
| absl::Notification read_done; |
| EXPECT_CALL(reactor, OnReadDone(true)) |
| .WillOnce(InvokeWithoutArgs([&read_done] { read_done.Notify(); })); |
| |
| // Trigger an event |
| nlohmann::json event = {{"MessageId", "Base.13.0.Success"}, |
| {"EventId", "1234"}}; |
| |
| third_party_voyager::SetRequest insert_event_req; |
| insert_event_req.set_req_id("test_insert_event"); |
| insert_event_req.add_req_fqp()->mutable_fqp()->set_specifier( |
| "/redfish/v1/EventService/Actions/EventService.SubmitTestEvent"); |
| insert_event_req.set_json(event.dump()); |
| third_party_voyager::Update insert_event_response; |
| grpc::ClientContext post_context; |
| ASSERT_OK( |
| stub->Post(&post_context, insert_event_req, &insert_event_response)); |
| |
| // Wait for read to complete |
| ASSERT_TRUE(read_done.WaitForNotificationWithTimeout(absl::Seconds(10))); |
| |
| EXPECT_EQ(response.req_id(), "test_subscribe"); |
| nlohmann::json received = nlohmann::json::parse( |
| response.data_points()[0].key_value().fields().at("data").string_val(), |
| nullptr, false); |
| ASSERT_FALSE(received.is_discarded()); |
| EXPECT_THAT(received["Events"], ElementsAre(event)); |
| |
| absl::Notification subscribe_done; |
| EXPECT_CALL(reactor, OnDone(StatusIs(absl::StatusCode::kCancelled))) |
| .WillOnce( |
| InvokeWithoutArgs([&subscribe_done] { subscribe_done.Notify(); })); |
| subscribe_context.TryCancel(); |
| EXPECT_TRUE(subscribe_done.WaitForNotificationWithTimeout(absl::Seconds(10))); |
| } |
| |
| } // namespace |
| |
| } // namespace milotic |