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