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