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