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