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