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