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