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