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