xref: /openbmc/bmcweb/redfish-core/lib/task.hpp (revision 1b8b02a4)
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 #include "bmcweb_config.h"
18 
19 #include "app.hpp"
20 #include "dbus_utility.hpp"
21 #include "event_service_manager.hpp"
22 #include "health.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             std::string uri = "/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             res.addHeader(boost::beast::http::field::location,
158                           uri + "/Monitor");
159             res.addHeader(boost::beast::http::field::retry_after,
160                           std::to_string(retryAfterSeconds));
161         }
162         else if (!gave204)
163         {
164             res.result(boost::beast::http::status::no_content);
165             gave204 = true;
166         }
167     }
168 
169     void finishTask()
170     {
171         endTime = std::chrono::system_clock::to_time_t(
172             std::chrono::system_clock::now());
173     }
174 
175     void extendTimer(const std::chrono::seconds& timeout)
176     {
177         timer.expires_after(timeout);
178         timer.async_wait(
179             [self = shared_from_this()](boost::system::error_code ec) {
180             if (ec == boost::asio::error::operation_aborted)
181             {
182                 return; // completed successfully
183             }
184             if (!ec)
185             {
186                 // change ec to error as timer expired
187                 ec = boost::asio::error::operation_aborted;
188             }
189             self->match.reset();
190             sdbusplus::message_t msg;
191             self->finishTask();
192             self->state = "Cancelled";
193             self->status = "Warning";
194             self->messages.emplace_back(
195                 messages::taskAborted(std::to_string(self->index)));
196             // Send event :TaskAborted
197             self->sendTaskEvent(self->state, self->index);
198             self->callback(ec, msg, self);
199         });
200     }
201 
202     static void sendTaskEvent(std::string_view state, size_t index)
203     {
204         std::string origin = "/redfish/v1/TaskService/Tasks/" +
205                              std::to_string(index);
206         std::string resType = "Task";
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         if (state == "Starting")
219         {
220             redfish::EventServiceManager::getInstance().sendEvent(
221                 redfish::messages::taskResumed(std::to_string(index)), origin,
222                 resType);
223         }
224         else if (state == "Running")
225         {
226             redfish::EventServiceManager::getInstance().sendEvent(
227                 redfish::messages::taskStarted(std::to_string(index)), origin,
228                 resType);
229         }
230         else if ((state == "Suspended") || (state == "Interrupted") ||
231                  (state == "Pending"))
232         {
233             redfish::EventServiceManager::getInstance().sendEvent(
234                 redfish::messages::taskPaused(std::to_string(index)), origin,
235                 resType);
236         }
237         else if (state == "Stopping")
238         {
239             redfish::EventServiceManager::getInstance().sendEvent(
240                 redfish::messages::taskAborted(std::to_string(index)), origin,
241                 resType);
242         }
243         else if (state == "Completed")
244         {
245             redfish::EventServiceManager::getInstance().sendEvent(
246                 redfish::messages::taskCompletedOK(std::to_string(index)),
247                 origin, resType);
248         }
249         else if (state == "Killed")
250         {
251             redfish::EventServiceManager::getInstance().sendEvent(
252                 redfish::messages::taskRemoved(std::to_string(index)), origin,
253                 resType);
254         }
255         else if (state == "Exception")
256         {
257             redfish::EventServiceManager::getInstance().sendEvent(
258                 redfish::messages::taskCompletedWarning(std::to_string(index)),
259                 origin, resType);
260         }
261         else if (state == "Cancelled")
262         {
263             redfish::EventServiceManager::getInstance().sendEvent(
264                 redfish::messages::taskCancelled(std::to_string(index)), origin,
265                 resType);
266         }
267         else
268         {
269             BMCWEB_LOG_INFO("sendTaskEvent: No events to send");
270         }
271     }
272 
273     void startTimer(const std::chrono::seconds& timeout)
274     {
275         if (match)
276         {
277             return;
278         }
279         match = std::make_unique<sdbusplus::bus::match_t>(
280             static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus),
281             matchStr,
282             [self = shared_from_this()](sdbusplus::message_t& message) {
283             boost::system::error_code ec;
284 
285             // callback to return True if callback is done, callback needs
286             // to update status itself if needed
287             if (self->callback(ec, message, self) == task::completed)
288             {
289                 self->timer.cancel();
290                 self->finishTask();
291 
292                 // Send event
293                 self->sendTaskEvent(self->state, self->index);
294 
295                 // reset the match after the callback was successful
296                 boost::asio::post(
297                     crow::connections::systemBus->get_io_context(),
298                     [self] { self->match.reset(); });
299                 return;
300             }
301             });
302 
303         extendTimer(timeout);
304         messages.emplace_back(messages::taskStarted(std::to_string(index)));
305         // Send event : TaskStarted
306         sendTaskEvent(state, index);
307     }
308 
309     std::function<bool(boost::system::error_code, sdbusplus::message_t&,
310                        const std::shared_ptr<TaskData>&)>
311         callback;
312     std::string matchStr;
313     size_t index;
314     time_t startTime;
315     std::string status;
316     std::string state;
317     nlohmann::json messages;
318     boost::asio::steady_timer timer;
319     std::unique_ptr<sdbusplus::bus::match_t> match;
320     std::optional<time_t> endTime;
321     std::optional<Payload> payload;
322     bool gave204 = false;
323     int percentComplete = 0;
324 };
325 
326 } // namespace task
327 
328 inline void requestRoutesTaskMonitor(App& app)
329 {
330     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/Monitor/")
331         .privileges(redfish::privileges::getTask)
332         .methods(boost::beast::http::verb::get)(
333             [&app](const crow::Request& req,
334                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
335                    const std::string& strParam) {
336         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
337         {
338             return;
339         }
340         auto find = std::ranges::find_if(
341             task::tasks,
342             [&strParam](const std::shared_ptr<task::TaskData>& task) {
343             if (!task)
344             {
345                 return false;
346             }
347 
348             // we compare against the string version as on failure
349             // strtoul returns 0
350             return std::to_string(task->index) == strParam;
351             });
352 
353         if (find == task::tasks.end())
354         {
355             messages::resourceNotFound(asyncResp->res, "Task", strParam);
356             return;
357         }
358         std::shared_ptr<task::TaskData>& ptr = *find;
359         // monitor expires after 204
360         if (ptr->gave204)
361         {
362             messages::resourceNotFound(asyncResp->res, "Task", strParam);
363             return;
364         }
365         ptr->populateResp(asyncResp->res);
366         });
367 }
368 
369 inline void requestRoutesTask(App& app)
370 {
371     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<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", strParam);
397             return;
398         }
399 
400         const std::shared_ptr<task::TaskData>& ptr = *find;
401 
402         asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
403         asyncResp->res.jsonValue["Id"] = strParam;
404         asyncResp->res.jsonValue["Name"] = "Task " + strParam;
405         asyncResp->res.jsonValue["TaskState"] = ptr->state;
406         asyncResp->res.jsonValue["StartTime"] =
407             redfish::time_utils::getDateTimeStdtime(ptr->startTime);
408         if (ptr->endTime)
409         {
410             asyncResp->res.jsonValue["EndTime"] =
411                 redfish::time_utils::getDateTimeStdtime(*(ptr->endTime));
412         }
413         asyncResp->res.jsonValue["TaskStatus"] = ptr->status;
414         asyncResp->res.jsonValue["Messages"] = ptr->messages;
415         asyncResp->res.jsonValue["@odata.id"] =
416             boost::urls::format("/redfish/v1/TaskService/Tasks/{}", strParam);
417         if (!ptr->gave204)
418         {
419             asyncResp->res.jsonValue["TaskMonitor"] =
420                 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor";
421         }
422 
423         asyncResp->res.jsonValue["HidePayload"] = !ptr->payload;
424 
425         if (ptr->payload)
426         {
427             const task::Payload& p = *(ptr->payload);
428             asyncResp->res.jsonValue["Payload"]["TargetUri"] = p.targetUri;
429             asyncResp->res.jsonValue["Payload"]["HttpOperation"] =
430                 p.httpOperation;
431             asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = p.httpHeaders;
432             asyncResp->res.jsonValue["Payload"]["JsonBody"] = p.jsonBody.dump(
433                 2, ' ', true, nlohmann::json::error_handler_t::replace);
434         }
435         asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete;
436         });
437 }
438 
439 inline void requestRoutesTaskCollection(App& app)
440 {
441     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/")
442         .privileges(redfish::privileges::getTaskCollection)
443         .methods(boost::beast::http::verb::get)(
444             [&app](const crow::Request& req,
445                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
446         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
447         {
448             return;
449         }
450         asyncResp->res.jsonValue["@odata.type"] =
451             "#TaskCollection.TaskCollection";
452         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks";
453         asyncResp->res.jsonValue["Name"] = "Task Collection";
454         asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size();
455         nlohmann::json& members = asyncResp->res.jsonValue["Members"];
456         members = nlohmann::json::array();
457 
458         for (const std::shared_ptr<task::TaskData>& task : task::tasks)
459         {
460             if (task == nullptr)
461             {
462                 continue; // shouldn't be possible
463             }
464             nlohmann::json::object_t member;
465             member["@odata.id"] =
466                 boost::urls::format("/redfish/v1/TaskService/Tasks/{}",
467                                     std::to_string(task->index));
468             members.emplace_back(std::move(member));
469         }
470         });
471 }
472 
473 inline void requestRoutesTaskService(App& app)
474 {
475     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/")
476         .privileges(redfish::privileges::getTaskService)
477         .methods(boost::beast::http::verb::get)(
478             [&app](const crow::Request& req,
479                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
480         if (!redfish::setUpRedfishRoute(app, req, asyncResp))
481         {
482             return;
483         }
484         asyncResp->res.jsonValue["@odata.type"] =
485             "#TaskService.v1_1_4.TaskService";
486         asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService";
487         asyncResp->res.jsonValue["Name"] = "Task Service";
488         asyncResp->res.jsonValue["Id"] = "TaskService";
489         asyncResp->res.jsonValue["DateTime"] =
490             redfish::time_utils::getDateTimeOffsetNow().first;
491         asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest";
492 
493         asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true;
494 
495         if constexpr (bmcwebEnableHealthPopulate)
496         {
497             auto health = std::make_shared<HealthPopulate>(asyncResp);
498             health->populate();
499         }
500         asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
501         asyncResp->res.jsonValue["ServiceEnabled"] = true;
502         asyncResp->res.jsonValue["Tasks"]["@odata.id"] =
503             "/redfish/v1/TaskService/Tasks";
504         });
505 }
506 
507 } // namespace redfish
508