From 462295771281bbd9901c688b8684b6c6930322c3 Mon Sep 17 00:00:00 2001 From: James Feist Date: Wed, 19 Feb 2020 15:11:58 -0800 Subject: Add TaskService This adds tasks service to Redfish and creates an example for crashdump. The TaskData object creates tasks that can be updated based on d-bus matches. It also has a configurable timeout using timers. Task Monitor uses these task objects to reply with a 202 until the async task is done, then a 204 when it is either failed or completed. Messages support will come in future commit. Tested: Validator passed, wrote script to poll monitor, verified that got 202 with location header and retry-after set correctly, then 204, then 404. Change-Id: I109e671baa1c1eeff1a11ae578e7361bf6ef9f14 Signed-off-by: James Feist --- redfish-core/include/redfish.hpp | 6 + redfish-core/lib/log_services.hpp | 52 ++--- redfish-core/lib/service_root.hpp | 1 + redfish-core/lib/task.hpp | 389 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 426 insertions(+), 22 deletions(-) create mode 100644 redfish-core/lib/task.hpp diff --git a/redfish-core/include/redfish.hpp b/redfish-core/include/redfish.hpp index b1fd820f14..cba2882a0c 100644 --- a/redfish-core/include/redfish.hpp +++ b/redfish-core/include/redfish.hpp @@ -33,6 +33,7 @@ #include "../lib/service_root.hpp" #include "../lib/storage.hpp" #include "../lib/systems.hpp" +#include "../lib/task.hpp" #include "../lib/thermal.hpp" #include "../lib/update_service.hpp" #ifdef BMCWEB_ENABLE_VM_NBDPROXY @@ -165,6 +166,11 @@ class RedfishService nodes.emplace_back(std::make_unique(app)); nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + nodes.emplace_back(std::make_unique(app)); + for (const auto& node : nodes) { node->initPrivileges(); diff --git a/redfish-core/lib/log_services.hpp b/redfish-core/lib/log_services.hpp index aede3661b3..2f065eca1d 100644 --- a/redfish-core/lib/log_services.hpp +++ b/redfish-core/lib/log_services.hpp @@ -19,6 +19,7 @@ #include "registries.hpp" #include "registries/base_message_registry.hpp" #include "registries/openbmc_message_registry.hpp" +#include "task.hpp" #include @@ -1849,30 +1850,37 @@ class OnDemandCrashdump : public Node { std::shared_ptr asyncResp = std::make_shared(res); - auto generateonDemandLogCallback = - [asyncResp](const boost::system::error_code ec, - const std::string &resp) { - if (ec) + auto generateonDemandLogCallback = [asyncResp]( + const boost::system::error_code + ec, + const std::string &resp) { + if (ec) + { + if (ec.value() == boost::system::errc::operation_not_supported) { - if (ec.value() == - boost::system::errc::operation_not_supported) - { - messages::resourceInStandby(asyncResp->res); - } - else if (ec.value() == - boost::system::errc::device_or_resource_busy) - { - messages::serviceTemporarilyUnavailable(asyncResp->res, - "60"); - } - else - { - messages::internalError(asyncResp->res); - } - return; + messages::resourceInStandby(asyncResp->res); } - asyncResp->res.result(boost::beast::http::status::no_content); - }; + else if (ec.value() == + boost::system::errc::device_or_resource_busy) + { + messages::serviceTemporarilyUnavailable(asyncResp->res, + "60"); + } + else + { + messages::internalError(asyncResp->res); + } + return; + } + std::shared_ptr task = task::TaskData::createTask( + [](boost::system::error_code, sdbusplus::message::message &, + const std::shared_ptr &) { return true; }, + "type='signal',interface='org.freedesktop.DBus.Properties'," + "member='PropertiesChanged',arg0namespace='com.intel." + "crashdump'"); + task->startTimer(std::chrono::minutes(5)); + task->populateResp(asyncResp->res); + }; crow::connections::systemBus->async_method_call( std::move(generateonDemandLogCallback), crashdumpObject, crashdumpPath, crashdumpOnDemandInterface, "GenerateOnDemandLog"); diff --git a/redfish-core/lib/service_root.hpp b/redfish-core/lib/service_root.hpp index 318639d515..1f4343e9a4 100644 --- a/redfish-core/lib/service_root.hpp +++ b/redfish-core/lib/service_root.hpp @@ -66,6 +66,7 @@ class ServiceRoot : public Node res.jsonValue["UUID"] = uuid; res.jsonValue["CertificateService"] = { {"@odata.id", "/redfish/v1/CertificateService"}}; + res.jsonValue["Tasks"] = {{"@odata.id", "/redfish/v1/TaskService"}}; res.end(); } diff --git a/redfish-core/lib/task.hpp b/redfish-core/lib/task.hpp new file mode 100644 index 0000000000..4540c81bf3 --- /dev/null +++ b/redfish-core/lib/task.hpp @@ -0,0 +1,389 @@ +/* +// 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 "node.hpp" + +#include +#include +#include + +namespace redfish +{ + +namespace task +{ +constexpr size_t maxTaskCount = 100; // arbitrary limit + +static std::deque> tasks; + +struct TaskData : std::enable_shared_from_this +{ + private: + TaskData(std::function &)> &&handler, + const std::string &match, size_t idx) : + callback(std::move(handler)), + matchStr(match), 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()) + + { + } + TaskData() = delete; + + public: + 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 &match, size_t idx) : + TaskData(std::move(handler), match, idx) + { + } + }; + + if (tasks.size() >= maxTaskCount) + { + 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); + std::string uri = "/redfish/v1/TaskService/Tasks/" + strIdx; + res.jsonValue = {{"@odata.id", uri}, + {"@odata.type", "#Task.v1_4_3.Task"}, + {"Id", strIdx}, + {"TaskState", state}, + {"TaskStatus", status}}; + res.addHeader(boost::beast::http::field::location, + uri + "/Monitor"); + 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(void) + { + endTime = std::chrono::system_clock::to_time_t( + std::chrono::system_clock::now()); + } + + void startTimer(const std::chrono::seconds &timeout) + { + match = std::make_unique( + static_cast(*crow::connections::systemBus), + matchStr, + [self = shared_from_this()](sdbusplus::message::message &message) { + boost::system::error_code ec; + + // set to complete before callback incase user wants a different + // status + self->state = "Completed"; + + // callback to return True if callback is done, callback needs + // to update status itself if needed + if (self->callback(ec, message, self)) + { + self->timer.cancel(); + self->finishTask(); + + // reset the match after the callback was successful + crow::connections::systemBus->get_io_context().post( + [self] { self->match.reset(); }); + return; + } + + // set back to running if callback returns false to keep + // callback alive + self->state = "Running"; + }); + 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 succesfully + } + if (!ec) + { + // change ec to error as timer expired + ec = boost::asio::error::operation_aborted; + } + self->match.reset(); + sdbusplus::message::message msg; + self->finishTask(); + self->state = "Cancelled"; + self->status = "Warning"; + self->callback(ec, msg, self); + }); + } + + 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; + bool gave204 = false; +}; + +} // namespace task + +class TaskMonitor : public Node +{ + public: + TaskMonitor(CrowApp &app) : + Node((app), "/redfish/v1/TaskService/Tasks//Monitor", + std::string()) + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response &res, const crow::Request &req, + const std::vector ¶ms) override + { + auto asyncResp = std::make_shared(res); + if (params.size() != 1) + { + messages::internalError(asyncResp->res); + return; + } + + const std::string &strParam = params[0]; + auto find = std::find_if( + task::tasks.begin(), task::tasks.end(), + [&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, "Monitor", strParam); + return; + } + std::shared_ptr &ptr = *find; + // monitor expires after 204 + if (ptr->gave204) + { + messages::resourceNotFound(asyncResp->res, "Monitor", strParam); + return; + } + ptr->populateResp(asyncResp->res); + } +}; + +class Task : public Node +{ + public: + Task(CrowApp &app) : + Node((app), "/redfish/v1/TaskService/Tasks/", std::string()) + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response &res, const crow::Request &req, + const std::vector ¶ms) override + { + auto asyncResp = std::make_shared(res); + if (params.size() != 1) + { + messages::internalError(asyncResp->res); + return; + } + + const std::string &strParam = params[0]; + auto find = std::find_if( + task::tasks.begin(), task::tasks.end(), + [&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, "Tasks", strParam); + return; + } + + 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"] = + crow::utility::getDateTime(ptr->startTime); + if (ptr->endTime) + { + asyncResp->res.jsonValue["EndTime"] = + crow::utility::getDateTime(*(ptr->endTime)); + } + asyncResp->res.jsonValue["TaskStatus"] = ptr->status; + asyncResp->res.jsonValue["Messages"] = ptr->messages; + asyncResp->res.jsonValue["@odata.id"] = + "/redfish/v1/TaskService/Tasks/" + strParam; + if (!ptr->gave204) + { + asyncResp->res.jsonValue["TaskMonitor"] = + "/redfish/v1/TaskService/Tasks/" + strParam + "/Monitor"; + } + } +}; + +class TaskCollection : public Node +{ + public: + TaskCollection(CrowApp &app) : Node(app, "/redfish/v1/TaskService/Tasks") + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response &res, const crow::Request &req, + const std::vector ¶ms) override + { + auto asyncResp = std::make_shared(res); + 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 + } + members.emplace_back( + nlohmann::json{{"@odata.id", "/redfish/v1/TaskService/Tasks/" + + std::to_string(task->index)}}); + } + } +}; + +class TaskService : public Node +{ + public: + TaskService(CrowApp &app) : Node(app, "/redfish/v1/TaskService") + { + entityPrivileges = { + {boost::beast::http::verb::get, {{"Login"}}}, + {boost::beast::http::verb::head, {{"Login"}}}, + {boost::beast::http::verb::patch, {{"ConfigureManager"}}}, + {boost::beast::http::verb::put, {{"ConfigureManager"}}}, + {boost::beast::http::verb::delete_, {{"ConfigureManager"}}}, + {boost::beast::http::verb::post, {{"ConfigureManager"}}}}; + } + + private: + void doGet(crow::Response &res, const crow::Request &req, + const std::vector ¶ms) override + { + auto asyncResp = std::make_shared(res); + 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"] = crow::utility::dateTimeNow(); + asyncResp->res.jsonValue["CompletedTaskOverWritePolicy"] = "Oldest"; + + // todo: if we enable events, change this to true + asyncResp->res.jsonValue["LifeCycleEventOnTaskStateChange"] = false; + + auto health = std::make_shared(asyncResp); + health->populate(); + asyncResp->res.jsonValue["Status"]["State"] = "Enabled"; + asyncResp->res.jsonValue["ServiceEnabled"] = true; + asyncResp->res.jsonValue["Tasks"] = { + {"@odata.id", "/redfish/v1/TaskService/Tasks"}}; + } +}; + +} // namespace redfish -- cgit v1.2.3