/* Copyright (c) 2020 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include "app.hpp" #include "dbus_utility.hpp" #include "event_service_manager.hpp" #include "generated/enums/resource.hpp" #include "generated/enums/task_service.hpp" #include "http/parsing.hpp" #include "query.hpp" #include "registries/privilege_registry.hpp" #include "task_messages.hpp" #include #include #include #include #include #include #include #include namespace redfish { namespace task { constexpr size_t maxTaskCount = 100; // arbitrary limit // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static std::deque> tasks; constexpr bool completed = true; struct Payload { explicit Payload(const crow::Request& req) : targetUri(req.url().encoded_path()), httpOperation(req.methodString()), httpHeaders(nlohmann::json::array()) { using field_ns = boost::beast::http::field; constexpr const std::array headerWhitelist = {field_ns::accept, field_ns::accept_encoding, field_ns::user_agent, field_ns::host, field_ns::connection, field_ns::content_length, field_ns::upgrade}; JsonParseResult ret = parseRequestAsJson(req, jsonBody); if (ret != JsonParseResult::Success) { return; } for (const auto& field : req.fields()) { if (std::ranges::find(headerWhitelist, field.name()) == headerWhitelist.end()) { continue; } std::string header; header.reserve( field.name_string().size() + 2 + field.value().size()); header += field.name_string(); header += ": "; header += field.value(); httpHeaders.emplace_back(std::move(header)); } } Payload() = delete; std::string targetUri; std::string httpOperation; nlohmann::json httpHeaders; nlohmann::json jsonBody; }; struct TaskData : std::enable_shared_from_this { private: TaskData( std::function&)>&& handler, const std::string& matchIn, size_t idx) : callback(std::move(handler)), matchStr(matchIn), index(idx), startTime(std::chrono::system_clock::to_time_t( std::chrono::system_clock::now())), status("OK"), state("Running"), messages(nlohmann::json::array()), timer(crow::connections::systemBus->get_io_context()) {} public: TaskData() = delete; static std::shared_ptr& createTask( std::function&)>&& handler, const std::string& match) { static size_t lastTask = 0; struct MakeSharedHelper : public TaskData { MakeSharedHelper( std::function&)>&& handler, const std::string& match2, size_t idx) : TaskData(std::move(handler), match2, idx) {} }; if (tasks.size() >= maxTaskCount) { const auto& last = tasks.front(); // destroy all references last->timer.cancel(); last->match.reset(); tasks.pop_front(); } return tasks.emplace_back(std::make_shared( std::move(handler), match, lastTask++)); } void populateResp(crow::Response& res, size_t retryAfterSeconds = 30) { if (!endTime) { res.result(boost::beast::http::status::accepted); std::string strIdx = std::to_string(index); boost::urls::url uri = boost::urls::format("/redfish/v1/TaskService/Tasks/{}", strIdx); res.jsonValue["@odata.id"] = uri; res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; res.jsonValue["Id"] = strIdx; res.jsonValue["TaskState"] = state; res.jsonValue["TaskStatus"] = status; boost::urls::url taskMonitor = boost::urls::format( "/redfish/v1/TaskService/TaskMonitors/{}", strIdx); res.addHeader(boost::beast::http::field::location, taskMonitor.buffer()); res.addHeader(boost::beast::http::field::retry_after, std::to_string(retryAfterSeconds)); } else if (!gave204) { res.result(boost::beast::http::status::no_content); gave204 = true; } } void finishTask() { endTime = std::chrono::system_clock::to_time_t( std::chrono::system_clock::now()); } void extendTimer(const std::chrono::seconds& timeout) { timer.expires_after(timeout); timer.async_wait( [self = shared_from_this()](boost::system::error_code ec) { if (ec == boost::asio::error::operation_aborted) { return; // completed successfully } if (!ec) { // change ec to error as timer expired ec = boost::asio::error::operation_aborted; } self->match.reset(); sdbusplus::message_t msg; self->finishTask(); self->state = "Cancelled"; self->status = "Warning"; self->messages.emplace_back( messages::taskAborted(std::to_string(self->index))); // Send event :TaskAborted self->sendTaskEvent(self->state, self->index); self->callback(ec, msg, self); }); } static void sendTaskEvent(std::string_view state, size_t index) { // TaskState enums which should send out an event are: // "Starting" = taskResumed // "Running" = taskStarted // "Suspended" = taskPaused // "Interrupted" = taskPaused // "Pending" = taskPaused // "Stopping" = taskAborted // "Completed" = taskCompletedOK // "Killed" = taskRemoved // "Exception" = taskCompletedWarning // "Cancelled" = taskCancelled nlohmann::json event; std::string indexStr = std::to_string(index); if (state == "Starting") { event = redfish::messages::taskResumed(indexStr); } else if (state == "Running") { event = redfish::messages::taskStarted(indexStr); } else if ((state == "Suspended") || (state == "Interrupted") || (state == "Pending")) { event = redfish::messages::taskPaused(indexStr); } else if (state == "Stopping") { event = redfish::messages::taskAborted(indexStr); } else if (state == "Completed") { event = redfish::messages::taskCompletedOK(indexStr); } else if (state == "Killed") { event = redfish::messages::taskRemoved(indexStr); } else if (state == "Exception") { event = redfish::messages::taskCompletedWarning(indexStr); } else if (state == "Cancelled") { event = redfish::messages::taskCancelled(indexStr); } else { BMCWEB_LOG_INFO("sendTaskEvent: No events to send"); return; } boost::urls::url origin = boost::urls::format("/redfish/v1/TaskService/Tasks/{}", index); EventServiceManager::getInstance().sendEvent(event, origin.buffer(), "Task"); } void startTimer(const std::chrono::seconds& timeout) { if (match) { return; } match = std::make_unique( static_cast(*crow::connections::systemBus), matchStr, [self = shared_from_this()](sdbusplus::message_t& message) { boost::system::error_code ec; // callback to return True if callback is done, callback needs // to update status itself if needed if (self->callback(ec, message, self) == task::completed) { self->timer.cancel(); self->finishTask(); // Send event self->sendTaskEvent(self->state, self->index); // reset the match after the callback was successful boost::asio::post( crow::connections::systemBus->get_io_context(), [self] { self->match.reset(); }); return; } }); extendTimer(timeout); messages.emplace_back(messages::taskStarted(std::to_string(index))); // Send event : TaskStarted sendTaskEvent(state, index); } std::function&)> callback; std::string matchStr; size_t index; time_t startTime; std::string status; std::string state; nlohmann::json messages; boost::asio::steady_timer timer; std::unique_ptr match; std::optional endTime; std::optional payload; bool gave204 = false; int percentComplete = 0; }; } // namespace task inline void requestRoutesTaskMonitor(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/TaskService/TaskMonitors//") .privileges(redfish::privileges::getTask) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& strParam) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } auto find = std::ranges::find_if( task::tasks, [&strParam](const std::shared_ptr& task) { if (!task) { return false; } // we compare against the string version as on failure // strtoul returns 0 return std::to_string(task->index) == strParam; }); if (find == task::tasks.end()) { messages::resourceNotFound(asyncResp->res, "Task", strParam); return; } std::shared_ptr& ptr = *find; // monitor expires after 204 if (ptr->gave204) { messages::resourceNotFound(asyncResp->res, "Task", strParam); return; } ptr->populateResp(asyncResp->res); }); } inline void requestRoutesTask(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks//") .privileges(redfish::privileges::getTask) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp, const std::string& strParam) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } auto find = std::ranges::find_if( task::tasks, [&strParam](const std::shared_ptr& task) { if (!task) { return false; } // we compare against the string version as on failure // strtoul returns 0 return std::to_string(task->index) == strParam; }); if (find == task::tasks.end()) { messages::resourceNotFound(asyncResp->res, "Task", strParam); return; } const std::shared_ptr& ptr = *find; asyncResp->res.jsonValue["@odata.type"] = "#Task.v1_4_3.Task"; asyncResp->res.jsonValue["Id"] = strParam; asyncResp->res.jsonValue["Name"] = "Task " + strParam; asyncResp->res.jsonValue["TaskState"] = ptr->state; asyncResp->res.jsonValue["StartTime"] = redfish::time_utils::getDateTimeStdtime(ptr->startTime); if (ptr->endTime) { asyncResp->res.jsonValue["EndTime"] = redfish::time_utils::getDateTimeStdtime( *(ptr->endTime)); } asyncResp->res.jsonValue["TaskStatus"] = ptr->status; asyncResp->res.jsonValue["Messages"] = ptr->messages; asyncResp->res.jsonValue["@odata.id"] = boost::urls::format( "/redfish/v1/TaskService/Tasks/{}", strParam); if (!ptr->gave204) { asyncResp->res.jsonValue["TaskMonitor"] = boost::urls::format( "/redfish/v1/TaskService/TaskMonitors/{}", strParam); } asyncResp->res.jsonValue["HidePayload"] = !ptr->payload; if (ptr->payload) { const task::Payload& p = *(ptr->payload); asyncResp->res.jsonValue["Payload"]["TargetUri"] = p.targetUri; asyncResp->res.jsonValue["Payload"]["HttpOperation"] = p.httpOperation; asyncResp->res.jsonValue["Payload"]["HttpHeaders"] = p.httpHeaders; asyncResp->res.jsonValue["Payload"]["JsonBody"] = p.jsonBody.dump( -1, ' ', true, nlohmann::json::error_handler_t::replace); } asyncResp->res.jsonValue["PercentComplete"] = ptr->percentComplete; }); } inline void requestRoutesTaskCollection(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/TaskService/Tasks/") .privileges(redfish::privileges::getTaskCollection) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.jsonValue["@odata.type"] = "#TaskCollection.TaskCollection"; asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService/Tasks"; asyncResp->res.jsonValue["Name"] = "Task Collection"; asyncResp->res.jsonValue["Members@odata.count"] = task::tasks.size(); nlohmann::json& members = asyncResp->res.jsonValue["Members"]; members = nlohmann::json::array(); for (const std::shared_ptr& task : task::tasks) { if (task == nullptr) { continue; // shouldn't be possible } nlohmann::json::object_t member; member["@odata.id"] = boost::urls::format("/redfish/v1/TaskService/Tasks/{}", std::to_string(task->index)); members.emplace_back(std::move(member)); } }); } inline void requestRoutesTaskService(App& app) { BMCWEB_ROUTE(app, "/redfish/v1/TaskService/") .privileges(redfish::privileges::getTaskService) .methods(boost::beast::http::verb::get)( [&app](const crow::Request& req, const std::shared_ptr& asyncResp) { if (!redfish::setUpRedfishRoute(app, req, asyncResp)) { return; } asyncResp->res.jsonValue["@odata.type"] = "#TaskService.v1_1_4.TaskService"; asyncResp->res.jsonValue["@odata.id"] = "/redfish/v1/TaskService"; asyncResp->res.jsonValue["Name"] = "Task Service"; asyncResp->res.jsonValue["Id"] = "TaskService"; asyncResp->res.jsonValue["DateTime"] = redfish::time_utils::getDateTimeOffsetNow().first; asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = task_service::OverWritePolicy::Oldest; asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = true; asyncResp->res.jsonValue["Status"]["State"] = resource::State::Enabled; asyncResp->res.jsonValue["ServiceEnabled"] = true; asyncResp->res.jsonValue["Tasks"]["@odata.id"] = "/redfish/v1/TaskService/Tasks"; }); } } // namespace redfish