blob: 30d99dcdf01692664b74874b34e5eefb44b2be96 [file] [log] [blame] [edit]
#include "action_validation.h"
#include <array>
#include <cstdint>
#include "condition.h"
#include "safepower_agent.pb.h"
#include "safepower_agent_config.pb.h"
#include "absl/log/check.h"
#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/time/time.h"
#include "google/protobuf/repeated_ptr_field.h"
#include "bmc/status_macros.h"
namespace safepower_agent {
static absl::Status ValidateBootCountCondition(
const safepower_agent_proto::Condition& precondition,
absl::string_view node_entity_tag,
safepower_agent_proto::SystemState system_state, absl::Time start_time,
int max_boots) {
auto node_state = system_state.mutable_node_state()->find(node_entity_tag);
if (node_state == system_state.mutable_node_state()->end()) {
return absl::UnavailableError(absl::StrFormat(
"Node %s not found in initial system state", node_entity_tag));
}
safepower_agent_proto::BootState* boot_state =
node_state->second.mutable_boot_state();
if (!boot_state->has_boot_counter()) {
return absl::UnavailableError(absl::StrFormat(
"Node %s does not have a boot counter", node_entity_tag));
}
int current_boot_count = boot_state->boot_counter();
for (int boot = 0; boot < max_boots; boot++) {
boot_state->set_boot_counter(current_boot_count + boot);
auto [status, matches] =
Condition::Matches(precondition, system_state, start_time, start_time);
if (!matches.empty()) {
return absl::OkStatus();
}
if (!status.ok()) {
return status;
}
}
return absl::InvalidArgumentError(absl::StrFormat(
"Boot count precondition not met before boot counter = %d",
current_boot_count + max_boots));
}
struct NodeChecksFound {
bool target_node = false;
bool other_node = false;
};
static absl::StatusOr<NodeChecksFound> IsAnyNodeChecked(
const google::protobuf::RepeatedPtrField<safepower_agent_proto::Condition>& condition,
absl::string_view node_entity_tag);
static absl::StatusOr<NodeChecksFound> IsNodeChecked(
const safepower_agent_proto::Condition& condition,
absl::string_view node_entity_tag) {
switch (condition.condition_type_case()) {
case safepower_agent_proto::Condition::kStateCondition:
if (condition.state_condition().node_entity_tag() == node_entity_tag) {
return NodeChecksFound{.target_node = true};
}
return NodeChecksFound{.other_node = true};
case safepower_agent_proto::Condition::kAnyOf:
return IsAnyNodeChecked(condition.any_of().conditions(), node_entity_tag);
case safepower_agent_proto::Condition::kAllOf:
return IsAnyNodeChecked(condition.all_of().conditions(), node_entity_tag);
default:
return NodeChecksFound{};
}
}
static absl::StatusOr<NodeChecksFound> IsAnyNodeChecked(
const google::protobuf::RepeatedPtrField<safepower_agent_proto::Condition>& condition,
absl::string_view node_entity_tag) {
NodeChecksFound result{};
for (const auto& sub_condition : condition) {
ASSIGN_OR_RETURN(auto found, IsNodeChecked(sub_condition, node_entity_tag));
result.target_node |= found.target_node;
result.other_node |= found.other_node;
if (result.target_node && result.other_node) {
break;
}
}
return result;
}
enum class TimeoutType : uint8_t {
kNone,
kMatch,
kAbort,
};
static TimeoutType HasTimeout(
const safepower_agent_proto::Condition& condition) {
switch (condition.condition_type_case()) {
case safepower_agent_proto::Condition::kTimeout:
return condition.timeout().abort() ? TimeoutType::kAbort
: TimeoutType::kMatch;
case safepower_agent_proto::Condition::kAnyOf: {
TimeoutType timeout_type = TimeoutType::kNone;
for (const auto& sub_condition : condition.any_of().conditions()) {
TimeoutType sub_type = HasTimeout(sub_condition);
if (sub_type == TimeoutType::kAbort) {
return TimeoutType::kAbort;
}
if (sub_type == TimeoutType::kMatch) {
timeout_type = TimeoutType::kMatch;
}
}
return timeout_type;
}
case safepower_agent_proto::Condition::kAllOf: {
TimeoutType timeout_type = TimeoutType::kMatch;
for (const auto& sub_condition : condition.all_of().conditions()) {
TimeoutType sub_type = HasTimeout(sub_condition);
if (sub_type == TimeoutType::kAbort) {
return TimeoutType::kAbort;
}
if (sub_type == TimeoutType::kNone) {
timeout_type = TimeoutType::kNone;
}
}
return timeout_type;
}
default:
return TimeoutType::kNone;
}
}
static absl::Status ValidateTimeout(
const safepower_agent_proto::Condition& condition, absl::Time start_time,
absl::Duration max_timeout) {
if (HasTimeout(condition) == TimeoutType::kNone) {
return absl::InvalidArgumentError("Timeout condition not found");
}
auto [status, matches] =
Condition::Matches(condition, {}, start_time, start_time + max_timeout);
if (!matches.empty()) {
return absl::OkStatus();
}
if (!status.ok()) {
return status;
}
return absl::InvalidArgumentError(
absl::StrFormat("Timeout precondition not met before timeout = %v",
start_time + max_timeout));
}
absl::Status ValidateRequest(
const safepower_agent_proto::StartActionRequest& request,
absl::string_view node_entity_tag,
const safepower_agent_proto::SystemState& initial_system_state,
absl::Time start_time,
const safepower_agent_config::ConditionValidationOptions& options) {
LOG(INFO) << "Validating request: " << request.DebugString();
// Boot count checks must exist if there is a precondition, and must check the
// _actuating_ node. This prevents reboot loops and unintentional reboots
// post-action.
RETURN_IF_ERROR(ValidateBootCountCondition(
request.precondition(), node_entity_tag, initial_system_state, start_time,
options.max_boots()));
// Timeout checks must exist if there is any precondition.
absl::Duration max_timeout = absl::Seconds(options.max_timeout_seconds());
if (request.has_precondition()) {
RETURN_IF_ERROR(
ValidateTimeout(request.precondition(), start_time, max_timeout));
}
// Timeout checks must exist on validation.
if (!request.has_validation()) {
return absl::OkStatus();
}
RETURN_IF_ERROR(
ValidateTimeout(request.validation(), start_time, max_timeout));
if (request.action().target_component().node_entity_tag().empty()) {
LOG(WARNING)
<< "No target node entity tag; skipping further validation checks.";
return absl::OkStatus();
}
// If there are any validation checks for node state, at least one must check
// the node that was actuated. This catches the case where the action and
// validation checks are on different nodes.
ASSIGN_OR_RETURN(
auto found,
IsNodeChecked(request.validation(),
request.action().target_component().node_entity_tag()));
if (!found.target_node) {
if (found.other_node) {
return absl::NotFoundError(absl::StrFormat(
"Check for node %s not found in validation",
request.action().target_component().node_entity_tag()));
}
LOG(WARNING) << "Validation does not check any node state.";
}
return absl::OkStatus();
}
} // namespace safepower_agent