Let's Write a Unit for Systemd

Let's Write a Unit for Systemd

Hello guys! Hope you're doing well. Here I'm with another article. houYouDoin.gif

Last article I have talked about controlling systemd services, and today lets write a unit for systemd. (Here, if you missed it)

What the heck is a unit file?

wth.jpeg

In systemd, a unit refers to any resource that the systemd knows how to operate on or manage. This is the primary object that the systemd knows how to deal with. These resources are defined with configuration files, which are called as unit files.

listing units

You can list out the units that have loaded into the memory with systemctl list-unitscommand. By default only units which are active, have pending jobs, or have failed are shown. Adding --all flag will change it.

image.png

In output there are following fields.

UNIT: The name of the systemd unit.

LOAD: This indicates if the unit's configuration is being loaded by systemd into the memory.

ACTIVE: This states the status of the unit and shows whether it is active or not.

SUB: This provides more detailed information about the unit, which will vary depending on the type of unit, state, and the method the unit runs in.

DESCRIPTION: A short description contains what each unit is and what it does.

systemctl --state=help gives you a list of values that can be in each state.

You can get units in a type-wise order with --type flag, and state-wise list with --state flag.

image.png

list-units only gives the output of the loaded and the tried-to-load service. If you wanna get all the available units you can use list-unit-filescommand.

Types of units

You may have seen different suffixes (.service, .socket, .device, etc.) that are added to units,while you are working with systemd. That suffix indicates the type of the units. Following are few suffixes that you may face.

  • .service: A service unit describes how to manage a service or application. This will include how to start or stop the service, under which circumstances it should be automatically started, and the dependencies and ordering information for related software.
  • .socket: A socket unit file describes a network or IPC socket, or a FIFO buffer that systemd uses for socket-based activation. These always have an associated .service file that will be started when activity is seen on the socket that this unit defines.
  • .device: A unit that describes a device that has been designated as needing systemd management by udev or the sysfs filesystem. Not all devices will have .device files. Some scenarios where .device units may be necessary are for ordering, mounting, and accessing the devices.
  • .mount: This unit defines a mountpoint on the system to be managed by systemd. These are named after the mount path, with slashes changed to dashes. Entries within /etc/fstab can have units created automatically.
  • .swap: This unit describes swap space on the system. The name of these units must reflect the device or file path of the space.
  • .target: A target unit is used to provide synchronization points for other units when booting up or changing states. They also can be used to bring the system to a new state. Other units specify their relation to targets to become tied to the target’s operations.
  • .timer: A .timer unit defines a timer that will be managed by systemd, similar to a cron job for delayed or scheduled activation. A matching unit will be started when the timer is reached.
  • .snapshot: A .snapshot unit is created automatically by the systemctl snapshot command. It allows you to reconstruct the current state of the system after making changes. Snapshots do not survive across sessions and are used to roll back temporary states.

Creating a unit file

systemd probes services in /usr/lib/systemd/system (packages installed by the package manager), /lib/systemd/system and /etc/systemd/system. If you wish to edit a unit file or make a new one /etc/systemd/system would be the best choice. It’s the first place where systemd seeks.

Let's create a unit file. But wait, first you need to learn the basic syntax.

image.png

Syntax

systemd units use .ini format as syntax. You have Sections, which are case-sensitive, i.e. UNIT and Unit are two different words.

Withing sections you have unit behavior and metadata defined using key=value format.

[Section]
key=value
key=value

[Unit] Section Directives

The first section found in most unit files is the [Unit] section. This is generally used for defining metadata for the unit and configuring the relationship of the unit to other units. Although section order does not matter to systemd when parsing the file, this section is often placed at the top because it provides an overview of the unit.

Some common directives that you will find in the [Unit] section are:

  • Description=: This directive can be used to describe the name and basic functionality of the unit. It is returned by various systemd tools, so it is good to set this to something short, specific, and informative.
  • Documentation=: This directive provides a location for a list of URIs for documentation. These can be either internally available man pages or web accessible URLs. The systemctl status command will expose this information, allowing for easy discoverability.
  • Requires=: This is a list of units that has to be activated before the current unit is activated, or else the current unit activation will be failed. These units are started in parallel with the current unit by default.
  • Wants=: This directive is similar to Requires=, but less strict. Systemd will attempt to start any units listed here when this unit is activated. If these units are not found or fail to start, the current unit will continue to function. This is the recommended way to configure most dependency relationships. Again, this implies a parallel activation unless modified by other directives.
  • BindsTo=: This directive is similar to Requires=, but also causes the current unit to stop when the associated unit terminates.
  • Before=: The units listed in this directive will not be started until the current unit is marked as started if they are activated at the same time. This does not imply a dependency relationship and must be used in conjunction with one of the above directives if this is desired.
  • After=: The units listed in this directive will be started before starting the current unit. This does not imply a dependency relationship and one must be established through the above directives if this is required.
  • Conflicts=: This can be used to list units that cannot be run at the same time as the current unit. Starting a unit with this relationship will cause the other units to be stopped.

Note: Using these directives and a handful of others, general information about the unit and its relationship to other units and the operating system can be established.

[Install] Section Directives

On the opposite side of unit file, the last section is often the [Install] section. This section is optional and is used to define the behavior or a unit if it is enabled or disabled. Because of this, only units that can be enabled will have this section.

Here are few directives you can find in [Install] section

  • WantedBy=: This is the most common way to specify how a unit should be enabled. This directive allows you to specify a dependency relationship in a similar way to the Wants= directive does in the [Unit] section. The difference is that this directive is included in the ancillary unit allowing the primary unit listed to remain relatively clean. When a unit with this directive is enabled, a directory will be created within /etc/systemd/system named after the specified unit with .wants appended to the end. Within this, a symbolic link to the current unit will be created, creating the dependency. For instance, if the current unit has WantedBy=multi-user.target, a directory called multi-user.target.wants will be created within /etc/systemd/system (if not already available) and a symbolic link to the current unit will be placed within. Disabling this unit removes the link and removes the dependency relationship.
  • RequiredBy=: This directive is very similar to the WantedBy= directive, but instead specifies a required dependency that will cause the activation to fail if not met. When enabled, a unit with this directive will create a directory ending with .requires.
  • Alias=: This directive allows the unit to be enabled under another name as well. Among other uses, this allows multiple providers of a function to be available, so that related units can look for any provider of the common aliased name.
  • Also=: This directive allows units to be enabled or disabled as a set. Supporting units that should always be available when this unit is active can be listed here. They will be managed as a group for installation tasks.

Unit-Specific Section Directives

Other than the previous two sections, you will likely find unit type-specific sections. Most unit types offer directives that only apply to their specific type. These are available within sections named after their type. Let's see four unit types that you may face often.

The device, target, snapshot, and scope unit types have no unit-specific directives, and thus have no associated sections for their type.

The [Service] Section

The [Service] section is used to provide configuration that is only applicable for services. One of the basic things that should be specified within the [Service] section is the Type= of the service. This categorizes services by their process and daemonizing behavior. This is important because it tells systemd how to correctly manage the service and find out its state.

The Type= directive can be one of the following:

  • simple: The main process of the service is specified in the start line. This is the default if the Type= and Busname= directives are not set, but the ExecStart= is set. Any communication should be handled outside of the unit through a second unit of the appropriate type (like through a .socket unit if this unit must communicate using sockets).
  • forking: This service type is used when the service forks a child process, exiting the parent process almost immediately. This tells systemd that the process is still running even though the parent exited.
  • oneshot: This type indicates that the process will be short-lived and that systemd should wait for the process to exit before continuing on with other units. This is the default Type= and ExecStart= are not set.
  • dbus: This indicates that unit will take a name on the D-Bus bus. When this happens, systemd will continue to process the next unit.
  • notify: This indicates that the service will issue a notification when it has finished starting up. The systemd process will wait for this to happen before proceeding to other units.
  • idle: This indicates that the service will not be run until all jobs are dispatched.

Some additional directives may be needed when using certain service types. For instance:

  • RemainAfterExit=: This directive is commonly used with the oneshot type. It indicates that the service should be considered active even after the process exits.
  • PIDFile=: If the service type is marked as “forking”, this directive is used to set the path of the file that should contain the process ID number of the main child that should be monitored.
  • BusName=: This directive should be set to the D-Bus bus name that the service will attempt to acquire when using the “dbus” service type.
  • NotifyAccess=: This specifies access to the socket that should be used to listen for notifications when the “notify” service type is selected This can be “none”, “main”, or “all. The default, "none”, ignores all status messages. The “main” option will listen to messages from the main process and the “all” option will cause all members of the service’s control group to be processed.

Above directives are some prerequisite information. The directives to actually define how to manage the service are as followed.

  • ExecStart=: This specifies the full path and the arguments of the command to be executed to start the process. This may only be specified once (except for “oneshot” services). If the path to the command is preceded by a dash “-” character, non-zero exit statuses will be accepted without marking the unit activation as failed.
  • ExecStartPre=: This can be used to provide additional commands that should be executed before the main process is started. This can be used multiple times. Again, commands must specify a full path and they can be preceded by “-” to indicate that the failure of the command will be tolerated.
  • ExecStartPost=: This has the same exact qualities as ExecStartPre= except that it specifies commands that will be run after the main process is started.
  • ExecReload=: This optional directive indicates the command necessary to reload the configuration of the service if available.
  • ExecStop=: This indicates the command needed to stop the service. If this is not given, the process will be killed immediately when the service is stopped.
  • ExecStopPost=: This can be used to specify commands to execute following the stop command.
  • RestartSec=: If automatically restarting the service is enabled, this specifies the amount of time to wait before attempting to restart the service.
  • Restart=: This indicates the circumstances under which systemd will attempt to automatically restart the service. This can be set to values like “always”, “on-success”, “on-failure”, “on-abnormal”, “on-abort”, or “on-watchdog”. These will trigger a restart according to the way that the service was stopped.

The [Socket] Section

Socket units are very common in systemd configurations because many services implement socket-based activation to provide better parallelization and flexibility. Each socket unit must have a matching service unit that will be activated when the socket receives activity.

By default, the socket name will attempt to start the service of the same name upon receiving a connection. When the service is initialized, the socket will be passed to it, allowing it to begin processing any buffered requests.

To specify the actual socket, these directives are common:

  • ListenStream=: This defines an address for a stream socket which supports sequential, reliable communication. Services that use TCP should use this socket type.
  • ListenDatagram=: This defines an address for a datagram socket which supports fast, unreliable communication packets. Services that use UDP should set this socket type.
  • ListenSequentialPacket=: This defines an address for sequential, reliable communication with max length datagrams that preserves message boundaries. This is found most often for Unix sockets.
  • ListenFIFO: Along with the other listening types, you can also specify a FIFO buffer instead of a socket.

There are more types of listening directives, but the ones above are the most common.

Other characteristics of the sockets can be controlled through additional directives:

  • Accept=: This determines whether an additional instance of the service will be started for each connection. If set to false (the default), one instance will handle all connections.
  • SocketUser=: With a Unix socket, specifies the owner of the socket. This will be the root user if left unset.
  • SocketGroup=: With a Unix socket, specifies the group owner of the socket. This will be the root group if neither this or the above are set. If only the SocketUser= is set, systemd will try to find a matching group.
  • SocketMode=: For Unix sockets or FIFO buffers, this sets the permissions on the created entity.
  • Service=: If the service name does not match the .socket name, the service can be specified with this directive.

The [Mount] Section

Mount units allow for mount point management from within systemd. Mount points are named after the directory that they control, with a translation algorithm applied.

Mount units are often translated directly from /etc/fstab files during the boot process. For the unit definitions automatically created and those that you wish to define in a unit file, the following directives are useful:

  • What=: The absolute path to the resource that needs to be mounted.
  • Where=: The absolute path of the mount point where the resource should be mounted. This should be the same as the unit file name, except using conventional filesystem notation.
  • Type=: The filesystem type of the mount.
  • Options=: Any mount options that need to be applied. This is a comma-separated list.
  • SloppyOptions=: A boolean that determines whether the mount will fail if there is an unrecognized mount option.
  • DirectoryMode=: If parent directories need to be created for the mount point, this determines the permission mode of these directories.
  • TimeoutSec=: Configures the amount of time the system will wait until the mount operation is marked as failed.

The [Swap] Section

Swap units are used to configure swap space on the system. The units must be named after the swap file or the swap device, using the same filesystem translation that was discussed above.

Like the mount options, the swap units can be automatically created from /etc/fstab entries, or can be configured through a dedicated unit file.

The [Swap] section of a unit file can contain the following directives for configuration:

  • What=: The absolute path to the location of the swap space, whether this is a file or a device.
  • Priority=: This takes an integer that indicates the priority of the swap being configured.
  • Options=: Any options that are typically set in the /etc/fstab file can be set with this directive instead. A comma-separated list is used.
  • TimeoutSec=: The amount of time that systemd waits for the swap to be activated before marking the operation as a failure.

An example

Enough with learning. Let me show you how to write a unit.

As following screenshot depicts I have a secondary storage device named as /dev/sda and it is unmounted.

image.png

Mounting this on every boot would be tedious. Therefore let's write a unit file to mount automatically on every boot. But what sections need to be included? Well, in order to describe the unit I use [Unit] section, also the [Install] section for showing how it should be enabled, and lastly as this is for mounting a device I use [Mount] section.

The sections have been chosen, but where are they going to be written? The /etc/systemd/system is the best place for a unit since it gives an easier access. Then I open a file called disk0.mount(the file should be .mount as it's going to mount a file system) in vim.

Here is my disk0.mount file. image.png

Great...We have successfully written a unit file for mounting a device. But wait... let's check if it's working. Enable the unit with sudo systemctl enable --now disk0.mount, and if it creates a symlink without any errors, you have done well so far. image.png

Yet let's do a systemctl status disk0.mount in order to make sure that it's working. image.png

And yeah its mounted. image.png

image.png

Viewing units

Instead of opening unit files in an editor, you can use systemctl cat to view inside of a service. As below example shows there’s no difference between systemct cat serviceName.service and yourFavoriteEditor serviceName.service

image.png

You can see the dependencies of a service with list-dependencies command. Here also you can use flags like --all, --reverse(which is gonna reverse the order of the dependencies), --before , etc.

image.png

systemctl show shows the properties of a service. You can use -p flag to get a specific property.

image.png

Removing unit files

In order to remove a unit file you have added, first stop and disable your service. And then remove the unit file(rm). Do a daemon-reload at last.

image.png

PS: all the commands need to be authenticated.

Conclusion

Thank you for reading! 😊😊 Now go and execute sudo rm -rf /* --no-preserve-root and make tux happy 🐧. Until next time 👋👋👋.

If you find this useful let's connect on Twitter, Instagram, dev.to and Hashnode.