book/operation/synit-config
Configuration files and directories
- On a running system:
/etc/syndicate/ - Source repository: [synit]/packaging/packages/synit-config/files/etc/syndicate
The root system bus is
started with a --config /etc/syndicate/boot command-line
argument, which causes it to execute configuration scripts in that
directory. In turn, the boot directory contains
instructions for loading configuration from other locations on the
filesystem.
This section will examine the layout of the configuration scripts and directories.
The boot layer
The files in /etc/syndicate/boot define the boot layer.
Console getty
The first thing the boot layer does, in 001-console-getty.pr,
is start a getty on /dev/console:
<require-service <daemon console-getty>>
<daemon console-getty "getty 0 /dev/console">
Ad-hoc execution of programs
Next, in 010-exec.pr, it installs a handler that responds to messages requesting ad-hoc execution of programs:
?? <exec ?argv ?restartPolicy> [
let ?id = timestamp
let ?facet = facet
let ?d = <temporary-exec $id $argv>
<run-service <daemon $d>>
<daemon $d { argv: $argv, readyOnStart: #f, restart: $restartPolicy }>
? <service-state <daemon $d> complete> [$facet ! stop]
? <service-state <daemon $d> failed> [$facet ! stop]
]
If the restart policy is not specified, it is defaulted to
on-error:
?? <exec ?argv> ! <exec $argv on-error>
“Milestone” pseudo-services
Then, in 010-milestone.pr, it defines how to respond to a request to run a “milestone” pseudo-service:
? <run-service <milestone ?m>> [
<service-state <milestone $m> started>
<service-state <milestone $m> ready>
]
The definition is trivial—when requested, simply declare success—but useful in that a “milestone” can be used as a proxy for a configuration state that other services can depend upon.
Concretely, milestones are used in two places at present: a
core milestone declares that the core layer of services is
ready, and a network milestone declares that initial
network configuration is complete.
Synthesis of service state “up”
The definition of
ServiceState includes ready, for long-running service
programs, and complete, for successful exit (exit status 0)
of “one-shot” service programs. In 010-service-state-up.pr,
we declare an alias up that is asserted in either of these
cases:
? <service-state ?x ready> <service-state $x up>
? <service-state ?x complete> <service-state $x up>
Loading of “core” and “services” layers
The final tasks of the boot layer are to load the “core” and “service” layers, respectively.
Services declared in the “core” layer are automatically marked as
dependencies of the <milestone core> pseudo-service,
and those declared in the “services” layer are automatically marked as
depending on <milestone core>.
+------+ +-----+ +-------+ +----+ +----+ +------------+ +----+
|docker| |modem| |network| |ntpd| |sshd| |userSettings| |wifi|
+---+--+ +--+--+ +---+---+ +--+-+ +--+-+ +------+-----+ +--+-+
| | | | | | |
+-------+--------+--------+------+----------+----------+
| depend on milestone core
services layer V
+----------------+
- - - - - - - - - - - -| milestone core |- - - - - - - - - - - - -
+--------+-------+
core layer | depended on by milestone core
+--------+--+-----------+
| | |
V V V
+-----+ +--------+ +-----------------+
|eudev| |hostname| |machine-dataspace|
+-----+ +--------+ +-----------------+
The core layer loader
For the core layer, in 020-load-core-layer.pr,
a configuration watcher is
started, monitoring /etc/syndicate/core for scripts
defining services to place into the layer. Instead of passing an
unattenuated reference to $config to the configuration
watcher, an attenuation
expression rewrites require-service assertions into
require-core-service assertions:
let ?sys = <* $config [<or [
<rewrite <require-service ?s> <require-core-service $s>>
<accept _>
]>]>
<require-service <config-watcher "/etc/syndicate/core" {
config: $sys
gatekeeper: $gatekeeper
log: $log
}>
Then, require-core-service is given meaning:
? <require-core-service ?s> [
<depends-on <milestone core> <service-state $s up>>
<require-service $s>
]
The services layer loader
The services layer is treated similarly in 030-load-services.pr,
except require-basic-service takes the place of
require-core-service, and the configuration watcher isn’t
started until <milestone core> is ready. Any
require-basic-service assertions are given meaning as
follows:
? <require-basic-service ?s> [
<depends-on $s <service-state <milestone core> up>>
<require-service $s>
]
The core layer: /etc/syndicate/core
The files in /etc/syndicate/core define the core layer.
The configdirs.pr
script brings in scripts in /run and
/usr/local analogues of the core config directory:
<require-service <config-watcher "/run/etc/syndicate/core" $.>>
<require-service <config-watcher "/usr/local/etc/syndicate/core" $.>>
The eudev.pr
script runs a udevd instance and, once it’s ready, starts
an initial scan:
<require-service <daemon eudev>>
<daemon eudev ["/sbin/udevd", "--children-max=5"]>
<require-service <daemon eudev-initial-scan>>
<depends-on <daemon eudev-initial-scan> <service-state <daemon eudev> up>>
<daemon eudev-initial-scan <one-shot "
echo '' > /proc/sys/kernel/hotplug &&
udevadm trigger --type=subsystems --action=add &&
udevadm trigger --type=devices --action=add &&
udevadm settle --timeout=30
">>
The hostname.pr script simply sets the machine hostname:
<require-service <daemon hostname>>
<daemon hostname <one-shot "hostname $(cat /etc/hostname)">>
{#machine-dataspace}Finally, the machine-dataspace.pr script declares a fresh, empty dataspace, and asserts a reference to it in a “well-known location” for use by other services later:
let ?ds = dataspace
<machine-dataspace $ds>
The services layer: /etc/syndicate/services
The files in /etc/syndicate/services define the services layer.
The configdirs.pr
script brings in /run and /usr/local service
definitions, analogous to the same file in the core layer:
<require-service <config-watcher "/run/etc/syndicate/services" $.>>
<require-service <config-watcher "/usr/local/etc/syndicate/services" $.>>
Networking core
The network.pr
script defines the <milestone network> pseudo-service
and starts a number of ancillary services for generically monitoring and
configuring system network interfaces.
First, <daemon interface-monitor> is a small
Python program, required by <milestone network>,
using Netlink sockets to track changes to interfaces and interface
state. It speaks the Syndicate network
protocol on its standard input and output, and publishes a service object which expects a
reference to the machine dataspace defined
earlier:
<require-service <daemon interface-monitor>>
<depends-on <milestone network> <service-state <daemon interface-monitor> ready>>
<daemon interface-monitor {
argv: "/usr/lib/synit/interface-monitor"
protocol: application/syndicate
}>
? <machine-dataspace ?machine> [
? <service-object <daemon interface-monitor> ?cap> [
$cap {
machine: $machine
}
]
]
The interface-monitor publishes assertions describing
interface presence and state to the machine dataspace. The network.pr
script responds to these assertions by requesting configuration of an
interface once it reaches a certain state. First, all interfaces are
enabled when they appear and disabled when they disappear:
$machine ? <interface ?ifname _ _ _ _ _ _> [
$config [
! <exec ["ip" "link" "set" $ifname "up"]>
?- ! <exec/restart ["ip" "link" "set" $ifname "down"] never>
]
]
Next, a DHCP client is invoked for any “normal” (wired-ethernet-like) interface in “up” state with a carrier:
$machine ? <interface ?ifname _ normal up up carrier _> [
$config <configure-interface $ifname <dhcp>>
]
$machine ? <interface ?ifname _ normal up unknown carrier _> [
$config <configure-interface $ifname <dhcp>>
]
$config ? <configure-interface ?ifname <dhcp>> [
<require-service <daemon <udhcpc $ifname>>>
]
$config ? <run-service <daemon <udhcpc ?ifname>>> [
<daemon <udhcpc $ifname> ["udhcpc" "-i" $ifname "-fR" "-s" "/usr/lib/synit/udhcpc.script"]>
]
We use a custom udhcpc script which modifies the default
script to give mobile-data devices a sensible routing metric.
The final pieces of network.pr are static configuration of the loopback interface:
<configure-interface "lo" <static "127.0.0.1/8">>
? <configure-interface ?ifname <static ?ipaddr>> [
! <exec ["ip" "address" "add" "dev" $ifname $ipaddr]>
?- ! <exec/restart ["ip" "address" "del" "dev" $ifname $ipaddr] never>
]
and conditional publication of a default-route record,
allowing services to detect when the internet is (nominally)
available:
$machine ? <route ?addressFamily default _ _ _ _> [
$config <default-route $addressFamily>
]
Wifi & Mobile Data
Building atop the networking core, wifi.pr and modem.pr provide the necessary support for wireless LAN and mobile data interfaces, respectively.
When interface-monitor detects presence of a wireless
LAN interface, wifi.pr reacts by starting wpa_supplicant
for the interface along with a small Python program,
wifi-daemon, that acts as a client to
wpa_supplicant, adding and removing networks and network
configuration according to selected-wifi-network assertions
in the machine dataspace.
$machine ? <interface ?ifname _ wireless _ _ _ _> [
$config [
<require-service <daemon <wpa_supplicant $ifname>>>
<depends-on
<daemon <wifi-daemon $ifname>>
<service-state <daemon <wpa_supplicant $ifname>> up>>
<require-service <daemon <wifi-daemon $ifname>>>
]
]
$config ? <run-service <daemon <wifi-daemon ?ifname>>> [
<daemon <wifi-daemon $ifname> {
argv: "/usr/lib/synit/wifi-daemon"
protocol: application/syndicate
}>
? <service-object <daemon <wifi-daemon $ifname>> ?cap> [
$cap {
machine: $machine
ifname: $ifname
}
]
]
$config ? <run-service <daemon <wpa_supplicant ?ifname>>> [
<daemon <wpa_supplicant $ifname> [
"wpa_supplicant" "-Dnl80211,wext" "-C/run/wpa_supplicant" "-i" $ifname
]>
]
The other tasks performed by wifi.pr are to request DHCP configuration for available wifi interfaces:
$machine ? <interface ?ifname _ wireless up up carrier _> [
$config <configure-interface $ifname <dhcp>>
]
and to relay selected-wifi-network records from user settings (described below) into the
machine dataspace, for wifi-daemon instances to pick
up:
$config ? <user-setting <?s <selected-wifi-network _ _ _>>> [ $machine += $s ]
Turning to modem.pr, which is currently hard-coded for Pinephone
devices, we see two main blocks of config. The simplest just starts the
eg25-manager daemon for controlling the Pinephone’s Quectel
modem, along with a simple monitoring script for restarting it if and
when /dev/EG25.AT disappears:
<daemon eg25-manager "eg25-manager">
<depends-on <daemon eg25-manager> <service-state <daemon eg25-manager-monitor> up>>
<daemon eg25-manager-monitor "/usr/lib/synit/eg25-manager-monitor">
The remainder of modem.pr handles cellular data, configured via the qmicli program.
<require-service <qmi-wwan "/dev/cdc-wdm0">>
<depends-on <qmi-wwan "/dev/cdc-wdm0"> <service-state <daemon eg25-manager> up>>
When the user settings
mobile-data-enabled and mobile-data-apn are
both present, it responds to qmi-wwan service requests by
invoking qmi-wwan-manager, a small shell script, for each
particular device and APN combination:
? <user-setting <mobile-data-enabled>> [
? <user-setting <mobile-data-apn ?apn>> [
? <run-service <qmi-wwan ?dev>> [
<require-service <daemon <qmi-wwan-manager $dev $apn>>>
]
]
]
? <run-service <daemon <qmi-wwan-manager ?dev ?apn>>> [
<daemon <qmi-wwan-manager $dev $apn> ["/usr/lib/synit/qmi-wwan-manager" $dev $apn]>
]
(Because qmicli is sometimes not well behaved, there is also code in modem.pr for restarting it in certain circumstances when it gets into a state where it reports errors but does not terminate.)
Simple daemons
A few simple daemons are also started as part of the services layer.
The docker.pr script starts the docker daemon, but only once the network configuration is available:
<require-service <daemon docker>>
<depends-on <daemon docker> <service-state <milestone network> up>>
<daemon docker "/usr/bin/dockerd --experimental 2>/var/log/docker.log">
The ntpd.pr script starts an NTP daemon, but only when an IPv4 default route exists:
<require-service <daemon ntpd>>
<depends-on <daemon ntpd> <default-route ipv4>>
<daemon ntpd "ntpd -d -n -p pool.ntp.org">
Finally, the sshd.pr script starts the OpenSSH server daemon after ensuring both that the network is available and that SSH host keys exist:
<require-service <daemon sshd>>
<depends-on <daemon sshd> <service-state <milestone network> up>>
<depends-on <daemon sshd> <service-state <daemon ssh-host-keys> complete>>
<daemon sshd "/usr/sbin/sshd -D">
<daemon ssh-host-keys <one-shot "ssh-keygen -A">>
User settings
A special folder, /etc/syndicate/user-settings, acts as
a persistent database of assertions relating to user settings, including
such things as wifi network credentials and preferences, mobile data
preferences, and so on. The userSettings.pr
script sets up the programs responsible for managing the folder.
The contents of the folder itself are managed by a small Python
program, user-settings-daemon, which responds to requests
arriving via the $config dataspace by adding and removing
files containing assertions in
/etc/syndicate/user-settings.
let ?settingsDir = "/etc/syndicate/user-settings"
<require-service <daemon user-settings-daemon>>
<daemon user-settings-daemon {
argv: "/usr/lib/synit/user-settings-daemon"
protocol: application/syndicate
}>
? <service-object <daemon user-settings-daemon> ?cap> [
$cap {
config: $config
settingsDir: $settingsDir
}
]
Each such file is named after the SHA-1 digest of the canonical form of the
assertion it contains. For example,
/etc/syndicate/user-settings/8814297f352be4ebbff19137770e619b2ebc5e91.pr
contains <mobile-data-enabled>.
The files in /etc/syndicate/user-settings are brought
into the main config dataspace by way of a rewriting configuration
watcher:
let ?settings = <* $config [ <rewrite ?item <user-setting $item>> ]>
<require-service <config-watcher $settingsDir { config: $settings }>>
Every assertion from /etc/syndicate/user-settings is
wrapped in a <user-setting ...> record before being
placed into the main $config dataspace.
Copyright © 2021–2023 Tony Garnock-Jones, CC BY 4.0
