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