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