xref: /openbmc/bmcweb/redfish-core/lib/task.hpp (revision 9f03894ecf12e0d0ffb0ba7855c256f33f959e44)
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         }
194         else if (!gave204)
195         {
196             res.result(boost::beast::http::status::no_content);
197             gave204 = true;
198         }
199     }
200 
finishTaskredfish::task::TaskData201     void finishTask()
202     {
203         endTime = std::chrono::system_clock::to_time_t(
204             std::chrono::system_clock::now());
205     }
206 
extendTimerredfish::task::TaskData207     void extendTimer(const std::chrono::seconds& timeout)
208     {
209         timer.expires_after(timeout);
210         timer.async_wait(
211             [self = shared_from_this()](boost::system::error_code ec) {
212                 if (ec == boost::asio::error::operation_aborted)
213                 {
214                     return; // completed successfully
215                 }
216                 if (!ec)
217                 {
218                     // change ec to error as timer expired
219                     ec = boost::asio::error::operation_aborted;
220                 }
221                 self->match.reset();
222                 sdbusplus::message_t msg;
223                 self->finishTask();
224                 self->state = "Cancelled";
225                 self->status = "Warning";
226                 self->messages.emplace_back(
227                     messages::taskAborted(std::to_string(self->index)));
228                 // Send event :TaskAborted
229                 sendTaskEvent(self->state, self->index);
230                 self->callback(ec, msg, self);
231             });
232     }
233 
sendTaskEventredfish::task::TaskData234     static void sendTaskEvent(std::string_view state, size_t index)
235     {
236         // TaskState enums which should send out an event are:
237         // "Starting" = taskResumed
238         // "Running" = taskStarted
239         // "Suspended" = taskPaused
240         // "Interrupted" = taskPaused
241         // "Pending" = taskPaused
242         // "Stopping" = taskAborted
243         // "Completed" = taskCompletedOK
244         // "Killed" = taskRemoved
245         // "Exception" = taskCompletedWarning
246         // "Cancelled" = taskCancelled
247         nlohmann::json event;
248         std::string indexStr = std::to_string(index);
249         if (state == "Starting")
250         {
251             event = redfish::messages::taskResumed(indexStr);
252         }
253         else if (state == "Running")
254         {
255             event = redfish::messages::taskStarted(indexStr);
256         }
257         else if ((state == "Suspended") || (state == "Interrupted") ||
258                  (state == "Pending"))
259         {
260             event = redfish::messages::taskPaused(indexStr);
261         }
262         else if (state == "Stopping")
263         {
264             event = redfish::messages::taskAborted(indexStr);
265         }
266         else if (state == "Completed")
267         {
268             event = redfish::messages::taskCompletedOK(indexStr);
269         }
270         else if (state == "Killed")
271         {
272             event = redfish::messages::taskRemoved(indexStr);
273         }
274         else if (state == "Exception")
275         {
276             event = redfish::messages::taskCompletedWarning(indexStr);
277         }
278         else if (state == "Cancelled")
279         {
280             event = redfish::messages::taskCancelled(indexStr);
281         }
282         else
283         {
284             BMCWEB_LOG_INFO("sendTaskEvent: No events to send");
285             return;
286         }
287         boost::urls::url origin =
288             boost::urls::format("/redfish/v1/TaskService/Tasks/{}", index);
289         EventServiceManager::getInstance().sendEvent(event, origin.buffer(),
290                                                      "Task");
291     }
292 
startTimerredfish::task::TaskData293     void startTimer(const std::chrono::seconds& timeout)
294     {
295         if (match)
296         {
297             return;
298         }
299         match = std::make_unique<sdbusplus::bus::match_t>(
300             static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus),
301             matchStr,
302             [self = shared_from_this()](sdbusplus::message_t& message) {
303                 boost::system::error_code ec;
304 
305                 // callback to return True if callback is done, callback needs
306                 // to update status itself if needed
307                 if (self->callback(ec, message, self) == task::completed)
308                 {
309                     self->timer.cancel();
310                     self->finishTask();
311 
312                     // Send event
313                     sendTaskEvent(self->state, self->index);
314 
315                     // reset the match after the callback was successful
316                     boost::asio::post(
317                         crow::connections::systemBus->get_io_context(),
318                         [self] { self->match.reset(); });
319                     return;
320                 }
321             });
322 
323         extendTimer(timeout);
324         messages.emplace_back(messages::taskStarted(std::to_string(index)));
325         // Send event : TaskStarted
326         sendTaskEvent(state, index);
327     }
328 
329     std::function<bool(boost::system::error_code, sdbusplus::message_t&,
330                        const std::shared_ptr<TaskData>&)>
331         callback;
332     std::string matchStr;
333     size_t index;
334     time_t startTime;
335     std::string status;
336     std::string state;
337     nlohmann::json messages;
338     boost::asio::steady_timer timer;
339     std::unique_ptr<sdbusplus::bus::match_t> match;
340     std::optional<time_t> endTime;
341     std::optional<Payload> payload;
342     bool gave204 = false;
343     int percentComplete = 0;
344 };
345 
346 } // namespace task
347 
requestRoutesTaskMonitor(App & app)348 inline void requestRoutesTaskMonitor(App& app)
349 {
350     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/TaskMonitors/<str>/")
351         .privileges(redfish::privileges::getTask)
352         .methods(boost::beast::http::verb::get)(
353             [&app](const crow::Request& req,
354                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
355                    const std::string& strParam) {
356                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
357                 {
358                     return;
359                 }
360                 auto find = std::ranges::find_if(
361                     task::tasks,
362                     [&strParam](const std::shared_ptr<task::TaskData>& task) {
363                         if (!task)
364                         {
365                             return false;
366                         }
367 
368                         // we compare against the string version as on failure
369                         // strtoul returns 0
370                         return std::to_string(task->index) == strParam;
371                     });
372 
373                 if (find == task::tasks.end())
374                 {
375                     messages::resourceNotFound(asyncResp->res, "Task",
376                                                strParam);
377                     return;
378                 }
379                 std::shared_ptr<task::TaskData>& ptr = *find;
380                 // monitor expires after 204
381                 if (ptr->gave204)
382                 {
383                     messages::resourceNotFound(asyncResp->res, "Task",
384                                                strParam);
385                     return;
386                 }
387                 ptr->populateResp(asyncResp->res);
388             });
389 }
390 
requestRoutesTask(App & app)391 inline void requestRoutesTask(App& app)
392 {
393     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/")
394         .privileges(redfish::privileges::getTask)
395         .methods(boost::beast::http::verb::get)(
396             [&app](const crow::Request& req,
397                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
398                    const std::string& strParam) {
399                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
400                 {
401                     return;
402                 }
403                 auto find = std::ranges::find_if(
404                     task::tasks,
405                     [&strParam](const std::shared_ptr<task::TaskData>& task) {
406                         if (!task)
407                         {
408                             return false;
409                         }
410 
411                         // we compare against the string version as on failure
412                         // strtoul returns 0
413                         return std::to_string(task->index) == strParam;
414                     });
415 
416                 if (find == task::tasks.end())
417                 {
418                     messages::resourceNotFound(asyncResp->res, "Task",
419                                                strParam);
420                     return;
421                 }
422 
423                 const std::shared_ptr<task::TaskData>& ptr = *find;
424 
425                 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
426                 asyncResp->res.jsonValue["Id"] = strParam;
427                 asyncResp->res.jsonValue["Name"] = "Task " + strParam;
428                 asyncResp->res.jsonValue["TaskState"] = ptr->state;
429                 asyncResp->res.jsonValue["StartTime"] =
430                     redfish::time_utils::getDateTimeStdtime(ptr->startTime);
431                 if (ptr->endTime)
432                 {
433                     asyncResp->res.jsonValue["EndTime"] =
434                         redfish::time_utils::getDateTimeStdtime(
435                             *(ptr->endTime));
436                 }
437                 asyncResp->res.jsonValue["TaskStatus"] = ptr->status;
438                 asyncResp->res.jsonValue["Messages"] = ptr->messages;
439                 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
440                     "/redfish/v1/TaskService/Tasks/{}", strParam);
441                 if (!ptr->gave204)
442                 {
443                     asyncResp->res.jsonValue["TaskMonitor"] =
444                         boost::urls::format(
445                             "/redfish/v1/TaskService/TaskMonitors/{}",
446                             strParam);
447                 }
448 
449                 asyncResp->res.jsonValue["HidePayload"] = !ptr->payload;
450 
451                 if (ptr->payload)
452                 {
453                     const task::Payload& p = *(ptr->payload);
454                     asyncResp->res.jsonValue["Payload"]["TargetUri"] =
455                         p.targetUri;
456                     asyncResp->res.jsonValue["Payload"]["HttpOperation"] =
457                         p.httpOperation;
458                     asyncResp->res.jsonValue["Payload"]["HttpHeaders"] =
459                         p.httpHeaders;
460                     asyncResp->res.jsonValue["Payload"]["JsonBody"] =
461                         p.jsonBody.dump(
462                             -1, ' ', true,
463                             nlohmann::json::error_handler_t::replace);
464                 }
465                 asyncResp->res.jsonValue["PercentComplete"] =
466                     ptr->percentComplete;
467             });
468 }
469 
requestRoutesTaskCollection(App & app)470 inline void requestRoutesTaskCollection(App& app)
471 {
472     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/")
473         .privileges(redfish::privileges::getTaskCollection)
474         .methods(boost::beast::http::verb::get)(
475             [&app](const crow::Request& req,
476                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
477                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
478                 {
479                     return;
480                 }
481                 asyncResp->res.jsonValue["@odata.type"] =
482                     "#TaskCollection.TaskCollection";
483                 asyncResp->res.jsonValue["@odata.id"] =
484                     "/redfish/v1/TaskService/Tasks";
485                 asyncResp->res.jsonValue["Name"] = "Task Collection";
486                 asyncResp->res.jsonValue["Members@odata.count"] =
487                     task::tasks.size();
488                 nlohmann::json& members = asyncResp->res.jsonValue["Members"];
489                 members = nlohmann::json::array();
490 
491                 for (const std::shared_ptr<task::TaskData>& task : task::tasks)
492                 {
493                     if (task == nullptr)
494                     {
495                         continue; // shouldn't be possible
496                     }
497                     nlohmann::json::object_t member;
498                     member["@odata.id"] =
499                         boost::urls::format("/redfish/v1/TaskService/Tasks/{}",
500                                             std::to_string(task->index));
501                     members.emplace_back(std::move(member));
502                 }
503             });
504 }
505 
requestRoutesTaskService(App & app)506 inline void requestRoutesTaskService(App& app)
507 {
508     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/")
509         .privileges(redfish::privileges::getTaskService)
510         .methods(boost::beast::http::verb::get)(
511             [&app](const crow::Request& req,
512                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
513                 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
514                 {
515                     return;
516                 }
517                 asyncResp->res.jsonValue["@odata.type"] =
518                     "#TaskService.v1_1_4.TaskService";
519                 asyncResp->res.jsonValue["@odata.id"] =
520                     "/redfish/v1/TaskService";
521                 asyncResp->res.jsonValue["Name"] = "Task Service";
522                 asyncResp->res.jsonValue["Id"] = "TaskService";
523                 asyncResp->res.jsonValue["DateTime"] =
524                     redfish::time_utils::getDateTimeOffsetNow().first;
525                 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] =
526                     task_service::OverWritePolicy::Oldest;
527 
528                 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] =
529                     true;
530 
531                 asyncResp->res.jsonValue["Status"]["State"] =
532                     resource::State::Enabled;
533                 asyncResp->res.jsonValue["ServiceEnabled"] = true;
534                 asyncResp->res.jsonValue["Tasks"]["@odata.id"] =
535                     "/redfish/v1/TaskService/Tasks";
536             });
537 }
538 
539 } // namespace redfish
540