| # gBMCWeb Plugin |
| |
| This document outlines the design of a plugin system for the Google BMC Redfish |
| server. It also provides a user guide for developers. |
| |
| ## Background |
| |
| ### gBMCWeb and Crow |
| |
| [CrowCpp/Crow](https://github.com/CrowCpp/Crow) is a C++ framework for creating |
| HTTP or Websocket web services. It |
| is easy to use, thanks to its routing system which is similar to |
| Python's [Flask](https://flask.palletsprojects.com/en/2.3.x/). |
| The core HTTP functionality of gBMCWeb is derived from Crow. |
| |
| #### Route: URL, Method, and Handler |
| |
| In Crow, a route determines what happens when your client connects to a |
| specific URL. gBMCWeb supports not only Redfish routes, but also other routes |
| such as WebSocket or dynamic routes, although there are no use cases of |
| non-Redfish routes inside Google. The three main components of a Redfish route |
| are: |
| |
| - URL, the relative path assigned to the route. In Redfish, a URL is |
| corresponding to a Redfish resource. A URL can have parameters. For instance, |
| the template “/redfish/v1/Chassis/<string>” matches requests to any Chassis. |
| |
| - Method, the HTTP method of the route. In Redfish, a server must implement GET, |
| POST, DELETE, and PATCH. |
| |
| - Handler, a piece of code that is executed whenever the client calls the |
| associated route. In gBMCWeb, a Redfish handler is typically a lambda |
| expression |
| or a function that takes a "Request" object, a "Response" object, and any |
| parameters in the URL template. |
| |
| In BMCWeb, you can use the `BMCWEB_ROUTE` macro to register a route. |
| |
| ### Lack of Plugin System |
| |
| One of the most frustrating drawbacks of OpenBMC/BMCweb is that it does not |
| support plugins. This means that, without merging code into the upstream, there |
| is no way to add new components to or modify its existing behaviors, without |
| patches in the Yocto meta layer. |
| |
| ## Overview |
| |
| This document proposes a plugin system that can be used to solve the problems |
| mentioned in the background section. The proposed system has the following |
| features: |
| |
| - It can be loaded as static libraries at compile, which means that the logic in |
| a plugin will not affect the main gBMCWeb daemon if it is not loaded. |
| |
| - It can extend existing behaviors, such as adding an OEM property to an |
| existing Redfish resource. |
| |
| - It can replace existing functionality, such as replacing the execution codes |
| of an existing Redfish resource completely. |
| |
| - It can add new Redfish resources or actions, such as implementing new Redfish |
| resources and its associated actions, especially OEM or experimental resources |
| that don’t affect other platforms. |
| |
| Using gBMCWeb plugins, you can achieve the following: |
| |
| - Faster iteration: machine-specific logic can now be implemented as plugins by |
| Google or vendors, which speeds up the process. |
| - Isolation: The main branch of gBMCWeb will become cleaner as hacks or |
| technical debts can be platform-specific plugins. Rest of the platforms won’t |
| be affected as they don’t include these plugins. |
| |
| ## Detailed Design |
| |
| ### Append |
| |
| The append plugin is supposed to be the most commonly used plugin by developers. |
| It allows inserting another asynchronous callback function after the original |
| handler. |
| |
| The implementation first locates the corresponding rule based on the given URL |
| and HTTP method. Then, it replaces the callback function with a new function, |
| inside which the old callback is executed first. When the AsyncResp is |
| destructed, the newly appended logic is executed before the Redfish parameter |
| handling codes. Finally, the resulting response is written back to the clients. |
| The following code snippet is the proposed implementation. |
| |
| The implementation can be found |
| in [this commit](https://gbmc.googlesource.com/gbmcweb/+/fa9601da5c5f414af4542c79df1d0a2d7d26b1a4) |
| |
| Users are expected to use the following macro to register an "Append" handler |
| for a specific route. |
| |
| ```c++ |
| #define REDFISH_HANDLER_APPEND(url, verb, func) \ |
| appendRedfishHandler<crow::black_magic::getParameterTag(url)>(url, verb, \ |
| func); |
| ``` |
| |
| For example, you can override a property of a Redfish handler. |
| |
| ```c++ |
| void crashdumpServiceGetPlugin( |
| const crow::Request&, const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, |
| const std::string&) |
| { |
| asyncResp->res.jsonValue["MaxNumberOfRecords"] = 10; |
| } |
| |
| |
| REDFISH_HANDLER_APPEND("/redfish/v1/Systems/<str>/LogServices/Crashdump/", |
| boost::beast::http::verb::get, |
| crashdumpServiceGetPlugin); |
| ``` |
| |
| ### Replace |
| |
| The replace handler can supersede the original callback of an existing route. |
| The implementation is rather straightforward. It first locates the corresponding |
| rule, as before. Then, it completely replaces the callback function with the |
| newly fed callback functions. |
| |
| The implementation can be found |
| in [this commit](https://gbmc.googlesource.com/gbmcweb/+/c2761daf22230759e69ad30be61f466ecdee2f1f). |
| |
| Users are expected to use the following macro to register a "Replace" handler |
| for a specific route. |
| |
| ```c++ |
| #define REDFISH_HANDLER_REPLACE(url, verb, func) \ |
| replaceRedfishHandler<crow::black_magic::getParameterTag(url)>(url, verb, \ |
| func); |
| ``` |
| |
| We shall demonstrate the Append handler and Replace handler together with the |
| following code snippet. The following code first replaces the existing static |
| handler of “GET /redfish” with an asynchronous DBus call that is always going to |
| fail via the replace plugin. Then it injects an extra code to run after the |
| asynchronous call finishes via the append plugin. |
| |
| ```c++ |
| void redfishGetReplaced(const crow::Request& req, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| if (!redfish::setUpRedfishRoute(req, asyncResp)) |
| { |
| return; |
| } |
| managedStore::GetManagedObjectStore()->PostDbusCallToIoContextThreadSafe( |
| [asyncResp](const boost::system::error_code ec, |
| const std::variant<std::string>&) { |
| if (ec) |
| { |
| asyncResp->res.jsonValue["dbus_call_result"] = "failed"; |
| return; |
| } |
| |
| }, |
| "non-exist-service", "/no/exist/object", |
| "org.freedesktop.DBus.Properties", "Get", |
| "xyz.openbmc_project.Non.Exist.Interface", "Property"); |
| asyncResp->res.jsonValue["v1"] = "hacked"; |
| } |
| |
| void redfishGetAppended(const crow::Request&, |
| const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) |
| { |
| if (asyncResp->res.jsonValue["dbus_call_result"] != "failed") { |
| asyncResp->res.jsonValue["plugin_is_working"] = "false"; |
| return; |
| } |
| asyncResp->res.jsonValue["plugin_is_working"] = "true"; |
| } |
| |
| REDFISH_HANDLER_REPLACE("/redfish/", boost::beast::http::verb::get, |
| redfishGetReplaced); |
| REDFISH_HANDLER_APPEND("/redfish/", boost::beast::http::verb::get, |
| redfishGetAppended); |
| ``` |
| |
| With a correctly implemented plugin system, we will see this result |
| |
| ```shell |
| GET http://localhost:18080/redfish/ |
| |
| { |
| "dbus_call_result": "failed", |
| "plugin_is_working": "true", |
| "v1": "hacked" |
| } |
| ``` |
| |
| ### Add |
| |
| The add plugin is another frequently used tool that allows users to add a new |
| route. The implementation of this plugin is the most straightforward. We simply |
| need to leverage the existing “newRuleTagged” template function. |
| |
| The implementation can be found |
| in [this commit](https://gbmc.googlesource.com/gbmcweb/+/1249173411d3d064a2d20bbaf39183d94d41437e). |
| |
| Users are expected to use the following macro to add a Redfish route. |
| |
| ```c++ |
| #define REDFISH_HANDLER_ADD(url, verb, privileges, func) \ |
| plugins::addRedfishHandler<crow::black_magic::getParameterTag(url)>( \ |
| url, verb, privileges, func); |
| ``` |
| |
| ### Remove |
| |
| The remove plugin is the inverse of the add plugin and should be used sparingly. |
| It permits users to delete an existing route. |
| |
| The implementation can be found |
| in [this commit](https://gbmc.googlesource.com/gbmcweb/+/53d6d73c9d06f6a2400615bb71a61728f9af4abc). |
| |
| Users are expected to use the following macro to remove a Redfish route. |
| |
| ```c++ |
| #define REDFISH_HANDLER_REMOVE(url, verb) \ |
| plugins::removeRedfishHandler<crow::black_magic::getParameterTag(url)>( \ |
| url, verb); |
| ``` |
| |
| ### Load Plugins |
| |
| gBMCWeb, which inherits from Crow, leverages a Trie-like structure in the |
| following sequence: |
| |
| 1. All routes are added by the “BMCWEB_ROUTE” macro and stored in a container of |
| type Rule. |
| 2. In the main thread, when the HTTP App starts running, the list of rules is |
| iterated and added to a Trie. Each leaf node contains a pointer to the Rule |
| object. |
| 3. The Trie is looked up when new HTTP requests arrive. If there is a match, the |
| callback function stored in the leaf node will be invoked. |
| |
| To load plugins, we need to inject code that can modify the rule container |
| before the trie is built and the HTTP App runs. |
| |
| The implementation can be found |
| in [this commit](https://gbmc.googlesource.com/gbmcweb/+/c783047b85f0d041e77c4a58c22cf9641c9e7b7e). |
| |
| ### Install Headers |
| |
| To make compilation of a plugin work, a few symbols have to be declared and |
| exposed to plugin libraries through headers. We have cleaned up the required |
| headers so they form a complete set. |
| |
| A distinct recipe has been developed for a plugin repository. Please |
| see <https://gbmc.googlesource.com/meta-gbmc-staging/+/refs/heads/master/recipes-phosphor/interfaces/gbmcweb-headers_git.bb>. |
| |
| ## Unit test |
| |
| See [macros_test.cpp](plugins/macros_test.cpp) for the implemented unit tests. |
| Feel free to contribute if you found a case is not coverred. |
| |
| ## Plugin Repo and Recipe |
| |
| A plugin needs to hosted by a GIT repo. We have developed an example repo for |
| users' reference. Please |
| see <https://gbmc.googlesource.com/gbmcweb-platform-plugins-example/+/refs/heads/master>. |
| |
| Its recipe is available |
| in <https://gbmc.googlesource.com/meta-gbmc-staging/+/refs/heads/master/recipes-phosphor/interfaces/gbmcweb-platform-plugins_git.bb>. |
| |
| ## Enable Plugin |
| |
| To enable gBMCWeb Plugin, firstly you need to enable the Meson flag in the |
| bmcweb recipe. |
| |
| ```python |
| # recipes-phosphor/interfaces/bmcweb_%.bbappend |
| PACKAGECONFIG: append = "gbmcweb-platform-plugins" |
| ``` |
| |
| Secondly, replace the SRC_URI and SRCREV of the `gbmcweb-platform-plugins` |
| recipe such that it points out to your plugin repo. |
| |
| ```python |
| # recipes-phosphor/interfaces/gbmcweb-platform-plugins_%.bbappend |
| SRC_URI = "git://$YOUR_HOST/$YOUR_REPO;branch=master;protocol=https" |
| SRCREV = "$YOUR_COMMIT" |
| ``` |
| |
| With above steps, the BMCWeb binary now will include and run the plugin at |
| startup. |