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