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 startTimer(const std::chrono::seconds &timeout) 172 { 173 match = std::make_unique<sdbusplus::bus::match::match>( 174 static_cast<sdbusplus::bus::bus &>(*crow::connections::systemBus), 175 matchStr, 176 [self = shared_from_this()](sdbusplus::message::message &message) { 177 boost::system::error_code ec; 178 179 // callback to return True if callback is done, callback needs 180 // to update status itself if needed 181 if (self->callback(ec, message, self) == task::completed) 182 { 183 self->timer.cancel(); 184 self->finishTask(); 185 186 // reset the match after the callback was successful 187 boost::asio::post( 188 crow::connections::systemBus->get_io_context(), 189 [self] { self->match.reset(); }); 190 return; 191 } 192 }); 193 timer.expires_after(timeout); 194 timer.async_wait( 195 [self = shared_from_this()](boost::system::error_code ec) { 196 if (ec == boost::asio::error::operation_aborted) 197 { 198 return; // completed succesfully 199 } 200 if (!ec) 201 { 202 // change ec to error as timer expired 203 ec = boost::asio::error::operation_aborted; 204 } 205 self->match.reset(); 206 sdbusplus::message::message msg; 207 self->finishTask(); 208 self->state = "Cancelled"; 209 self->status = "Warning"; 210 self->messages.emplace_back( 211 messages::taskAborted(std::to_string(self->index))); 212 self->callback(ec, msg, self); 213 }); 214 messages.emplace_back(messages::taskStarted(std::to_string(index))); 215 } 216 217 std::function<bool(boost::system::error_code, sdbusplus::message::message &, 218 const std::shared_ptr<TaskData> &)> 219 callback; 220 std::string matchStr; 221 size_t index; 222 time_t startTime; 223 std::string status; 224 std::string state; 225 nlohmann::json messages; 226 boost::asio::steady_timer timer; 227 std::unique_ptr<sdbusplus::bus::match::match> match; 228 std::optional<time_t> endTime; 229 std::optional<Payload> payload; 230 bool gave204 = false; 231 }; 232 233 } // namespace task 234 235 class TaskMonitor : public Node 236 { 237 public: 238 TaskMonitor(CrowApp &app) : 239 Node((app), "/redfish/v1/TaskService/Tasks/<str>/Monitor/", 240 std::string()) 241 { 242 entityPrivileges = { 243 {boost::beast::http::verb::get, {{"Login"}}}, 244 {boost::beast::http::verb::head, {{"Login"}}}, 245 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 246 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 247 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 248 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 249 } 250 251 private: 252 void doGet(crow::Response &res, const crow::Request &req, 253 const std::vector<std::string> ¶ms) override 254 { 255 auto asyncResp = std::make_shared<AsyncResp>(res); 256 if (params.size() != 1) 257 { 258 messages::internalError(asyncResp->res); 259 return; 260 } 261 262 const std::string &strParam = params[0]; 263 auto find = std::find_if( 264 task::tasks.begin(), task::tasks.end(), 265 [&strParam](const std::shared_ptr<task::TaskData> &task) { 266 if (!task) 267 { 268 return false; 269 } 270 271 // we compare against the string version as on failure strtoul 272 // returns 0 273 return std::to_string(task->index) == strParam; 274 }); 275 276 if (find == task::tasks.end()) 277 { 278 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 279 return; 280 } 281 std::shared_ptr<task::TaskData> &ptr = *find; 282 // monitor expires after 204 283 if (ptr->gave204) 284 { 285 messages::resourceNotFound(asyncResp->res, "Monitor", strParam); 286 return; 287 } 288 ptr->populateResp(asyncResp->res); 289 } 290 }; 291 292 class Task : public Node 293 { 294 public: 295 Task(CrowApp &app) : 296 Node((app), "/redfish/v1/TaskService/Tasks/<str>/", std::string()) 297 { 298 entityPrivileges = { 299 {boost::beast::http::verb::get, {{"Login"}}}, 300 {boost::beast::http::verb::head, {{"Login"}}}, 301 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 302 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 303 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 304 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 305 } 306 307 private: 308 void doGet(crow::Response &res, const crow::Request &req, 309 const std::vector<std::string> ¶ms) override 310 { 311 auto asyncResp = std::make_shared<AsyncResp>(res); 312 if (params.size() != 1) 313 { 314 messages::internalError(asyncResp->res); 315 return; 316 } 317 318 const std::string &strParam = params[0]; 319 auto find = std::find_if( 320 task::tasks.begin(), task::tasks.end(), 321 [&strParam](const std::shared_ptr<task::TaskData> &task) { 322 if (!task) 323 { 324 return false; 325 } 326 327 // we compare against the string version as on failure strtoul 328 // returns 0 329 return std::to_string(task->index) == strParam; 330 }); 331 332 if (find == task::tasks.end()) 333 { 334 messages::resourceNotFound(asyncResp->res, "Tasks", strParam); 335 return; 336 } 337 338 std::shared_ptr<task::TaskData> &ptr = *find; 339 340 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; 341 asyncResp->res.jsonValue["Id"] = strParam; 342 asyncResp->res.jsonValue["Name"] = "Task " + strParam; 343 asyncResp->res.jsonValue["TaskState"] = ptr->state; 344 asyncResp->res.jsonValue["StartTime"] = 345 crow::utility::getDateTime(ptr->startTime); 346 if (ptr->endTime) 347 { 348 asyncResp->res.jsonValue["EndTime"] = 349 crow::utility::getDateTime(*(ptr->endTime)); 350 } 351 asyncResp->res.jsonValue["TaskStatus"] = ptr->status; 352 asyncResp->res.jsonValue["Messages"] = ptr->messages; 353 asyncResp->res.jsonValue["@odata.id"] = 354 "/redfish/v1/TaskService/Tasks/" + strParam; 355 if (!ptr->gave204) 356 { 357 asyncResp->res.jsonValue["TaskMonitor"] = 358 "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor"; 359 } 360 if (ptr->payload) 361 { 362 asyncResp->res.jsonValue["Payload"] = *(ptr->payload); 363 } 364 } 365 }; 366 367 class TaskCollection : public Node 368 { 369 public: 370 TaskCollection(CrowApp &app) : Node(app, "/redfish/v1/TaskService/Tasks/") 371 { 372 entityPrivileges = { 373 {boost::beast::http::verb::get, {{"Login"}}}, 374 {boost::beast::http::verb::head, {{"Login"}}}, 375 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 376 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 377 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 378 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 379 } 380 381 private: 382 void doGet(crow::Response &res, const crow::Request &req, 383 const std::vector<std::string> ¶ms) override 384 { 385 auto asyncResp = std::make_shared<AsyncResp>(res); 386 asyncResp->res.jsonValue["@odata.type"] = 387 "#TaskCollection.TaskCollection"; 388 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; 389 asyncResp->res.jsonValue["Name"] = "Task Collection"; 390 asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); 391 nlohmann::json &members = asyncResp->res.jsonValue["Members"]; 392 members = nlohmann::json::array(); 393 394 for (const std::shared_ptr<task::TaskData> &task : task::tasks) 395 { 396 if (task == nullptr) 397 { 398 continue; // shouldn't be possible 399 } 400 members.emplace_back( 401 nlohmann::json{{"@odata.id", "/redfish/v1/TaskService/Tasks/" + 402 std::to_string(task->index)}}); 403 } 404 } 405 }; 406 407 class TaskService : public Node 408 { 409 public: 410 TaskService(CrowApp &app) : Node(app, "/redfish/v1/TaskService/") 411 { 412 entityPrivileges = { 413 {boost::beast::http::verb::get, {{"Login"}}}, 414 {boost::beast::http::verb::head, {{"Login"}}}, 415 {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, 416 {boost::beast::http::verb::put, {{"ConfigureManager"}}}, 417 {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, 418 {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; 419 } 420 421 private: 422 void doGet(crow::Response &res, const crow::Request &req, 423 const std::vector<std::string> ¶ms) override 424 { 425 auto asyncResp = std::make_shared<AsyncResp>(res); 426 asyncResp->res.jsonValue["@odata.type"] = 427 "#TaskService.v1_1_4.TaskService"; 428 asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; 429 asyncResp->res.jsonValue["Name"] = "Task Service"; 430 asyncResp->res.jsonValue["Id"] = "TaskService"; 431 asyncResp->res.jsonValue["DateTime"] = crow::utility::dateTimeNow(); 432 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; 433 434 // todo: if we enable events, change this to true 435 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = false; 436 437 auto health = std::make_shared<HealthPopulate>(asyncResp); 438 health->populate(); 439 asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; 440 asyncResp->res.jsonValue["ServiceEnabled"] = true; 441 asyncResp->res.jsonValue["Tasks"] = { 442 {"@odata.id", "/redfish/v1/TaskService/Tasks"}}; 443 } 444 }; 445 446 } // namespace redfish 447