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 "dbus_utility.hpp" 20 #include "event_service_manager.hpp" 21 #include "query.hpp" 22 #include "registries/privilege_registry.hpp" 23 #include "task_messages.hpp" 24 25 #include <boost/asio/post.hpp> 26 #include <boost/asio/steady_timer.hpp> 27 #include <sdbusplus/bus/match.hpp> 28 29 #include <chrono> 30 #include <memory> 31 #include <variant> 32 33 namespace redfish 34 { 35 36 namespace task 37 { 38 constexpr size_t maxTaskCount = 100; // arbitrary limit 39 40 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) 41 static std::deque<std::shared_ptr<struct TaskData>> tasks; 42 43 constexpr bool completed = true; 44 45 struct Payload 46 { 47 explicit Payload(const crow::Request& req) : 48 targetUri(req.url), httpOperation(req.methodString()), 49 httpHeaders(nlohmann::json::array()), 50 jsonBody(nlohmann::json::parse(req.body, nullptr, false)) 51 { 52 using field_ns = boost::beast::http::field; 53 constexpr const std::array<boost::beast::http::field, 7> 54 headerWhitelist = {field_ns::accept, field_ns::accept_encoding, 55 field_ns::user_agent, field_ns::host, 56 field_ns::connection, field_ns::content_length, 57 field_ns::upgrade}; 58 59 if (jsonBody.is_discarded()) 60 { 61 jsonBody = nullptr; 62 } 63 64 for (const auto& field : req.fields) 65 { 66 if (std::find(headerWhitelist.begin(), headerWhitelist.end(), 67 field.name()) == headerWhitelist.end()) 68 { 69 continue; 70 } 71 std::string header; 72 header.reserve(field.name_string().size() + 2 + 73 field.value().size()); 74 header += field.name_string(); 75 header += ": "; 76 header += field.value(); 77 httpHeaders.emplace_back(std::move(header)); 78 } 79 } 80 Payload() = delete; 81 82 std::string targetUri; 83 std::string httpOperation; 84 nlohmann::json httpHeaders; 85 nlohmann::json jsonBody; 86 }; 87 88 struct TaskData : std::enable_shared_from_this<TaskData> 89 { 90 private: 91 TaskData( 92 std::function<bool(boost::system::error_code, sdbusplus::message_t&, 93 const std::shared_ptr<TaskData>&)>&& handler, 94 const std::string& matchIn, size_t idx) : 95 callback(std::move(handler)), 96 matchStr(matchIn), index(idx), 97 startTime(std::chrono::system_clock::to_time_t( 98 std::chrono::system_clock::now())), 99 status("OK"), state("Running"), messages(nlohmann::json::array()), 100 timer(crow::connections::systemBus->get_io_context()) 101 102 {} 103 104 public: 105 TaskData() = delete; 106 107 static std::shared_ptr<TaskData>& createTask( 108 std::function<bool(boost::system::error_code, sdbusplus::message_t&, 109 const std::shared_ptr<TaskData>&)>&& handler, 110 const std::string& match) 111 { 112 static size_t lastTask = 0; 113 struct MakeSharedHelper : public TaskData 114 { 115 MakeSharedHelper( 116 std::function<bool(boost::system::error_code, 117 sdbusplus::message_t&, 118 const std::shared_ptr<TaskData>&)>&& handler, 119 const std::string& match2, size_t idx) : 120 TaskData(std::move(handler), match2, idx) 121 {} 122 }; 123 124 if (tasks.size() >= maxTaskCount) 125 { 126 const auto& last = tasks.front(); 127 128 // destroy all references 129 last->timer.cancel(); 130 last->match.reset(); 131 tasks.pop_front(); 132 } 133 134 return tasks.emplace_back(std::make_shared<MakeSharedHelper>( 135 std::move(handler), match, lastTask++)); 136 } 137 138 void populateResp(crow::Response& res, size_t retryAfterSeconds = 30) 139 { 140 if (!endTime) 141 { 142 res.result(boost::beast::http::status::accepted); 143 std::string strIdx = std::to_string(index); 144 std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx; 145 146 res.jsonValue["@odata.id"] = uri; 147 res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 148 res.jsonValue["Id"] = strIdx; 149 res.jsonValue["TaskState"] = state; 150 res.jsonValue["TaskStatus"] = status; 151 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() 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_t 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 static 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_t>( 275 static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus), 276 matchStr, 277 [self = shared_from_this()](sdbusplus::message_t& 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_t&, 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_t> match; 315 std::optional<time_t> endTime; 316 std::optional<Payload> payload; 317 bool gave204 = false; 318 int percentComplete = 0; 319 }; 320 321 } // namespace task 322 323 inline void requestRoutesTaskMonitor(App& app) 324 { 325 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/Monitor/") 326 .privileges(redfish::privileges::getTask) 327 .methods(boost::beast::http::verb::get)( 328 [&app](const crow::Request& req, 329 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 330 const std::string& strParam) { 331 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 332 { 333 return; 334 } 335 auto find = std::find_if( 336 task::tasks.begin(), task::tasks.end(), 337 [&strParam](const std::shared_ptr<task::TaskData>& task) { 338 if (!task) 339 { 340 return false; 341 } 342 343 // we compare against the string version as on failure 344 // strtoul returns 0 345 return std::to_string(task->index) == strParam; 346 }); 347 348 if (find == task::tasks.end()) 349 { 350 messages::resourceNotFound(asyncResp->res, "Task", strParam); 351 return; 352 } 353 std::shared_ptr<task::TaskData>& ptr = *find; 354 // monitor expires after 204 355 if (ptr->gave204) 356 { 357 messages::resourceNotFound(asyncResp->res, "Task", strParam); 358 return; 359 } 360 ptr->populateResp(asyncResp->res); 361 }); 362 } 363 364 inline void requestRoutesTask(App& app) 365 { 366 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/") 367 .privileges(redfish::privileges::getTask) 368 .methods(boost::beast::http::verb::get)( 369 [&app](const crow::Request& req, 370 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 371 const std::string& strParam) { 372 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 373 { 374 return; 375 } 376 auto find = std::find_if( 377 task::tasks.begin(), task::tasks.end(), 378 [&strParam](const std::shared_ptr<task::TaskData>& task) { 379 if (!task) 380 { 381 return false; 382 } 383 384 // we compare against the string version as on failure 385 // strtoul returns 0 386 return std::to_string(task->index) == strParam; 387 }); 388 389 if (find == task::tasks.end()) 390 { 391 messages::resourceNotFound(asyncResp->res, "Task", strParam); 392 return; 393 } 394 395 const 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 redfish::time_utils::getDateTimeStdtime(ptr->startTime); 403 if (ptr->endTime) 404 { 405 asyncResp->res.jsonValue["EndTime"] = 406 redfish::time_utils::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 + "/Monitor"; 416 } 417 if (ptr->payload) 418 { 419 const task::Payload& p = *(ptr->payload); 420 asyncResp->res.jsonValue["Payload"]["TargetUri"] = p.targetUri; 421 asyncResp->res.jsonValue["Payload"]["HttpOperation"] = 422 p.httpOperation; 423 asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = p.httpHeaders; 424 asyncResp->res.jsonValue["Payload"]["JsonBody"] = p.jsonBody.dump( 425 2, ' ', true, nlohmann::json::error_handler_t::replace); 426 } 427 asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete; 428 }); 429 } 430 431 inline void requestRoutesTaskCollection(App& app) 432 { 433 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/") 434 .privileges(redfish::privileges::getTaskCollection) 435 .methods(boost::beast::http::verb::get)( 436 [&app](const crow::Request& req, 437 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 438 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 439 { 440 return; 441 } 442 asyncResp->res.jsonValue["@odata.type"] = 443 "#TaskCollection.TaskCollection"; 444 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; 445 asyncResp->res.jsonValue["Name"] = "Task Collection"; 446 asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); 447 nlohmann::json& members = asyncResp->res.jsonValue["Members"]; 448 members = nlohmann::json::array(); 449 450 for (const std::shared_ptr<task::TaskData>& task : task::tasks) 451 { 452 if (task == nullptr) 453 { 454 continue; // shouldn't be possible 455 } 456 nlohmann::json::object_t member; 457 member["@odata.id"] = 458 "redfish/v1/TaskService/Tasks/" + std::to_string(task->index); 459 members.emplace_back(std::move(member)); 460 } 461 }); 462 } 463 464 inline void requestRoutesTaskService(App& app) 465 { 466 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/") 467 .privileges(redfish::privileges::getTaskService) 468 .methods(boost::beast::http::verb::get)( 469 [&app](const crow::Request& req, 470 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 471 if (!redfish::setUpRedfishRoute(app, req, asyncResp)) 472 { 473 return; 474 } 475 asyncResp->res.jsonValue["@odata.type"] = 476 "#TaskService.v1_1_4.TaskService"; 477 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; 478 asyncResp->res.jsonValue["Name"] = "Task Service"; 479 asyncResp->res.jsonValue["Id"] = "TaskService"; 480 asyncResp->res.jsonValue["DateTime"] = 481 redfish::time_utils::getDateTimeOffsetNow().first; 482 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; 483 484 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true; 485 486 auto health = std::make_shared<HealthPopulate>(asyncResp); 487 health->populate(); 488 asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; 489 asyncResp->res.jsonValue["ServiceEnabled"] = true; 490 asyncResp->res.jsonValue["Tasks"]["@odata.id"] = 491 "/redfish/v1/TaskService/Tasks"; 492 }); 493 } 494 495 } // namespace redfish 496