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