AnsibleLinux AutomationNetwork Automation

Ansible AWX – Taking a simple VMWare Center deployment even further with NetBox.

Is the end nigh for my IP Address spreadsheet?

In my last post I mentioned how much I disdained managing IP addresses. Well in this post I am going to show you how I am slowly fighting off the beast that is the IP address management spreadsheet (or whatever you want to call it).

The Knight in shining armour – NetBox.

If you have not heard of NetBox and have no idea what it is go check it out the link and YouTube video below.

NetBox – https://netbox.readthedocs.io/en/stable/

The NetBox documentation and this video with Hank Preston give you an overview that NetBox is basically a Source of Truth (SoT) tool. It is not going to be your only SoT, but it could/should be for IPAM and DCIM plus more.

Effecitvely I have chosen NetBox because I was introduced to it at a previous workplace and I fell in love with it. It is free, well documented and API is very helpful and powerful. It was also very easy to install and get running with as a Docker container. If you are interested in testing out NetBox I suggest to start using it as a Docker Container, you will up and running very quickly.

NetBox Docker – https://github.com/netbox-community/netbox-docker

Also for the first time ever, I have created a YouTube Video going over this blog post and running through an example of the playbook deploying. It’s quite rough and raw being my first time. Still learning a lot about recording and using OBS. But hope you enjoy it anyway or it is helpful!

Repo: https://github.com/danielbostock/awxplays

Pre-Requisites

  • Ansible 2.9+
  • AWX
  • Ansible VMWare and NetBox Galaxy Collections
  • NetBox
  • VMWare VCenter & ESXi server (evaluation licenses or licensed)
  • VCenter Templates

The source

Since NetBox will be my SoT, I then need to maintain consistency and ensure that all the relevant information is stored in NetBox. I could manually do all this work in NetBox if I wanted to, but why do that when NetBox has a huge amount of support for both Ansible and Python.

The purpose of this post is to explain what this playbook is doing in the context of using within AWX(or Tower). I will also attempt to explain in the best way I can, why I chose to do it this way and how it works in the way I have done it. I will start with the different types of vars used, and how/why I used conditionals with some of these vars. In the middle I will discuss the respective modules for NetBox and Vcenter. Finally I will finish it off by showing you how it all works and what the end result is.

Also as an additional help I have included some documentation in the playbook and vars file as to the purpose of all the vars. As with all things feel free to completely chop and change as you need.

Disclaimer: This playbook is written in a Lab context and everything I have done has been done to keep things simple for the sake of this post. Do not replicate this in a production environment, but please take what you want out of my playbook or simply learn from it and completely make your own.

Before I dive into the playbook and breaking it down, I will just quickly outline the purpose of this playbook and what it is trying to achieve.

  1. Create a VM entry in NetBox and add respective data:
    1. Assign Interface
    2. Assign IP address to the interface
  2. Deploy a VM to Vcenter with the following configuration:
    1. Use template to deploy the VM
    2. Apply IP address config as defined by NetBox tasks to the host
    3. Assign DNS config to the host as per environment configuration

The Playbook

---

- name: "Deploy VM Host & NetBox"
  connection: local
  hosts: localhost
  gather_facts: False
  collections:
    - netbox.netbox
  vars_files:
    - ./vars/labvars.yaml

  tasks:

##IP Prefixes used by Netbox

### Set the Corporate Envirionment vars
    - name: Set vars for environment Corporate
      set_fact:
          nb_prefix: "{{ corp_prefix }}"
          vcenter_folder: "{{ corp_name }}"
          host_net_vlan: "{{ corp_name }}"
          host_net_ipnetmask: "{{ corp_netmask }}"
          host_net_ipgwy: "{{ corp_ipgwy }}"
          host_net_dns: "{{ corp_dns }}"
          host_tag: "{{ corp_tag }}"
          host_name_prefix: "{{ corp_host_name_prefix }}"
      when: host_env == "Corporate"

### Set the Production Envirionment vars
    - name: Set vars for environment DMZ
      set_fact:
          nb_prefix: "{{ prod_prefix }}"
          vcenter_folder: "{{ prod_name }}"
          host_net_vlan: "{{ prod_name }}"
          host_net_ipnetmask: "{{ prod_netmask }}"
          host_net_ipgwy: "{{ prod_ipgwy }}"
          host_net_dns: "{{ prod_dns }}"
          host_tag: "{{ prod_tag }}"
          host_name_prefix: "{{ prod_host_name_prefix }}"
      when: host_env == "Production"

### Set the Build Envirionment vars
    - name: Set vars for environment Build
      set_fact:
          nb_prefix: "{{ build_prefix }}"
          vcenter_folder: "{{ build_name }}"
          host_net_vlan: "{{ build_name }}"
          host_net_ipnetmask: "{{ build_netmask }}"
          host_net_ipgwy: "{{ build_ipgwy }}"
          host_net_dns: "{{ build_dns }}"
          host_tag: "{{ build_tag }}"
          host_name_prefix: "{{ prod_host_name_prefix }}"
      when: host_env == "Build"

## NetBox Platforms

### CentOS Templates
    - name: Set NB Platform var dependent on template chosen
      set_fact:
          host_plat: CentOS8
      when: host_template == "centos8"

### Ubuntu Templates
    - name: Set NB Platform var dependent on template chosen
      set_fact:
          host_plat: Ubuntu19Server
      when: host_template == "ubuntu19_server"


### Windows Templates
    - name: Set NB Platform var dependent on template chosen
      set_fact:
          host_plat: WinSvr2k19
      when: host_template == "win2k19"
  
    - name: Set NB Platform var dependent on template chosen
      set_fact:
          host_plat: WinSvr2k16
      when: host_template == "win2k16"

    - name: NB Task 1 - Create a new NetBox VM entry
      netbox_virtual_machine:
        netbox_url: "{{ nb_url }}"
        validate_certs: False
        netbox_token: "{{ nb_token }}"
        data:
          name: "{{host_name_prefix}}{{ host_name }}"
          virtual_machine_role: "{{ host_role }}"
          site: "{{ host_site }}"
          tenant: "{{ ten }}"
          cluster: "{{ host_cluster }}"
          disk: "{{ host_disk }}"
          vcpus: "{{ host_vcpu }}"
          memory: "{{ host_mem }}"
          platform: "{{ host_plat }}"
          status: Active
        state: present
    
    - name: NB Task 2 - Create an Interface for NetBox VM entry
      netbox_vm_interface:
        netbox_url: "{{ nb_url }}"
        validate_certs: False
        netbox_token: "{{ nb_token }}"
        data:
          name: "{{ host_net_name }}"
          untagged_vlan: "{{ host_net_vlan }}"
          description: "{{ host_net_desc }}"
          virtual_machine: "{{ host_name_prefix }}{{ host_name }}"
          mode: Access
          enabled: yes
        state: present
      
    - name: NB Task 3 - Get new IP address, assign to VM interface, then create NB entry
      netbox_ip_address:
        netbox_url: "{{ nb_url }}"
        validate_certs: False
        netbox_token: "{{ nb_token }}"
        data:
          prefix: "{{ nb_prefix }}"
          dns_name: "{{ host_name_prefix }}{{ host_name }}.lab.local"
          description: "{{ host_desc }}"
          tenant: "{{ ten }}"
          assigned_object:
            name: "{{ host_net_name }}"
            virtual_machine: "{{ host_name_prefix }}{{ host_name }}"
        state: new
      register: ip


    - name: IP Task 1 - Copy new IP address to file
      local_action:
        module: copy
        content: "{{ ip.ip_address.address }}"
        dest: "{{ ipaddrtxt }}"
    
    - name: IP Task 2 - Replace /x netmask in gathered IP from NetBox
      replace:
        path: "{{ ipaddrtxt }}"
        regexp: \/\w+$
        replace: ''
      
    - name: IP Task 3 - Create var from the ippaddr.txt file
      set_fact:
        ipaddr: "{{ lookup('file', '{{ ipaddrtxt }}') }}"

    - name: VCENTER Task 1 - Create a virtual machine from a template
      community.vmware.vmware_guest:
        hostname: "{{ vcenter_host }}"
        username: "{{ vcenter_user }}"
        password: "{{ vcenter_pass }}"
        esxi_hostname: "{{ esxi_host }}"
        datacenter: "{{ vcenter_dc }}"
        validate_certs: no
        folder: "{{ vcenter_folder }}"
        name: "{{ host_name_prefix }}{{ host_name }}"
        state: poweredon
        template: "{{host_template}}"
        disk:
        - size_gb: "{{ host_disk }}"
          type: "{{ host_disktype }}"
          datastore: "{{ host_dstore }}"
        hardware:
          memory_mb: "{{ host_mem }}"
          num_cpus: "{{ host_vcpu }}"
          num_cpu_cores_per_socket: "{{ host_cores }}"
          scsi: paravirtual
          memory_reservation_lock: True
          hotadd_cpu: True
          hotremove_cpu: True
          hotadd_memory: True
          version: 14 # Hardware version of virtual machine
        networks:
        - name: "{{ host_net_vlan }}"
          type: static
          device_type: vmxnet3
          ip: "{{ ipaddr }}"
          netmask: "{{ host_net_netmask }}"
          gateway: "{{ host_net_ipgwy }}"
          dns_servers: "{{ host_net_dns }}"
        wait_for_ip_address: yes
        wait_for_ip_address_timeout: 300
      delegate_to: localhost
      register: deploy

Playbook Tasks

As per the outline above, there quite a few tasks that need to be organised and completed. Therefore it is helpful to break the playbooks tasks down into categories. In this playbook, I can break the categories down to the following 3 category groupings.

  1. Var/Conditional Tasks
  2. NetBox tasks
  3. Vcenter tasks

Var/Conditional Tasks

I want to be clear that set_fact is not technically an Ansible variable. It surprisingly is a… Fact… However it is helpful to still consider a fact as a var, especially in how I am using it in this playbook. But there are caveats, and like all things if you should know the rules before you break them. So go spend 15-20mins learning about vars and set_facts – you won’t regret it.

In summary the set_facts tasks check the string that is stored as the host_env & host_template var and will then proceed to make the series of vars (facts) if it matches a specific string, in this case Build, Corporate or Production. As you will note all the environment set_facts use vars, they use vars located within the vars_file.

By shifting the vars used here to a vars file, I can re-use these vars which are not unique to my lab over and over again. I don’t need to come back and recreate them per playbook.

I can also encrypt them when and if they do contain sensitive data. I suggest you make a new vars file for passwords then encrypt it with ansible-vault and add that to the vars_files: that are imported at the start of the playbook. For the sake of simplicity with this post I have not done this here and the var for the passwords are located in the repo vars file but it is empty. If you chose to copy this playbook please do the above suggested series of actions to ensure your passwords are safe!

If I group these series of set_facts tasks, these would be the groupings.

  • Grouping 1 – Network Environments
  • Grouping 2 – NetBox Platforms

Network Environments

Network environments effectively represent different lab networks. As explained earlier I have at this moment only three environments. Naturally I want unique vars for each environment.

So in summary when the end user selects the environment “Corporate”, they will have all the following variables assigned to it.

          nb_prefix: "{{ corp_prefix }}"
          vcenter_folder: "{{ corp_name }}"
          host_net_vlan: "{{ corp_name }}"
          host_net_ipnetmask: "{{ corp_netmask }}"
          host_net_ipgwy: "{{ corp_ipgwy }}"
          host_net_dns: "{{ corp_dns }}"
          host_tag: "{{ corp_tag }}"
          host_name_prefix: "{{ corp_host_name_prefix }}"

Since all the other conditionals in the set_facts tasks won’t be matched they will be ignored and these aforementioned vars will be used throughout the rest of the playbook.

NetBox Platforms

NetBox platforms are a very broad construct or way of organising hardware devices and assigning an operating system to them. It allows for good organising of all your NetBox devices by their respective operating system. Further to that, NetBox integrates with Napalm and this platform ID informs NetBox which drivers to load if you use Napalm that is.

For the sake of consistency I am keeping it part of my playbook. I am using the host_template var result as the conditional for this one. I could have asked instead that the the user “Specify VM OS” in the survey. But that is actually an unnecessary question in my scenario here, even if I had more versions of the same template, it’s still the same OS. Instead of being lazy by asking this question, I could modify my conditonal statement(s) to handle these slight changes in templates.

As much as I think it is good to present lots of options, if I am just asking them because I want a nicer named var (or don’t want to make more conditions) then I am wasting the users time and just being lazy. I like going to a restaurant and having 8 options as opposed to 100.

In this playbook, when the user chooses centos8 for example, it means they are on a CentOS8 platform. If I had another option there centos8_gui, then it would still be a CentOS8 platform. My conditional when: statement would be more open and use a wildcard instead to ensure I find centos8_gui for example.

when: host_template is match(“centos8.*”)

However I must stress this is not a broad stroke, there are unique cases here where you would have two platforms that are almost the same but for unique reasons to you and your business they are not the same. That is up to you decide and determine.

So whilst this is a simple conditional it actually is only simple because of decisions I made and this is an important lesson to remember and consider whenever writing any code even if it is a simple playbook.

NetBox Tasks

The 3 tasks for NetBox, and they are quite self explanatory. The ordering is actually what is important. For me I found this ordering the cleanest order of deploying the changes.

By the way if you didn’t care for NetBox storing information of your VM’s through NetBox and simply wanted IP addressing, you could drop the first two tasks and it will all work fine you will just need to remove the following task lines from the NB Task 3.

assigned_object:
   name: "{{ host_net_name }}"
   virtual_machine: "{{ host_name_prefix }}{{ host_name }}"

NetBox Tasks Structure

  1. Create VM entry
    • Create Interface
      • Assign IP address to interface

If you think about the following tasks as having some form of hierachial structure then that is what it kind of looks like. However you can create an IP address for example without either of the two but then attach it to the VM and the interface. You can also have a VM without an IP or an interface attached to it. However I have found it helpful to think of this structure for the purpose of my playbook writing. You choose what you prefer and let me know why you think your way of doing it is better!

IP Tasks

This series of 3 small tasks is almost easy to quickly go past but actually it gave me a bit of grief initially in writing the playbook. NetBox output is 10.xxx.xxx.xxx/xx whereas Vcenter when entering the ip address does not want the netmask included. It wants the full subnet decimal numbering included as a separate parameter. Which I have provided by using a conditional var (the network environment variable).

I therefore made a small series of tasks which corrects this. The file is intended to be constantly overwritten. This file should not at all be used long term for recording of an IP address that is the purpose of NetBox. Potentially there is a cleaner way to do what I just did here, and I will learn that in the future, however if you know instantly what I could have done better please feel free to share.

Vcenter Tasks

There are some slight changes here, I have started using the VMWare galaxy collection. You can find this helpful collection here .

I have also modified some of the var names as well, but the most significant change is the addition of network configuration. I have added the following parameters into the module that I want to configure based on NetBox and also the vars_file.

        networks:
        - name: "{{ host_net_vlan }}"
          type: static
          device_type: vmxnet3
          ip: "{{ ipaddr }}"
          netmask: "{{ host_net_netmask }}"
          gateway: "{{ host_net_ipgwy }}"
          dns_servers: "{{ host_net_dns }}"
        wait_for_ip_address: yes
        wait_for_ip_address_timeout: 300

I have left configured a few parameters. The reason for this in my environment and this playbook for AWX, I don’t expect them to be needed to be changed. The wait_for_ip_address command is important, and equally is the timeout parameter. The former is important because with a Linux host Vcenter requires Perl to be installed, and Windows requires sysprep (only with certain version).

Vcenter then does some cool direct interactions with the VM using vmware tools to make configuration changes. Ansible will sit here waiting for these changes to be applied and the server to be rebooted. I set the timer to 300 seconds because in my environment it should be deployed by then, however modify this respective to your environment.

Up until now I haven’t really talked about the potential of what happens when there is an error. Ansible is pretty good at catching errors and ending the script. However there are some modules where this is very hard to catch. If network configuration fails to deploy, the module will still be marked as changed and the playbook from AWX perspective and Ansible perspective will be successful. How do I catch this? Well I will explain this in the next post.

Playbook In Action

Conclusion

How many hours do you think over all the years have you spent in getting the right IP addresses (or providing them to others) and then maintaining an accurate spreadsheet? Well this is just the beginning of removing the need for doing this. I am not saying that overnight you can just get rid of your spreadsheet, but this is the beginning.

Having this kind of playbook integrated into AWX

The next post I will discuss how I will handle some error checking within the playbook. Ensuring that the playbook can advise of errors or even handle errors in some way and correct them is essential if I want to move this from a helpful script to a production tool I need to ensure I can trust this playbook to do what it needs to without my intervention or constant maintenance.

Please feel free to leave me any comments of advice, fact correction or links to helpful articles, I don’t profess to be an Ansible or AWX guru and I appreciate all your comments.

Leave a Reply

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