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