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