This page describes Kea REST API from the developer's perspective. This might be of some use for projects that intend to implement an API. This is not a design per se, more like a retrospection.
Kea API Overview
Kea can receive JSON commands and responds with JSON responses. The commands can be sent over UNIX socket or via HTTP/HTTPS. Kea daemons (kea-dhcp4, kea-dhcp6, kea-d2) used to require CA (control agent), additional proxy that did UNIX-HTTP translation, but recent Kea versions (starting with 2.7.4) now have native HTTP/HTTPS support.
An example command:
{
"command": "version-get"
}
An example response:
[
{
"arguments": {
"extended": "2.4.1\ntarball\nlinked with:\nlog4cplus 2.0.8\n"
},
"result": 0,
"text": "2.4.1"
}
]
Not truly ReSTFul API
Kea management interface is called REST API, but it's not really RESTful. All commands are sent to the same URL. All commands are sent using POST. The command is sent in the JSON and not as part of the URL. So purists would complain this is not really a proper REST API. Users don't care. The API is available since 2017. We got one complaint about non-rest nature from our internal developer. Zero negative feedback from the user base. People don't care, because it's easy to use. We get requests to add extra commands or expose extra fields, or use some additional filtering parameters and such. So people do use this API extensively.
Commands
The commands are using object
-verb
naming condition, e.g. version-get
, subnet4-add
. This has the nice benefit of the similar commands being grouped together. This is useful for automatic organization of the documentation and was envisioned to nicely place with tab-completion environments. Sadly, we have some exceptions to this rule that we can't correct for historical reasons.
Ease of use
One of the fundamental reasons why this API is successful is its ease of use. We do have kea-shell
tool to access it, but it's just couple lines of python code using urllib. So it acts more like an example "here's how you do this in python". I think many users simply use curl
. If your API can't be accessed with simple generic purpose tools, it is too complex.
UNIX and HTTP
We initially opted to use UNIX sockets, because they were simple and we didn't have http library back then. It also provided a basic security model that every sysadmin readily understood. If we had http lib back then, we'd probably go straight to have something listening on loopback.
Control Agent
Kea uses separate daemons for different things. The three most popular are kea-dhcp4 (for handling DHCPv4 traffic), kea-dhcp6 (you guessed right, DHCPv6) and kea-d2 (or kea-dhcp-ddns, a DNS update daemon that updates forward and reverse zones depending on lease status). They all open separate UNIX sockets. The CA (control agent) daemon opens one HTTP socket. When sending commands to CA, extra parameter service
is necessary to specify, which daemon the command is intended at. If not specified, it's the CA that will attempt to handle the command. A good exercise is to start all daemons and play with list-commands
. CA has a handful of commands, DHCPv4/v6 has hundreds.
Result status
Each command response has an integer field result
that provides a status in definite terms. 0 is success, 1 is error, 2 is unsupported. We later added 3 meaning empty (command was completed successfully, but no data was affected or returned) and 4 conflict (command could not apply requested configuration changes because they were in conflict with the server state). The overall intention was that it should be possible to check this status automatically.
In most cases, also text
is returned that provides verbose indication what was completed or why the command failed. This serves two purposes. First, to give some hints why command failed and what could possibly be done by the user to fix the problem. Second, explains what was completed ('1234 subnets deleted.'), so user understands what was the result of his command and could cry if it wasn't the intended result ('oops, just wanted to delete single subnet with id 1234').
Forward and backward compatibility
The API started VERY modest. It first appeared in Kea 1.2.0 (2017) and was envisioned to grow. As such, the two first commands implemented were version-get
and list-commands
(which should have been commands-list
, but we can't fix it now). The version-get
is probably not exciting, but it serves several very important aspects. First, it was easy to implement, so developers could focus on the generic commands handling infrastructure. Second, it's often used as a sanity check if the server is up and responsive. Third, it is useful for managing applications, such as Stork, to behave differently depending on the software version.
Extensibility
Kea has CommandMgr class that register a callback for each command. The dynamic nature of this was one of the crucial factors in this API success. Over time, Kea grew the list of commands considerably. Also, Kea has the concept of hooks, loadable optional libraries that can provide extra functionality. Often, the hooks provide extra commands. In some cases, the whole purpose of an extra hook is to provide additional commands. See host_cmds
, subnet_cmds
as examples.
Another important design decision was the way parser was structured. The parser attempts to extract parameters it knows and ignores others. It should not throw an error to help with future and backward compatibility. Newer Kea versions may have additional parameters support that this Kea version does not understand yet.
Here's an example command that attempts to retrieve host reservation for specified MAC address:
{
"command": "reservation-get",
"arguments": {
"subnet-id": 123,
"identifier-type": 'hw-address',
"identifier": "01:02:03:04:05:06"
}
}
Newer Kea versions can be told to retrieve this information from specific backend only:
{
"command": "reservation-get",
"arguments": {
"subnet-id": 123,
"identifier-type": 'hw-address',
"identifier": "01:02:03:04:05:06"
"operation-target": "database"
}
}
When an API client tries to send this to older Kea, it still should work. (The best client implementation would call the version-get first and then determine that this older Kea doesn't support this parameter and don't send it, but API clients often don't get through such hassles).
Documentation
Every API command is documented using separate JSON file. Those JSON files are then used to generate documentation. But it's more than just a plain list. There are lists provided for specific daemons, hooks and we could also generate fancier lists (which commands appeared in release x.y.z, which commands were supported by release a.b.c and so on). You can see the files in the src/share/api directory.
This approach is extensible and it serves its purpose beyond just documentation. As an example, we added access
parameter, which broadly segregated commands into more benign read-only commands and those who could potentially cause damage (write commands, such as delete subnets, shutdown server, overwrite something etc). Eventually this grew into a dedicated RBAC (role-based access control) hook, but the nice thing is that it's reasonably easy to add a property to commands.
There's a simple script doc/sphinx/api2doc.py that read those JSON files and produce rst, ready to use with Sphinx.
Extended JSON
Kea uses something we refer to as extended JSON. We support JSON-proper with some custom extensions: comments (c++ style //
, c style /* ... */
, and bash style # ...
), includes (<?include "file.json"?>
) and extra commas ({ "foo": "bar", }
). The comments are popular among users who craft their JSON configs manually. Sadly, there's no easy way to store the data and retrieve it later. Most comments are lost, although we have now a mechanism to allow users to store and retrieve their comments.
User context
User context is really powerful feature. Almost any data - configuration entity, such as subnets, host reservations, networks etc, but also dynamic elements, such as leases - can have any data attached, as long as it is proper JSON. Kea does not process this data in general (with some exceptions), just makes it available to users. This can be used for many goals: users can add comments. this could be something trivial (like "floor": 1
added to subnets), but can be more structured (details of the user of specified host reservation - first/last name of the user, his phone number, etc).
Some hooks can use this additional data.
In some cases, Kea can be told to store additional info about the clients, for more complex queries about clients could be answered. See Comments and user context for details.
Anything can be reconfigured
Since very early days, Kea provided four major commands: config-get
, which returns the whole configuration as JSON; config-set
which sets new configuration; config-reload
, which is like sending SIGHUP (it tells Kea to re-read its config from disk); and config-write
, which forces Kea to write its running configuration as JSON config to disk.
This set allows users to do pretty powerful stuff with the API: get the config, change whatever needs changing, push it back, and if it's working, write it to disk.
Changing the whole config is costly operation (in terms of time and service interruption). We now have many dedicated calls that change only parts of the configuration (such as add/update/delete reservation/subnet/network, etc) that are more optimized for typical daily use operations. Those are often provided by premium (paid) hooks. This offers a reasonable compromise - you can technically do it with open source and smaller deployments do it, but if you're large and can't afford couple seconds interruption, there's a commercial route to make it more efficient.
Security
Kea can use HTTP or HTTPS. HTTPS requires TLS certs configuration. Kea can use strong authentication model (clients need certificates to connect, the client certs can be verified by the server). Kea also has basic HTTP authentication available. It's easier to use (user/password), but it's less secure and should not be used over plain HTTP. It might be sufficient in some deployments.
Additional info: section 7.2 and 7.3.
Things that could have been done better
-
One thing that we messed up in Kea is the response cardinality. Some commands could possibly return one or multiple sets of responses. A good example would be "give me all reservations in a subnet" or "give me a reservation for IP address 192.0.2.1". For simple get operations, this is functional, but there are some operations that could update multiple elements. We haven't figured out how to handle partial failures, e.g. user wants to update 10 reservations. Kea updated 6 and failed on 7th. Should be abort? Revert changes (rollback is difficult)? Continue and report which updates failed?
-
Naming. In some cases, we want to attempt addition of a new entity, e.g. host reservation and we wanted it to fail if such reservation already exists. Those commands are called
*-add
. In some code that was developed after we got more experience with API, we want to do so calledupsert
or update or insert (if doesn't exist, add it. If exists, overwrite it). Those commands are called*-set
. We also have some commands that specifically update existing entries*-update
. This is nuanced and confusing. We should have decided what's the general approach to adding new entities should be and stick with it. -
object-verb naming. We got the generic naming convention early, but not from the beginning. Some of the early commands does not follow the naming convention. This is confusing, but for historical reasons it's impossible (or very laborious) to fix it now.
-
If we started the API right now, we would consider using swagger. But it's uncertain if we did use it. Overall, swagger is great - it provides generic interface with ability to generate client code for many languages. However, swagger codegen requires Java to generate. It's not fun to add another language to your developer's toolchain. There are incompatibilities between OpenAPI 2.0 and 3.0. There are alternative implementations, such as goswagger, but they have their own limitations. On the flip side, the web documentation for OpenAPI is interactive, so you can send commands from the browser. That's great for some experimentation. But is it worth paying with the hassle of dealing with Java, more complex toolchain and extra layer of potential incompatibilities?
-
Single or plural? We wanted the command names to be as intuitive as possible. The names use singular, no matter how many entities we are expected to return. So command to get a single reservation would be
reservation-get
and command to get a list of IPv4 subnets would besubnet4-list
(subnet4, not subnets4). The entities in the config use the same naming convention, so there's{ "Dhcp4": { "subnet4": [ (list of subnets here) ] } }
, despite the fact that usually there's more than one subnet). Sadly, this is not always consistent as it should be. Too late to fix. -
If we started from scratch again, we would've done it proper ReST - with each data structure having its own URL, proper CRUD support, etc.