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