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