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