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