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