1# IPMI Architecture 2 3IPMI is made up of commands and channels. Commands are provided by providers 4and channels are processes called bridges. Commands are received from external 5sources (system admin users) via the bridges and routed to the providers' 6command handlers via the main IPMI daemon. 7 8## High-Level Overview 9 10IPMI is all about commands and responses. Channels provide a mechanism for 11transporting the data, each with a slightly different protocol and transport 12layer, but ultimately, the highest level data is a raw IPMI command consisting 13of a NetFn/LUN, Command, and optional data. Each response is likewise a 14Completion Code and optional data. So the first step is to break apart 15channels and the IPMI queue. 16 17 18``` 19 /------------------\ 20 /----------------------------\ | | 21 | KCS/BT - Host | <-All IPMI cmds-> | | 22 | | | | 23 \----------------------------/ | IPMI Daemon | 24 | (ipmid) | 25 | | 26 /-----------------------------\ | | 27 | LAN - RMCP+ | | | 28 | /--------------------------\| | | 29 | |*Process the Session and || <-All IPMI cmds-> | | 30 | | SOL commands. || except session | | 31 | |*Create Session Objs || and SOL cmds | | 32 | \--------------------------/| | | 33 \-----------------------------/ \------------------/ 34 : ^ ^ 35 : | | 36 : | | 37 /-------------------------\ | | 38 | Active session/SOL Objs | <---------Query the session-/ | 39 | - Properties | and SOL data via Dbus | 40 \-------------------------/ | 41 V 42 ---------------------\ 43 | D-Bus services | 44 | ------------------ | 45 | Do work on behalf | 46 | of IPMI commands | 47 ---------------------/ 48``` 49 50The IPMI messages that get passed to and from the IPMI daemon (ipmid) are 51basically the equivalent of ipmitool's "raw" commands with a little more 52information about the message. 53 54```less 55Message Data: 56 byte:LUN - LUN from netFn/LUN pair (0-3, as per the IPMI spec) 57 byte:netFn - netFn from netFn/LUN pair (as per the IPMI spec) 58 byte:cmd - IPMI command ID (as per the IPMI spec) 59 array<byte>:data - optional command data (as per the IPMI spec) 60 dict<string:variant>:options - optional additional meta-data 61 "userId":int - IPMI user ID of caller (0 for session-less channels) 62 "privilege": enum:privilege - ADMIN, USER, OPERATOR, CALLBACK; 63 must be less than or equal to the privilege of the user and less 64 than or equal to the max privilege of this channel 65``` 66 67```less 68Response Data: 69 byte:CC - IPMI completion code 70 array<byte>:data - optional response data 71``` 72 73A channel bridge, upon receiving a new IPMI command, will extract the 74necessary fields from whatever transport format (RMCP+, IPMB, KCS, etc.) 75For session-based channels (RMCP+) the bridge is responsible for establishing 76the session with credentials and determining the maximum privilege available 77for this session. The bridge then takes the extracted command, data, and 78possibly user and privilge information, and encodes them in a D-Bus method call 79to send to the IPMI daemon, ipmid. The daemon will take the message, and 80attempt to find an appropriate handler in its handler tables. If a handler is 81found, it will attempt to extract the required parameters for the handler and 82pass them along. The handler will return a tuple of response parameters, which 83will get packed back into a D-Bus response message and sent back to the calling 84channel's bridge. The bridge will then re-package the response into its 85transport protocol and send it off. 86 87The next part is to provide a higher-level, strongly-typed, modern C++ 88mechanism for registering handlers. Each handler will specify exactly what 89arguments are needed for the command and what types will be returned in the 90response. This way, the ipmid queue can unpack requests and pack responses in a 91safe manner. Because the handler packing and unpacking code is templated, it 92is written mostly in headers. 93 94 95## Details and Implementation 96 97For session-less channels (like BT, KCS, and IPMB), the only privilege check 98will be to see that the requested privilege is less than or equal to the 99channel's maximum privilege. If the channel has a session and authenticates 100users, the privilege must be less than or equal to the channel's maximum 101privilege and the user's maximum privilege. 102 103Ipmid takes the LUN/netFN/Cmd tuple and looks up the corresponding handler 104function. If the requested privilege is less than or equal to the required 105privilege for the given registered command, the request may proceed. If any of 106these checks fail, ipmid returns with _Insufficient Privilege_. 107 108At this point, the IPMI command is run through the filter hooks. The default 109hook is ACCEPT, where the command just passes onto the execution phase. But 110OEMs and providers can register hooks that would ultimately block IPMI commands 111from executing, much like the IPMI 2.0 Spec's Firmware Firewall. The hook would 112be passed in the context of the IPMI call and the raw content of the call and 113has the opportunity to return any valid IPMI completion code. Any non-zero 114completion code would prevent the command from executing and would be returned 115to the caller. 116 117The network channel bridges (netipmid), executing one process per interface, 118handle session (RMCP+) and SOL commands and responses to those commands. 119Get/Set SOL configuration, Get session info, close session commands can also be 120requested through KCS/BT interface, ipmid daemon must also need details about 121session and SOL. In order to maintain sync between ipmid and netipmid 122daemon, session and SOL are exposed in D-Bus, which ipmid can query and respond 123to commands issued through host interface (KCS/BT). 124 125The next phase is parameter unpacking and validation. This is done by 126compiler-generated code with variadic templates at handler registration time. 127The registration function is a templated function that allows any type of 128handler to be passed in so that the types of the handler can be extracted and 129unpacked. 130 131This is done in the core IPMI library (from a high level) like this: 132```cpp 133template <typename Handler> 134class IpmiHandler 135{ 136 public: 137 explicit IpmiHandler(Handler&& handler) : 138 handler_(std::forward<Handler>(handler)) 139 { 140 } 141 message::Response::ptr call(message::Request::ptr request) 142 { 143 message::Response::ptr response = request->makeResponse(); 144 // < code to deduce UnpackArgsType and ResultType from Handler > 145 UnpackArgsType unpackArgs; 146 ipmi::Cc unpackError = request->unpack(unpackArgs); 147 if (unpackError != ipmi::ccSuccess) 148 { 149 response->cc = unpackError; 150 return response; 151 } 152 ResultType result = std::apply(handler_, unpackArgs); 153 response->cc = std::get<0>(result); 154 auto payload = std::get<1>(result); 155 response->pack(*payload); 156 return response; 157 } 158 private: 159 Handler handler_; 160}; 161 ... 162 namespace ipmi { 163 constexpr NetFn netFnApp = 0x06; 164 namespace app { 165 constexpr Cmd cmdSetUserAccessCommand = 0x43; 166 } 167 class Context { 168 NetFn netFn; 169 Cmd cmd; 170 int channel; 171 int userId; 172 Privilege priv; 173 boost::asio::yield_context* yield; // for async operations 174 }; 175 } 176``` 177While some IPMI handlers would look like this: 178```cpp 179ipmi::RspType<> setUserAccess( 180 std::shared_ptr<ipmi::Context> context, 181 uint4_t channelNumber, 182 bool ipmiEnable, 183 bool linkAuth, 184 bool callbackRestricted, 185 bool changeBit, 186 uint6_t userId, 187 uint2_t reserved1, 188 uint4_t privLimit, 189 uint4_t reserved2, 190 std::optional<uint4_t> userSessionLimit, 191 std::optional<uint4_t> reserved3) { 192 ... 193 return ipmi::ResponseSuccess(); 194} 195 196ipmi::RspType<uint8_t, // max user IDs 197 uint6_t, // count of enabled user IDs 198 uint2_t, // user ID enable status 199 uint8_t, // count of fixed user IDs 200 uint4_t // user privilege for given channel 201 bool, // ipmi messaging enabled 202 bool, // link authentication enabled 203 bool, // callback access 204 bool, // reserved bit 205 > 206 getUserAccess(std::shared_ptr<ipmi::Context> context, uint8_t channelNumber, 207 uint8_t userId) 208{ 209 if (<some error condition>) 210 { 211 return ipmi::response(ipmi::ccParmOutOfRange); 212 } 213 // code to get 214 const auto& [usersMax, status, usersEnabled, usersFixed, access, priv] = 215 getSdBus()->yield_method_call(*context->yield, ...); 216 return ipmi::responseSuccess(usersMax, usersEnabled, status, usersFixed, 217 priv, access.messaging, access.linkAuth, 218 access.callback, false); 219} 220 221void providerInitFn() 222{ 223 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnApp, 224 ipmi::app::cmdSetUserAccessCommand, 225 ipmi::Privilege::Admin, 226 setUserAccess); 227 ipmi::registerHandler(ipmi::prioOpenBmcBase, ipmi::netFnApp, 228 ipmi::app::cmdGetUserAccessCommand, 229 ipmi::Privilege::Operator, 230 getUserAccess); 231} 232``` 233 234 235Ipmid providers are all executed as boost::asio::coroutines. This means that 236they can take advantage of any of the boost::asio async method calls in a way 237that looks like synchronous code, but will execute asynchronously by yielding 238control of the processor to the run loop via the yield_context object. Use 239the yield_context object by passing it as a 'callback' which will then cause 240the calling coroutine to yield the processor until its IO is ready, at which 241point it will 'return' much like a synchronous call. 242 243Ideally, all handlers would take advantage of the asynchronous capabilities of 244ipmid via the boost::asio::yield_context. This means that the queue could have 245multiple in-flight calls that are waiting on another D-Bus call to return. With 246asynchronous calls, this will not block the rest of the queue's operation, 247The sdbusplus::asio::connection that is available to all providers via the 248getSdBus() function provides yield_method_call() which is an asynchronous 249D-Bus call mechanism that 'looks' like a synchronous call. It is important that 250any global data that an asynchronous handler uses is protected as if the 251handler is multi-threaded. Since many of the resources used in IPMI handlers 252are actually D-Bus objects, this is not likely a common issue because of the 253serialization that happens via the D-Bus calls. 254 255Using templates, it is possible to extract the return type and argument types 256of the handlers and use that to unpack (and validate) the arguments from the 257incoming request and then pack the result back into a vector of bytes to send 258back to the caller. The deserializer will keep track of the number of bits it 259has unpacked and then compare it with the total number of bits that the method 260is requesting. In the example, we are assuming that the non-full-byte integers 261are packed bits in the message in most-significant-bit first order (same order 262the specification describes them in). Optional arguments can be used easily 263with C++17's std::optional. 264 265Some types that are supported are as follows: 266* standard integer types (uint8_t, uint16_t, uint32_t, int, long, etc.) 267* bool (extracts a single bit, same as uint1_t) 268* multi-precision integers (uint<N>) 269* std::bitset<N> 270* std::optional<T> - extracts T if there are enough remaining bits/bytes 271* std::array<T, N> - extracts N elements of T if possible 272* std::vector<T> - extracts elements of T from the remaining bytes 273* any additional types can be created by making a pack/unpack template 274 275For partial byte types, the least-significant bits of the next full byte are 276extracted first. While this is opposite of the order the IPMI specification is 277written, it makes the code simple. Multi-byte fields are extracted as LSByte 278first (little-endian) to match the IPMI specification. 279 280When returning partial byte types, they are also packed into the reply in the 281least-significant bit first order. As each byte fills up, the full byte gets 282pushed onto the byte stream. For examples of how the packing and unpacking is 283used, see the unit test cases in test/message/pack.cpp and 284test/message/unpack.cpp. 285 286As an example this is how a bitset is unpacked 287```cpp 288/** @brief Specialization of UnpackSingle for std::bitset<N> 289 */ 290template <size_t N> 291struct UnpackSingle<std::bitset<N>> 292{ 293 static int op(Payload& p, std::bitset<N>& t) 294 { 295 static_assert(N <= (details::bitStreamSize - CHAR_BIT)); 296 size_t count = N; 297 // acquire enough bits in the stream to fulfill the Payload 298 if (p.fillBits(count)) 299 { 300 return -1; 301 } 302 fixed_uint_t<details::bitStreamSize> bitmask = ((1 << count) - 1); 303 t |= (p.bitStream & bitmask).convert_to<unsigned long long>(); 304 p.bitStream >>= count; 305 p.bitCount -= count; 306 return 0; 307 } 308}; 309``` 310 311If a handler needs to unpack a variable payload, it is possible to request 312the Payload parameter. When the Payload parameter is present, any remaining, 313unpacked bytes are available for inspection. Using the same unpacking calls 314that were used to unpack the other parameters, the remaining parameters can 315be manually unpacked. This is helpful for multi-function commands like Set 316LAN Configuration Parameters, where the first two bytes are fixed, but the 317remaining 3:N bytes vary based on the selected parameter. 318