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!
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.
- Create a VM entry in NetBox and add respective data:
- Assign Interface
- Assign IP address to the interface
- Deploy a VM to Vcenter with the following configuration:
- Use template to deploy the VM
- Apply IP address config as defined by NetBox tasks to the host
- 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.
- Var/Conditional Tasks
- NetBox tasks
- 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
- Create VM entry
- Create Interface
- Assign IP address to interface
- Create 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
You must be logged in to post a comment.
Recent Comments