blob: 32b0c90ae9c2ef1f6d8b7689485c3278621e186e [file] [log] [blame] [view]
# 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.