Unifying custom scripts system-wide with rpm on Red Hat/CentOS

Objective

Our goal is to build rpm packages with custom content, unifying scripts across any number of systems, including versioning, deployment and undeployment.

Operating System and Software Versions

  • Operating system: Red Hat Enterprise Linux 7.5
  • Software: rpm-build 4.11.3+

Requirements

Privileged access to the system for install, normal access for build.

Difficulty

MEDIUM

Conventions

  • # – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
  • $ – given linux commands to be executed as a regular non-privileged user

Introduction

One of the core feature of any Linux system is that they are built for automation. If a task may need to be executed more than one time – even with some part of it changing on next run – a sysadmin is provided with countless tools to automate it, from simple shell scripts run by hand on demand (thus eliminating typo errors, or only save some keyboard hits) to complex scripted systems where tasks run from cron at a specified time, interacting with each other, working with the result of another script, maybe controlled by a central management system etc.

While this freedom and rich toolset indeed adds to productivity, there is a catch: as a sysadmin, you write a useful script on a system, which proves to be useful on another, so you copy the script over. On a third system the script is useful too, but with minor modification – maybe a new feature useful only in that system, reachable with a new parameter. Generalization in mind, you extend the script to provide the new feature, and complete the task it was written for as well. Now you have two versions of the script, the first is on the first two systems, the second in on the third system.

You have 1024 computers running in the datacenter, and 256 of them will need some of the functionality provided by that script. In time you will have 64 versions of the script all over, every version doing its job. On the next system deployment you need a feature you recall you coded at some version, but which? And on which systems are they?

On RPM based systems, such as Red Hat flavors, a sysadmin can take advantage of the package manager to create order in the custom content, including simple shell scripts that may not provide else but the tools the admin wrote for convenience.

In this tutorial we will build a custom rpm for Red Hat Enterprise Linux 7.5 containing two bash scripts, parselogs.sh and pullnews.sh to provide a way that all systems have the latest version of these scripts in the /usr/local/sbin directory, and thus on the path of any user who logs in to the system.



Distributions, major and minor versions

In general, the minor and major version of the build machine should be the same as the systems the package is to be deployed, as well as the distribution to ensure compatibility. If there are various versions of a given distribution, or even different distributions with many versions in your environment (oh, joy!), you should set up build machines for each. To cut the work short, you can just set up build environment for each distribution and each major version, and have them on the lowest minor version existing in your environment for the given major version. Of cause they don’t need to be physical machines, and only need to be running at build time, so you can use virtual machines or containers.

In this tutorial our work is much easier, we only deploy two scripts that have no dependencies at all (except bash), so we will build noarch packages which stand for “not architecture dependent”, we’ll also not specify the distribution the package is built for. This way we can install and upgrade them on any distribution that uses rpm, and to any version – we only need to ensure that the build machine’s rpm-build package is on the oldest version in the environment.

Setting up building environment

To build custom rpm packages, we need to install the rpm-build package:

# yum install rpm-build

From now on, we do not use root user, and for a good reason. Building packages does not require root privilege, and you don’t want to break your building machine.

Building the first version of the package

Let’s create the directory structure needed for building:

$ mkdir -p rpmbuild/SPECS

Our package is called admin-scripts, version 1.0. We create a specfile that specifies the metadata, contents and tasks performed by the package. This is a simple text file we can create with our favorite text editor, such as vi. The previously installed rpmbuild package will fill your empty specfile with template data if you use vi to create an empty one, but for this tutorial consider the specification below called admin-scripts-1.0.spec:



Name:           admin-scripts
Version:        1
Release:        0
Summary:        FooBar Inc. IT dept. admin scripts
Packager:       John Doe 
Group:          Application/Other
License:        GPL
URL:            www.foobar.com/admin-scripts
Source0:        %{name}-%{version}.tar.gz
BuildArch:      noarch

%description
Package installing latest version the admin scripts used by the IT dept.

%prep
%setup -q


%build

%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/usr/local/sbin
cp scripts/* $RPM_BUILD_ROOT/usr/local/sbin/

%clean
rm -rf $RPM_BUILD_ROOT

%files
%defattr(-,root,root,-)
%dir /usr/local/sbin
/usr/local/sbin/parselogs.sh
/usr/local/sbin/pullnews.sh

%doc

%changelog
* Wed Aug 1 2018 John Doe 
- release 1.0 - initial release

Place the specfile in the rpmbuild/SPEC directory we created earlier.

We need the sources referenced in the specfile – in this case the two shell scripts. Let’s create the directory for the sources (called as the package name appended with the main version):

$ mkdir -p rpmbuild/SOURCES/admin-scripts-1/scripts

And copy/move the scripts into it:

$ ls rpmbuild/SOURCES/admin-scripts-1/scripts/
parselogs.sh  pullnews.sh


As this tutorial is not about shell scripting, the contents of these scripts are irrelevant. As we will create a new version of the package, and the pullnews.sh is the script we will demonstrate with, it’s source in the first version is as below:

#!/bin/bash
echo "news pulled"
exit 0

Do not forget to add the appropriate rights to the files in the source – in our case, execution right:

chmod +x rpmbuild/SOURCES/admin-scripts-1/scripts/*.sh


Now we create a tar.gz archive from the source in the same directory:

cd rpmbuild/SOURCES/ && tar -czf admin-scripts-1.tar.gz admin-scripts-1

We are ready to build the package:

rpmbuild --bb rpmbuild/SPECS/admin-scripts-1.0.spec

We’ll get some output about the build, and if anything goes wrong, errors will be shown (for example, missing file or path). If all goes well, our new package will appear in the RPMS directory generated by default under the rpmbuild directory (sorted into subdirectories by architecture):

$ ls rpmbuild/RPMS/noarch/
admin-scripts-1-0.noarch.rpm

We have created a simple yet fully functional rpm package. We can query it for all the metadata we supplied earlier:

$ rpm -qpi rpmbuild/RPMS/noarch/admin-scripts-1-0.noarch.rpm 
Name        : admin-scripts
Version     : 1
Release     : 0
Architecture: noarch
Install Date: (not installed)
Group       : Application/Other
Size        : 78
License     : GPL
Signature   : (none)
Source RPM  : admin-scripts-1-0.src.rpm
Build Date  : 2018. aug.  1., Wed, 13.27.34 CEST
Build Host  : build01.foobar.com
Relocations : (not relocatable)
Packager    : John Doe 
URL         : www.foobar.com/admin-scripts
Summary     : FooBar Inc. IT dept. admin scripts
Description :
Package installing latest version the admin scripts used by the IT dept.


And of cause we can install it (with root privileges):

Installing custom scripts with rpm

Installing custom scripts with rpm


As we installed the scripts into a directory that is on every user’s $PATH, you can run them as any user in the system, from any directory:

$ pullnews.sh 
news pulled


The package can be distributed as it is, and can be pushed into repositories available to any number of systems. To do so is out of the scope of this tutorial – however, building another version of the package is certainly not.

Building another version of the package

Our package and the extremely useful scripts in it become popular in no time, considering they are reachable anywhere with a simple yum install admin-scripts within the environment. There will be soon many requests for some improvements – in this example, many votes come from happy users that the pullnews.sh should print another line on execution, this feature would save the whole company. We need to build another version of the package, as we don’t want to install another script, but a new version of it with the same name and path, as the sysadmins in our organization already rely on it heavily.

First we change the source of the pullnews.sh in the SOURCES to something even more complex:

#!/bin/bash
echo "news pulled"
echo "another line printed"
exit 0

We need to recreate the tar.gz with the new source content – we can use the same filename as the first time, as we don’t change version, only release (and so the Source0 reference will be still valid). Note that we delete the previous archive first:

cd rpmbuild/SOURCES/ && rm -f admin-scripts-1.tar.gz && tar -czf admin-scripts-1.tar.gz admin-scripts-1

Now we create another specfile with a higher release number:

cp rpmbuild/SPECS/admin-scripts-1.0.spec rpmbuild/SPECS/admin-scripts-1.1.spec

We don’t change much on the package itself, so we simply administrate the new version as shown below:

Name:           admin-scripts
Version:        1
Release:        1
Summary:        FooBar Inc. IT dept. admin scripts
Packager:       John Doe 
Group:          Application/Other
License:        GPL
URL:            www.foobar.com/admin-scripts
Source0:        %{name}-%{version}.tar.gz
BuildArch:      noarch

%description
Package installing latest version the admin scripts used by the IT dept.

%prep
%setup -q


%build

%install
rm -rf $RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/usr/local/sbin
cp scripts/* $RPM_BUILD_ROOT/usr/local/sbin/

%clean
rm -rf $RPM_BUILD_ROOT

%files
%defattr(-,root,root,-)
%dir /usr/local/sbin
/usr/local/sbin/parselogs.sh
/usr/local/sbin/pullnews.sh

%doc

%changelog
* Wed Aug 22 2018 John Doe 
- release 1.1 - pullnews.sh v1.1 prints another line
* Wed Aug 1 2018 John Doe 
- release 1.0 - initial release


All done, we can build another version of our package containing the updated script. Note that we reference the specfile with the higher version as the source of the build:

rpmbuild --bb rpmbuild/SPECS/admin-scripts-1.1.spec

If the build is successful, we now have two versions of the package under our RPMS directory:

ls rpmbuild/RPMS/noarch/
admin-scripts-1-0.noarch.rpm  admin-scripts-1-1.noarch.rpm

And now we can install the “advanced” script, or upgrade if it is already installed.

Upgrading custom scripts with rpm

Upgrading custom scripts with rpm

And our sysadmins can see that the feature request is landed in this version:

rpm -q --changelog admin-scripts
* Wed aug 22 2018 John Doe 
- release 1.1 - pullnews.sh v1.1 prints another line

* Wed aug 01 2018 John Doe 
- release 1.0 - initial release

Conclusion

We wrapped our custom content into versioned rpm packages. This means no older versions left scattered across systems, everything is in it’s place, on the version we installed or upgraded to. RPM gives the ability to replace old stuff needed only in previous versions, can add custom dependencies or provide some tools or services our other packages rely on. With effort, we can pack nearly any of our custom content into rpm packages, and distribute it across our environment, not only with ease, but with consistency.