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