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