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