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 } 194 else if (!gave204) 195 { 196 res.result(boost::beast::http::status::no_content); 197 gave204 = true; 198 } 199 } 200 201 void finishTask() 202 { 203 endTime = std::chrono::system_clock::to_time_t( 204 std::chrono::system_clock::now()); 205 } 206 207 void extendTimer(const std::chrono::seconds& timeout) 208 { 209 timer.expires_after(timeout); 210 timer.async_wait( 211 [self = shared_from_this()](boost::system::error_code ec) { 212 if (ec == boost::asio::error::operation_aborted) 213 { 214 return; // completed successfully 215 } 216 if (!ec) 217 { 218 // change ec to error as timer expired 219 ec = boost::asio::error::operation_aborted; 220 } 221 self->match.reset(); 222 sdbusplus::message_t msg; 223 self->finishTask(); 224 self->state = "Cancelled"; 225 self->status = "Warning"; 226 self->messages.emplace_back( 227 messages::taskAborted(std::to_string(self->index))); 228 // Send event :TaskAborted 229 sendTaskEvent(self->state, self->index); 230 self->callback(ec, msg, self); 231 }); 232 } 233 234 static void sendTaskEvent(std::string_view state, size_t index) 235 { 236 // TaskState enums which should send out an event are: 237 // "Starting" = taskResumed 238 // "Running" = taskStarted 239 // "Suspended" = taskPaused 240 // "Interrupted" = taskPaused 241 // "Pending" = taskPaused 242 // "Stopping" = taskAborted 243 // "Completed" = taskCompletedOK 244 // "Killed" = taskRemoved 245 // "Exception" = taskCompletedWarning 246 // "Cancelled" = taskCancelled 247 nlohmann::json::object_t event; 248 std::string indexStr = std::to_string(index); 249 if (state == "Starting") 250 { 251 event = redfish::messages::taskResumed(indexStr); 252 } 253 else if (state == "Running") 254 { 255 event = redfish::messages::taskStarted(indexStr); 256 } 257 else if ((state == "Suspended") || (state == "Interrupted") || 258 (state == "Pending")) 259 { 260 event = redfish::messages::taskPaused(indexStr); 261 } 262 else if (state == "Stopping") 263 { 264 event = redfish::messages::taskAborted(indexStr); 265 } 266 else if (state == "Completed") 267 { 268 event = redfish::messages::taskCompletedOK(indexStr); 269 } 270 else if (state == "Killed") 271 { 272 event = redfish::messages::taskRemoved(indexStr); 273 } 274 else if (state == "Exception") 275 { 276 event = redfish::messages::taskCompletedWarning(indexStr); 277 } 278 else if (state == "Cancelled") 279 { 280 event = redfish::messages::taskCancelled(indexStr); 281 } 282 else 283 { 284 BMCWEB_LOG_INFO("sendTaskEvent: No events to send"); 285 return; 286 } 287 boost::urls::url origin = 288 boost::urls::format("/redfish/v1/TaskService/Tasks/{}", index); 289 EventServiceManager::getInstance().sendEvent(event, origin.buffer(), 290 "Task"); 291 } 292 293 void startTimer(const std::chrono::seconds& timeout) 294 { 295 if (match) 296 { 297 return; 298 } 299 match = std::make_unique<sdbusplus::bus::match_t>( 300 static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus), 301 matchStr, 302 [self = shared_from_this()](sdbusplus::message_t& message) { 303 boost::system::error_code ec; 304 305 // callback to return True if callback is done, callback needs 306 // to update status itself if needed 307 if (self->callback(ec, message, self) == task::completed) 308 { 309 self->timer.cancel(); 310 self->finishTask(); 311 312 // Send event 313 sendTaskEvent(self->state, self->index); 314 315 // reset the match after the callback was successful 316 boost::asio::post( 317 crow::connections::systemBus->get_io_context(), 318 [self] { self->match.reset(); }); 319 return; 320 } 321 }); 322 323 extendTimer(timeout); 324 messages.emplace_back(messages::taskStarted(std::to_string(index))); 325 // Send event : TaskStarted 326 sendTaskEvent(state, index); 327 } 328 329 std::function<bool(boost::system::error_code, sdbusplus::message_t&, 330 const std::shared_ptr<TaskData>&)> 331 callback; 332 std::string matchStr; 333 size_t index; 334 time_t startTime; 335 std::string status; 336 std::string state; 337 nlohmann::json messages; 338 boost::asio::steady_timer timer; 339 std::unique_ptr<sdbusplus::bus::match_t> match; 340 std::optional<time_t> endTime; 341 std::optional<Payload> payload; 342 bool gave204 = false; 343 int percentComplete = 0; 344 }; 345 346 } // namespace task 347 348 inline void requestRoutesTaskMonitor(App& app) 349 { 350 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/TaskMonitors/<str>/") 351 .privileges(redfish::privileges::getTask) 352 .methods(boost::beast::http::verb::get)( 353 [&app](const crow::Request& req, 354 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 355 const std::string& strParam) { 356 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 357 { 358 return; 359 } 360 auto find = std::ranges::find_if( 361 task::tasks, 362 [&strParam](const std::shared_ptr<task::TaskData>& task) { 363 if (!task) 364 { 365 return false; 366 } 367 368 // we compare against the string version as on failure 369 // strtoul returns 0 370 return std::to_string(task->index) == strParam; 371 }); 372 373 if (find == task::tasks.end()) 374 { 375 messages::resourceNotFound(asyncResp->res, "Task", 376 strParam); 377 return; 378 } 379 std::shared_ptr<task::TaskData>& ptr = *find; 380 // monitor expires after 204 381 if (ptr->gave204) 382 { 383 messages::resourceNotFound(asyncResp->res, "Task", 384 strParam); 385 return; 386 } 387 ptr->populateResp(asyncResp->res); 388 }); 389 } 390 391 inline void requestRoutesTask(App& app) 392 { 393 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/") 394 .privileges(redfish::privileges::getTask) 395 .methods(boost::beast::http::verb::get)( 396 [&app](const crow::Request& req, 397 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 398 const std::string& strParam) { 399 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 400 { 401 return; 402 } 403 auto find = std::ranges::find_if( 404 task::tasks, 405 [&strParam](const std::shared_ptr<task::TaskData>& task) { 406 if (!task) 407 { 408 return false; 409 } 410 411 // we compare against the string version as on failure 412 // strtoul returns 0 413 return std::to_string(task->index) == strParam; 414 }); 415 416 if (find == task::tasks.end()) 417 { 418 messages::resourceNotFound(asyncResp->res, "Task", 419 strParam); 420 return; 421 } 422 423 const std::shared_ptr<task::TaskData>& ptr = *find; 424 425 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 426 asyncResp->res.jsonValue["Id"] = strParam; 427 asyncResp->res.jsonValue["Name"] = "Task " + strParam; 428 asyncResp->res.jsonValue["TaskState"] = ptr->state; 429 asyncResp->res.jsonValue["StartTime"] = 430 redfish::time_utils::getDateTimeStdtime(ptr->startTime); 431 if (ptr->endTime) 432 { 433 asyncResp->res.jsonValue["EndTime"] = 434 redfish::time_utils::getDateTimeStdtime( 435 *(ptr->endTime)); 436 } 437 asyncResp->res.jsonValue["TaskStatus"] = ptr->status; 438 asyncResp->res.jsonValue["Messages"] = ptr->messages; 439 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( 440 "/redfish/v1/TaskService/Tasks/{}", strParam); 441 if (!ptr->gave204) 442 { 443 asyncResp->res.jsonValue["TaskMonitor"] = 444 boost::urls::format( 445 "/redfish/v1/TaskService/TaskMonitors/{}", 446 strParam); 447 } 448 449 asyncResp->res.jsonValue["HidePayload"] = !ptr->payload; 450 451 if (ptr->payload) 452 { 453 const task::Payload& p = *(ptr->payload); 454 asyncResp->res.jsonValue["Payload"]["TargetUri"] = 455 p.targetUri; 456 asyncResp->res.jsonValue["Payload"]["HttpOperation"] = 457 p.httpOperation; 458 asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = 459 p.httpHeaders; 460 asyncResp->res.jsonValue["Payload"]["JsonBody"] = 461 p.jsonBody.dump( 462 -1, ' ', true, 463 nlohmann::json::error_handler_t::replace); 464 } 465 asyncResp->res.jsonValue["PercentComplete"] = 466 ptr->percentComplete; 467 }); 468 } 469 470 inline void requestRoutesTaskCollection(App& app) 471 { 472 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/") 473 .privileges(redfish::privileges::getTaskCollection) 474 .methods(boost::beast::http::verb::get)( 475 [&app](const crow::Request& req, 476 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 477 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 478 { 479 return; 480 } 481 asyncResp->res.jsonValue["@odata.type"] = 482 "#TaskCollection.TaskCollection"; 483 asyncResp->res.jsonValue["@odata.id"] = 484 "/redfish/v1/TaskService/Tasks"; 485 asyncResp->res.jsonValue["Name"] = "Task Collection"; 486 asyncResp->res.jsonValue["Members@odata.count"] = 487 task::tasks.size(); 488 nlohmann::json& members = asyncResp->res.jsonValue["Members"]; 489 members = nlohmann::json::array(); 490 491 for (const std::shared_ptr<task::TaskData>& task : task::tasks) 492 { 493 if (task == nullptr) 494 { 495 continue; // shouldn't be possible 496 } 497 nlohmann::json::object_t member; 498 member["@odata.id"] = 499 boost::urls::format("/redfish/v1/TaskService/Tasks/{}", 500 std::to_string(task->index)); 501 members.emplace_back(std::move(member)); 502 } 503 }); 504 } 505 506 inline void requestRoutesTaskService(App& app) 507 { 508 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/") 509 .privileges(redfish::privileges::getTaskService) 510 .methods(boost::beast::http::verb::get)( 511 [&app](const crow::Request& req, 512 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 513 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 514 { 515 return; 516 } 517 asyncResp->res.jsonValue["@odata.type"] = 518 "#TaskService.v1_1_4.TaskService"; 519 asyncResp->res.jsonValue["@odata.id"] = 520 "/redfish/v1/TaskService"; 521 asyncResp->res.jsonValue["Name"] = "Task Service"; 522 asyncResp->res.jsonValue["Id"] = "TaskService"; 523 asyncResp->res.jsonValue["DateTime"] = 524 redfish::time_utils::getDateTimeOffsetNow().first; 525 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = 526 task_service::OverWritePolicy::Oldest; 527 528 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = 529 true; 530 531 asyncResp->res.jsonValue["Status"]["State"] = 532 resource::State::Enabled; 533 asyncResp->res.jsonValue["ServiceEnabled"] = true; 534 asyncResp->res.jsonValue["Tasks"]["@odata.id"] = 535 "/redfish/v1/TaskService/Tasks"; 536 }); 537 } 538 539 } // namespace redfish 540