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