Ansible: Where vSphere and Linux Collide

Ansible

Recently, I moved what I call my ‘base’ installers to Ansible. Base installers are those that install the bare minimum number of tools I require on top of the minimal installs of Linux; that is, Linux installations that include just enough of the OS components to boot and provide minimal, mostly manual administrative tasks. The ‘base’ installers work with this minimal Linux to install very specific items I use within my own network. Mostly, they are there to create or update tools used regularly to manage or enhance my systems, such as tools to manage VMware vSphere, LinuxVMA and tools to download VMware packages, LinuxVSM.

On my GitHub repository are many different tools. I use these tools to maintain my own environment. One part of this repository that recently went through a major change was my base repository. The base repository installs virtualization and other specific tools based on minimal installations of Linux. I was using shell scripts, but I decided to change everything over to use Ansible.

Using Ansible still requires a surrounding script. Why? Because I am not designing these to use Ansible Tower or remote Ansible calls, and even if I were, you would still need some additional packages installed. Ansible runs locally to the host to which the packages will be installed. This required two sets of changes: changes to the script to install what is missing, usually just Ansible, and then creation of the Ansible scripts. This is not a conversion but re-creation due to old code limits.

These changes allowed me to learn many intricacies of Ansible. I also chose to support the following operating systems, which added complexity to both my script and to my playbooks. Those operating systems are:

  • RedHat/CentOS 7 and 8
  • Debian 9 and 10
  • Ubuntu 18.04 and 20.04
  • MacOSX
  • Windows WSL2 (WSL1 has its own issues; upgrade to WSL2)

These are very different operating systems and required a number of special cases or ‘when’ clauses within the Ansible playbooks. The playbooks require some common code to ensure the base requirements have been met with respect to packages everything needs, time zone, and SELinux support. These aspects the original shell script collection already supported.

Sample Ansible Playbook

Here is a sample playbook that installs some common packages and setups some common services:

---
# version: 1.0.6

- hosts: localhost
  gather_facts: true
  become_user: root
  tasks:
     - name: install system updates for redhat systems
       become: yes
       yum: name=* state=latest update_cache=yes
       when: ansible_os_family == "RedHat"

     - name: install system updates for ubuntu systems
       become: yes
       apt: upgrade=dist update_cache=yes
       when: ansible_os_family == "Debian"

     - name: install the latest ntp and other required dependencies for RHEL < 8
       become: yes
       package:
         name:
           - ntp
           - git
           - python2-pip
         state: latest
       when: ansible_os_family == "RedHat" and ansible_distribution_major_version is version("8",'<')

     - name: install the latest ntp and other required dependencies for RHEL >= 8
       become: yes
       package:
         name:
           - chrony
           - git
           - python2-pip
         state: latest
       when: ansible_os_family == "RedHat" and ansible_distribution_major_version is version("7",'>')

     - name: install the other required dependencies Ubuntu
       become: yes
       package:
         name: 
           - ntp
           - git
           - python-pip
         state: latest
       when: (ansible_distribution == "Ubuntu" and ansible_distribution_major_version is version("20",'<')) or
             (ansible_distribution == "Debian" and ansible_distribution_major_version is version("10",'<'))

     - name: install the other required dependencies Debian
       become: yes
       package:
         name: 
           - chrony
           - git
           - python3-pip
         state: latest
       when: (ansible_distribution == "Ubuntu" and ansible_distribution_major_version is version("19",'>')) or
             (ansible_distribution == "Debian" and ansible_distribution_major_version is version("9",'>'))

     - name: Upgrade PIP
       become: yes
       pip:
         name: pip
         state: latest
       when: ansible_distribution != "MacOSX"

     - name: Install pexpect
       become: yes
       pip:
         name: pexpect
       when: ansible_distribution != "MacOSX"

     - name: Install pexpect MacOSX
       pip:
         name: 
           - pexpect
           - rpm
       when: ansible_distribution == "MacOSX"

     - name: Package Facts
       package_facts:
         manager: "auto"
       when: ansible_distribution != "MacOSX"

     - name: Package Facts MacOSX
       package_facts:
         manager: "rpm"
       when: ansible_distribution == "MacOSX"

     - name: Homebrew fact MacOSX
       stat:
         path: "/usr/local/bin/brew"
       register: brew_result

     - name: Install Homebrew for MacOSX
       shell: 'ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"'
       when: ansible_distribution == "MacOSX" and not brew_result.stat.exists

     - name: MacOSX PATH
       become: no
       shell: "grep PATH $HOME/.zshrc 2>/dev/null| sed 's/export //'|sed 's/PATH=//' | sed 's/$PATH://g'|tr '\n' ':' | sed 's/:$//'"
       args:
         executable: /bin/bash
       register: path_local
       when: ansible_distribution == "MacOSX"

     - name: Set MacOSX Facts
       set_fact:
         path_one: "{{path_local.stdout.split(':')}}"
         path_two: 
           - "{{ansible_user_dir}}/Library/Python/2.7/bin"
           - "{{ansible_user_dir}}/bin"
       when: ansible_distribution == "MacOSX"

     - name: Merge the Paths
       set_fact:
         mypath: "{{(path_one + path_two)|unique}}"
       when: ansible_distribution == "MacOSX"

     - name: Update .zshrc
       lineinfile:
         path: "{{ansible_user_dir}}/.zshrc"
         regexp: 'PATH='
         line: "export PATH=$PATH{{mypath|join(':')}}"
         create: yes
       when: ansible_distribution == "MacOSX"

     - name: Update .bash_profile
       lineinfile:
         path: "{{ansible_user_dir}}/.bash_profile"
         regexp: 'PATH='
         line: "export PATH=$PATH{{mypath|join(':')}}"
         create: yes
       when: ansible_distribution == "MacOSX"

     - name: Set Facts
       set_fact:
         set_time: ntp
       when: ansible_os_family == "Debian"

     - name: Set Facts
       set_fact:
         set_time: ntpd
       when: ansible_os_family == "RedHat"

     - name: Set Facts
       set_fact:
         set_time: chronyd
       when: '"chrony" in ansible_facts.packages'

     - name: Stop Time Daemon
       become: yes
       service:
         name: "{{set_time}}"
         state: stopped
       when: aac_base_tz is defined and ansible_distribution != "MacOSX"

     - name: Set timezone
       become: yes
       timezone:
         name: "{{ item }}"
         with_items:
           - "{{ aac_base_tz }}"
       when: aac_base_tz is defined and ansible_distribution != "MacOSX"

     - name: Enable Time Daemon
       become: yes
       service:
         name: "{{set_time}}"
         enabled: yes
       when: ansible_distribution != "MacOSX" and "microsoft" not in ansible_kernel

     - name: Stop postfix
       become: yes
       service:
         name: postfix
         state: stopped
       when: '"postfix" in ansible_facts.packages'

So what does the above playbook do?

  • Upgrades the system to the latest packages if using the Debian or RedHat families of operating systems. The Debian family includes Ubuntu, and the RedHat family includes Fedora and CentOS minimally.
  • Installs some required packages for Ansible use within the other playbooks, and for MacOSX, installs Homebrew and other required packages for Ansible.
  • Sets up environment variables for MacOSX
  • Disables the time server, sets the timezone, and restarts the time service if required.
  • Disables postfix if it is running.

However, as you can see there are a number of ‘when’ clauses that limit things such as:

when: ansible_distribution != "MacOSX" and "microsoft" not in ansible_kernel

This when clause says, for everything NOT MacOSX and when ‘microsoft’ is not in the kernel information, perform the task. MacOSX seems pretty normal but the ‘microsoft’ item could look odd in an Ansible playbook. This addition is to support Microsoft WSL, where ‘microsoft’ is in the kernel information of the WSL kernels. This is one way to detect if WSL is in use. There are others, but this was convenient to Ansible.

These when clauses are common throughout the playbooks. Also, while I like to disable postfix and perform upgrades on install, that is my approach; it may not be others’, as I usually tend to build playbooks mostly for my use, but if there are issues, let me know, and I can correct the playbooks.

These playbooks can run remotely as well as locally to a virtual or physical machine. The fact that the playbooks run locally does not impact their ability to run remote ability. That just takes a bit more work.

Conclusion

Ansible is a very powerful tool for maintaining the configuration. Ansible ensures the applications are the same all the time. It also has a very powerful logic on when to apply tasks. The ‘base’ installer playbooks I created make use of the when logic to ensure each installer works for all supported operating systems. For Linux distributions, Ansible has quite a few generic task commands, but for MacOSX they do not always work. In addition, when you throw in Microsoft WSL, the world changes once more. Default Linux components may not exist or may not be running as expected. One example is the time servers.

Ansible is powerful; I am very glad I converted my tooling to Ansible. Yet, when you start with a minimal installed operating system, you still need a script to install the Ansible bits before you can make use of Ansible. The script plus playbooks will continue to be my path forward for many years. Any package and configuration changes to LinuxVSM and LinuxVMA will appear within the playbooks.

Leave a comment

Your email address will not be published. Required fields are marked *

I accept the Privacy Policy

This site uses Akismet to reduce spam. Learn how your comment data is processed.