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>
.
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)">>
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.