Sometimes You just want to run a task once, the first time a system boots. There are a bunch of ways you could do this, but I was working on trying to solve a specific use case for a customer. In this case, we’d like to run some tasks on the first boot of a system after its been through kickstart. Of course there are probably a dozen ways to solve this, but this one seemed elegant to me. So after I figured it out, I decided to write it up here.

After a Kickstart install of RHEL, you can add %post commands which will perform a few tasks at the very end of installation, but still from within the anaconda install environment. This is a very limited shell, and in this case, we were trying to setup a network interface for production use. I’m not going to go into the specifics on how to setup a network interface, but you could imagine a script which uses nmcli to define things like IP, Netmask, Gateway, even bond config. None of this is fully functional from within anaconda. It is, however functional in the post-installed system after its first boot.

There are plenty of ways to actually run something once, but in order to run it reliably after a kickstart, on the very next boot, you might want to leverage something like the systems Init system. In the old days, you could throw a script into /etc/init.d and delete it after a successful run. You could have used rc.local to run a task, and then clean it up afterward. In a systemd world though, you’d probably want to do it with a unit file. That’s what I’m going to do here. This isn’t rocket science, but it did take me a little research to get right.

There are a few pieces here. First you’d want to make a unit file, place it, and enable it. Then you’s want to point that unit file at a script that does whatever you want done. Then you’d want that script to clean up after itself. For added cleanliness you’d probably want the unit file to not execute if the script doesn’t exist.

The unit file

First you’ll create the unit file, and place it in /etc/systemd/system (on the installed filesystem, on the anaconnda build ramfs). I named it firststart.service. That file should look something like this:

[Unit]
Description=My First Boot Script
ConditionFirstBoot=yes

[Service]
Type=oneshot
ExecStart=/path/to/yourscript

[Install]
WantedBy=multi-user.target

Then you’ll need to enable it in systemd, This will need to be executed in a chrooted environment in your new system, which is where %post should get you.

systemctl enable firststart.service

The script file

In my case, I just threw the script in /tmp, as this was a test. Make sure it’s a proper script, and that it’s executable, and has the proper SELinux context if youre running with SELinux enforcing (You _are_ running with SELinux enforcing, right?), the context is bin_t.

If you’re trying to be tidy, you can also make the script clean itself up after a successful run, by adding rm $0 inside of whatever conditionals you can cook up that verify that it actually ran.

My script isn’t that fancy, as its just a test.

#!/usr/bin/bash
echo "I am attempting to run `date`"
echo "I am running now `date`" > /tmp/i_ran
rm $0

Test

If you did everything right, then after the install is completed, and the sytem reboots, systemd should start up your script ONLY on the first boot, and execute whatever you asked it to execute.

Close

Can you do this other ways? Sure! Maybe you can use cloud-init, maybe you can just cron, maybe you can use at. As with anything in Linux, there are a few ways to skin that cat. The one feels like a winner to me though. If you do it right, it’ll integrate right into Systemd, which is already good at starting things on demand. It will also generate a nice Journal entry if it completes or not, and if you include debugging info like I did in my dirty little script, you should get that logged in the journal.

I hope you’ve found this useful, thanks for reading!