Creating Plugins for the Cockpit User Interface
Note: This post was updated in May 2020 to adjust to reflect Cockpit UI and development best practices.
Cockpit is a web-based graphical interface for servers. You can easily add your own custom pages to the navigation.
For this tutorial you need to install your distribution’s cockpit packages or build it from git.
We’ll make a package called pinger that checks whether your server has network connectivity to the Internet by pinging another host. It’s simple and not too fancy. The package will spawn a process on the server to do all the work.
This example package is already included in the Cockpit sources. You can look it over and modify it.
To start, let’s get ready for development by launching a terminal on your local computer.
First, create a project directory and download the example:
mkdir pinger; cd pinger
curl -O https://raw.githubusercontent.com/cockpit-project/cockpit/master/examples/pinger/manifest.json
curl -O https://raw.githubusercontent.com/cockpit-project/cockpit/master/examples/pinger/ping.html
curl -O https://raw.githubusercontent.com/cockpit-project/cockpit/master/examples/pinger/pinger.js
Cockpit pages — more specifically their HTML and Javascript files — live in package directories. In the package directory there’s also a manifest.json file which tells Cockpit about the package. The pinger
directory above is such a package. Its manifest.json
file looks like this:
{
"version": 0,
"tools": {
"pinger": {
"label": "Pinger",
"path": "ping.html"
}
}
}
The manifest above has a "tools"
subsection, which means that it will appear in the lower section of the menu (see the manifest documentation for details). Each tool is listed in the menu by Cockpit. The "path"
is the name of the HTML file that implements the tool, and the "label"
is the text to show in the menu.
You’ll notice that we haven’t told Cockpit about how to find the package yet. To do so you either copy or symlink the package into one of two places:
~/.local/share/cockpit
in your home directory. It’s used for user specific packages and ones that you’re developing. You can edit these on the fly and refresh your browser to see changes./usr/share/cockpit
is the location for installed packages available to all users of a system. Changing files in this path requires administrator (“root”) privileges. These should not be changed while Cockpit is running.
Since we’re going to be actively editing this package, let’s symlink it into the first location, in your home directory.
mkdir -p ~/.local/share/cockpit
ln -snf $PWD ~/.local/share/cockpit/pinger
To list the Cockpit packages which are installed, use the following command:
$ cockpit-bridge --packages
...
pinger: /home/.../.local/share/cockpit/pinger
...
You should see pinger
listed among all the active modules, like the above example.
Log into Cockpit on this machine with your current user name, as the package is installed in your home directory. (If you’re already logged in to Cockpit with your user account, you can simply reload your browser.)
You should now see a new item:
The pinger tool itself looks like this:
Lets take a look at the pinger HTML, and see how it works.
<!DOCTYPE html>
<html>
<head>
<title>Pinger</title>
<meta charset="utf-8">
<link href="../base1/cockpit.css" type="text/css" rel="stylesheet">
<script src="../base1/cockpit.js"></script>
</head>
<body>
<div class="container-fluid">
<table class="form-table-ct">
<tr>
<td><label class="control-label" for="address">Address</label></td>
<td><input class="form-control" id="address" value="8.8.8.8"></td>
</tr>
<tr>
<td><button class="pf-c-button pf-m-primary" id="ping">Ping</button></td>
<td><span id="result"></span></td>
</tr>
</table>
<pre id="output"></pre>
</div>
<script src="pinger.js"></script>
</body>
</html>
In <head>
, the cockpit.css
file is linked in so our tool matches the style of the rest of Cockpit.
Also included in the <head>
block: cockpit.js
is the basic API for interacting with the system, as well as Cockpit itself. You can find detailed documentation in the Cockpit guide.
The HTML is pretty basic. It defines a little form with a field to type an address, a button to click to start pinging, and an area to present output and results.
The logic lives in pinger.js
, shown in full here:
const address = document.getElementById("address");
const output = document.getElementById("output");
const result = document.getElementById("result");
const button = document.getElementById("ping");
function ping_run() {
cockpit.spawn(["ping", "-c", "4", address.value])
.stream(ping_output)
.then(ping_success)
.catch(ping_fail);
result.innerHTML = "";
output.innerHTML = "";
}
function ping_success() {
result.style.color = "green";
result.innerHTML = "success";
}
function ping_fail() {
result.style.color = "red";
result.innerHTML = "fail";
}
function ping_output(data) {
output.append(document.createTextNode(data));
}
// Connect the button to starting the "ping" process
button.addEventListener("click", ping_run);
// Send a 'init' message. This tells integration tests that we are ready to go
cockpit.transport.wait(function() { });
First we get a bunch of variables pointing to the HTML elements we want to interact with. Next we attach a handler to the Ping button so that the ping_run()
function is called when it is clicked.
function ping_run() {
}
...
button.addEventListener("click", ping_run);
The ping_run()
function is where the magic happens. cockpit.spawn lets you spawn processes on the server and interact with them via stdin
and stdout
.
Here we spawn the ping
command with some arguments:
cockpit.spawn(["ping", "-c", "4", address.val()])
In a web browser you cannot block and wait until a method call completes. Anything that doesn’t happen instantaneously gets its results reported back to you by means of callback handlers.
JavaScript has a standard interface called a Promise. You add handlers by calling the .then()
or .catch()
methods and registering callbacks (Note: historically these have been called .done()
and .fail()
, but these should not be used any more in new code.) The cockpit.spawn
specific .stream()
handler registers a callback to be invoked whenever the process produces output.
<some promise>
.stream(ping_output)
.then(ping_success)
.catch(ping_fail);
}
The ping_success()
and ping_fail()
and ping_output()
update the display as you would expect.
Warning: Don’t start long-running, uninterruptible processes in this naïve way. Browser tabs are easily closed, network connections get severed, laptops suspend, and so on. Take care to use a mechanism like systemd-run (or similar) for processes that should not be interrupted, such as installation procedures.
This should be enough to get you started with your first useful (but admittedly basic) Cockpit page! Please see the Contributing page for more documentation on where to grow from here.