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 { 64 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: 108 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 123 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 */ 158 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 172 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 223 void finishTask() 224 { 225 endTime = std::chrono::system_clock::to_time_t( 226 std::chrono::system_clock::now()); 227 } 228 229 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 256 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 315 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 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 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 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 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