Asteris

Managing Systemd with Converge

// Rebecca Skinner // Converge

Converge 0.6.0 adds support for the systemd.unit.state resource, which will allow you to control systemd services from converge. In this article, we’ll talk about what capabilities have been added to converge for controlling systemd processes.

Platform Requirements

Converge only supports systemd on Linux systems. In particular, only systems with systemd: installed, running, and accessible via dbus are supported. Some examples of systemd enabled distributions include: Ubuntu 16+, RHEL/Centos 7+, Debian 8+, and Container Linux. Docker images will not typically support systemd, so if you plan to use the systemd resource to configure a container, it’s important to first verify the availablity of systemd.

An Overview of systemd

Before we begin looking at how converge works with systemd, it’s helpful to take a quick look over the parts of systemd that will be relevant to our article. Feel free to Skip Ahead if you’re already familiar with how systemd works.

Systemd Units

In systemd each individual configurable task is called a unit. Unit files can be started, stopped, and restarted. Each unit will have some general information about their current state and configuration. Different tasks require different types of unit files. You can find the full list of the unit file types and their descriptions here. In our example we’ll be using a service unit and a socket unit.

Systemd Unit States

Unit files have both an active state, and a load state. The load state determines whether the unit file will be available through calls like ListUnits. A file that is available in the normal systemd unit file locations will automatically be loaded when some operation is performed on it.

The active state specifies whether the unit file is running, not running, or in the middle of a state transition. Converge’s systemd resource does not expose state transitions, but they may be visible through manual commands such as systemctl.

Installing A Service

In the following examples we’ll be using an example systemd service. If you are following along with this guide you can download the service and sample files.

Installing a systemd service requires writing the systemd unit files to a directory in the systemd unit file search path. In this example we’re writing to /lib/systemd/system/. Systemd unit files have their own format which is discussed in the documentation.

The file.content resource can be used for writing out systemd unit files. In the example below we’ll write out two unit files, one for the service and another for its socket. We’ll also use task.query to request that systemd reload its service daemon; reloading the daemon as a last step will prevent systemd from warning that the unit files have changed (in this case, been created).

param "logfile" {
  default = "/var/log/example-service.log"
}

file.content "example.service" {
  destination = "/lib/systemd/system/example.service"
  content = <<EOF
[Unit]
Description=A well behaved daemon that does nothing
Requires=example.socket
After=network.target auditd.service

[Service]
ExecStart=/usr/sbin/example-service --log-to {{param `logfile`}} --foreground
ExecReload=/usr/bin/example-service --reload
PIDFile=/var/run/example-service.pid
KillSignal=SIGINT
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify

[Install]
WantedBy=multi-user.target
EOF
}

file.content "example.socket" {
  destination = "/lib/systemd/system/example.socket"
  content = <<EOF
[Unit]
Description=Example service local unix socket

[Socket]
ListenDatagram=/tmp/example-service.socket
SocketMode=0622

[Install]
WantedBy=sockets.target
EOF
}

task "install-binary" {
  interpreter = "/bin/bash"
  check = "[[ -f /usr/sbin/example-service ]]"
  apply = "cp service /usr/sbin/example-service"
}

task.query "reload-systemd" {
  query = "systemctl daemon-reload"
}

Enabling and Disabling Services

The systemd.unit.state resources does not (yet) offer first-class support for enabling and disabling services, but you can easily do set your services to start automatically at boot time using task. In the example below we call systemctl directly to see if our service is enabled; if not we enable it.

task "enable-example-service" {
  interpreter = "/bin/bash"
  check       = "systemctl is-enabled example.service"
  apply       = "systemctl enable example.service"
  depends     = ["file.content.example.service"]
}

Controlling Processes

The systemd.unit.state resource allows you to control the state of a loaded systemd unit. In the example below we ensure that the ssh service is running:

param "state" {
  default = "running"
}

systemd.unit.state "example.service" {
  unit = "example.service"
  state = "running"
  depends = ["file.content.example.service","file.content.example.socket","task.install-binary","task.query.reload-systemd"]
}

In this example we specify the name of the service: unit = "example.service" and the desired state based on the provided param (default: state = "running").

The unit states that you can set through converge are:

The specific meaning of these states is dependent on the underlying unit type. For a service, it means that the process was successfully started and is active when converge checks it’s state. For a socket it means that systemd has created the socket and will buffer data until the socket is handed over to the owning process.

Reloading Service Configurations

The systemd.unit.state resource supports systemd’s service reload capabilities. This can be used with units that support the systemd reload capability to instruct a service daemon to reload it’s configuration. In the example below we disable password authentication for the SSH daemon, then reload it’s configuration.

param "sshd_config" {
  default = "/etc/ssh/sshd_config"
}

task "disable-password-authentication" {
  check = "grep '^PasswordAuthentication no' {{param `sshd_config`}}"
  apply = "sed -i -e 's/^#PasswordAuthentication no/PasswordAuthentication no/' {{param `sshd_config`}}"
}

systemd.unit.state "sshd" {
  unit = "sshd.service"
  state = "running"
  reload = true
  depends = ["task.disable-password-authentication"]
}

Sending Signals

The systemd.unit.state resource supports sending arbitrary unix signals to a running process. Signals may be sent via signal name or the signal number. In the examples below we’ll send the SIGUSR1 signal to our example process process using the name and number:

systemd.unit.state "hup-process" {
  unit          = "example.service"
  signal_number = 10
}
systemd.unit.state "hup-process" {
  unit          = "bad.service"
  signal_number = "sigusr1"
}

Unix signals can also be used to reload a configuration file if the underlying process does not support the systemd reload command. Consult the documentation for the individual services or your distribution documentation.

Unit Inspection

The systemd resource exports a great deal of information about the units that are running on the system. It’s possible to get information about a process without making any changes by creating a systemd.unit.state resource and specifying only the unit name. In the example below we’re specifying a service unit, then looking up the name of the user that the daemon is running under.

systemd.unit.state "example" {
  unit = "example.service"
}

file.content "service-info" {
  destination = "out.txt"
  content     = "Process is running under: {{lookup `systemd.unit.state.example.service_properties.User`}}"
}

There are many different global and unit-type specific properties that are exported by the systemd.unit.state resource. See the full resource documentation for a list of the exported fields.

Conclusion

Converge offers a convenient mechanism for both controlling and inspecting processes that are governed by systemd. Try using the systemd.resource.state resource next time you are making configuration changes to processes so that you can seamlessly reload configurations and control running process state.