xref: /openbmc/bmcweb/redfish-core/lib/task.hpp (revision f263e09c)
1 /*
2 // Copyright (c) 2020 Intel 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 "app.hpp"
19 #include "dbus_utility.hpp"
20 #include "event_service_manager.hpp"
21 #include "health.hpp"
22 #include "http/parsing.hpp"
23 #include "query.hpp"
24 #include "registries/privilege_registry.hpp"
25 #include "task_messages.hpp"
26 
27 #include <boost/asio/post.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <boost/url/format.hpp>
30 #include <sdbusplus/bus/match.hpp>
31 
32 #include <chrono>
33 #include <memory>
34 #include <variant>
35 
36 namespace redfish
37 {
38 
39 namespace task
40 {
41 constexpr size_t maxTaskCount = 100; // arbitrary limit
42 
43 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
44 static std::deque<std::shared_ptr<struct TaskData>> tasks;
45 
46 constexpr bool completed = true;
47 
48 struct Payload
49 {
50     explicit Payload(const crow::Request& req) :
51         targetUri(req.url().encoded_path()), httpOperation(req.methodString()),
52         httpHeaders(nlohmann::json::array())
53     {
54         using field_ns = boost::beast::http::field;
55         constexpr const std::array<boost::beast::http::field, 7>
56             headerWhitelist = {field_ns::accept,     field_ns::accept_encoding,
57                                field_ns::user_agent, field_ns::host,
58                                field_ns::connection, field_ns::content_length,
59                                field_ns::upgrade};
60 
61         JsonParseResult ret = parseRequestAsJson(req, jsonBody);
62         if (ret != JsonParseResult::Success)
63         {
64             return;
65         }
66 
67         for (const auto& field : req.fields())
68         {
69             if (std::find(headerWhitelist.begin(), headerWhitelist.end(),
70                           field.name()) == headerWhitelist.end())
71             {
72                 continue;
73             }
74             std::string header;
75             header.reserve(field.name_string().size() + 2 +
76                            field.value().size());
77             header += field.name_string();
78             header += ": ";
79             header += field.value();
80             httpHeaders.emplace_back(std::move(header));
81         }
82     }
83     Payload() = delete;
84 
85     std::string targetUri;
86     std::string httpOperation;
87     nlohmann::json httpHeaders;
88     nlohmann::json jsonBody;
89 };
90 
91 struct TaskData : std::enable_shared_from_this<TaskData>
92 {
93   private:
94     TaskData(
95         std::function<bool(boost::system::error_code, sdbusplus::message_t&,
96                            const std::shared_ptr<TaskData>&)>&& handler,
97         const std::string& matchIn, size_t idx) :
98         callback(std::move(handler)),
99         matchStr(matchIn), index(idx),
100         startTime(std::chrono::system_clock::to_time_t(
101             std::chrono::system_clock::now())),
102         status("OK"), state("Running"), messages(nlohmann::json::array()),
103         timer(crow::connections::systemBus->get_io_context())
104 
105     {}
106 
107   public:
108     TaskData() = delete;
109 
110     static std::shared_ptr<TaskData>& createTask(
111         std::function<bool(boost::system::error_code, sdbusplus::message_t&,
112                            const std::shared_ptr<TaskData>&)>&& handler,
113         const std::string& match)
114     {
115         static size_t lastTask = 0;
116         struct MakeSharedHelper : public TaskData
117         {
118             MakeSharedHelper(
119                 std::function<bool(boost::system::error_code,
120                                    sdbusplus::message_t&,
121                                    const std::shared_ptr<TaskData>&)>&& handler,
122                 const std::string& match2, size_t idx) :
123                 TaskData(std::move(handler), match2, idx)
124             {}
125         };
126 
127         if (tasks.size() >= maxTaskCount)
128         {
129             const auto& last = tasks.front();
130 
131             // destroy all references
132             last->timer.cancel();
133             last->match.reset();
134             tasks.pop_front();
135         }
136 
137         return tasks.emplace_back(std::make_shared<MakeSharedHelper>(
138             std::move(handler), match, lastTask++));
139     }
140 
141     void populateResp(crow::Response& res, size_t retryAfterSeconds = 30)
142     {
143         if (!endTime)
144         {
145             res.result(boost::beast::http::status::accepted);
146             std::string strIdx = std::to_string(index);
147             std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx;
148 
149             res.jsonValue["@odata.id"] = uri;
150             res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
151             res.jsonValue["Id"] = strIdx;
152             res.jsonValue["TaskState"] = state;
153             res.jsonValue["TaskStatus"] = status;
154 
155             res.addHeader(boost::beast::http::field::location,
156                           uri + "/Monitor");
157             res.addHeader(boost::beast::http::field::retry_after,
158                           std::to_string(retryAfterSeconds));
159         }
160         else if (!gave204)
161         {
162             res.result(boost::beast::http::status::no_content);
163             gave204 = true;
164         }
165     }
166 
167     void finishTask()
168     {
169         endTime = std::chrono::system_clock::to_time_t(
170             std::chrono::system_clock::now());
171     }
172 
173     void extendTimer(const std::chrono::seconds& timeout)
174     {
175         timer.expires_after(timeout);
176         timer.async_wait(
177             [self = shared_from_this()](boost::system::error_code ec) {
178             if (ec == boost::asio::error::operation_aborted)
179             {
180                 return; // completed successfully
181             }
182             if (!ec)
183             {
184                 // change ec to error as timer expired
185                 ec = boost::asio::error::operation_aborted;
186             }
187             self->match.reset();
188             sdbusplus::message_t msg;
189             self->finishTask();
190             self->state = "Cancelled";
191             self->status = "Warning";
192             self->messages.emplace_back(
193                 messages::taskAborted(std::to_string(self->index)));
194             // Send event :TaskAborted
195             self->sendTaskEvent(self->state, self->index);
196             self->callback(ec, msg, self);
197         });
198     }
199 
200     static void sendTaskEvent(std::string_view state, size_t index)
201     {
202         std::string origin = "/redfish/v1/TaskService/Tasks/" +
203                              std::to_string(index);
204         std::string resType = "Task";
205         // TaskState enums which should send out an event are:
206         // "Starting" = taskResumed
207         // "Running" = taskStarted
208         // "Suspended" = taskPaused
209         // "Interrupted" = taskPaused
210         // "Pending" = taskPaused
211         // "Stopping" = taskAborted
212         // "Completed" = taskCompletedOK
213         // "Killed" = taskRemoved
214         // "Exception" = taskCompletedWarning
215         // "Cancelled" = taskCancelled
216         if (state == "Starting")
217         {
218             redfish::EventServiceManager::getInstance().sendEvent(
219                 redfish::messages::taskResumed(std::to_string(index)), origin,
220                 resType);
221         }
222         else if (state == "Running")
223         {
224             redfish::EventServiceManager::getInstance().sendEvent(
225                 redfish::messages::taskStarted(std::to_string(index)), origin,
226                 resType);
227         }
228         else if ((state == "Suspended") || (state == "Interrupted") ||
229                  (state == "Pending"))
230         {
231             redfish::EventServiceManager::getInstance().sendEvent(
232                 redfish::messages::taskPaused(std::to_string(index)), origin,
233                 resType);
234         }
235         else if (state == "Stopping")
236         {
237             redfish::EventServiceManager::getInstance().sendEvent(
238                 redfish::messages::taskAborted(std::to_string(index)), origin,
239                 resType);
240         }
241         else if (state == "Completed")
242         {
243             redfish::EventServiceManager::getInstance().sendEvent(
244                 redfish::messages::taskCompletedOK(std::to_string(index)),
245                 origin, resType);
246         }
247         else if (state == "Killed")
248         {
249             redfish::EventServiceManager::getInstance().sendEvent(
250                 redfish::messages::taskRemoved(std::to_string(index)), origin,
251                 resType);
252         }
253         else if (state == "Exception")
254         {
255             redfish::EventServiceManager::getInstance().sendEvent(
256                 redfish::messages::taskCompletedWarning(std::to_string(index)),
257                 origin, resType);
258         }
259         else if (state == "Cancelled")
260         {
261             redfish::EventServiceManager::getInstance().sendEvent(
262                 redfish::messages::taskCancelled(std::to_string(index)), origin,
263                 resType);
264         }
265         else
266         {
267             BMCWEB_LOG_INFO << "sendTaskEvent: No events to send";
268         }
269     }
270 
271     void startTimer(const std::chrono::seconds& timeout)
272     {
273         if (match)
274         {
275             return;
276         }
277         match = std::make_unique<sdbusplus::bus::match_t>(
278             static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus),
279             matchStr,
280             [self = shared_from_this()](sdbusplus::message_t& message) {
281             boost::system::error_code ec;
282 
283             // callback to return True if callback is done, callback needs
284             // to update status itself if needed
285             if (self->callback(ec, message, self) == task::completed)
286             {
287                 self->timer.cancel();
288                 self->finishTask();
289 
290                 // Send event
291                 self->sendTaskEvent(self->state, self->index);
292 
293                 // reset the match after the callback was successful
294                 boost::asio::post(
295                     crow::connections::systemBus->get_io_context(),
296                     [self] { self->match.reset(); });
297                 return;
298             }
299             });
300 
301         extendTimer(timeout);
302         messages.emplace_back(messages::taskStarted(std::to_string(index)));
303         // Send event : TaskStarted
304         sendTaskEvent(state, index);
305     }
306 
307     std::function<bool(boost::system::error_code, sdbusplus::message_t&,
308                        const std::shared_ptr<TaskData>&)>
309         callback;
310     std::string matchStr;
311     size_t index;
312     time_t startTime;
313     std::string status;
314     std::string state;
315     nlohmann::json messages;
316     boost::asio::steady_timer timer;
317     std::unique_ptr<sdbusplus::bus::match_t> match;
318     std::optional<time_t> endTime;
319     std::optional<Payload> payload;
320     bool gave204 = false;
321     int percentComplete = 0;
322 };
323 
324 } // namespace task
325 
326 inline void requestRoutesTaskMonitor(App& app)
327 {
328     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/Monitor/")
329         .privileges(redfish::privileges::getTask)
330         .methods(boost::beast::http::verb::get)(
331             [&app](const crow::Request& req,
332                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
333                    const std::string& strParam) {
334         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
335         {
336             return;
337         }
338         auto find = std::find_if(
339             task::tasks.begin(), task::tasks.end(),
340             [&strParam](const std::shared_ptr<task::TaskData>& task) {
341             if (!task)
342             {
343                 return false;
344             }
345 
346             // we compare against the string version as on failure
347             // strtoul returns 0
348             return std::to_string(task->index) == strParam;
349             });
350 
351         if (find == task::tasks.end())
352         {
353             messages::resourceNotFound(asyncResp->res, "Task", strParam);
354             return;
355         }
356         std::shared_ptr<task::TaskData>& ptr = *find;
357         // monitor expires after 204
358         if (ptr->gave204)
359         {
360             messages::resourceNotFound(asyncResp->res, "Task", strParam);
361             return;
362         }
363         ptr->populateResp(asyncResp->res);
364         });
365 }
366 
367 inline void requestRoutesTask(App& app)
368 {
369     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/")
370         .privileges(redfish::privileges::getTask)
371         .methods(boost::beast::http::verb::get)(
372             [&app](const crow::Request& req,
373                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
374                    const std::string& strParam) {
375         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
376         {
377             return;
378         }
379         auto find = std::find_if(
380             task::tasks.begin(), task::tasks.end(),
381             [&strParam](const std::shared_ptr<task::TaskData>& task) {
382             if (!task)
383             {
384                 return false;
385             }
386 
387             // we compare against the string version as on failure
388             // strtoul returns 0
389             return std::to_string(task->index) == strParam;
390             });
391 
392         if (find == task::tasks.end())
393         {
394             messages::resourceNotFound(asyncResp->res, "Task", strParam);
395             return;
396         }
397 
398         const std::shared_ptr<task::TaskData>& ptr = *find;
399 
400         asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
401         asyncResp->res.jsonValue["Id"] = strParam;
402         asyncResp->res.jsonValue["Name"] = "Task " + strParam;
403         asyncResp->res.jsonValue["TaskState"] = ptr->state;
404         asyncResp->res.jsonValue["StartTime"] =
405             redfish::time_utils::getDateTimeStdtime(ptr->startTime);
406         if (ptr->endTime)
407         {
408             asyncResp->res.jsonValue["EndTime"] =
409                 redfish::time_utils::getDateTimeStdtime(*(ptr->endTime));
410         }
411         asyncResp->res.jsonValue["TaskStatus"] = ptr->status;
412         asyncResp->res.jsonValue["Messages"] = ptr->messages;
413         asyncResp->res.jsonValue["@odata.id"] =
414             boost::urls::format("/redfish/v1/TaskService/Tasks/{}", strParam);
415         if (!ptr->gave204)
416         {
417             asyncResp->res.jsonValue["TaskMonitor"] =
418                 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor";
419         }
420         if (ptr->payload)
421         {
422             const task::Payload& p = *(ptr->payload);
423             asyncResp->res.jsonValue["Payload"]["TargetUri"] = p.targetUri;
424             asyncResp->res.jsonValue["Payload"]["HttpOperation"] =
425                 p.httpOperation;
426             asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = p.httpHeaders;
427             asyncResp->res.jsonValue["Payload"]["JsonBody"] = p.jsonBody.dump(
428                 2, ' ', true, nlohmann::json::error_handler_t::replace);
429         }
430         asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete;
431         });
432 }
433 
434 inline void requestRoutesTaskCollection(App& app)
435 {
436     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/")
437         .privileges(redfish::privileges::getTaskCollection)
438         .methods(boost::beast::http::verb::get)(
439             [&app](const crow::Request& req,
440                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
441         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
442         {
443             return;
444         }
445         asyncResp->res.jsonValue["@odata.type"] =
446             "#TaskCollection.TaskCollection";
447         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks";
448         asyncResp->res.jsonValue["Name"] = "Task Collection";
449         asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size();
450         nlohmann::json& members = asyncResp->res.jsonValue["Members"];
451         members = nlohmann::json::array();
452 
453         for (const std::shared_ptr<task::TaskData>& task : task::tasks)
454         {
455             if (task == nullptr)
456             {
457                 continue; // shouldn't be possible
458             }
459             nlohmann::json::object_t member;
460             member["@odata.id"] =
461                 boost::urls::format("/redfish/v1/TaskService/Tasks/{}",
462                                     std::to_string(task->index));
463             members.emplace_back(std::move(member));
464         }
465         });
466 }
467 
468 inline void requestRoutesTaskService(App& app)
469 {
470     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/")
471         .privileges(redfish::privileges::getTaskService)
472         .methods(boost::beast::http::verb::get)(
473             [&app](const crow::Request& req,
474                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
475         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
476         {
477             return;
478         }
479         asyncResp->res.jsonValue["@odata.type"] =
480             "#TaskService.v1_1_4.TaskService";
481         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService";
482         asyncResp->res.jsonValue["Name"] = "Task Service";
483         asyncResp->res.jsonValue["Id"] = "TaskService";
484         asyncResp->res.jsonValue["DateTime"] =
485             redfish::time_utils::getDateTimeOffsetNow().first;
486         asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest";
487 
488         asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true;
489 
490         auto health = std::make_shared<HealthPopulate>(asyncResp);
491         health->populate();
492         asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
493         asyncResp->res.jsonValue["ServiceEnabled"] = true;
494         asyncResp->res.jsonValue["Tasks"]["@odata.id"] =
495             "/redfish/v1/TaskService/Tasks";
496         });
497 }
498 
499 } // namespace redfish
500