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