blob: 2fb5079f00aad438ad5d3e04690484e723162fff [file] [log] [blame] [edit]
#include "ssh_actions_plugin.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/synchronization/notification.h"
#include "absl/time/time.h"
#include <source_location>
#include "redfish_query_engine/http/codes.h"
#include "nlohmann/json.hpp"
#include "metrics.h"
#include "monitoring.h"
#include "proxy.h"
#include "proxy_builder.h"
#include "proxy_config.pb.h"
#include "redfish_plugin.h"
#include "request_response.h"
#include "ssh_client.h"
namespace milotic {
using SshActionsConfig = ::milotic_grpc_proxy::Plugin::SshActions;
SshActionsPlugin::SshActionsPlugin(const SshActionsConfig& config)
: common_command_options_({.timeout = absl::Seconds(10),
.retry_interval = absl::Milliseconds(100),
.use_pty = false}) {
if (config.execute_timeout_sec() > 0) {
common_command_options_.timeout =
absl::Seconds(config.execute_timeout_sec());
}
if (config.execute_retry_interval_ms() > 0) {
common_command_options_.retry_interval =
absl::Milliseconds(config.execute_retry_interval_ms());
}
for (const SshActionsConfig::Action& action : config.actions()) {
// sanity check
if (action.args_count() >= 3 || action.args_count() < 0) {
LOG(ERROR) << "Invalid 'args_count' configuration for command: '"
<< action.ssh_command() << "', skipping.";
continue;
}
actions_[action.action_path()] = {
.command = action.ssh_command(),
.timeout = absl::Seconds(action.timeout_sec()),
.args_count = static_cast<int>(action.args_count())};
}
}
absl::Status SshActionsPlugin::Initialize(Proxy* proxy) {
ssh_client_ = proxy->GetResources().ssh_client.get();
if (ssh_client_ == nullptr) {
return absl::UnavailableError("SSH client not available");
}
proxy_ = proxy;
return absl::OkStatus();
}
RedfishPlugin::RequestAction SshActionsPlugin::PreprocessRequest(
RedfishPlugin::RequestVerb verb, ProxyRequest& request) {
if (verb == RedfishPlugin::RequestVerb::kPost &&
actions_.contains(request.GetPath())) {
VLOG(1) << "Handle " << request.GetPath();
return RedfishPlugin::RequestAction::kHandle;
}
return RedfishPlugin::RequestAction::kNext;
}
namespace {
ProxyResponse ErrorResponse(
absl::string_view uri, const absl::Status& status,
std::source_location location = std::source_location::current()) {
LOG(ERROR).AtLocation(location.file_name(), static_cast<int>(location.line())) << status;
CommonMetrics::Get().ssh_action_response_code.Increment(
{uri, ecclesia::HttpResponseCode::HTTP_CODE_ERROR});
return ProxyResponse{
{.code = ecclesia::HttpResponseCode::HTTP_CODE_ERROR,
.body = status.ToString(absl::StatusToStringMode::kWithNoExtraData),
.headers = {{"Content-Type", "text/plain"}}}};
}
} // namespace
absl::StatusOr<std::string> SshActionsPlugin::GetCommand(
const SshActionsPlugin::Action& action,
std::unique_ptr<ProxyRequest> request) {
// Parse the optional command arguments from the request body.
std::string command;
auto obj = nlohmann::json::parse(request->GetBody(), nullptr,
/* allow_exceptions */ false,
/* ignore_comments */ false);
auto args = obj.find("args");
int args_count = 0;
if (args != std::end(obj)) {
if (args->is_array()) {
args_count = static_cast<int>(args->size());
} else {
args_count = 1;
}
}
if (args_count != action.args_count) {
return absl::InvalidArgumentError(absl::StrFormat(
"Got %d arguments, expected %d.", args_count, action.args_count));
}
switch (args_count) {
case 0:
command = action.command;
break;
case 1: {
std::string* string_ptr = args->is_array()
? args->at(0).get_ptr<std::string*>()
: args->get_ptr<std::string*>();
if (string_ptr == nullptr) {
return absl::InvalidArgumentError("Invalid 'args' argument type");
}
command = absl::Substitute(action.command, *string_ptr);
} break;
case 2: {
std::string* string_ptr[2] = {args->at(0).get_ptr<std::string*>(),
args->at(1).get_ptr<std::string*>()};
if (string_ptr[0] == nullptr || string_ptr[1] == nullptr) {
return absl::InvalidArgumentError("Invalid 'args' argument type");
}
command =
absl::Substitute(action.command, *string_ptr[0], *string_ptr[1]);
} break;
default:
return absl::InternalError(absl::StrFormat(
"args_count value %d is not handled in code", action.args_count));
break;
}
return command;
}
absl::StatusOr<ProxyResponse> SshActionsPlugin::HandleRequest(
RedfishPlugin::RequestVerb verb, std::unique_ptr<ProxyRequest> request) {
class Callbacks : public ExecutionCallbacks {
public:
virtual absl::Status OnStdout(ExecutionContext* context,
absl::string_view data) {
stdout_buffer_.append(data);
return absl::OkStatus();
}
virtual absl::Status OnStderr(ExecutionContext* context,
absl::string_view data) {
stderr_buffer_.append(data);
return absl::OkStatus();
}
virtual void OnDone(ExecutionContext* context, absl::StatusOr<int> result) {
result_ = std::move(result);
done_.Notify();
}
static absl::StatusOr<nlohmann::json> GetJsonOutput(
Callbacks&& callbacks, absl::Duration timeout) {
if (!callbacks.done_.WaitForNotificationWithTimeout(timeout)) {
return absl::DeadlineExceededError(
"Timeout waiting for command to complete");
}
if (!callbacks.result_.ok()) {
LOG(ERROR) << "Failed to run command: " << callbacks.result_.status();
return callbacks.result_.status();
}
return nlohmann::json{
{"StdOut", std::move(callbacks.stdout_buffer_)},
{"StdErr", std::move(callbacks.stderr_buffer_)},
{"Result", *callbacks.result_},
};
}
private:
std::string stdout_buffer_;
std::string stderr_buffer_;
absl::StatusOr<int> result_;
absl::Notification done_;
} execution_callbacks;
std::string uri(request->GetPath());
LatencyMonitor latency_monitor(&CommonMetrics::Get().ssh_action_latency,
{uri});
// `at` is safe to call. We have already validated that the action is in the
// map in PreprocessRequest.
const Action& action = actions_.at(request->GetPath());
auto command = GetCommand(action, std::move(request));
if (!command.ok()) {
return ErrorResponse(uri, command.status());
}
absl::StatusOr<std::unique_ptr<ExecutionContext>> context =
ssh_client_->ExecuteCommand(command.value(), &execution_callbacks,
common_command_options_);
if (!context.ok()) {
return ErrorResponse(uri, context.status());
}
absl::StatusOr<nlohmann::json> result =
Callbacks::GetJsonOutput(std::move(execution_callbacks), action.timeout);
if (!result.ok()) {
return ErrorResponse(uri, result.status());
}
CommonMetrics::Get().ssh_action_response_code.Increment(
{uri, ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK});
return ProxyResponse(
ecclesia::HttpResponseCode::HTTP_CODE_REQUEST_OK, result.value().dump(),
{{"Content-Type", "application/json"}, {"OData-Version", "4.0"}});
}
REGISTER_REDFISH_PLUGIN(ssh_actions, SshActionsPlugin);
} // namespace milotic