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