Adventures in Gurp: Building a pkg Server
20 July 2025

I held off adding an “execute arbitrary command” feature to gurp. I don’t want it to be just another way to run stupid shell scripts. But, I needed to build a pkg repo (to host stuff I build myself, and to set up one of those you must run a couple of commands.

I had already run into this situation before, when I needed to set the scheduler class of my global zones. I chose then to create a new misc doer (“doer” is what I call the things that “do” the “things”) as a home for arbitrary tasks. That doer also enables SMB users and sets the local NFS domain, but they’re proper OS configuration tasks, and building a pkg repo didn’t feel like that: its’ application-specific.

One of the numerous controversial decisions made by the pkg team was to not support the old SYSV postinstall style scripts. Instead, packages install a transient SMF service, which runs once and performs any actions the package requires. This, I thought, was the pattern I should follow.

Services

To install a service on Ansible, I had to copy in an XML manifest, check the current state of the service by shelling out to svcs and registering the stdout of the command, then inspect that stdout and run svccfg import (or not), and finally assert the state of the service. It was a pain, and I wanted to do better in gurp.

My approach ended up with three doers. svc simply asserts the state of a service.

(svc/ensure "sysdef/telegraf" :state "online")

smf lets you define a service as a chunk of Janet. Here’s how I define the service I use to start my illumos Telegraf fork.

(smf/ensure "telegraf"
    :description "Run Telegraf agent"
    :fmri svc
    :start-method
      {:exec "/opt/site/lib/smf/method/telegraf start"
       :timeout 60
       :context
          {:user "telegraf"
           :group "daemon"
           :privileges "basic,file_dac_search,sys_admin,proc_owner,proc_zone"}}))

As usual, gurp has what I think are sensible defaults. For instance, if you don’t provide a stop-method, you get the :kill one that most services use.

The smf doer was written to do only the things I needed, and at the time I didn’t need transient services, so I added a :duration key. If you define :duration "transient", gurp adds to the generated manifest a property-group like this:

<property_group name='startd' type='framework'>
  <propval name='duration' type='astring' value='transient'/>
</property_group>

Next problem. The pkg server runs as a named instance of the svc:/application/pkg/server service, in my case svc:/application/pkg/server:sysdef. You have to create that instance yourself, then set properties on it to make it work right. gurp absolutely could not do this.

As usual, I tried a couple of approaches. First I defined :instances in the svcprop doer. This was confusing to use, because there was no way to tie properties to instances unless I introduced more complicated structures. I tried defining instances in the svc doer, but that was no good because that runs after svcprop.

In the end I decided to make it automagic. A svcprop resource is defined with the instance name, in this case svc:/application/pkg/server:sysdef. If the sysdef instance does not exist, gurp will use svccfg to create it. Then it can create any property groups you defined, and the properties they contain. Finally it refreshes the service, and the svc doer asserts the state at the end of the run.

# repo-root and pkg-log-dir are defined elsewhere, and (pathcat) is function
# which builds fully-qualified paths from components.
(svcprop/ensure repo-svc
  :property-groups {:pkg "application"}
  :properties {:pkg/inst_root repo-root
               :pkg/readonly false
               :pkg/log_errors (pathcat pkg-log-dir "error.log")
               :pkg/log_access (pathcat pkg-log-dir "access.log")})

(svc/ensure repo-svc :state "online"))

Whilst I was in the svcprop doer I made it properly handle different property types. An initial decision to treat everything as a String had been fine so far, but it was bound to catch up with me sooner or later, and the pkg server uses bools. I chose to support astrings, bools, and integers. I can’t see any reason why I’d ever want to deal with count and time, but the enum is there, and they can be added if they ever need to be.

Now I can define my pkg server like this.

(import ../globals) 

(indoc startup-method-template `
  #!/bin/sh -e
  # We may be re-creating a zone with an existing repo.
  if ! test -d "{{ repo-root}}/publisher"
  then
	  /usr/bin/pkgrepo create {{ repo-root }}
  fi
  
	/usr/bin/pkgrepo set -s {{ repo-root }} publisher/prefix={{ repo-name }}
`)

(def repo-name "sysdef")
(def repo-root "/repo")
(def log-dir "/var/log/pkg")
(def setup-method (pathcat globals/site-smf-method
                           (string repo-name "-repo-setup.sh")))
(def setup-svc (string "svc:/sysdef/application/" repo-name "-setup"))
(def repo-svc (string "svc:/application/pkg/server:" repo-name))

(role pkg-server
      (directory/ensure log-dir
                        :owner "pkg5srv"
                        :group "daemon")

      (directory/ensure (pathcat log-dir "server")
                        :owner "pkg5srv"
                        :group "daemon")

      (directory/ensure repo-root
                        :owner "pkg5srv"
                        :group "pkg5srv")

      (smf/ensure setup-svc
                  :svc-name "pkg-setup"
                  :fmri setup-svc
                  :description "transient service to create pkg repo"
                  :duration "transient"
                  :start-method {:exec setup-method})

      (file/ensure setup-method
                   :mode "0755"
                   :content (template-out
                             setup-method-template
                             {:repo-name repo-name
                              :repo-root repo-root})

      (svcprop/ensure repo-svc
                      :property-groups {:pkg "application"}
                      :properties {:pkg/inst_root repo-root
                                   :pkg/readonly false
                                   :pkg/log_errors (pathcat log-dir "error.log")
                                   :pkg/log_access (pathcat log-dir "access.log")})

      (svc/ensure repo-svc :state "online"))

Which, given the amount of work it does, is pretty tight.

tags