Cross-distribution packaging: Providing a systemd service

We use the upcoming opsi 4.1 to remove obsolete raiminigs that accumulated over time. This is also a good time to get the packaging up to date.

On systems running systemd we used some hacks with opsi 4.0 that allowed us to treat support for systemd as an optional feature. Now we want to make systemd first class, remove hacks and use the standards that the various distributions recommend.

I want to show how we end up packaging opsiconfd to work with systems using Debians .deb package format and those using .rpm. .deb files work the same across all major distributions and we serve Debian, Ubuntu and Univention Corporate Server (UCS) with the same setup. On the RPM-side we focus on the distributions provided by SUSE (SLES, openSUSE), Red Hat (RHEL) and the open source variant CentOS.

.deb

Debian packaging really shines in this regard! They make things extremely easy for a package maintainer and I can hardly image how this could be any easier.

We store opsiconfd.service in the debian folder of our package. This will lead to proper distribution of the file into the right places.

The control file needs a dependency to systemd if this it not yet there. We add dh-systemd (>= 1.5) to the Build-Depends. This allows us to call dh with the parameter --with systemd in our rules file.

With all this in place we removed all the hacks we had because we do not need them anymore. Great!

.rpm

This is where things get ugly. If you have some booze around: this is a good time to pour yourself one!

It is a miracle to me why the various .rpm based distributions are unable to provide a way that works for all distributions. At least they all seem to have settled to the use of macros but why can't they use the same name for all of their macros? So armed with knowledge from Fedoras packaging guide and openSUSEs systemd packaging guidelines we can get to work!

The BuildRequires in our .spec must list systemd there. If you are running SUSE you also want to have systemd-rpm-macros:

%if 0%{?suse_version} >= 1210
BuildRequires: systemd-rpm-macros
%endif

For runtime requirements we can make use of a macro: %{?systemd_requires}.

For our %pre section SUSE-based distributions have their own macro. Red Hat-style distributions lack this and you will only notice that when you are installing the package - a little trap left to the unobservant packager.

%if 0%{?suse_version}
%service_add_pre opsiconfd.service
%endif

We use the %install to adjust various differences between distributions. First we make sure to reference the correct service names in our unit - they are different not only between distributions but also between versions. It would be much nicer if the Debian and RPM world could use the same names for their services!

We use the same unit for all distributions and therefore simply alter debian/opsiconfd.service. We decided against having different unit files for different distributions because we do not want to have to maintain multiple files.

%if 0%{?suse_version} >= 1315 || 0%{?centos_version} >= 700 || 0%{?rhel_version} >= 700
    # Adjusting to the correct service names
    sed --in-place "s/=smbd.service/=smb.service/" "debian/opsiconfd.service" || true
    sed --in-place "s/=isc-dhcp-server.service/=dhcpd.service/" "debian/opsiconfd.service" || true
%endif

We use mkdir in our unit the create directories required during runtime. If you are doing the same you need to make sure to adjust the path to it!

MKDIR_PATH=$(which mkdir)
CHOWN_PATH=$(which chown)
sed --in-place "s!=-/bin/mkdir!=-$MKDIR_PATH!" "debian/opsiconfd.service" || true
sed --in-place "s!=-/bin/chown!=-$CHOWN_PATH!" "debian/opsiconfd.service" || true

In the end we install our service file through install -D -m 644 debian/opsiconfd.service %{buildroot}%{_unitdir}/opsiconfd.service. Noticed %{_unitdir}? This is indeed a placeholder that works on every distribution I came across!

Let's continue to %post. This one is easy once we are equipped with more distribution specific switches. We add the following:

%if 0%{?rhel_version} || 0%{?centos_version}
%systemd_post opsiconfd.service
%else
%service_add_post opsiconfd.service
%endif

This now repeats with slightly different macros for %preun and %postun. The good thing about this is that these sections are empty except for the systemd macros.

%preun
%if 0%{?rhel_version} || 0%{?centos_version}
%systemd_preun opsiconfd.service
%else
%service_del_preun opsiconfd.service
%endif

%postun
%if 0%{?rhel_version} || 0%{?centos_version}
%systemd_postun opsiconfd.service
%else
%service_del_postun opsiconfd.service
%endif

The last thing we need to do is to list our unit under %files. Again we can leverage %{_unitdir} for this:

%{_unitdir}/opsiconfd.service

Systemd units are not to be listed as config files. In addition SUSE distribution builds of the package may fail because of rpmlint complains about units in /var/... listed as config files.

What a ride! But at least we now have proper support for systemd in our package!