Gurp 2.0.0
02 May 2026

In line with semantic and pride versioning, v2.0.0 of Gurp is ready to go.

Gurp v1 was an entirely personal project, with the aim of doing everything my existing Ansible setup did. V2 goes beyond that, adding features other people might want.

The HISTORY gives you the cold facts, but I want to give my hypothetical users a bit more context around certain changes.

UX

This is a major version number bump, so what’s changed?

The thing that will most likely break existing code is that we’ve now formalised the concept of “helpers”. In 1.x we had functions like (zone-fs) to help manage zone filesystems. These floated around in the top-level DSL namespace, only tied to the doer that needed them by a naming convention. Now, these things belong to the doer, as shown by their namespace. So (zone-fs) and (smf-method) are now (zone/fs) and (smf/method).

We’ve also shed things like (hostname), which are better expressed as facts. More on those later.

We’ve replaced the show command with doers and describe. gurp doers lists all the supported doers, and gurp describe zone will output the documentation for the zone doer, including a table of properties and usage examples.

Due to changes in the way Gurp embeds its DSL library, we dropped support for the -L option, which let you override the embedded library with a local one. This was actually useful in development, so I might bring it back one day.

The old symlink doer is now link, because it also supports hard links. Symbolic links are the default though, so just changing the name keeps the old behaviour.

And what’s new?

Doers now have proper definitions. As an example, here’s the pertinent part of the link doer:

(def doer :link)
(def description "Create and remove links.")
(def name-is "Qualified path to the link that will be created")
(def mandatory-props-ensure
  {:source
   {:types [:string]
    :help "The file to which we will link"}
   :force-link
   {:types [:boolean]
    :help "If the link target already exists and this flag is true, Gurp will
           remove it and replace it with a link. If false, a pre-existing target
           causes an error"}
   :type
   {:types [:string]
    :help "The type of link: symbolic or hard"}})
(def optional-props-ensure {})
(def mandatory-props-remove {})
(def optional-props-remove {})
(def defaults-ensure
  {:type "symbolic"
   :force-link false})
(def defaults-remove {})

...

(def notes
  ["If the source doesn't exist, you get an error."
   "Files and directories are ensured before links, so you can link Gurp-managed
    resources."])

This information gets used all over the place. First, when Gurp examines a link/ensure or link/remove resource, it checks to see if the properties supplied are allowed, and of the right type. It also checks that everything that should be there, is there. If any of these checks fail, the user gets a helpful error:

unexpected property :owner. Valid properties are :force-link, :source, :type, :label
did not find mandatory property :source. Mandatory properties are :force-link, :source, :type

The :help properties and the notes definition are used to generate the output of gurp describe link, and also to generate the online documentation, so they are always up-to-date and correct.

The examples you see in the documentation are part of Gurp’s codebase and they are also used in the DSL unit tests, the back-end unit tests, and Merp, Gurp’s functional test suite. This keeps everything super-tight and reliable, though can be a bit of a pain when one of the examples changes for a good reason.

I love a REPL, so Gurp had to have one.

$ gurp repl
repl:1:> (fact :hostname)
"serv-build"
repl:2:> (fact :ip-interfaces)
{"build_net0" { :class "IP" :current "bm-------Z4-" :persistent "-4-" :state "ok"}}
repl:3:> (pathcat "a" "b" "c")
"/a/b/c"

Facts again. We’ll come to that, I promise. You can also apply (or see the potential effect of) snippets of Gurp code with apply --exec:

$ gurp apply --noop --exec '(directory/ensure "/home/rob" :owner "root" :mode "0700")'
2026-05-09T11:29:21.014011Z  INFO doers::types: Configuring host: gurp-runner
2026-05-09T11:29:21.014516Z  INFO util::file: /home/rob: setting user: group to 0:0
2026-05-09T11:29:21.014550Z  INFO util::file: /home/rob: changing mode 0755 -> 0700
2026-05-09T11:29:21.014718Z  INFO commands::apply::init: Run time: 14.966ms
2026-05-09T11:29:21.014746Z  INFO commands::apply::init: resources: 1  changes: 1

While developing and testing Gurp, I found I often had to clean up partially-created runs. To facilitate this I added the very dangerous --destroy-everything-you-touch option to apply. This turns all your ensure resources into removes.As I say. Very dangerous. Is deliberately long, with no short option, so you won’t do it by accident.

Normally Gurp applies your ensures before your removes. But you can reverse this with apply --remove-first. You don’t need it until you need it.

There have also been various quality-of-life improvements to the DSL.

Networking

The first person who looked at Gurp 1.0.0 immediately asked “how do I configure networking?”. And the answer was “you can’t”, because my flat little home LAN didn’t need any.

Gurp gained the ability to manage VNICs, IP interfaces, IP addresses and IP properties in 1.x releases, but they had slightly messy and inconsistent interfaces, which have now been made uniform. 2.0 also adds

Zones

Zone support has improved since I wrote about it in 2025.

Most of the improvements that the user can see are around images. For starters, you can now build illumos branded zones, which can only be built from an image. You can also, when building bhyve zones, use zst images, which is pretty much essential if you want to run, say, OmniOS inside OmniOS, which is how Gurp’s (also greatly improved) test suite works. Every zone type used to have its image path defined in a different way, but now they’re all unified under a top-level :image property.

You can also easily specify limitpriv, hostid, ip-type, pool , acpi and boot-rom when you define a zone.

Sandboxing, and Facts

Gurp has two core principles which conflict.

Firstly, there is no “command” doer. Gurp does not provide a way to shell out when applies config. As I’ve said before, if it’s important enough to run on a production machine, it should be written in a proper language with proper tests.

Secondly, Gurp uses a real, imperative programming language to define content. Though the runtime phase cannot execute arbitrary commands, the compile phase can. This is open to abuse from well- and ill-intentioned users.

Fortunately, Janet has a sandbox feature, which is enabled in Gurp 2.0.0.

Gurp’s embedded Janet interpreter now does not allow command- or thread-execution, filesystem modification, or network access. (There are others too, but they’re the main ones.)

This decision, though, wasn’t entirely easy. By sandboxing, have I turned my lovely Janet DSL into a brackety YAML? We still, of course, have real looping, variables and conditionals, but compile-time execution was super-useful, to the point that the DSL depended on it.

So, I built in an escape-hatch. Though straight Janet subprocesses will error at compile-time, the DSL provides (run-cmd) and (run-safe-cmd), implemented in the Rust backend. They compare their arguments against RUN_CMD and RUN_SAFE_CMD constants; hardcoded vecs or permissible commands. The latter checks the whole command, args and all; the former checks the command, but allows any arguments. It’s a bit like a sudoers.

We already saw the new (fact) function. It uses RUN_SAFE_CMD to make calls a user is likely to need. Results are returned as appropriate Janet structures which you can interrogate as needed. You also have read-only filesystem access.

$ gurp repl
repl:1:> (def zones (fact :zones))
...
repl:2:> (length zones)
22
repl:3:> (keys zones)
@["serv-ws" "serv-records" "serv-fs" "serv-backup" "serv-grafana" "global" "serv-build"...]
repl:4:> (filter |(= "lx" ($ :brand)) zones)
@[{:brand "lx" :id 1 :ip "excl" :path "/zones/serv-grafana" :status "running"}]
repl:5:> (first (lines (slurp "/etc/release")))
"OmniOS v11 r151056t"
repl:6:> ((os/stat "/etc/passwd") :permissions)
"rw-r--r--"

The lists of sandbox capabilities and allowed commands are very restrictive and you cannot change them without recompiling Gurp. This, being Rust, is very simple, so don’t be shy about it if you need to be more permissive.

Telemetry and Memory Usage

Gurp has produced telemetry since the very early days, and looking at my dashboard I found it was sometimes using very large amounts of memory. This was because it always read files into memory to compare them which, with large binaries, doesn’t make a lot of sense.

Gurp now endeavours to stream files wherever it can, which is almost always. We’ve also, in order to keep the client as light as possible, shifted the burden of file hashing to the server.

The client and server both send memory telemetry (if it’s enabled), and they’re both very efficient:

Gurp 2.0.0 server memory usage

That’s serving a couple of dozen clients. Here are some of them:

Gurp 2.0.client server memory usage

All telemetry now uses the OpenTelemetry format, where previously it used InfluxDB.

Client/Server

I’ve already written a fairly detailed description of the way Gurp compiles and sends machine configs in server mode, but briefly, Gurp used to compile config on the server. It now assembles the config on the server, but compiles it on the client. This means you can safely refer to local facts, or examine the local filesystem in your config.

And…

What else? Gurp now uses a lock file to stop runs piling up. Gurp itself is crazy fast, but if you happen to install something like the texlive package, it might matter.

Error reporting is improved, with lots of thiserror and anyhow::Context right through the codebase.

There’s a system-cert doer, which writes a cert to /etc/ssl/certs and re-hashes.

You can use numeric IDs as well as user names when you declare file and directory resources.

Gurp can serve up its own binary via the /api/v1/gurp-binary path, which makes bootstrapping a new host extremely efficient.

2.x and 3.0?

I have lots of ideas for the future. If there’s anything you would like to see in Gurp, please do open an issue or submit a PR. There’s even a doc to help you write new doers.

Peace and Gurping. Believe.

tags