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