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