Linux Systemd – Execute ExecStop When ExecStart Script Exits

kvm-virtualizationlinuxsystemd

I am a little confused with the behavior of systemd services. I have the above systemd service.

[Unit]
After=libvirtd.service

[Service]
Type=simple
Environment=VM_XML=xxxxxxx
ExecStartPre=/usr/bin/bash /usr/local/lib/common/createQcowImage.sh ${VM_XML}
ExecStart=/usr/bin/bash /usr/local/lib/common/createVM.sh ${VM_XML}
ExecStop=/usr/bin/bash /usr/local/lib/common/destroyVM.sh ${VM_XML}
Restart=always

[Install]
WantedBy=multi-user.target

When this unit starts inside the createVM.sh script it creates a VM and it monitors it state. In case the PID for the VM does not exist any more the script exits with return code 1. What I noticed is that when this happens the ExecStop is executed (I was manitoring the /var/log/messages and when I destroyed manually the VM with virsh destroy I sow the echo that I put for debugging inside the script executed from ExecStop to be printed in /var/log/messages). Is this default behaviour of systemd? To execute the ExecStop when unit exits (I also tried to exit with code 0 and it was same bahaviour). This is not what I really want to do because after I destroy a VM the ExecStop tries to destroy the same VM which bring a systemd unit failure. Is there any way to avoid that?

Best Answer

You need to have a good knowledge of all the options meaning if you want to create a systemd service configuration that behaves as expected.

I suggest you to start reading the documentation (you can use e.g. man systemd.service) about Type=, ExecStart=, ExecStop=. Maybe Type=exec, or Type=oneshot would be more appropriate for your scripts?

The behaviour that you described seems to me to be as expected indeed.

To handle the case that your VM was already stopped by its user, you need to test whether it still runs and avoid destroying it if not. Also don't forget that the ExecStop has to wait for the machine to be stopped and not just ask it to stop.

I attach my user service that I use to launch VirtualBox VM, maybe you can find some inspiration there.

[Unit]
Description=Virtual Machine as a headless service.
After=network.target virtualbox.target
Before=runlevel2.target shutdown.target

[Service]
Type=simple
Restart=no
Environment=VMNAME=my-vbox-vm
Environment=TIMEOUT=60
#ExecStartPre=VBoxManage snapshot $VMNAME take "before service start"
ExecStart=VBoxHeadless --startvm $VMNAME
# Sleep might be needed if there is some ExecStartPost later, to make sure that the VM was really powered up
#ExecStartPost=sleep 1
# sometimes it happens that the machine is paused from some reason
#ExecStartPost=bash -c "VBoxManage showvminfo $VMNAME | grep -E ^State:\\\\s+paused && VBoxManage controlvm $VMNAME resume || exit 0"
# If the VM uses the VirtualBox hdd encryption, so a line like below is needed to unlock the drive
#ExecStartPost=VBoxManage controlvm $VMNAME addencpassword $VMNAME "/home/user/VirtualBox VMs/my-vbxo-vm/my-vbox-vm.vmdk.pass"
# The 'savestate' is the only reliable way to turn off the machine.
# Optionally sending acpishutdown, can be considered, but better that needs to use the savestate as a fallback anyway,
# because the acpishutdown can be from various reasons ignored by the VM or delayed significantly.
#
# Alternatives for ExecStop
# - save state
#   (should be reliable but not really turning off the VM;
#    exit 0 necessary to handle the situation that VM was turned off already,
#    from inside)
#   ExecStop=bash -c "VBoxManage controlvm $VMNAME savestate || exit 0"
# - stop vm using acpi, with active waiting
#   (not reliable - if acpi signal ignored by VM, or shutdown takes too long,
#    systemd will just kill it after some timeout, (normally 90s);
#    note that the active waiting is necessary,
#    otherwise systemd might kill the process before shutdown is finished)
#   ExecStop=bash -c "VBoxManage controlvm $VMNAME acpipowerbutton && while (VBoxManage list runningvms | grep $VMNAME); do sleep 1; done"
# - stop vm using acpi or, after a timeout, with save state
#   (timeout let's say max 60s, to keep 30s for possible savestate;
#    exit 0 necessary in order not to have the service in error state when stopped already by acpi, or maybe even by user from inside of VM)
ExecStop=bash -c "VBoxManage controlvm $VMNAME acpipowerbutton && (let timeout=$TIMEOUT; while VBoxManage list runningvms | grep --silent $VMNAME && test $timeout -gt 0; do sleep 1; let timeout=$timeout-1; done) && VBoxManage list runningvms | grep --silent $VMNAME && VBoxManage controlvm $VMNAME savestate || exit 0"

[Install]
WantedBy=default.target
Related Topic