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 explicit Payload(const crow::Request& req) : 43 targetUri(req.url), httpOperation(req.methodString()), 44 httpHeaders(nlohmann::json::array()), 45 jsonBody(nlohmann::json::parse(req.body, nullptr, false)) 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 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 struct TaskData : std::enable_shared_from_this<TaskData> 84 { 85 private: 86 TaskData( 87 std::function<bool(boost::system::error_code, sdbusplus::message_t&, 88 const std::shared_ptr<TaskData>&)>&& handler, 89 const std::string& matchIn, size_t idx) : 90 callback(std::move(handler)), 91 matchStr(matchIn), index(idx), 92 startTime(std::chrono::system_clock::to_time_t( 93 std::chrono::system_clock::now())), 94 status("OK"), state("Running"), messages(nlohmann::json::array()), 95 timer(crow::connections::systemBus->get_io_context()) 96 97 {} 98 99 public: 100 TaskData() = delete; 101 102 static std::shared_ptr<TaskData>& createTask( 103 std::function<bool(boost::system::error_code, sdbusplus::message_t&, 104 const std::shared_ptr<TaskData>&)>&& handler, 105 const std::string& match) 106 { 107 static size_t lastTask = 0; 108 struct MakeSharedHelper : public TaskData 109 { 110 MakeSharedHelper( 111 std::function<bool(boost::system::error_code, 112 sdbusplus::message_t&, 113 const std::shared_ptr<TaskData>&)>&& handler, 114 const std::string& match2, size_t idx) : 115 TaskData(std::move(handler), match2, idx) 116 {} 117 }; 118 119 if (tasks.size() >= maxTaskCount) 120 { 121 const auto& last = tasks.front(); 122 123 // destroy all references 124 last->timer.cancel(); 125 last->match.reset(); 126 tasks.pop_front(); 127 } 128 129 return tasks.emplace_back(std::make_shared<MakeSharedHelper>( 130 std::move(handler), match, lastTask++)); 131 } 132 133 void populateResp(crow::Response& res, size_t retryAfterSeconds = 30) 134 { 135 if (!endTime) 136 { 137 res.result(boost::beast::http::status::accepted); 138 std::string strIdx = std::to_string(index); 139 std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx; 140 141 res.jsonValue["@odata.id"] = uri; 142 res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 143 res.jsonValue["Id"] = strIdx; 144 res.jsonValue["TaskState"] = state; 145 res.jsonValue["TaskStatus"] = status; 146 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_t 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_t>( 270 static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus), 271 matchStr, 272 [self = shared_from_this()](sdbusplus::message_t& 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_t&, 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_t> 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)) 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, "Task", strParam); 346 return; 347 } 348 std::shared_ptr<task::TaskData>& ptr = *find; 349 // monitor expires after 204 350 if (ptr->gave204) 351 { 352 messages::resourceNotFound(asyncResp->res, "Task", strParam); 353 return; 354 } 355 ptr->populateResp(asyncResp->res); 356 }); 357 } 358 359 inline void requestRoutesTask(App& app) 360 { 361 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/") 362 .privileges(redfish::privileges::getTask) 363 .methods(boost::beast::http::verb::get)( 364 [&app](const crow::Request& req, 365 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 366 const std::string& strParam) { 367 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 368 { 369 return; 370 } 371 auto find = std::find_if( 372 task::tasks.begin(), task::tasks.end(), 373 [&strParam](const std::shared_ptr<task::TaskData>& task) { 374 if (!task) 375 { 376 return false; 377 } 378 379 // we compare against the string version as on failure 380 // strtoul returns 0 381 return std::to_string(task->index) == strParam; 382 }); 383 384 if (find == task::tasks.end()) 385 { 386 messages::resourceNotFound(asyncResp->res, "Task", strParam); 387 return; 388 } 389 390 const std::shared_ptr<task::TaskData>& ptr = *find; 391 392 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 393 asyncResp->res.jsonValue["Id"] = strParam; 394 asyncResp->res.jsonValue["Name"] = "Task " + strParam; 395 asyncResp->res.jsonValue["TaskState"] = ptr->state; 396 asyncResp->res.jsonValue["StartTime"] = 397 redfish::time_utils::getDateTimeStdtime(ptr->startTime); 398 if (ptr->endTime) 399 { 400 asyncResp->res.jsonValue["EndTime"] = 401 redfish::time_utils::getDateTimeStdtime(*(ptr->endTime)); 402 } 403 asyncResp->res.jsonValue["TaskStatus"] = ptr->status; 404 asyncResp->res.jsonValue["Messages"] = ptr->messages; 405 asyncResp->res.jsonValue["@odata.id"] = 406 "/redfish/v1/TaskService/Tasks/" + strParam; 407 if (!ptr->gave204) 408 { 409 asyncResp->res.jsonValue["TaskMonitor"] = 410 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor"; 411 } 412 if (ptr->payload) 413 { 414 const task::Payload& p = *(ptr->payload); 415 asyncResp->res.jsonValue["Payload"]["TargetUri"] = p.targetUri; 416 asyncResp->res.jsonValue["Payload"]["HttpOperation"] = 417 p.httpOperation; 418 asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = p.httpHeaders; 419 asyncResp->res.jsonValue["Payload"]["JsonBody"] = p.jsonBody.dump( 420 2, ' ', true, nlohmann::json::error_handler_t::replace); 421 } 422 asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete; 423 }); 424 } 425 426 inline void requestRoutesTaskCollection(App& app) 427 { 428 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/") 429 .privileges(redfish::privileges::getTaskCollection) 430 .methods(boost::beast::http::verb::get)( 431 [&app](const crow::Request& req, 432 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 433 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 434 { 435 return; 436 } 437 asyncResp->res.jsonValue["@odata.type"] = 438 "#TaskCollection.TaskCollection"; 439 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; 440 asyncResp->res.jsonValue["Name"] = "Task Collection"; 441 asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); 442 nlohmann::json& members = asyncResp->res.jsonValue["Members"]; 443 members = nlohmann::json::array(); 444 445 for (const std::shared_ptr<task::TaskData>& task : task::tasks) 446 { 447 if (task == nullptr) 448 { 449 continue; // shouldn't be possible 450 } 451 nlohmann::json::object_t member; 452 member["@odata.id"] = 453 "redfish/v1/TaskService/Tasks/" + std::to_string(task->index); 454 members.emplace_back(std::move(member)); 455 } 456 }); 457 } 458 459 inline void requestRoutesTaskService(App& app) 460 { 461 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/") 462 .privileges(redfish::privileges::getTaskService) 463 .methods(boost::beast::http::verb::get)( 464 [&app](const crow::Request& req, 465 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 466 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 467 { 468 return; 469 } 470 asyncResp->res.jsonValue["@odata.type"] = 471 "#TaskService.v1_1_4.TaskService"; 472 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; 473 asyncResp->res.jsonValue["Name"] = "Task Service"; 474 asyncResp->res.jsonValue["Id"] = "TaskService"; 475 asyncResp->res.jsonValue["DateTime"] = 476 redfish::time_utils::getDateTimeOffsetNow().first; 477 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; 478 479 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true; 480 481 auto health = std::make_shared<HealthPopulate>(asyncResp); 482 health->populate(); 483 asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; 484 asyncResp->res.jsonValue["ServiceEnabled"] = true; 485 asyncResp->res.jsonValue["Tasks"]["@odata.id"] = 486 "/redfish/v1/TaskService/Tasks"; 487 }); 488 } 489 490 } // namespace redfish 491