xref: /openbmc/bmcweb/features/redfish/lib/task.hpp (revision 45ca1b868e47978a4d2e8ebb680cb384e804c97e)
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     Payload(const crow::Request& req) :
43         targetUri(req.url), httpOperation(req.methodString()),
44         httpHeaders(nlohmann::json::array())
45 
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         jsonBody = nlohmann::json::parse(req.body, nullptr, false);
55         if (jsonBody.is_discarded())
56         {
57             jsonBody = nullptr;
58         }
59 
60         for (const auto& field : req.fields)
61         {
62             if (std::find(headerWhitelist.begin(), headerWhitelist.end(),
63                           field.name()) == headerWhitelist.end())
64             {
65                 continue;
66             }
67             std::string header;
68             header.reserve(field.name_string().size() + 2 +
69                            field.value().size());
70             header += field.name_string();
71             header += ": ";
72             header += field.value();
73             httpHeaders.emplace_back(std::move(header));
74         }
75     }
76     Payload() = delete;
77 
78     std::string targetUri;
79     std::string httpOperation;
80     nlohmann::json httpHeaders;
81     nlohmann::json jsonBody;
82 };
83 
84 struct TaskData : std::enable_shared_from_this<TaskData>
85 {
86   private:
87     TaskData(std::function<bool(boost::system::error_code,
88                                 sdbusplus::message::message&,
89                                 const std::shared_ptr<TaskData>&)>&& handler,
90              const std::string& matchIn, size_t idx) :
91         callback(std::move(handler)),
92         matchStr(matchIn), index(idx),
93         startTime(std::chrono::system_clock::to_time_t(
94             std::chrono::system_clock::now())),
95         status("OK"), state("Running"), messages(nlohmann::json::array()),
96         timer(crow::connections::systemBus->get_io_context())
97 
98     {}
99 
100   public:
101     TaskData() = delete;
102 
103     static std::shared_ptr<TaskData>& createTask(
104         std::function<bool(boost::system::error_code,
105                            sdbusplus::message::message&,
106                            const std::shared_ptr<TaskData>&)>&& handler,
107         const std::string& match)
108     {
109         static size_t lastTask = 0;
110         struct MakeSharedHelper : public TaskData
111         {
112             MakeSharedHelper(
113                 std::function<bool(boost::system::error_code,
114                                    sdbusplus::message::message&,
115                                    const std::shared_ptr<TaskData>&)>&& handler,
116                 const std::string& match2, size_t idx) :
117                 TaskData(std::move(handler), match2, idx)
118             {}
119         };
120 
121         if (tasks.size() >= maxTaskCount)
122         {
123             auto& last = tasks.front();
124 
125             // destroy all references
126             last->timer.cancel();
127             last->match.reset();
128             tasks.pop_front();
129         }
130 
131         return tasks.emplace_back(std::make_shared<MakeSharedHelper>(
132             std::move(handler), match, lastTask++));
133     }
134 
135     void populateResp(crow::Response& res, size_t retryAfterSeconds = 30)
136     {
137         if (!endTime)
138         {
139             res.result(boost::beast::http::status::accepted);
140             std::string strIdx = std::to_string(index);
141             std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx;
142             res.jsonValue = {{"@odata.id", uri},
143                              {"@odata.type", "#Task.v1_4_3.Task"},
144                              {"Id", strIdx},
145                              {"TaskState", state},
146                              {"TaskStatus", status}};
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::message 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::match>(
270             static_cast<sdbusplus::bus::bus&>(*crow::connections::systemBus),
271             matchStr,
272             [self = shared_from_this()](sdbusplus::message::message& 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::message&,
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::match> 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->res))
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, "Monitor",
346                                                strParam);
347                     return;
348                 }
349                 std::shared_ptr<task::TaskData>& ptr = *find;
350                 // monitor expires after 204
351                 if (ptr->gave204)
352                 {
353                     messages::resourceNotFound(asyncResp->res, "Monitor",
354                                                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->res))
370                 {
371                     return;
372                 }
373                 auto find = std::find_if(
374                     task::tasks.begin(), task::tasks.end(),
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, "Tasks",
389                                                strParam);
390                     return;
391                 }
392 
393                 std::shared_ptr<task::TaskData>& ptr = *find;
394 
395                 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
396                 asyncResp->res.jsonValue["Id"] = strParam;
397                 asyncResp->res.jsonValue["Name"] = "Task " + strParam;
398                 asyncResp->res.jsonValue["TaskState"] = ptr->state;
399                 asyncResp->res.jsonValue["StartTime"] =
400                     crow::utility::getDateTimeStdtime(ptr->startTime);
401                 if (ptr->endTime)
402                 {
403                     asyncResp->res.jsonValue["EndTime"] =
404                         crow::utility::getDateTimeStdtime(*(ptr->endTime));
405                 }
406                 asyncResp->res.jsonValue["TaskStatus"] = ptr->status;
407                 asyncResp->res.jsonValue["Messages"] = ptr->messages;
408                 asyncResp->res.jsonValue["@odata.id"] =
409                     "/redfish/v1/TaskService/Tasks/" + strParam;
410                 if (!ptr->gave204)
411                 {
412                     asyncResp->res.jsonValue["TaskMonitor"] =
413                         "/redfish/v1/TaskService/Tasks/" + strParam +
414                         "/Monitor";
415                 }
416                 if (ptr->payload)
417                 {
418                     const task::Payload& p = *(ptr->payload);
419                     asyncResp->res.jsonValue["Payload"] = {
420                         {"TargetUri", p.targetUri},
421                         {"HttpOperation", p.httpOperation},
422                         {"HttpHeaders", p.httpHeaders},
423                         {"JsonBody",
424                          p.jsonBody.dump(
425                              2, ' ', true,
426                              nlohmann::json::error_handler_t::replace)}};
427                 }
428                 asyncResp->res.jsonValue["PercentComplete"] =
429                     ptr->percentComplete;
430             });
431 }
432 
433 inline void requestRoutesTaskCollection(App& app)
434 {
435     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/")
436         .privileges(redfish::privileges::getTaskCollection)
437         .methods(boost::beast::http::verb::get)(
438             [&app](const crow::Request& req,
439                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
440                 if (!redfish::setUpRedfishRoute(app, req, asyncResp->res))
441                 {
442                     return;
443                 }
444                 asyncResp->res.jsonValue["@odata.type"] =
445                     "#TaskCollection.TaskCollection";
446                 asyncResp->res.jsonValue["@odata.id"] =
447                     "/redfish/v1/TaskService/Tasks";
448                 asyncResp->res.jsonValue["Name"] = "Task Collection";
449                 asyncResp->res.jsonValue["Members@odata.count"] =
450                     task::tasks.size();
451                 nlohmann::json& members = asyncResp->res.jsonValue["Members"];
452                 members = nlohmann::json::array();
453 
454                 for (const std::shared_ptr<task::TaskData>& task : task::tasks)
455                 {
456                     if (task == nullptr)
457                     {
458                         continue; // shouldn't be possible
459                     }
460                     members.emplace_back(nlohmann::json{
461                         {"@odata.id", "/redfish/v1/TaskService/Tasks/" +
462                                           std::to_string(task->index)}});
463                 }
464             });
465 }
466 
467 inline void requestRoutesTaskService(App& app)
468 {
469     BMCWEB_ROUTE(app, "/redfish/v1/TaskService/")
470         .privileges(redfish::privileges::getTaskService)
471         .methods(boost::beast::http::verb::get)(
472             [&app](const crow::Request& req,
473                    const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
474                 if (!redfish::setUpRedfishRoute(app, req, asyncResp->res))
475                 {
476                     return;
477                 }
478                 asyncResp->res.jsonValue["@odata.type"] =
479                     "#TaskService.v1_1_4.TaskService";
480                 asyncResp->res.jsonValue["@odata.id"] =
481                     "/redfish/v1/TaskService";
482                 asyncResp->res.jsonValue["Name"] = "Task Service";
483                 asyncResp->res.jsonValue["Id"] = "TaskService";
484                 asyncResp->res.jsonValue["DateTime"] =
485                     crow::utility::getDateTimeOffsetNow().first;
486                 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] =
487                     "Oldest";
488 
489                 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] =
490                     true;
491 
492                 auto health = std::make_shared<HealthPopulate>(asyncResp);
493                 health->populate();
494                 asyncResp->res.jsonValue["Status"]["State"] = "Enabled";
495                 asyncResp->res.jsonValue["ServiceEnabled"] = true;
496                 asyncResp->res.jsonValue["Tasks"] = {
497                     {"@odata.id", "/redfish/v1/TaskService/Tasks"}};
498             });
499 }
500 
501 } // namespace redfish
502