xref: /openbmc/bmcweb/redfish-core/lib/task.hpp (revision bd79bce8)
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