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