How to deploy Vmware VM’s using Ansible from Cloned Templates
QUICK OVERVIEW OF WHAT ANSIBLE IS..
Ansible is a radically simple IT automation engine that automates cloud provisioning, configuration management, application deployment, intra-service orchestration, and many other IT needs.
Designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.
It uses no agents and no additional custom security infrastructure, so it’s easy to deploy – and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allow you to describe your automation jobs in a way that approaches plain English.
On this page, we’ll give you a really quick overview so you can see things in context. For more detail, hop over to docs.ansible.com.
EFFICIENT ARCHITECTURE
Ansible works by connecting to your nodes and pushing out small programs, called “Ansible modules” to them. These programs are written to be resource models of the desired state of the system. Ansible then executes these modules (over SSH by default), and removes them when finished.
Your library of modules can reside on any machine, and there are no servers, daemons, or databases required. Typically you’ll work with your favorite terminal program, a text editor, and probably a version control system to keep track of changes to your content.
Example power on and off a vm, or connect or disconnect network, etc.
For the purposes of this post we are are going to dive into using “vmware_guest” module by way of using http api authentication session & cookies. There are many other python modules which you can search in the ansible documentation and or ansible-galaxy
https://docs.ansible.com/ansible/latest/collections/community/vmware/index.html
Now it definitely helps to be able to code in python or at least be able to read python code, however completely not necessary. Anyone with basic understanding of bash scripting can learn ansible. I could teach a newbie ansible in a couple days. Sharing is caring.
Anyone who says otherwise……don’t hire them.
Ansible Setup:
Pre-Module install Steps:
Requirements
The below requirements are needed on the host that executes this module.
Note: Now on centos its not available out of the box
Centos 7 PIP install:
Install PyVmomi:
It will look like…..
[root@nick roles]# pip install –upgrade pyvmomi
Collecting pyvmomi
Downloading https://files.pythonhosted.org/packages/ba/69/4e8bfd6b0aae49382e1ab9e3ce7de9ea6318eac007b3076e6006dbe5a7cd/pyvmomi-7.0.1.tar.gz (584kB)
100% |████████████████████████████████| 593kB 861kB/s
Cache entry deserialization failed, entry ignored
Collecting requests>=2.3.0 (from pyvmomi)
Downloading https://files.pythonhosted.org/packages/29/c1/24814557f1d22c56d50280771a17307e6bf87b70727d975fd6b2ce6b014a/requests-2.25.1-py2.py3-none-any.whl (61kB)
100% |████████████████████████████████| 61kB 3.5MB/s
Collecting six>=1.7.3 (from pyvmomi)
Downloading https://files.pythonhosted.org/packages/ee/ff/48bde5c0f013094d729fe4b0316ba2a24774b3ff1c52d924a8a4cb04078a/six-1.15.0-py2.py3-none-any.whl
Cache entry deserialization failed, entry ignored
Collecting certifi>=2017.4.17 (from requests>=2.3.0->pyvmomi)
Downloading https://files.pythonhosted.org/packages/5e/a0/5f06e1e1d463903cf0c0eebeb751791119ed7a4b3737fdc9a77f1cdfb51f/certifi-2020.12.5-py2.py3-none-any.whl (147kB)
100% |████████████████████████████████| 153kB 6.5MB/s
Cache entry deserialization failed, entry ignored
Collecting urllib3<1.27,>=1.21.1 (from requests>=2.3.0->pyvmomi)
Downloading https://files.pythonhosted.org/packages/f5/71/45d36a8df68f3ebb098d6861b2c017f3d094538c0fb98fa61d4dc43e69b9/urllib3-1.26.2-py2.py3-none-any.whl (136kB)
100% |████████████████████████████████| 143kB 6.9MB/s
Cache entry deserialization failed, entry ignored
Collecting idna<3,>=2.5 (from requests>=2.3.0->pyvmomi)
Downloading https://files.pythonhosted.org/packages/a2/38/928ddce2273eaa564f6f50de919327bf3a00f091b5baba8dfa9460f3a8a8/idna-2.10-py2.py3-none-any.whl (58kB)
100% |████████████████████████████████| 61kB 4.4MB/s
Cache entry deserialization failed, entry ignored
Collecting chardet<5,>=3.0.2 (from requests>=2.3.0->pyvmomi)
Downloading https://files.pythonhosted.org/packages/19/c7/fa589626997dd07bd87d9269342ccb74b1720384a4d739a1872bd84fbe68/chardet-4.0.0-py2.py3-none-any.whl (178kB)
100% |████████████████████████████████| 184kB 3.5MB/s
Installing collected packages: certifi, urllib3, idna, chardet, requests, six, pyvmomi
Found existing installation: certifi 2018.4.16
Uninstalling certifi-2018.4.16:
Successfully uninstalled certifi-2018.4.16
Found existing installation: urllib3 1.22
Uninstalling urllib3-1.22:
Successfully uninstalled urllib3-1.22
Found existing installation: idna 2.6
Uninstalling idna-2.6:
Successfully uninstalled idna-2.6
Found existing installation: chardet 3.0.4
Uninstalling chardet-3.0.4:
Successfully uninstalled chardet-3.0.4
Found existing installation: requests 2.18.4
Uninstalling requests-2.18.4:
Successfully uninstalled requests-2.18.4
Found existing installation: six 1.9.0
Uninstalling six-1.9.0:
Successfully uninstalled six-1.9.0
Running setup.py install for pyvmomi … done
Successfully installed certifi-2020.12.5 chardet-4.0.0 idna-2.10 pyvmomi-7.0.1 requests-2.25.1 six-1.15.0 urllib3-1.26.2
You are using pip version 10.0.1, however version 20.3.3 is available.
You should consider upgrading via the ‘pip install –upgrade pip’ command.
(You noticed this at the bottom)
A lot of the time you need to upgrade pip for the modules to install as python is always evolving at a fast pace
So run
[root@nick roles]# pip install –upgrade pip
Collecting pip
Downloading https://files.pythonhosted.org/packages/54/eb/4a3642e971f404d69d4f6fa3885559d67562801b99d7592487f1ecc4e017/pip-20.3.3-py2.py3-none-any.whl (1.5MB)
100% |████████████████████████████████| 1.5MB 799kB/s
Installing collected packages: pip
Found existing installation: pip 8.1.2
Uninstalling pip-8.1.2:
Successfully uninstalled pip-8.1.2
Successfully installed pip-10.0.1
You get the idea……
Vpshere – Pre-requistes for this to work:
You will need a vmware user who has api access permission for the following items. If the user you have setup in vcenter is unable to see these items. This module will fail. You do not need a user with full admin privileges, which is what a lot of documentation says online cryptically. I have tested this and confirmed that is not the case. Obivously, its way better to just give admin privileges to the user and trust the people you hire and use ansible vault to hide the credentials. Which we will get into later….
You can also check these parameters in your code by validating using assertions to see if they are all working with your user prior to moving on the next task.
– vSphere API configuration
– VM details
– vcenter_host
– cluster
– datacenter
– folder
– vm_disk_size
– vm_cpu_count
– vm_memory
– vm_vlan
– vm_vlan_name
– vm_dvswitch
– vm_datatstore
–vmware tools and or open_vm_tools must installed the clone template (super important)
Okay so now were on setting up the vmware_guest module using yaml code.
Setting vmware_guest module on ansible:
Now what I like to do is set everything up as a role in ansible to call in your playbooks, it keeps things cleaner and its much easier to find spacing mistakes in your code when writing in yaml. Lots of NBTo aid in checking for mistakes. But ultimately its experience. I’m a bit of both but I tend just pop a vi open and just write and much in there
Note: Now we do most of our work in this directory. Your primary yaml file is always called “main.yml” Your playbooks always look for this file when trying to call python modules.
Setting up the yaml:
1. First stage of the yaml is use the http login to the vcenter host and successfully authenticate and then grab those session cookies to carry out the next set of tasks which utilise the vmware_guest module.
– name: Login into vCenter and get cookies
delegate_to: localhost
uri:
url: https://{{ vcenter_host }}/rest/com/vmware/cis/session
force_basic_auth: yes
validate_certs: no
method: POST
user: ‘{{ vcenter_username }}’
password: ‘{{ vcenter_password }}’
register: login
2. Okay so this where we are now actually calling the vmware_guest module in yaml. You can see that the code has a lot of areas that are variablelised. These variable are passed in a couple of ways. You need to pass the defaults through the defaults directory we created earlier, and the second is host specific variables which will be under your host_vars directory under your inventory structure, which we will get into later.
Note: Now remember this is code to deploy from an existing cloned template you have sitting on datastore somewhere in your environment. The process to deploy a vm using kickstart using DHCP that’s bit different to setup I wrote this to help out those people who cant see the wisdom and efficiency of having DHCP’d deployments
You will be passing these variables
– name: Create a VM
vmware_guest:
hostname: “{{ vcenter_host }}”
username: “{{ vcenter_username }}”
password: “{{ vcenter_password }}”
validate_certs: False
cluster: “{{ vcenter_cluster }}”
datacenter: “{{ vcenter_dc }}”
Note: name: This will be the name of the new vm created. Keep in mind the vm host will also be setup with a shortname for the hostname of the server not the FQDN. You can probably fix this using vmshell or I used a completely separate role to setup the network for physical machines which uses jinja templates and inside the role I passed the new name as a variable. But that’s for another post
name: “{{ inventory_hostname }}”
folder: “{{ vm_folder }}”
template: “{{ VMTemplate }}”
state: “{{ vm_state }}”
Note: guest_id: this is what kind of OS will the VM Run, almost every hypervisor asks that prior to creating a vm. You can find the list online.
guest_id: “{{ vm_guest_id }}”
Note: disk: this section you could technically pass it through as a variable in your host_vars on the specific hosts, but since were using a template. I kept these parameters static here inside the role.
disk:
– size_gb: 80
type: thin
datastore: “{{ vm_datastore }}”
– size_gb: 100
type: thin
datastore: “{{ vm_datastore }}”
hardware:
memory_mb: “{{ vm_memory }}”
num_cpus: “{{ vm_cpu_count }}”
scsi: paravirtual
Note: Customization: This section is very important because without it your dns in /etc/resolv.conf will not be configured correctly. A lot of people have a hell of time with this on the net, as the parsing of this in yaml is bit tricky, and people resort to using vm_guest_file to update the /etc/resolv.conf, which sucks because now you need the root/pass via ssh. My way will work
customization:
dns_servers: “{{ vm_dns_servers }}”
dns_suffix: “{{ vm_dns_suffix }}”
Note: networks: This section is the section which will use vmware-tools or open_vm_tools to update the network config on host after powering on the vm, but before the OS is booted, provided you said to power it on in your host_var file. This section helps people get around the issue of having no DHCP and having to deploy each server using the same static address on a dedicated vlan. This section will go and update the vm network parameters and the template vm will deploy on a whatever vlan, with different ip, gateway, netmask. It will also register a new mac address to the vm, so you don’t end up with vm’s with duplicate mac-addresses. Lastly, it will update /etc/hosts with the new ip and shortname of the server
networks:
– name: “{{ vm_vlan_name }}”
type: static
dvswitch_name: “{{ vm_dvswitch }}”
ip: “{{ vm_ip }}”
netmask: “{{ vm_netmask }}”
gateway: “{{ vm_gateway }}”
start_connected: “{{ vm_connected }}”
# wait_for_ip_address: yes (this is if you are using DHCP)
delegate_to: localhost
register: vm_deploy
Note: This section is just spits out verbose information on the how the build went and the mac-address of the vm. This hand to pay attention to so you can ensure your template mac and your new vm don’t have duplicate macs. If you do. You will need to go into vshere find the VM. Remove the network and readd it manually, to register a new mac
– debug:
var: vm_deploy.instance.hw_eth0.macaddress
– debug:
var: deploy_vm
– debug:
var: mac
Not: Its easier to put all your defaults here and then comment out the ones you want to pass through your host_vars specific files after you got it working the way you want.
—
vm_disks: 100
vm_cpu_count: 2
vm_state: present
vm_memory: 2048
#vm_datastore: vmfs-datastore1234
vcenter_username: BruceWayne
vcenter_password: ( you will put ansible_vault encrypted variable here, for now just put in your password for testing)
vm_dvswitch: DvSwitch
vcenter_cluster: ProdCluster
vcenter_host: vcenter.nicktailor.com
vcenter_dc: London
#vm_folder: /Production/Unix/
#vm_vlan_name: VM76123
vm_guest_id: rhel7_64Guest
#VMTemplate: redhat-template2020
Ansible Hosts and Inventory:
Okay so this is where everyone handles things uniquely. I personally like to take the approach of creating inventory based on environment. Its logical and the best way to manage hosts in very large infrastructures.
So if you have DEV/STAGING/PRODUCTION as your environments. Then I would set it up as such
1. Inside your /etc/ansible directory create the following
For the purposes of this post we are just going to
create one group
=====================================
[All]
nicktestvm.nicktailor.com ansible_host=192.168.1.200
=====================================
Note: ansible_host=(ip) This is used when you want to override dns of the host and tell ansible. Do not resolve the dns this host only connect to this ip. You don’t need this here, however if your’re using ‘a’ static address to deploy vm’s initially and not using vmwre_tools to configure the network, and went with SSH after for configuration of the host. Then it will need to know which host to connect to setup the network. So I just like to have there in case I want to temporary tell ansible look here for this server.
Note: You can see all the variables that were in the role and defaults are now being passed through here for this specific host. It has to be done in this fashion for it all work correctly. If you pass all this through the role may crap out on you.
#vm_requirements
vm_ip: 192.168.1.86
vm_netmask: 255.255.255.0
vm_gateway: 192.168.1.1
vm_vlan_name: VM76123
VMTemplate: redhat-template2020
vm_folder: /Production/Unix
vm_state: poweredon
vm_connected: true
vm_datastore: vmfs-datastore1234
note: vm_dns_servers: this section is very important. This was the only way I could get the dns server to parse and update the /etc/resolv.conf properly. If you list them out individually as one lineers. It seems to be a bug and will simply empty out the file, which will leave your vm unable to resolve dns.
vm_dns_servers: [8.8.8.1, 8.8.8.2]
vm_dns_suffix: nicktailor.co.uk
Setting Ansible Vault and Encrypted variables:
there a couple of ways to do this you can do it via file, or via prompt. I’m going to show you how to do it via file.
Note: the –-name is the variable you want to pass in your code. So whatever you call that has to be there.
ansible-vault encrypt_string –vault-id 1@vault.pass.txt ‘vcenter-password-here’ –name ‘vcenter_password‘
vcenter_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;1
31623638366337643437633065623538663565336232333863303763336364396438663032363364
3665376363663839306165663435356365643965343364310a313832393261363466393237666666
36666437626563386366653938383565663361646333333732336439356633616231653639626465
3130656134383365320a323032366238303366336562653865663130333963316237393839373830
65396139323739323266643961653766333633366638336435613933373966643561
Encryption successful
vcenter_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;1
31623638366337643437633065623538663565336232333863303763336364396438663032363364
3665376363663839306165663435356365643965343364310a313832393261363466393237666666
36666437626563386366653938383565663361646333333732336439356633616231653639626465
3130656134383365320a323032366238303366336562653865663130333963316237393839373830
65396139323739323266643961653766333633366638336435613933373966643561
- • You should also store the vault password somewhere offsite in some password database and delete the vault.pass.txt file you created.
Deploy VM with ansible:
– hosts: all
gather_facts: no
roles:
– role: ansible-vmware-deploy
Note: Important thing to remember when deploying linux machines from a template is that all your machines will have the same ‘Network’ UUID as the template machine. If you define these…. You will need to write some code to fix that up after the VM is deployed and powered up. Check out the link below on how to do that.
http://www.nicktailor.com/?p=1177
Special Note: if you attempt to deploy multiple hosts at the same time. This will deploy 5 clones in parallel at a time and not one by one. Which will reduce deployment time significantly. I didnt bother to see if i could override this….:)
Output log of successful automated ansible deploy:
[root@nickansible]# ansible-playbook –i inventory/DEV/hosts standard_build.yml –ask-vault –limit ‘nicktestvm‘
Vault password: (paste password here in your shell window)
PLAY [all] ****************************************************************************************************************************************************************************************************
TASK [ansible-vmware-deploy : Validate Project Requirements] **********************************************************************************************************************************************
ok
TASK [ansible-vmware-deploy : Login into vCenter and get cookies] *****************************************************************************************************************************************
ok: [nicktestvm]
TASK [ansible-vmware-deploy : Create a VM] ****************************************************************************************************************************************************************
changed: [nicktestvm]
TASK [ansible-vmware-deploy : debug] **********************************************************************************************************************************************************************
ok: [nicktestvm] => {
“vm_deploy.instance.hw_eth0.macaddress”: “00:40:51:53:11:a6”
}
nicktestvm : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0