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 "app.hpp"
19 #include "dbus_utility.hpp"
20 #include "event_service_manager.hpp"
21 #include "generated/enums/resource.hpp"
22 #include "generated/enums/task_service.hpp"
23 #include "http/parsing.hpp"
24 #include "query.hpp"
25 #include "registries/privilege_registry.hpp"
26 #include "task_messages.hpp"
27
28 #include <boost/asio/post.hpp>
29 #include <boost/asio/steady_timer.hpp>
30 #include <boost/url/format.hpp>
31 #include <sdbusplus/bus/match.hpp>
32
33 #include <chrono>
34 #include <memory>
35 #include <ranges>
36 #include <variant>
37
38 namespace redfish
39 {
40
41 namespace task
42 {
43 constexpr size_t maxTaskCount = 100; // arbitrary limit
44
45 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
46 static std::deque<std::shared_ptr<struct TaskData>> tasks;
47
48 constexpr bool completed = true;
49
50 struct Payload
51 {
Payloadredfish::task::Payload52 explicit Payload(const crow::Request& req) :
53 targetUri(req.url().encoded_path()), httpOperation(req.methodString()),
54 httpHeaders(nlohmann::json::array())
55 {
56 using field_ns = boost::beast::http::field;
57 constexpr const std::array<boost::beast::http::field, 7>
58 headerWhitelist = {field_ns::accept, field_ns::accept_encoding,
59 field_ns::user_agent, field_ns::host,
60 field_ns::connection, field_ns::content_length,
61 field_ns::upgrade};
62
63 JsonParseResult ret = parseRequestAsJson(req, jsonBody);
64 if (ret != JsonParseResult::Success)
65 {
66 return;
67 }
68
69 for (const auto& field : req.fields())
70 {
71 if (std::ranges::find(headerWhitelist, field.name()) ==
72 headerWhitelist.end())
73 {
74 continue;
75 }
76 std::string header;
77 header.reserve(
78 field.name_string().size() + 2 + field.value().size());
79 header += field.name_string();
80 header += ": ";
81 header += field.value();
82 httpHeaders.emplace_back(std::move(header));
83 }
84 }
85 Payload() = delete;
86
87 std::string targetUri;
88 std::string httpOperation;
89 nlohmann::json httpHeaders;
90 nlohmann::json jsonBody;
91 };
92
93 struct TaskData : std::enable_shared_from_this<TaskData>
94 {
95 private:
TaskDataredfish::task::TaskData96 TaskData(
97 std::function<bool(boost::system::error_code, sdbusplus::message_t&,
98 const std::shared_ptr<TaskData>&)>&& handler,
99 const std::string& matchIn, size_t idx) :
100 callback(std::move(handler)), matchStr(matchIn), index(idx),
101 startTime(std::chrono::system_clock::to_time_t(
102 std::chrono::system_clock::now())),
103 status("OK"), state("Running"), messages(nlohmann::json::array()),
104 timer(crow::connections::systemBus->get_io_context())
105
106 {}
107
108 public:
109 TaskData() = delete;
110
createTaskredfish::task::TaskData111 static std::shared_ptr<TaskData>& createTask(
112 std::function<bool(boost::system::error_code, sdbusplus::message_t&,
113 const std::shared_ptr<TaskData>&)>&& handler,
114 const std::string& match)
115 {
116 static size_t lastTask = 0;
117 struct MakeSharedHelper : public TaskData
118 {
119 MakeSharedHelper(
120 std::function<bool(boost::system::error_code,
121 sdbusplus::message_t&,
122 const std::shared_ptr<TaskData>&)>&& handler,
123 const std::string& match2, size_t idx) :
124 TaskData(std::move(handler), match2, idx)
125 {}
126 };
127
128 if (tasks.size() >= maxTaskCount)
129 {
130 const auto& last = tasks.front();
131
132 // destroy all references
133 last->timer.cancel();
134 last->match.reset();
135 tasks.pop_front();
136 }
137
138 return tasks.emplace_back(std::make_shared<MakeSharedHelper>(
139 std::move(handler), match, lastTask++));
140 }
141
populateRespredfish::task::TaskData142 void populateResp(crow::Response& res, size_t retryAfterSeconds = 30)
143 {
144 if (!endTime)
145 {
146 res.result(boost::beast::http::status::accepted);
147 std::string strIdx = std::to_string(index);
148 boost::urls::url uri =
149 boost::urls::format("/redfish/v1/TaskService/Tasks/{}", strIdx);
150
151 res.jsonValue["@odata.id"] = uri;
152 res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
153 res.jsonValue["Id"] = strIdx;
154 res.jsonValue["TaskState"] = state;
155 res.jsonValue["TaskStatus"] = status;
156
157 boost::urls::url taskMonitor = boost::urls::format(
158 "/redfish/v1/TaskService/TaskMonitors/{}", strIdx);
159
160 res.addHeader(boost::beast::http::field::location,
161 taskMonitor.buffer());
162 res.addHeader(boost::beast::http::field::retry_after,
163 std::to_string(retryAfterSeconds));
164 }
165 else if (!gave204)
166 {
167 res.result(boost::beast::http::status::no_content);
168 gave204 = true;
169 }
170 }
171
finishTaskredfish::task::TaskData172 void finishTask()
173 {
174 endTime = std::chrono::system_clock::to_time_t(
175 std::chrono::system_clock::now());
176 }
177
extendTimerredfish::task::TaskData178 void extendTimer(const std::chrono::seconds& timeout)
179 {
180 timer.expires_after(timeout);
181 timer.async_wait(
182 [self = shared_from_this()](boost::system::error_code ec) {
183 if (ec == boost::asio::error::operation_aborted)
184 {
185 return; // completed successfully
186 }
187 if (!ec)
188 {
189 // change ec to error as timer expired
190 ec = boost::asio::error::operation_aborted;
191 }
192 self->match.reset();
193 sdbusplus::message_t msg;
194 self->finishTask();
195 self->state = "Cancelled";
196 self->status = "Warning";
197 self->messages.emplace_back(
198 messages::taskAborted(std::to_string(self->index)));
199 // Send event :TaskAborted
200 self->sendTaskEvent(self->state, self->index);
201 self->callback(ec, msg, self);
202 });
203 }
204
sendTaskEventredfish::task::TaskData205 static void sendTaskEvent(std::string_view state, size_t index)
206 {
207 // TaskState enums which should send out an event are:
208 // "Starting" = taskResumed
209 // "Running" = taskStarted
210 // "Suspended" = taskPaused
211 // "Interrupted" = taskPaused
212 // "Pending" = taskPaused
213 // "Stopping" = taskAborted
214 // "Completed" = taskCompletedOK
215 // "Killed" = taskRemoved
216 // "Exception" = taskCompletedWarning
217 // "Cancelled" = taskCancelled
218 nlohmann::json event;
219 std::string indexStr = std::to_string(index);
220 if (state == "Starting")
221 {
222 event = redfish::messages::taskResumed(indexStr);
223 }
224 else if (state == "Running")
225 {
226 event = redfish::messages::taskStarted(indexStr);
227 }
228 else if ((state == "Suspended") || (state == "Interrupted") ||
229 (state == "Pending"))
230 {
231 event = redfish::messages::taskPaused(indexStr);
232 }
233 else if (state == "Stopping")
234 {
235 event = redfish::messages::taskAborted(indexStr);
236 }
237 else if (state == "Completed")
238 {
239 event = redfish::messages::taskCompletedOK(indexStr);
240 }
241 else if (state == "Killed")
242 {
243 event = redfish::messages::taskRemoved(indexStr);
244 }
245 else if (state == "Exception")
246 {
247 event = redfish::messages::taskCompletedWarning(indexStr);
248 }
249 else if (state == "Cancelled")
250 {
251 event = redfish::messages::taskCancelled(indexStr);
252 }
253 else
254 {
255 BMCWEB_LOG_INFO("sendTaskEvent: No events to send");
256 return;
257 }
258 boost::urls::url origin =
259 boost::urls::format("/redfish/v1/TaskService/Tasks/{}", index);
260 EventServiceManager::getInstance().sendEvent(event, origin.buffer(),
261 "Task");
262 }
263
startTimerredfish::task::TaskData264 void startTimer(const std::chrono::seconds& timeout)
265 {
266 if (match)
267 {
268 return;
269 }
270 match = std::make_unique<sdbusplus::bus::match_t>(
271 static_cast<sdbusplus::bus_t&>(*crow::connections::systemBus),
272 matchStr,
273 [self = shared_from_this()](sdbusplus::message_t& message) {
274 boost::system::error_code ec;
275
276 // callback to return True if callback is done, callback needs
277 // to update status itself if needed
278 if (self->callback(ec, message, self) == task::completed)
279 {
280 self->timer.cancel();
281 self->finishTask();
282
283 // Send event
284 self->sendTaskEvent(self->state, self->index);
285
286 // reset the match after the callback was successful
287 boost::asio::post(
288 crow::connections::systemBus->get_io_context(),
289 [self] { self->match.reset(); });
290 return;
291 }
292 });
293
294 extendTimer(timeout);
295 messages.emplace_back(messages::taskStarted(std::to_string(index)));
296 // Send event : TaskStarted
297 sendTaskEvent(state, index);
298 }
299
300 std::function<bool(boost::system::error_code, sdbusplus::message_t&,
301 const std::shared_ptr<TaskData>&)>
302 callback;
303 std::string matchStr;
304 size_t index;
305 time_t startTime;
306 std::string status;
307 std::string state;
308 nlohmann::json messages;
309 boost::asio::steady_timer timer;
310 std::unique_ptr<sdbusplus::bus::match_t> match;
311 std::optional<time_t> endTime;
312 std::optional<Payload> payload;
313 bool gave204 = false;
314 int percentComplete = 0;
315 };
316
317 } // namespace task
318
requestRoutesTaskMonitor(App & app)319 inline void requestRoutesTaskMonitor(App& app)
320 {
321 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/TaskMonitors/<str>/")
322 .privileges(redfish::privileges::getTask)
323 .methods(boost::beast::http::verb::get)(
324 [&app](const crow::Request& req,
325 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
326 const std::string& strParam) {
327 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
328 {
329 return;
330 }
331 auto find = std::ranges::find_if(
332 task::tasks,
333 [&strParam](const std::shared_ptr<task::TaskData>& task) {
334 if (!task)
335 {
336 return false;
337 }
338
339 // we compare against the string version as on failure
340 // strtoul returns 0
341 return std::to_string(task->index) == strParam;
342 });
343
344 if (find == task::tasks.end())
345 {
346 messages::resourceNotFound(asyncResp->res, "Task",
347 strParam);
348 return;
349 }
350 std::shared_ptr<task::TaskData>& ptr = *find;
351 // monitor expires after 204
352 if (ptr->gave204)
353 {
354 messages::resourceNotFound(asyncResp->res, "Task",
355 strParam);
356 return;
357 }
358 ptr->populateResp(asyncResp->res);
359 });
360 }
361
requestRoutesTask(App & app)362 inline void requestRoutesTask(App& app)
363 {
364 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/<str>/")
365 .privileges(redfish::privileges::getTask)
366 .methods(boost::beast::http::verb::get)(
367 [&app](const crow::Request& req,
368 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
369 const std::string& strParam) {
370 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
371 {
372 return;
373 }
374 auto find = std::ranges::find_if(
375 task::tasks,
376 [&strParam](const std::shared_ptr<task::TaskData>& task) {
377 if (!task)
378 {
379 return false;
380 }
381
382 // we compare against the string version as on failure
383 // strtoul returns 0
384 return std::to_string(task->index) == strParam;
385 });
386
387 if (find == task::tasks.end())
388 {
389 messages::resourceNotFound(asyncResp->res, "Task",
390 strParam);
391 return;
392 }
393
394 const std::shared_ptr<task::TaskData>& ptr = *find;
395
396 asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task";
397 asyncResp->res.jsonValue["Id"] = strParam;
398 asyncResp->res.jsonValue["Name"] = "Task " + strParam;
399 asyncResp->res.jsonValue["TaskState"] = ptr->state;
400 asyncResp->res.jsonValue["StartTime"] =
401 redfish::time_utils::getDateTimeStdtime(ptr->startTime);
402 if (ptr->endTime)
403 {
404 asyncResp->res.jsonValue["EndTime"] =
405 redfish::time_utils::getDateTimeStdtime(
406 *(ptr->endTime));
407 }
408 asyncResp->res.jsonValue["TaskStatus"] = ptr->status;
409 asyncResp->res.jsonValue["Messages"] = ptr->messages;
410 asyncResp->res.jsonValue["@odata.id"] = boost::urls::format(
411 "/redfish/v1/TaskService/Tasks/{}", strParam);
412 if (!ptr->gave204)
413 {
414 asyncResp->res.jsonValue["TaskMonitor"] =
415 boost::urls::format(
416 "/redfish/v1/TaskService/TaskMonitors/{}",
417 strParam);
418 }
419
420 asyncResp->res.jsonValue["HidePayload"] = !ptr->payload;
421
422 if (ptr->payload)
423 {
424 const task::Payload& p = *(ptr->payload);
425 asyncResp->res.jsonValue["Payload"]["TargetUri"] =
426 p.targetUri;
427 asyncResp->res.jsonValue["Payload"]["HttpOperation"] =
428 p.httpOperation;
429 asyncResp->res.jsonValue["Payload"]["HttpHeaders"] =
430 p.httpHeaders;
431 asyncResp->res.jsonValue["Payload"]["JsonBody"] =
432 p.jsonBody.dump(
433 -1, ' ', true,
434 nlohmann::json::error_handler_t::replace);
435 }
436 asyncResp->res.jsonValue["PercentComplete"] =
437 ptr->percentComplete;
438 });
439 }
440
requestRoutesTaskCollection(App & app)441 inline void requestRoutesTaskCollection(App& app)
442 {
443 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/")
444 .privileges(redfish::privileges::getTaskCollection)
445 .methods(boost::beast::http::verb::get)(
446 [&app](const crow::Request& req,
447 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
448 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
449 {
450 return;
451 }
452 asyncResp->res.jsonValue["@odata.type"] =
453 "#TaskCollection.TaskCollection";
454 asyncResp->res.jsonValue["@odata.id"] =
455 "/redfish/v1/TaskService/Tasks";
456 asyncResp->res.jsonValue["Name"] = "Task Collection";
457 asyncResp->res.jsonValue["Members@odata.count"] =
458 task::tasks.size();
459 nlohmann::json& members = asyncResp->res.jsonValue["Members"];
460 members = nlohmann::json::array();
461
462 for (const std::shared_ptr<task::TaskData>& task : task::tasks)
463 {
464 if (task == nullptr)
465 {
466 continue; // shouldn't be possible
467 }
468 nlohmann::json::object_t member;
469 member["@odata.id"] =
470 boost::urls::format("/redfish/v1/TaskService/Tasks/{}",
471 std::to_string(task->index));
472 members.emplace_back(std::move(member));
473 }
474 });
475 }
476
requestRoutesTaskService(App & app)477 inline void requestRoutesTaskService(App& app)
478 {
479 BMCWEB_ROUTE(app, "/redfish/v1/TaskService/")
480 .privileges(redfish::privileges::getTaskService)
481 .methods(boost::beast::http::verb::get)(
482 [&app](const crow::Request& req,
483 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
484 if (!redfish::setUpRedfishRoute(app, req, asyncResp))
485 {
486 return;
487 }
488 asyncResp->res.jsonValue["@odata.type"] =
489 "#TaskService.v1_1_4.TaskService";
490 asyncResp->res.jsonValue["@odata.id"] =
491 "/redfish/v1/TaskService";
492 asyncResp->res.jsonValue["Name"] = "Task Service";
493 asyncResp->res.jsonValue["Id"] = "TaskService";
494 asyncResp->res.jsonValue["DateTime"] =
495 redfish::time_utils::getDateTimeOffsetNow().first;
496 asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] =
497 task_service::OverWritePolicy::Oldest;
498
499 asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] =
500 true;
501
502 asyncResp->res.jsonValue["Status"]["State"] =
503 resource::State::Enabled;
504 asyncResp->res.jsonValue["ServiceEnabled"] = true;
505 asyncResp->res.jsonValue["Tasks"]["@odata.id"] =
506 "/redfish/v1/TaskService/Tasks";
507 });
508 }
509
510 } // namespace redfish
511