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