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(crow::Response& res, const crow::Request&, 335 const std::vector<std::string>& params) override 336 { 337 auto asyncResp = std::make_shared<AsyncResp>(res); 338 if (params.size() != 1) 339 { 340 messages::internalError(asyncResp->res); 341 return; 342 } 343 344 const std::string& strParam = params[0]; 345 auto find = std::find_if( 346 task::tasks.begin(), task::tasks.end(), 347 [&strParam](const std::shared_ptr<task::TaskData>& task) { 348 if (!task) 349 { 350 return false; 351 } 352 353 // we compare against the string version as on failure strtoul 354 // returns 0 355 return std::to_string(task->index) == strParam; 356 }); 357 358 if (find == task::tasks.end()) 359 { 360 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 361 return; 362 } 363 std::shared_ptr<task::TaskData>& ptr = *find; 364 // monitor expires after 204 365 if (ptr->gave204) 366 { 367 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 368 return; 369 } 370 ptr->populateResp(asyncResp->res); 371 } 372 }; 373 374 class Task : public Node 375 { 376 public: 377 Task(App& app) : 378 Node((app), "/redfish/v1/TaskService/Tasks/<str>/", std::string()) 379 { 380 entityPrivileges = { 381 {boost::beast::http::verb::get, {{"Login"}}}, 382 {boost::beast::http::verb::head, {{"Login"}}}, 383 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 384 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 385 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 386 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 387 } 388 389 private: 390 void doGet(crow::Response& res, const crow::Request&, 391 const std::vector<std::string>& params) override 392 { 393 auto asyncResp = std::make_shared<AsyncResp>(res); 394 if (params.size() != 1) 395 { 396 messages::internalError(asyncResp->res); 397 return; 398 } 399 400 const std::string& strParam = params[0]; 401 auto find = std::find_if( 402 task::tasks.begin(), task::tasks.end(), 403 [&strParam](const std::shared_ptr<task::TaskData>& task) { 404 if (!task) 405 { 406 return false; 407 } 408 409 // we compare against the string version as on failure strtoul 410 // returns 0 411 return std::to_string(task->index) == strParam; 412 }); 413 414 if (find == task::tasks.end()) 415 { 416 messages::resourceNotFound(asyncResp->res, "Tasks", strParam); 417 return; 418 } 419 420 std::shared_ptr<task::TaskData>& ptr = *find; 421 422 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 423 asyncResp->res.jsonValue["Id"] = strParam; 424 asyncResp->res.jsonValue["Name"] = "Task " + strParam; 425 asyncResp->res.jsonValue["TaskState"] = ptr->state; 426 asyncResp->res.jsonValue["StartTime"] = 427 crow::utility::getDateTime(ptr->startTime); 428 if (ptr->endTime) 429 { 430 asyncResp->res.jsonValue["EndTime"] = 431 crow::utility::getDateTime(*(ptr->endTime)); 432 } 433 asyncResp->res.jsonValue["TaskStatus"] = ptr->status; 434 asyncResp->res.jsonValue["Messages"] = ptr->messages; 435 asyncResp->res.jsonValue["@odata.id"] = 436 "/redfish/v1/TaskService/Tasks/" + strParam; 437 if (!ptr->gave204) 438 { 439 asyncResp->res.jsonValue["TaskMonitor"] = 440 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor"; 441 } 442 if (ptr->payload) 443 { 444 const task::Payload& p = *(ptr->payload); 445 asyncResp->res.jsonValue["Payload"] = { 446 {"TargetUri", p.targetUri}, 447 {"HttpOperation", p.httpOperation}, 448 {"HttpHeaders", p.httpHeaders}, 449 {"JsonBody", p.jsonBody.dump()}}; 450 } 451 asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete; 452 } 453 }; 454 455 class TaskCollection : public Node 456 { 457 public: 458 TaskCollection(App& app) : Node(app, "/redfish/v1/TaskService/Tasks/") 459 { 460 entityPrivileges = { 461 {boost::beast::http::verb::get, {{"Login"}}}, 462 {boost::beast::http::verb::head, {{"Login"}}}, 463 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 464 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 465 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 466 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 467 } 468 469 private: 470 void doGet(crow::Response& res, const crow::Request&, 471 const std::vector<std::string>&) override 472 { 473 auto asyncResp = std::make_shared<AsyncResp>(res); 474 asyncResp->res.jsonValue["@odata.type"] = 475 "#TaskCollection.TaskCollection"; 476 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; 477 asyncResp->res.jsonValue["Name"] = "Task Collection"; 478 asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); 479 nlohmann::json& members = asyncResp->res.jsonValue["Members"]; 480 members = nlohmann::json::array(); 481 482 for (const std::shared_ptr<task::TaskData>& task : task::tasks) 483 { 484 if (task == nullptr) 485 { 486 continue; // shouldn't be possible 487 } 488 members.emplace_back( 489 nlohmann::json{{"@odata.id", "/redfish/v1/TaskService/Tasks/" + 490 std::to_string(task->index)}}); 491 } 492 } 493 }; 494 495 class TaskService : public Node 496 { 497 public: 498 TaskService(App& app) : Node(app, "/redfish/v1/TaskService/") 499 { 500 entityPrivileges = { 501 {boost::beast::http::verb::get, {{"Login"}}}, 502 {boost::beast::http::verb::head, {{"Login"}}}, 503 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 504 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 505 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 506 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 507 } 508 509 private: 510 void doGet(crow::Response& res, const crow::Request&, 511 const std::vector<std::string>&) override 512 { 513 auto asyncResp = std::make_shared<AsyncResp>(res); 514 asyncResp->res.jsonValue["@odata.type"] = 515 "#TaskService.v1_1_4.TaskService"; 516 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; 517 asyncResp->res.jsonValue["Name"] = "Task Service"; 518 asyncResp->res.jsonValue["Id"] = "TaskService"; 519 asyncResp->res.jsonValue["DateTime"] = crow::utility::dateTimeNow(); 520 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; 521 522 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true; 523 524 auto health = std::make_shared<HealthPopulate>(asyncResp); 525 health->populate(); 526 asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; 527 asyncResp->res.jsonValue["ServiceEnabled"] = true; 528 asyncResp->res.jsonValue["Tasks"] = { 529 {"@odata.id", "/redfish/v1/TaskService/Tasks"}}; 530 } 531 }; 532 533 } // namespace redfish 534