xref: /openbmc/phosphor-fan-presence/control/json/actions/action.hpp (revision 64b5ac203518568ec8b7569d0e785352278f2472)
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