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