| #include "ssh_actions_plugin.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "gmock.h" |
| #include "gunit.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/types/span.h" |
| #include "redfish_query_engine/protobuf/parse.h" |
| #include "nlohmann/json_fwd.hpp" |
| #include "mock_ssh_client.h" |
| #include "proxy.h" |
| #include "proxy_config.pb.h" |
| #include "redfish_plugin.h" |
| #include "request_response.h" |
| #include "ssh_client.h" |
| #include "voyager/deferrable_priority_queue.hpp" |
| #include "voyager/priority_queue.hpp" |
| #include "thread/thread.h" |
| #include "thread/thread_options.h" |
| |
| namespace { |
| |
| using ::testing::_; |
| using ::testing::Contains; |
| using ::testing::Invoke; |
| using ::testing::Pair; |
| using ::testing::StrictMock; |
| using ::testing::status::StatusIs; |
| |
| using ::milotic::ExecutionCallbacks; |
| using ::milotic::Proxy; |
| using ::milotic::RedfishPlugin; |
| using ::milotic::SshActionsPlugin; |
| using ::voyager::DeferrablePriorityQueue; |
| |
| using ProxyRequest = ::milotic::ProxyRequest; |
| using ProxyResponse = ::milotic::ProxyResponse; |
| |
| using SshActionsConfig = milotic_grpc_proxy::Plugin::SshActions; |
| |
| using ::milotic::MockExecutionContext; |
| using ::milotic::MockSshClient; |
| |
| struct TestEnv { |
| explicit TestEnv( |
| std::unique_ptr<RedfishPlugin> plugin = |
| std::make_unique<SshActionsPlugin>( |
| ecclesia::ParseTextAsProtoOrDie<SshActionsConfig>(R"pb( |
| actions: { |
| action_path: "/google/v1/Test/Actions/Action.1" |
| ssh_command: "ssh command a" |
| timeout_sec: 60 |
| } |
| actions: { |
| action_path: "/google/v1/Test/Actions/Action.2" |
| ssh_command: "ssh command b" |
| timeout_sec: 30 |
| } |
| actions: { |
| action_path: "/google/v1/Test/Actions/Action.p1" |
| ssh_command: "ssh command $0" |
| timeout_sec: 30 |
| args_count : 1 |
| } |
| actions: { |
| action_path: "/google/v1/Test/Actions/Action.p2" |
| ssh_command: "ssh command 1:$1 0:$0" |
| timeout_sec: 30 |
| args_count : 2 |
| } |
| )pb"))) |
| : queue(1), |
| proxy("http://test_endpoint:1234", absl::MakeSpan(&plugin, 1), &queue, |
| {.ssh_client = std::make_unique<MockSshClient>()}), |
| queue_thread(thread::Options().set_joinable(true), "TestEnv", this, |
| &TestEnv::ProcessQueue) { |
| queue_thread.Start(); |
| } |
| |
| ~TestEnv() { |
| queue.Shutdown(false); |
| queue_thread.Join(); |
| } |
| |
| MockSshClient& SshClient() const { |
| return static_cast<MockSshClient&>(*proxy.GetResources().ssh_client); |
| } |
| |
| void ProcessQueue() { |
| while (queue.ProcessQueue(1).ok()) { |
| } |
| } |
| |
| DeferrablePriorityQueue queue; |
| Proxy proxy; |
| MemberThread<TestEnv> queue_thread; |
| }; |
| |
| TEST(SshActionsPluginTest, PluginHandlesRequests) { |
| TestEnv env; |
| |
| EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command a", _, _)) |
| .WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks, |
| const milotic::SshClient::CommandOptions& options) { |
| auto context = std::make_unique<StrictMock<MockExecutionContext>>(); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data a1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error a1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error a2")); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data a2")); |
| callbacks->OnDone(context.get(), 1); |
| return context; |
| })); |
| |
| EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command b", _, _)) |
| .WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks, |
| const milotic::SshClient::CommandOptions& options) { |
| auto context = std::make_unique<StrictMock<MockExecutionContext>>(); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data b1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error b1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error b2")); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data b2")); |
| callbacks->OnDone(context.get(), 2); |
| return context; |
| })); |
| |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job1, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| env.proxy.CreateRequest("/google/v1/Test/Actions/Action.1"))); |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job2, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| env.proxy.CreateRequest("/google/v1/Test/Actions/Action.2"))); |
| |
| ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response()); |
| |
| ASSERT_EQ(job2->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response2, job2->response()); |
| |
| EXPECT_EQ(response1.code, 200); |
| const nlohmann::json expected_a = {{"StdOut", "data a1data a2"}, |
| {"StdErr", "error a1error a2"}, |
| {"Result", 1}}; |
| EXPECT_EQ(response1.GetBodyJson(), expected_a); |
| EXPECT_THAT(response1.headers, |
| Contains(Pair("Content-Type", "application/json"))); |
| EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0"))); |
| EXPECT_EQ(response2.code, 200); |
| const nlohmann::json expected_b = {{"StdOut", "data b1data b2"}, |
| {"StdErr", "error b1error b2"}, |
| {"Result", 2}}; |
| EXPECT_EQ(response2.GetBodyJson(), expected_b); |
| } |
| |
| TEST(SshActionsPluginTest, PluginForwardsOtherUrls) { |
| TestEnv env; |
| |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| env.proxy.CreateRequest("/google/v1/Test/Actions/Not.Supported"))); |
| |
| ASSERT_EQ(job->Wait(), voyager::Job::JobState::kDone); |
| |
| EXPECT_THAT(job->response(), StatusIs(absl::StatusCode::kUnimplemented)); |
| } |
| |
| TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgs) { |
| TestEnv env; |
| |
| EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command a", _, _)) |
| .WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks, |
| const milotic::SshClient::CommandOptions& options) { |
| auto context = std::make_unique<StrictMock<MockExecutionContext>>(); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data a1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error a1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error a2")); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data a2")); |
| callbacks->OnDone(context.get(), 1); |
| return context; |
| })); |
| |
| EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command b", _, _)) |
| .WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks, |
| const milotic::SshClient::CommandOptions& options) { |
| auto context = std::make_unique<StrictMock<MockExecutionContext>>(); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data b1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error b1")); |
| EXPECT_OK(callbacks->OnStderr(context.get(), "error b2")); |
| EXPECT_OK(callbacks->OnStdout(context.get(), "data b2")); |
| callbacks->OnDone(context.get(), 2); |
| return context; |
| })); |
| |
| auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1"); |
| request_a->body = "{ \"args\" : \"a\" }"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job1, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| std::move(request_a))); |
| |
| auto request_b = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1"); |
| request_b->body = "{ \"args\" : [ \"b\" ] }"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job2, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| std::move(request_b))); |
| |
| ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response()); |
| |
| ASSERT_EQ(job2->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response2, job2->response()); |
| |
| EXPECT_EQ(response1.code, 200); |
| const nlohmann::json expected_a = {{"StdOut", "data a1data a2"}, |
| {"StdErr", "error a1error a2"}, |
| {"Result", 1}}; |
| EXPECT_EQ(response1.GetBodyJson(), expected_a); |
| EXPECT_THAT(response1.headers, |
| Contains(Pair("Content-Type", "application/json"))); |
| EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0"))); |
| EXPECT_EQ(response2.code, 200); |
| const nlohmann::json expected_b = {{"StdOut", "data b1data b2"}, |
| {"StdErr", "error b1error b2"}, |
| {"Result", 2}}; |
| EXPECT_EQ(response2.GetBodyJson(), expected_b); |
| } |
| |
| TEST(SshActionsPluginTest, PluginHandlesRequestsWithTwoArgs) { |
| TestEnv env; |
| |
| EXPECT_CALL(env.SshClient(), ExecuteCommand("ssh command 1:b 0:a", _, _)) |
| .WillOnce(Invoke([](absl::string_view, ExecutionCallbacks* callbacks, |
| const milotic::SshClient::CommandOptions& options) { |
| auto context = std::make_unique<StrictMock<MockExecutionContext>>(); |
| callbacks->OnDone(context.get(), 1); |
| return context; |
| })); |
| |
| auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p2"); |
| request_a->body = "{ \"args\" : [ \"a\", \"b\" ] }"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job1, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| std::move(request_a))); |
| |
| ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response()); |
| |
| EXPECT_EQ(response1.code, 200); |
| EXPECT_THAT(response1.headers, |
| Contains(Pair("Content-Type", "application/json"))); |
| EXPECT_THAT(response1.headers, Contains(Pair("OData-Version", "4.0"))); |
| } |
| |
| TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgsInvalid) { |
| TestEnv env; |
| |
| auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1"); |
| request_a->body = "{ \"args\" : 13 }"; |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job1, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| std::move(request_a))); |
| |
| ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response()); |
| |
| EXPECT_EQ(response1.code, 500); |
| EXPECT_THAT(response1.headers, |
| Contains(Pair("Content-Type", "text/plain"))); |
| } |
| |
| TEST(SshActionsPluginTest, PluginHandlesRequestsWithArgsMissing) { |
| TestEnv env; |
| |
| auto request_a = env.proxy.CreateRequest("/google/v1/Test/Actions/Action.p1"); |
| ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<Proxy::RequestJob> job1, |
| env.proxy.DispatchRequestToQueue( |
| RedfishPlugin::RequestVerb::kPost, |
| std::move(request_a))); |
| |
| ASSERT_EQ(job1->Wait(), voyager::Job::JobState::kDone); |
| ASSERT_OK_AND_ASSIGN(ProxyResponse response1, job1->response()); |
| |
| EXPECT_EQ(response1.code, 500); |
| EXPECT_THAT(response1.headers, |
| Contains(Pair("Content-Type", "text/plain"))); |
| } |
| |
| } // namespace |