Programster's Blog

Tutorials focusing on Linux, programming, and open-source

Ansible - Run A Local Script On Remote Server

Let's imagine you have a script to set up your servers from scratch. In this case, let's call it setup.sh and for now, it will just be a BASH script that updates a debian/ubuntu server and installs git.

This will work with any type of script, it doesn't have to be bash

This tutorial uses the script core module, which was intoroduced in Ansible 1.5 so you should almost definitely have it.

Steps

Create the Setup Script

Lets create the script on our Ansible server because it needs to be local.

#!/bin/bash
sudo apt update
sudo apt-get dist-upgrade -y
sudo apt-get install git -y

For this tutorial I have put it in /home/stuart/scripts/setup.sh.

Update The Hosts File

If your starting out, then you need to create a group in your hosts file that we will run the setup script on. For this tutorial, my group is my-servers.

Create the Playbook

Now we need to create the ansible playbook that will transfer that script to the remote server(s) and execute it. For this tutorial, I am calling it setup.yml.

- hosts: my-servers
  sudo: true
  tasks:
    - script: /home/stuart/scripts/setup.sh

Execute!

Now run the following command to execute your script. You may or may not need to swap out the $USER variable.

ansible-playbook setup.yml --user=$USER

The user you connect with will need passwordless sudo privileges.

Execute only once

If you want to ensure that your script only ever gets executed once, then you can simply tweak your playbook to be like this:

- hosts: my-servers
  sudo: true
  tasks:
    - script: /home/stuart/scripts/setup.sh --creates /home/stuart/installed-git.txt

You will see that we added --creates which specifies the location of a file to check and if it exists, our script will not be executed. It does not implicitly create a blank file at that location, or pipe all the output to that location. Hence we need to update our local script to create that file so that subsequent calls will not be executed.

#!/bin/bash
sudo apt update
sudo apt-get dist-upgrade -y
sudo apt-get install git -y

# Create our file to check for in future
echo "installed git!" >> /home/stuart/installed-git.txt

Removes Option

If we created an uninstall script, we would want that to also only executed once, and only if we have already executed the installation script. Thus we would want to check if the installed-git.txt file exists, and only run if it doesn't. Luckily the --removes option provides this functionality.

For this, our uninstall.yml playbook would be like so:

- hosts: my-servers
  sudo: true
  tasks:
    - script: /home/stuart/scripts/uninstall-git.sh --removes /home/stuart/installed-git.txt

Our local uninstall-git.sh script would be like so:

#!/bin/bash
sudo apt-get remove git -y

# Remove our file
rm /home/stuart/installed-git.txt

Just like the --creates option, it does not implicitly remove the file for you, it only make ansible execute the script the file already exists. Your script has to remove the file.

Get Output Of Script

If the purpose of your script is to do something like tell you if the server needs rebooting or not, then you probably want to be able to see the output of the script. For example, here is a script that will check if the server's /var/run/reboot-required file is set, indicating that the server needs rebooting.

#!/bin/bash
if [ -f /var/run/reboot-required ]; then
    echo 'reboot required'
else
    echo "I'm good."
fi

We need to be able to see if the script outputted reboot required or just I'm good. To do this we use the register keyword in our playbook and then use debug like so:

- hosts: apt-servers
  tasks:
    - name: Check if reboot is required
      script: /home/ansible/scripts/check.reboot.required.sh
      register: output

    - debug: var=output.stdout_lines

This should result in you being able to see the ouptut in the debug section like so:

TASK [debug] ***********************************
ok: [server1.programster.org] => {
    "output.stdout_lines": [
        "reboot required"
    ]
}
ok: [server2.programster.org] => {
    "output.stdout_lines": [
        "reboot required"
    ]
}
ok: [server3.programster.org] => {
    "output.stdout_lines": [
        "I'm good."
    ]
}

Coloring Output

I wanted to take this further and color the I'm good responses in green, and the reboot required responses in red. The script would look as follows:

#!/bin/bash
if [ -f /var/run/reboot-required ]; then
    echo -e "\e[31mreboot required"
else
    echo -e "\e[32mI'm good."
fi

Unfortunately, rather than getting the colored responses, you get the code back like so:

ok: [server3.programster.org] => {
    "output.stdout_lines": [
        "\u001b[31mreboot required"
    ]
}

There are two ways to get around this. You can go with a full solution of using a callback plugin (which I have yet to do) or use the following line in your BASH script before you call the playbook....

export ANSIBLE_STDOUT_CALLBACK=debug

... and use at least one -v. E.g.

export ANSIBLE_STDOUT_CALLBACK=debug
ansible-playbook /home/ansible/playbooks/check.reboot.required.yaml -v

The downside of this is that there is just so much output, that I found it easier just to skim over the previous setup without colored formatting.

References