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