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 inline void to_json(nlohmann::json& j, const Payload& p) 84 { 85 j = {{"TargetUri", p.targetUri}, 86 {"HttpOperation", p.httpOperation}, 87 {"HttpHeaders", p.httpHeaders}, 88 {"JsonBody", p.jsonBody.dump()}}; 89 } 90 91 struct TaskData : std::enable_shared_from_this<TaskData> 92 { 93 private: 94 TaskData(std::function<bool(boost::system::error_code, 95 sdbusplus::message::message&, 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, 112 sdbusplus::message::message&, 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::message&, 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 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 std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx; 149 res.jsonValue = {{"@odata.id", uri}, 150 {"@odata.type", "#Task.v1_4_3.Task"}, 151 {"Id", strIdx}, 152 {"TaskState", state}, 153 {"TaskStatus", status}}; 154 res.addHeader(boost::beast::http::field::location, 155 uri + "/Monitor"); 156 res.addHeader(boost::beast::http::field::retry_after, 157 std::to_string(retryAfterSeconds)); 158 } 159 else if (!gave204) 160 { 161 res.result(boost::beast::http::status::no_content); 162 gave204 = true; 163 } 164 } 165 166 void finishTask() 167 { 168 endTime = std::chrono::system_clock::to_time_t( 169 std::chrono::system_clock::now()); 170 } 171 172 void extendTimer(const std::chrono::seconds& timeout) 173 { 174 timer.expires_after(timeout); 175 timer.async_wait( 176 [self = shared_from_this()](boost::system::error_code ec) { 177 if (ec == boost::asio::error::operation_aborted) 178 { 179 return; // completed successfully 180 } 181 if (!ec) 182 { 183 // change ec to error as timer expired 184 ec = boost::asio::error::operation_aborted; 185 } 186 self->match.reset(); 187 sdbusplus::message::message msg; 188 self->finishTask(); 189 self->state = "Cancelled"; 190 self->status = "Warning"; 191 self->messages.emplace_back( 192 messages::taskAborted(std::to_string(self->index))); 193 // Send event :TaskAborted 194 self->sendTaskEvent(self->state, self->index); 195 self->callback(ec, msg, self); 196 }); 197 } 198 199 void sendTaskEvent(const std::string_view state, size_t index) 200 { 201 std::string origin = 202 "/redfish/v1/TaskService/Tasks/" + std::to_string(index); 203 std::string resType = "Task"; 204 // TaskState enums which should send out an event are: 205 // "Starting" = taskResumed 206 // "Running" = taskStarted 207 // "Suspended" = taskPaused 208 // "Interrupted" = taskPaused 209 // "Pending" = taskPaused 210 // "Stopping" = taskAborted 211 // "Completed" = taskCompletedOK 212 // "Killed" = taskRemoved 213 // "Exception" = taskCompletedWarning 214 // "Cancelled" = taskCancelled 215 if (state == "Starting") 216 { 217 redfish::EventServiceManager::getInstance().sendEvent( 218 redfish::messages::taskResumed(std::to_string(index)), origin, 219 resType); 220 } 221 else if (state == "Running") 222 { 223 redfish::EventServiceManager::getInstance().sendEvent( 224 redfish::messages::taskStarted(std::to_string(index)), origin, 225 resType); 226 } 227 else if ((state == "Suspended") || (state == "Interrupted") || 228 (state == "Pending")) 229 { 230 redfish::EventServiceManager::getInstance().sendEvent( 231 redfish::messages::taskPaused(std::to_string(index)), origin, 232 resType); 233 } 234 else if (state == "Stopping") 235 { 236 redfish::EventServiceManager::getInstance().sendEvent( 237 redfish::messages::taskAborted(std::to_string(index)), origin, 238 resType); 239 } 240 else if (state == "Completed") 241 { 242 redfish::EventServiceManager::getInstance().sendEvent( 243 redfish::messages::taskCompletedOK(std::to_string(index)), 244 origin, resType); 245 } 246 else if (state == "Killed") 247 { 248 redfish::EventServiceManager::getInstance().sendEvent( 249 redfish::messages::taskRemoved(std::to_string(index)), origin, 250 resType); 251 } 252 else if (state == "Exception") 253 { 254 redfish::EventServiceManager::getInstance().sendEvent( 255 redfish::messages::taskCompletedWarning(std::to_string(index)), 256 origin, resType); 257 } 258 else if (state == "Cancelled") 259 { 260 redfish::EventServiceManager::getInstance().sendEvent( 261 redfish::messages::taskCancelled(std::to_string(index)), origin, 262 resType); 263 } 264 else 265 { 266 BMCWEB_LOG_INFO << "sendTaskEvent: No events to send"; 267 } 268 } 269 270 void startTimer(const std::chrono::seconds& timeout) 271 { 272 if (match) 273 { 274 return; 275 } 276 match = std::make_unique<sdbusplus::bus::match::match>( 277 static_cast<sdbusplus::bus::bus&>(*crow::connections::systemBus), 278 matchStr, 279 [self = shared_from_this()](sdbusplus::message::message& message) { 280 boost::system::error_code ec; 281 282 // callback to return True if callback is done, callback needs 283 // to update status itself if needed 284 if (self->callback(ec, message, self) == task::completed) 285 { 286 self->timer.cancel(); 287 self->finishTask(); 288 289 // Send event 290 self->sendTaskEvent(self->state, self->index); 291 292 // reset the match after the callback was successful 293 boost::asio::post( 294 crow::connections::systemBus->get_io_context(), 295 [self] { self->match.reset(); }); 296 return; 297 } 298 }); 299 300 extendTimer(timeout); 301 messages.emplace_back(messages::taskStarted(std::to_string(index))); 302 // Send event : TaskStarted 303 sendTaskEvent(state, index); 304 } 305 306 std::function<bool(boost::system::error_code, sdbusplus::message::message&, 307 const std::shared_ptr<TaskData>&)> 308 callback; 309 std::string matchStr; 310 size_t index; 311 time_t startTime; 312 std::string status; 313 std::string state; 314 nlohmann::json messages; 315 boost::asio::steady_timer timer; 316 std::unique_ptr<sdbusplus::bus::match::match> match; 317 std::optional<time_t> endTime; 318 std::optional<Payload> payload; 319 bool gave204 = false; 320 }; 321 322 } // namespace task 323 324 class TaskMonitor : public Node 325 { 326 public: 327 TaskMonitor(App& app) : 328 Node((app), "/redfish/v1/TaskService/Tasks/<str>/Monitor/", 329 std::string()) 330 { 331 entityPrivileges = { 332 {boost::beast::http::verb::get, {{"Login"}}}, 333 {boost::beast::http::verb::head, {{"Login"}}}, 334 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 335 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 336 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 337 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 338 } 339 340 private: 341 void doGet(crow::Response& res, const crow::Request&, 342 const std::vector<std::string>& params) override 343 { 344 auto asyncResp = std::make_shared<AsyncResp>(res); 345 if (params.size() != 1) 346 { 347 messages::internalError(asyncResp->res); 348 return; 349 } 350 351 const std::string& strParam = params[0]; 352 auto find = std::find_if( 353 task::tasks.begin(), task::tasks.end(), 354 [&strParam](const std::shared_ptr<task::TaskData>& task) { 355 if (!task) 356 { 357 return false; 358 } 359 360 // we compare against the string version as on failure strtoul 361 // returns 0 362 return std::to_string(task->index) == strParam; 363 }); 364 365 if (find == task::tasks.end()) 366 { 367 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 368 return; 369 } 370 std::shared_ptr<task::TaskData>& ptr = *find; 371 // monitor expires after 204 372 if (ptr->gave204) 373 { 374 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 375 return; 376 } 377 ptr->populateResp(asyncResp->res); 378 } 379 }; 380 381 class Task : public Node 382 { 383 public: 384 Task(App& app) : 385 Node((app), "/redfish/v1/TaskService/Tasks/<str>/", std::string()) 386 { 387 entityPrivileges = { 388 {boost::beast::http::verb::get, {{"Login"}}}, 389 {boost::beast::http::verb::head, {{"Login"}}}, 390 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 391 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 392 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 393 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 394 } 395 396 private: 397 void doGet(crow::Response& res, const crow::Request&, 398 const std::vector<std::string>& params) override 399 { 400 auto asyncResp = std::make_shared<AsyncResp>(res); 401 if (params.size() != 1) 402 { 403 messages::internalError(asyncResp->res); 404 return; 405 } 406 407 const std::string& strParam = params[0]; 408 auto find = std::find_if( 409 task::tasks.begin(), task::tasks.end(), 410 [&strParam](const std::shared_ptr<task::TaskData>& task) { 411 if (!task) 412 { 413 return false; 414 } 415 416 // we compare against the string version as on failure strtoul 417 // returns 0 418 return std::to_string(task->index) == strParam; 419 }); 420 421 if (find == task::tasks.end()) 422 { 423 messages::resourceNotFound(asyncResp->res, "Tasks", strParam); 424 return; 425 } 426 427 std::shared_ptr<task::TaskData>& ptr = *find; 428 429 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 430 asyncResp->res.jsonValue["Id"] = strParam; 431 asyncResp->res.jsonValue["Name"] = "Task " + strParam; 432 asyncResp->res.jsonValue["TaskState"] = ptr->state; 433 asyncResp->res.jsonValue["StartTime"] = 434 crow::utility::getDateTime(ptr->startTime); 435 if (ptr->endTime) 436 { 437 asyncResp->res.jsonValue["EndTime"] = 438 crow::utility::getDateTime(*(ptr->endTime)); 439 } 440 asyncResp->res.jsonValue["TaskStatus"] = ptr->status; 441 asyncResp->res.jsonValue["Messages"] = ptr->messages; 442 asyncResp->res.jsonValue["@odata.id"] = 443 "/redfish/v1/TaskService/Tasks/" + strParam; 444 if (!ptr->gave204) 445 { 446 asyncResp->res.jsonValue["TaskMonitor"] = 447 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor"; 448 } 449 if (ptr->payload) 450 { 451 asyncResp->res.jsonValue["Payload"] = *(ptr->payload); 452 } 453 } 454 }; 455 456 class TaskCollection : public Node 457 { 458 public: 459 TaskCollection(App& app) : Node(app, "/redfish/v1/TaskService/Tasks/") 460 { 461 entityPrivileges = { 462 {boost::beast::http::verb::get, {{"Login"}}}, 463 {boost::beast::http::verb::head, {{"Login"}}}, 464 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 465 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 466 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 467 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 468 } 469 470 private: 471 void doGet(crow::Response& res, const crow::Request&, 472 const std::vector<std::string>&) override 473 { 474 auto asyncResp = std::make_shared<AsyncResp>(res); 475 asyncResp->res.jsonValue["@odata.type"] = 476 "#TaskCollection.TaskCollection"; 477 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; 478 asyncResp->res.jsonValue["Name"] = "Task Collection"; 479 asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); 480 nlohmann::json& members = asyncResp->res.jsonValue["Members"]; 481 members = nlohmann::json::array(); 482 483 for (const std::shared_ptr<task::TaskData>& task : task::tasks) 484 { 485 if (task == nullptr) 486 { 487 continue; // shouldn't be possible 488 } 489 members.emplace_back( 490 nlohmann::json{{"@odata.id", "/redfish/v1/TaskService/Tasks/" + 491 std::to_string(task->index)}}); 492 } 493 } 494 }; 495 496 class TaskService : public Node 497 { 498 public: 499 TaskService(App& app) : Node(app, "/redfish/v1/TaskService/") 500 { 501 entityPrivileges = { 502 {boost::beast::http::verb::get, {{"Login"}}}, 503 {boost::beast::http::verb::head, {{"Login"}}}, 504 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 505 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 506 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 507 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 508 } 509 510 private: 511 void doGet(crow::Response& res, const crow::Request&, 512 const std::vector<std::string>&) override 513 { 514 auto asyncResp = std::make_shared<AsyncResp>(res); 515 asyncResp->res.jsonValue["@odata.type"] = 516 "#TaskService.v1_1_4.TaskService"; 517 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; 518 asyncResp->res.jsonValue["Name"] = "Task Service"; 519 asyncResp->res.jsonValue["Id"] = "TaskService"; 520 asyncResp->res.jsonValue["DateTime"] = crow::utility::dateTimeNow(); 521 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; 522 523 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true; 524 525 auto health = std::make_shared<HealthPopulate>(asyncResp); 526 health->populate(); 527 asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; 528 asyncResp->res.jsonValue["ServiceEnabled"] = true; 529 asyncResp->res.jsonValue["Tasks"] = { 530 {"@odata.id", "/redfish/v1/TaskService/Tasks"}}; 531 } 532 }; 533 534 } // namespace redfish 535