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