1 /**
2 * Copyright © 2022 IBM Corporation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 #pragma once
17
18 #include "../utils/flight_recorder.hpp"
19 #include "../zone.hpp"
20 #include "config_base.hpp"
21 #include "group.hpp"
22
23 #include <nlohmann/json.hpp>
24 #include <phosphor-logging/lg2.hpp>
25
26 #include <algorithm>
27 #include <format>
28 #include <functional>
29 #include <iterator>
30 #include <map>
31 #include <memory>
32 #include <numeric>
33
34 namespace phosphor::fan::control::json
35 {
36
37 using json = nlohmann::json;
38
39 /**
40 * @class ActionParseError - A parsing error exception
41 *
42 * A parsing error exception that can be used to terminate the application
43 * due to not being able to successfully parse a configured action.
44 */
45 class ActionParseError : public std::runtime_error
46 {
47 public:
48 ActionParseError() = delete;
49 ActionParseError(const ActionParseError&) = delete;
50 ActionParseError(ActionParseError&&) = delete;
51 ActionParseError& operator=(const ActionParseError&) = delete;
52 ActionParseError& operator=(ActionParseError&&) = delete;
53 ~ActionParseError() = default;
54
55 /**
56 * @brief Action parsing error object
57 *
58 * When parsing an action from the JSON configuration, any critical
59 * attributes that fail to be parsed for an action can throw an
60 * ActionParseError exception to log the parsing failure details and
61 * terminate the application.
62 *
63 * @param[in] name - Name of the action
64 * @param[in] details - Additional details of the parsing error
65 */
ActionParseError(const std::string & name,const std::string & details)66 ActionParseError(const std::string& name, const std::string& details) :
67 std::runtime_error(
68 std::format("Failed to parse action {} [{}]", name, details)
69 .c_str())
70 {}
71 };
72
73 /**
74 * @brief Function used in creating action objects
75 *
76 * @param[in] jsonObj - JSON object for the action
77 * @param[in] groups - Groups of dbus objects the action uses
78 * @param[in] zones - Zones the action runs against
79 *
80 * Creates an action object given the JSON configuration, list of groups and
81 * sets the zones the action should run against.
82 */
83 template <typename T>
createAction(const json & jsonObj,const std::vector<Group> & groups,std::vector<std::reference_wrapper<Zone>> & zones)84 std::unique_ptr<T> createAction(
85 const json& jsonObj, const std::vector<Group>& groups,
86 std::vector<std::reference_wrapper<Zone>>& zones)
87 {
88 // Create the action and set its list of zones
89 auto action = std::make_unique<T>(jsonObj, groups);
90 action->setZones(zones);
91 return action;
92 }
93
94 /**
95 * @class ActionBase - Base action object
96 *
97 * Base class for fan control's event actions
98 */
99 class ActionBase : public ConfigBase
100 {
101 public:
102 ActionBase() = delete;
103 ActionBase(const ActionBase&) = delete;
104 ActionBase(ActionBase&&) = delete;
105 ActionBase& operator=(const ActionBase&) = delete;
106 ActionBase& operator=(ActionBase&&) = delete;
107 virtual ~ActionBase() = default;
108
109 /**
110 * @brief Base action object
111 *
112 * @param[in] jsonObj - JSON object containing name and any profiles
113 * @param[in] groups - Groups of dbus objects the action uses
114 *
115 * All actions derived from this base action object must be given a name
116 * that uniquely identifies the action. Optionally, a configured action can
117 * have a list of explicit profiles it should be included in, otherwise
118 * always include the action where no profiles are given.
119 */
ActionBase(const json & jsonObj,const std::vector<Group> & groups)120 ActionBase(const json& jsonObj, const std::vector<Group>& groups) :
121 ConfigBase(jsonObj), _groups(groups),
122 _uniqueName(getName() + "-" + std::to_string(_actionCount++))
123 {}
124
125 /**
126 * @brief Get the groups configured on the action
127 *
128 * @return List of groups
129 */
getGroups() const130 inline const auto& getGroups() const
131 {
132 return _groups;
133 }
134
135 /**
136 * @brief Set the zones the action is run against
137 *
138 * @param[in] zones - Zones the action runs against
139 *
140 * By default, the zones are set when the action object is created
141 */
setZones(std::vector<std::reference_wrapper<Zone>> & zones)142 virtual void setZones(std::vector<std::reference_wrapper<Zone>>& zones)
143 {
144 _zones = zones;
145 }
146
147 /**
148 * @brief Add a zone to the list of zones the action is run against if its
149 * not already there
150 *
151 * @param[in] zone - Zone to add
152 */
addZone(Zone & zone)153 virtual void addZone(Zone& zone)
154 {
155 auto itZone =
156 std::find_if(_zones.begin(), _zones.end(),
157 [&zone](std::reference_wrapper<Zone>& z) {
158 return z.get().getName() == zone.getName();
159 });
160 if (itZone == _zones.end())
161 {
162 _zones.emplace_back(std::reference_wrapper<Zone>(zone));
163 }
164 }
165
166 /**
167 * @brief Run the action
168 *
169 * Run the action function associated to the derived action object
170 * that performs a specific tasks on a zone configured by a user.
171 *
172 * @param[in] zone - Zone to run the action on
173 */
174 virtual void run(Zone& zone) = 0;
175
176 /**
177 * @brief Trigger the action to run against all of its zones
178 *
179 * This is the function used by triggers to run the actions against all the
180 * zones that were configured for the action to run against.
181 */
run()182 void run()
183 {
184 std::for_each(_zones.begin(), _zones.end(),
185 [this](Zone& zone) { this->run(zone); });
186 }
187
188 /**
189 * @brief Returns a unique name for the action.
190 *
191 * @return std::string - The name
192 */
getUniqueName() const193 const std::string& getUniqueName() const
194 {
195 return _uniqueName;
196 }
197
198 /**
199 * @brief Set the name of the owning Event.
200 *
201 * Adds it to the unique name in parentheses. If desired,
202 * the action child classes can do something else with it.
203 *
204 * @param[in] name - The event name
205 */
setEventName(const std::string & name)206 virtual void setEventName(const std::string& name)
207 {
208 if (!name.empty())
209 {
210 _uniqueName += '(' + name + ')';
211 }
212 }
213
214 /**
215 * @brief Dump the action as JSON
216 *
217 * For now just dump its group names
218 *
219 * @return json
220 */
dump() const221 json dump() const
222 {
223 json groups = json::array();
224 std::for_each(_groups.begin(), _groups.end(),
225 [&groups](const auto& group) {
226 groups.push_back(group.getName());
227 });
228 json output;
229 output["groups"] = groups;
230 return output;
231 }
232
233 protected:
234 /**
235 * @brief Logs a message to the flight recorder using
236 * the unique name of the action.
237 *
238 * @param[in] message - The message to log
239 */
record(const std::string & message) const240 void record(const std::string& message) const
241 {
242 FlightRecorder::instance().log(getUniqueName(), message);
243 }
244
245 /* Groups configured on the action */
246 const std::vector<Group> _groups;
247
248 private:
249 /* Zones configured on the action */
250 std::vector<std::reference_wrapper<Zone>> _zones;
251
252 /* Unique name of the action.
253 * It's just the name plus _actionCount at the time of action creation. */
254 std::string _uniqueName;
255
256 /* Running count of all actions */
257 static inline size_t _actionCount = 0;
258 };
259
260 /**
261 * @class ActionFactory - Factory for actions
262 *
263 * Factory that registers and retrieves actions based on a given name.
264 */
265 class ActionFactory
266 {
267 public:
268 ActionFactory() = delete;
269 ActionFactory(const ActionFactory&) = delete;
270 ActionFactory(ActionFactory&&) = delete;
271 ActionFactory& operator=(const ActionFactory&) = delete;
272 ActionFactory& operator=(ActionFactory&&) = delete;
273 ~ActionFactory() = default;
274
275 /**
276 * @brief Registers an action
277 *
278 * Registers an action as being available for configuration use. The action
279 * is registered by its name and a function used to create the action
280 * object. An action fails to be registered when another action of the same
281 * name has already been registered. Actions with the same name would cause
282 * undefined behavior, therefore are not allowed.
283 *
284 * Actions are registered prior to starting main().
285 *
286 * @param[in] name - Name of the action to register
287 *
288 * @return The action was registered, otherwise an exception is thrown.
289 */
290 template <typename T>
regAction(const std::string & name)291 static bool regAction(const std::string& name)
292 {
293 auto it = actions.find(name);
294 if (it == actions.end())
295 {
296 actions[name] = &createAction<T>;
297 }
298 else
299 {
300 lg2::error("Action '{NAME}' is already registered", "NAME", name);
301 throw std::runtime_error("Actions with the same name found");
302 }
303
304 return true;
305 }
306
307 /**
308 * @brief Gets a registered action's object
309 *
310 * Gets a registered action's object of a given name from the JSON
311 * configuration data provided.
312 *
313 * @param[in] name - Name of the action to create/get
314 * @param[in] jsonObj - JSON object for the action
315 * @param[in] groups - Groups of dbus objects the action uses
316 * @param[in] zones - Zones the action runs against
317 *
318 * @return Pointer to the action object.
319 */
getAction(const std::string & name,const json & jsonObj,const std::vector<Group> & groups,std::vector<std::reference_wrapper<Zone>> && zones)320 static std::unique_ptr<ActionBase> getAction(
321 const std::string& name, const json& jsonObj,
322 const std::vector<Group>& groups,
323 std::vector<std::reference_wrapper<Zone>>&& zones)
324 {
325 auto it = actions.find(name);
326 if (it != actions.end())
327 {
328 return it->second(jsonObj, groups, zones);
329 }
330 else
331 {
332 // Construct list of available actions
333 auto acts = std::accumulate(
334 std::next(actions.begin()), actions.end(),
335 actions.begin()->first, [](auto list, auto act) {
336 return std::move(list) + ", " + act.first;
337 });
338 lg2::error(
339 "Action '{NAME}' is not registered. Available actions are {AVAILABLE_ACTIONS}",
340 "NAME", name, "AVAILABLE_ACTIONS", acts);
341 throw std::runtime_error("Unsupported action name given");
342 }
343 }
344
345 private:
346 /* Map to store the available actions and their creation functions */
347 static inline std::map<std::string,
348 std::function<std::unique_ptr<ActionBase>(
349 const json&, const std::vector<Group>&,
350 std::vector<std::reference_wrapper<Zone>>&)>>
351 actions;
352 };
353
354 /**
355 * @class ActionRegister - Registers an action class
356 *
357 * Base action registration class that is extended by an action object so
358 * that action is registered and available for use.
359 */
360 template <typename T>
361 class ActionRegister
362 {
363 public:
364 ActionRegister(const ActionRegister&) = delete;
365 ActionRegister(ActionRegister&&) = delete;
366 ActionRegister& operator=(const ActionRegister&) = delete;
367 ActionRegister& operator=(ActionRegister&&) = delete;
368 virtual ~ActionRegister() = default;
ActionRegister()369 ActionRegister()
370 {
371 // Templates instantiated when used, need to assign a value
372 // here so the compiler doesnt remove it
373 registered = true;
374 }
375
376 private:
377 /* Register actions in the factory */
378 static inline bool registered = ActionFactory::regAction<T>(T::name);
379 };
380
381 } // namespace phosphor::fan::control::json
382