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 astring
s, bool
s, 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.