| #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 |