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