Coverage Report

Created: 2026-09-04 06:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gpowerd/action_validation.cc
Line
Count
Source
1
#include "action_validation.h"
2
3
#include <array>
4
#include <cstdint>
5
6
#include "condition.h"
7
#include "action.pb.h"
8
#include "safepower_agent_config.pb.h"
9
#include "system_state.pb.h"
10
#include "absl/log/check.h"
11
#include "absl/log/log.h"
12
#include "absl/status/status.h"
13
#include "absl/status/statusor.h"
14
#include "absl/strings/str_format.h"
15
#include "absl/strings/string_view.h"
16
#include "absl/time/time.h"
17
#include "bmc/status_macros.h"
18
#include "google/protobuf/repeated_ptr_field.h"
19
20
namespace safepower_agent {
21
22
static absl::Status ValidateBootCountCondition(
23
    const safepower_agent_proto::Condition& precondition,
24
    absl::string_view node_entity_tag,
25
    safepower_agent_proto::SystemState system_state, absl::Time start_time,
26
6.49k
    int max_boots) {
27
6.49k
  auto node_state = system_state.mutable_node_state()->find(std::string(node_entity_tag));
28
6.49k
  if (node_state == system_state.mutable_node_state()->end()) {
29
2.83k
    return absl::UnavailableError(absl::StrFormat(
30
2.83k
        "Node %s not found in initial system state", node_entity_tag));
31
2.83k
  }
32
3.66k
  safepower_agent_proto::BootState* boot_state =
33
3.66k
      node_state->second.mutable_boot_state();
34
3.66k
  if (!boot_state->has_boot_counter()) {
35
572
    return absl::UnavailableError(absl::StrFormat(
36
572
        "Node %s does not have a boot counter", node_entity_tag));
37
572
  }
38
3.09k
  int current_boot_count = boot_state->boot_counter();
39
14.6k
  for (int boot = 0; boot < max_boots; boot++) {
40
13.5k
    boot_state->set_boot_counter(current_boot_count + boot);
41
13.5k
    auto [status, matches] =
42
13.5k
        Condition::Matches(precondition, system_state, start_time, start_time);
43
13.5k
    if (!matches.empty()) {
44
1.82k
      return absl::OkStatus();
45
1.82k
    }
46
11.7k
    if (!status.ok()) {
47
134
      return status;
48
134
    }
49
11.7k
  }
50
1.13k
  return absl::InvalidArgumentError(absl::StrFormat(
51
1.13k
      "Boot count precondition not met before boot counter = %d",
52
1.13k
      current_boot_count + max_boots));
53
3.09k
}
54
55
struct NodeChecksFound {
56
  bool target_node = false;
57
  bool other_node = false;
58
};
59
60
static absl::StatusOr<NodeChecksFound> IsAnyNodeChecked(
61
    const google::protobuf::RepeatedPtrField<safepower_agent_proto::Condition>& condition,
62
    absl::string_view node_entity_tag);
63
64
static absl::StatusOr<NodeChecksFound> IsNodeChecked(
65
    const safepower_agent_proto::Condition& condition,
66
8.56k
    absl::string_view node_entity_tag) {
67
8.56k
  switch (condition.condition_type_case()) {
68
688
    case safepower_agent_proto::Condition::kStateCondition:
69
688
      if (condition.state_condition().node_entity_tag() == node_entity_tag) {
70
84
        return NodeChecksFound{.target_node = true};
71
84
      }
72
604
      return NodeChecksFound{.other_node = true};
73
2.37k
    case safepower_agent_proto::Condition::kAnyOf:
74
2.37k
      return IsAnyNodeChecked(condition.any_of().conditions(), node_entity_tag);
75
605
    case safepower_agent_proto::Condition::kAllOf:
76
605
      return IsAnyNodeChecked(condition.all_of().conditions(), node_entity_tag);
77
4.89k
    default:
78
4.89k
      return NodeChecksFound{};
79
8.56k
  }
80
8.56k
}
81
82
static absl::StatusOr<NodeChecksFound> IsAnyNodeChecked(
83
    const google::protobuf::RepeatedPtrField<safepower_agent_proto::Condition>& condition,
84
2.97k
    absl::string_view node_entity_tag) {
85
2.97k
  NodeChecksFound result{};
86
8.48k
  for (const auto& sub_condition : condition) {
87
8.48k
    ASSIGN_OR_RETURN(auto found, IsNodeChecked(sub_condition, node_entity_tag));
88
8.48k
    result.target_node |= found.target_node;
89
8.48k
    result.other_node |= found.other_node;
90
8.48k
    if (result.target_node && result.other_node) {
91
5
      break;
92
5
    }
93
8.48k
  }
94
2.97k
  return result;
95
2.97k
}
96
97
enum class TimeoutType : uint8_t {
98
  kNone,
99
  kMatch,
100
  kAbort,
101
};
102
103
static TimeoutType HasTimeout(
104
23.1k
    const safepower_agent_proto::Condition& condition) {
105
23.1k
  switch (condition.condition_type_case()) {
106
1.39k
    case safepower_agent_proto::Condition::kTimeout:
107
1.39k
      return condition.timeout().abort() ? TimeoutType::kAbort
108
1.39k
                                         : TimeoutType::kMatch;
109
3.39k
    case safepower_agent_proto::Condition::kAnyOf: {
110
3.39k
      TimeoutType timeout_type = TimeoutType::kNone;
111
6.05k
      for (const auto& sub_condition : condition.any_of().conditions()) {
112
6.05k
        TimeoutType sub_type = HasTimeout(sub_condition);
113
6.05k
        if (sub_type == TimeoutType::kAbort) {
114
43
          return TimeoutType::kAbort;
115
43
        }
116
6.01k
        if (sub_type == TimeoutType::kMatch) {
117
68
          timeout_type = TimeoutType::kMatch;
118
68
        }
119
6.01k
      }
120
3.35k
      return timeout_type;
121
3.39k
    }
122
2.26k
    case safepower_agent_proto::Condition::kAllOf: {
123
2.26k
      TimeoutType timeout_type = TimeoutType::kMatch;
124
15.0k
      for (const auto& sub_condition : condition.all_of().conditions()) {
125
15.0k
        TimeoutType sub_type = HasTimeout(sub_condition);
126
15.0k
        if (sub_type == TimeoutType::kAbort) {
127
414
          return TimeoutType::kAbort;
128
414
        }
129
14.6k
        if (sub_type == TimeoutType::kNone) {
130
13.2k
          timeout_type = TimeoutType::kNone;
131
13.2k
        }
132
14.6k
      }
133
1.84k
      return timeout_type;
134
2.26k
    }
135
16.1k
    default:
136
16.1k
      return TimeoutType::kNone;
137
23.1k
  }
138
23.1k
}
139
140
static absl::Status ValidateTimeout(
141
    const safepower_agent_proto::Condition& condition, absl::Time start_time,
142
2.06k
    absl::Duration max_timeout) {
143
2.06k
  if (HasTimeout(condition) == TimeoutType::kNone) {
144
1.12k
    return absl::InvalidArgumentError("Timeout condition not found");
145
1.12k
  }
146
946
  auto [status, matches] =
147
946
      Condition::Matches(condition, {}, start_time, start_time + max_timeout);
148
946
  if (!matches.empty()) {
149
848
    return absl::OkStatus();
150
848
  }
151
98
  if (!status.ok()) {
152
5
    return status;
153
5
  }
154
93
  return absl::InvalidArgumentError(
155
93
      absl::StrFormat("Timeout precondition not met before timeout = %s",
156
93
                      absl::FormatTime(start_time + max_timeout)));
157
98
}
158
159
absl::Status ValidateRequest(
160
    const safepower_agent_proto::StartActionRequest& request,
161
    absl::string_view node_entity_tag,
162
    const safepower_agent_proto::SystemState& initial_system_state,
163
    absl::Time start_time,
164
6.49k
    const safepower_agent_config::ConditionValidationOptions& options) {
165
6.49k
  LOG(INFO) << "Validating request: " << request.DebugString();
166
  // Boot count checks must exist if there is a precondition, and must check the
167
  // _actuating_ node. This prevents reboot loops and unintentional reboots
168
  // post-action.
169
6.49k
  RETURN_IF_ERROR(ValidateBootCountCondition(
170
6.49k
      request.precondition(), node_entity_tag, initial_system_state, start_time,
171
6.49k
      options.max_boots()));
172
  // Timeout checks must exist if there is any precondition.
173
1.82k
  absl::Duration max_timeout = absl::Seconds(options.max_timeout_seconds());
174
1.82k
  if (request.has_precondition()) {
175
1.70k
    RETURN_IF_ERROR(
176
1.70k
        ValidateTimeout(request.precondition(), start_time, max_timeout));
177
1.70k
  }
178
  // Timeout checks must exist on validation.
179
834
  if (!request.has_validation()) {
180
469
    return absl::OkStatus();
181
469
  }
182
365
  RETURN_IF_ERROR(
183
365
      ValidateTimeout(request.validation(), start_time, max_timeout));
184
138
  if (request.action().target_component().node_entity_tag().empty()) {
185
63
    LOG(WARNING)
186
63
        << "No target node entity tag; skipping further validation checks.";
187
63
    return absl::OkStatus();
188
63
  }
189
  // If there are any validation checks for node state, at least one must check
190
  // the node that was actuated. This catches the case where the action and
191
  // validation checks are on different nodes.
192
150
  ASSIGN_OR_RETURN(
193
150
      auto found,
194
150
      IsNodeChecked(request.validation(),
195
150
                    request.action().target_component().node_entity_tag()));
196
150
  if (!found.target_node) {
197
60
    if (found.other_node) {
198
30
      return absl::NotFoundError(absl::StrFormat(
199
30
          "Check for node %s not found in validation",
200
30
          request.action().target_component().node_entity_tag()));
201
30
    }
202
30
    LOG(WARNING) << "Validation does not check any node state.";
203
30
  }
204
45
  return absl::OkStatus();
205
150
}
206
207
}  // namespace safepower_agent