In this article, I will describe how to deploy Cisco’s Virtual Wireless Controller (vWLC) on VMWare Fusion using Ansible.

Although Ansible is most often used for deploying infrastructure and applications or orchestrating continuous delivery workflows, it can be used to automate almost any task.

I will create an Ansible playbook that will automate all of the manual tasks you would normally take to get the vWLC up and running.

UPDATE 6th October, 2015: I have refactored the original playbook that was the subject of this article into a reusable Ansible Galaxy role. This article has been updated accordingly and has numerous changes from the original article.

Cisco Virtual Wireless Controller

The vWLC is a software appliance in Cisco’s Wireless Controller product line that can manage up to 200 access points and 6000 clients.

Although you most often deploy the vWLC in production to VMWare ESX, it is very useful to be able to run a vWLC up on your laptop for testing and demonstration purposes.

The Problem

So what’s the problem we are trying to solve here?

Well, part of creating CloudHotspot is having a development environment that incorporates Cisco’s vWLC for testing and development purposes.

I wanted to automate deployment of the vWLC, initially looking to:

  • Vagrant - a no go, the rather strange SSH arrangement on the vWLC pretty much breaks the model Vagrant is expecting to work with.
  • Packer - I was able to create a Packer build that configured a vWLC from scratch, but sending a bunch of key presses over VNC with carefully timed pauses seemed somewhat fragile.

The only other method I could come up with was to leverage the vWLC AutoInstall feature, which uses the classic TFTP network boot approach employed by Cisco IOS and many other network devices.

AutoInstall requires a few ancillary pieces of infrastructure to get it working:

VMWare Fusion ships with a DHCP server for the host networking functions, and OS X ships with a TFTP daemon (disabled by default), so all of the necessary ingredients to make AutoInstall work are already available in a VMWare Fusion environment.

All that is needed is a little orchestration and automation magic from Ansible.

Quick Start

This purpose of this article is to explain in detail how the Ansible role called

1
mixja.vwlc
that I’ve published on Ansible Galaxy actually works.

However it is worthwhile to give a quick overview of how to use the playbook before we delve into details. A sample playbook is published on Github which you should use to get started.

The target user experience here is to:

  • Download the OVA appliance from CCO
  • Review the Quick Start section in the sample playbook README. Here you will note that you have to provide a valid OVA source image and a destination root for the deployed virtual machine.
  • Define configuration parameters as described in the sample playbook README.
  • Run the playbook as demonstrated below.

To install the Ansible Galaxy role:

1
ansible-galaxy install mixja.vwlc

To run the playbook:

1
ansible-playbook site.yml

And to run the playbook and overwrite a previous installation:

1
ansible-playbook site.yml --extra-vars wlc_vm_overwrite=true

Workflow

The rest of this article will now discuss the

1
mixja.vwlc
role in detail. The source for this role is published on Github, and is published on Ansible Galaxy.

The high-level workflow of the playbook is as follows:

Deploying the Virtual Machine

Deploying the OVA image sounds simple enough but to make the playbook fairly idiot-proof and user friendly, we need to consider a couple of what-if scenarios:

  • What if we’ve already deployed the image to the desired location?
  • What if a virtual machine is running at the desired location?

We need to handle these scenarios appropriately, before the virtual machine can be extracted from the OVA image.

Establishing some Facts

The first set of tasks in the role are described in the

1
set_facts.yml
file, which are used to create a few internal variables used throughout the role:

---
- name: Pad VM destination root path with /
  set_fact: wlc_vm_padded_destination='{{ wlc_vm_root }}/'
- name: Extract root directory name from padded VM root path
  set_fact: wlc_vm_safe_dst="{{ wlc_vm_padded_destination | dirname }}"
- name: Get full path to VM folder
  set_fact: wlc_vm_safe_dst_full_path="{{ wlc_vm_safe_dst }}/{{ wlc_vm_name }}.vmwarevm"
- name: Get full path fo VMX file
  set_fact: wlc_vm_vmx_path="{{ wlc_vm_safe_dst_full_path }}/{{ wlc_vm_name }}.vmx"

The above tasks require the following inputs:

  • 1
    wlc_vm_root
    
    - Defines the root folder where the virtual machine will be deployed. This must be specified by the user as input to the role.
  • 1
    wlc_vm_name
    
    - Defines the name of the virtual machine. The default value is
    1
    wlc01
    

Making some Checks

Next, the tasks in

1
checks.yml
verify that the
1
ovftool
utility is installed, and determines if a virtual machine is already deployed at the target destination path:

VMWare provides a free tool called

1
ovftool
to registered VMWare users, and this tool must be installed to deploy virtual machines from OVA files using the command line.

---
- name: Check for ovftool
  shell: pkgutil --pkgs | awk '/com.vmware.ovftool.application/'
  register: wlc_pkgutil_ovftool
  changed_when: False
- name: Fail if VMWare OVF Tools are not installed
  fail: msg="VMWare OVF Tools are required.  Please install and retry."
  when: 'not {{ wlc_pkgutil_ovftool.stdout | match("com.vmware.ovftool.application") }}'
- name: Get ovftool path
  shell: pkgutil --files com.vmware.ovftool.application | grep -FE 'ovftool$'
  register: wlc_ovftool_path
  changed_when: false
- name: Check VM path
  stat: path='{{ wlc_vm_safe_dst_full_path }}'
  register: wlc_vm_exists
  changed_when: False
- name: Fail if VM path exists
  fail: msg="VM already exists.  Please set wlc_vm_overwrite variable to any value to overwrite the existing VM"
  when: (wlc_vm_exists.stat.isdir is defined) and (wlc_vm_overwrite is not defined)

The

1
name: Check VM path
task checks if the desired VM location already exists. Note the following convention to create this location:

1
{{ wlc_vm_root }}/{{ wlc_vm_name }}.vmwarevm

So the VM location will be

1
/path/to/vm/root/wlc01.vmwarevm
assuming a VM name of
1
wlc01
.

Note I’m using a calculated

1
vm_safe_dst_full_path
variable, which is derived from the above convention. This variable is manipulated to ensure we get the correct full path without any duplicate forward slashes.

If the VM location already exists, the entire playbook is configured to fail in the

1
name: Fail if VM path exists
task, unless the
1
wlc_vm_overwrite
variable is defined with any value.

This approach protects you from accidentally overwriting an existing virtual machine, but still allows you to explicitly overwrite it if that is your intention as demonstrated below:

1
$ ansible-playbook site.yml --extra-vars wlc_vm_overwrite=true

Creating the VM Location

With initial facts set and checks out of the way, the

1
create_vm.yml
tasks create the virtual machine folder.

---
- name: Get VMX path if existing VM is running
  shell: "'{{ wlc_vmrun_path }}' list | grep -F '{{ wlc_vm_safe_dst_full_path }}' || true"
  register: wlc_vmx_path
  when: wlc_vm_exists.stat.isdir is defined
  changed_when: wlc_vmx_path is defined and wlc_vmx_path.stdout != ""
  notify:
    - stop vm hard
    - pause three seconds
- meta: flush_handlers
- name: Remove existing VM path
  file: path='{{ wlc_vm_safe_dst_full_path }}' state=absent
- name: Create VM path
  file: path='{{ wlc_vm_safe_dst_full_path }}' state=directory
- name: Extract OVA using ovftool
  command: "'/{{ wlc_ovftool_path.stdout }}' '{{ wlc_ova_source }}' '{{ wlc_vm_vmx_path }}'"

Before creating the VM location I check if there is an existing VM running (assuming the VM location already exists and

1
wlc_vm_overwrite
has been defined).

As the intention in this scenario is to overwrite an existing VM, we need to first stop the VM (if it is running) in order to remove the existing VM folder and files.

To do this, I use the

1
vmrun list
command which is included as part of the VMWare Fusion application. This is defined in the
1
name: Get VMX path if existing VM is running
task, using good old
1
grep
to extract the full path of the running VM vmx file at the target VM location.

Here’s an example of the full output of the

1
vmrun list
command:

$ /Applications/VMware\ Fusion.app/Contents/Library/vmrun list
Total running VMs: 1
/Users/jmenga/Virtual Machines.localized/wlc01.vmwarevm/wlc01.vmx

Note I use

1
changed_when
with a boolean expression to determine if the VM is actually running. This is useful as the
1
stop vm hard
and
1
pause three seconds
handlers will only be called if
1
changed_when
evaluates to true:

- name: stop vm hard
  command: '"{{ wlc_vmrun_path }}" stop "{{ wlc_vm_vmx_path }}" hard'
  become: no
  
- name: pause three seconds
  pause: seconds=3

A few points to note here:

  • I explicitly force the
    1
    stop vm hard
    
    handler to run in the context of the user executing the playbook. If you call this handler from a task that is running as root, the handler will run as root unless you specify
    1
    become: no
    
    . This is important for the
    1
    vmrun
    
    command, as it only lists Virtual Machines running in the context of each user.
  • The
    1
    pause three seconds
    
    handler prevents a race condition where the existing virtual machine shutdown may not complete gracefully before the next task that attempts to remove the existing virtual machine.
  • The
    1
    meta: flush_handlers
    
    task in
    1
    create_vm.yml
    
    forces handlers to execute immediately. By default, handlers run at the end of a play, which may not be the desired behaviour.

At this point, the existing VM location (if it previously existed) can be safely removed using the

1
name: Remove existing VM path
task and the target VM location created using the
1
name: Create VM path
task.

The final step is to deploy the virtual machine from the OVA image, which is completed in the

1
name: Extract OVA using ovftool
task. This task references the
1
wlc_ova_source
variable, which must be provided explicitly as input to the role. The user must supply their own vWLC OVA image, which can be downloaded from Cisco (CCO login required).

Pre-creating the target VM location alters the behaviour of the

1
ovftool
, which is used to deploy the virtual machine from the OVA image. If you run this command and the destination VMX parent folder does not exist, ovftool behaves difficultly and creates another folder in the format
1
<vm name>.vmwarevm
under the specified parent folder and then places the vmx file in this folder. To avoid this behaviour, you must precreate the target VM parent folder.

Introspecting the Virtual Machine

The next tasks that are executed are defined in the

1
introspect.yml
file:

---
- name: Configure service port as Share with my Mac
  lineinfile: >
    dest='{{ wlc_vm_vmx_path }}'
    regexp='^ethernet0.connectionType ='
    line='ethernet0.connectionType = "nat"'
  notify:
    - start vm
    - stop vm
- meta: flush_handlers
- name: Get service port MAC address
  shell: cat '{{ wlc_vm_vmx_path }}' | awk -F'"' '/ethernet0.generatedAddress = /{print $2}'
  register: wlc_vm_mac_address
  changed_when: False
- name: Get vmnet8 IP address
  shell: ifconfig vmnet8 | awk '/inet/{print $2}'
  register: wlc_vmnet8_ip_address
  changed_when: False
- name: Set host IP address fact
  set_fact: wlc_host_ip_address={{ wlc_vmnet8_ip_address.stdout }}
- name: Set VM MAC access fact
  set_fact: wlc_vm_mac_address={{ wlc_vm_mac_address.stdout }}
- name: Set VM IP address fact
  set_fact: wlc_vm_ip_address={{ wlc_vmnet8_ip_address.stdout | regex_replace(wlc_vm_mgmt_ip_regex_match, wlc_vm_mgmt_ip_regex_replace) }}
- name: Set VM IP gateway fact
  set_fact: wlc_vm_ip_gateway={{ wlc_vmnet8_ip_address.stdout | regex_replace(wlc_vm_mgmt_ip_regex_match, wlc_vm_mgmt_gateway_regex_replace) }}

The

1
name: Configure Ethernet0 as Share with my Mac
task reconfigures the
1
ethernet0
network interface connection type to Share with my Mac using the very useful
1
lineinfile
Ansible module. This results in the following entry in the vmx file:

1
ethernet0.connectionType = "nat"

This setting is important, as it ensures the service port on the vWLC appliance will use internal VMWare Fusion NAT networking mode and the VMWare Fusion DHCP server. The other

1
ethernet1
interface will remain in the default bridged networking mode.

The Cisco vWLC appliance comes with two network interfaces.

1
ethernet0
is the service port and
1
ethernet1
is the management port that connects access points.

At the end of this first task, the virtual machine is started and then immediately stopped. The reason for this is that we need to generate a MAC address for the virtual machine service port interface, which does not happen until the virtual machine is started for the first time. After bouncing the virtual machine, the

1
ethernet0.generatedAddress
key in the virtual machine vmx file will be populated with a MAC address:

1
ethernet0.generatedAddress = "00:0c:29:0d:ec:56"

The

1
name: Get service port MAC address
task parses the virtual machine VMX file to retrieve the MAC address, which is required to configure a DHCP reservation for the vWLC virtual machine service port.

Notice that

1
awk
is our friend here :) You may notice that I use
1
awk
and
1
grep
interchangeably and in general the usual differences apply. One key difference is that
1
grep
always returns an error if there is no match, where as
1
awk
does not. This can litter your playbook output with unsightly errors, even if you choose to ignore errors (which IMHO is an Ansible antipattern). One way to work around the
1
grep
error return code issue is to add on
1
|| true
at the end of the
1
grep
command.

The

1
name: Get vmnet8 IP address
task determines the IP address being used for the
1
Share with my Mac
vmnet8 network adapter. This interface is connected to the service port of the vWLC virtual machine, and because the OS X TFTP daemon binds to all network interfaces, we can specify this IP address as the TFTP server address. We also can derive the network portion of the vmnet8 network adapter, which we will need to configure our DHCP reservation later on.

The final tasks set a number of facts that are required for later tasks:

  • 1
    wlc_host_ip_address
    
    - the host IP address of the
    1
    vmnet8
    
    adapter. This IP address is used as the TFTP server address by vWLC.
  • 1
    wlc_vm_mac_address
    
    - the MAC address of the vWLC service port.
  • 1
    vlc_vm_ip_address
    
    - the IP address of the vWLC service port. This is calculated as the network portion of the
    1
    vmnet8
    
    IP address combined with the value of the
    1
    wlc_vm_svc_ip_octet
    
    variable. As the
    1
    vmnet8
    
    IP address always uses a /24 subnet mask, this results in the first three octets of the
    1
    vmnet8
    
    IP address plus the
    1
    wlc_vm_svc_ip_octet
    
    value. E.g. given a
    1
    vmnet8
    
    IP address of 192.168.100.1 and
    1
    wlc_vm_svc_ip_octet
    
    value of 121, the
    1
    vlc_vm_ip_address
    
    will be 192.168.100.121.
  • 1
    wlc_vm_ip_gateway
    
    - the router and DNS server address for the
    1
    vmnet8
    
    network. On VMWare Fusion, this is always the .2 address on the
    1
    vmnet8
    
    network (e.g. 192.168.100.2 continuing on from the previous example).

These facts are used to configure TFTP and DHCP settings as you will see shortly.

Configuring the OS X TFTP Server

OS X ships with a TFTP server that is disabled by default. The

1
tftp.yml
play defines the various tasks required to configure and enable the TFTP server:

---
- name: Check if TFTP daemon is running
  shell: launchctl list | awk /com.apple.tftp/
  become: yes
  register: wlc_tftp_daemon_status
  changed_when: wlc_tftp_daemon_status.stdout != ""
  notify:
    - stop system tftp daemon
- name: Deploy TFTP plist
  template: 
    src: "tftp.plist.j2" 
    dest: "{{ wlc_tftp_plist }}"
    mode: 0644
  become: yes
- name: Ensure TFTP path exists
  file: 
    path: "{{ wlc_tftp_path }}"
    state: directory
    mode: 0777
- name: Deploy WLC file
  template:
    src: "{{ wlc_config_file | default('ciscowlc.cfg.j2') }}"
    dest: "{{ wlc_tftp_path }}/ciscowlc.cfg"
  changed_when: true
  notify:
    - start user tftp daemon

The first task determines if a TFTP daemon is currently running. If this is the case, the

1
stop system tftp daemon
handler in
1
handlers/main.yml
stops the TFTP daemon:

- name: stop system tftp daemon
  command: launchctl unload '{{ wlc_system_tftp_plist }}'
  become: yes

OS X El Capitan includes a new feature called system integrity protection, which prevents even root/sudo access from modifying OS X system files. This includes the standard

1
/System/Library/LaunchDaemons/tftp.plist
file (which is the value of the
1
wlc_system_tftp_plist
variable in the handler above) that is used to configure the OS X TFTP server. Although you can disable system integrity protection, the role avoids having to do this by creating a plist file outside of the protected OS X system file system (hence the reference to user and system TFTP daemons in the tasks).

In the

1
name: Deploy TFTP plist
task, a Jinja 2 template is used to configure the relevant settings in the file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Disabled</key>
  <false/>
  <key>Label</key>
  <string>com.apple.tftpd</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/libexec/tftpd</string>
    <string>-i</string>
    <string>{{ wlc_tftp_path }}</string>
  </array>
  <key>inetdCompatibility</key>
  <dict>
    <key>Wait</key>
    <true/>
  </dict>
  <key>InitGroups</key>
  <true/>
  <key>Sockets</key>
  <dict>
    <key>Listeners</key>
    <dict>
      <key>SockServiceName</key>
      <string>tftp</string>
      <key>SockType</key>
      <string>dgram</string>
    </dict>
  </dict>
</dict>
</plist>

The template enables the TFTP server by setting the

1
<key>Disabled</key>
value to
1
<false/>
and also includes the
1
wlc_tftp_path
variable (
1
/Users/Shared/tftp
by default) to specify the folder that the TFTP server should serve. The
1
name: Ensure TFTP path exists
task ensures this folder is present.

The

1
name: Deploy WLC file
task then deploys the Cisco vWLC configuration file that will served via TFTP. The playbook allows you to provide your own config file by setting the
1
wlc_config_file
variable - if this variable is not defined, the playbook deploys a basic configuration derived from the
1
templates/ciscowlc.cfg.j2
template:

# WLC Config Begin

config mdns service origin all AirTunes 
config mdns service create AirTunes _raop._tcp.local. origin all lss disable 
config mdns service origin all Airplay 
config mdns service create Airplay _airplay._tcp.local. origin all lss disable 
config mdns service origin all HP_Photosmart_Printer_1 
config mdns service query enable HP_Photosmart_Printer_1 
config mdns service create HP_Photosmart_Printer_1 _universal._sub._ipp._tcp.local. origin all lss disable query enable 
config mdns service origin all HP_Photosmart_Printer_2 
config mdns service query enable HP_Photosmart_Printer_2 
config mdns service create HP_Photosmart_Printer_2 _cups._sub._ipp._tcp.local. origin all lss disable query enable 
config mdns service origin all HomeSharing 
config mdns service query enable HomeSharing 
config mdns service create HomeSharing _home-sharing._tcp.local. origin all lss disable query enable 
config mdns service origin all Printer-IPP 
config mdns service create Printer-IPP _ipp._tcp.local. origin all lss disable 
config mdns service origin all Printer-IPPS 
config mdns service create Printer-IPPS _ipps._tcp.local. origin all lss disable 
config mdns service origin all Printer-LPD 
config mdns service create Printer-LPD _printer._tcp.local. origin all lss disable 
config mdns service origin all Printer-SOCKET 
config mdns service create Printer-SOCKET _pdl-datastream._tcp.local. origin all lss disable 
config mdns profile service add default-mdns-profile AirTunes 
config mdns profile service add default-mdns-profile Airplay 
config mdns profile service add default-mdns-profile HP_Photosmart_Printer_1 
config mdns profile service add default-mdns-profile HP_Photosmart_Printer_2 
config mdns profile service add default-mdns-profile HomeSharing 
config mdns profile service add default-mdns-profile Printer-IPP 
config mdns profile service add default-mdns-profile Printer-IPPS 
config mdns profile service add default-mdns-profile Printer-LPD 
config mdns profile service add default-mdns-profile Printer-SOCKET 
config mdns profile create default-mdns-profile 
config ap packet-dump truncate 0 
config ap packet-dump buffer-size 2048 
config ap packet-dump capture-time 10 
config ap preferred-mode ipv4 all 
config 802.11a cac voice sip bandwidth 64 sample-interval 20 
config 802.11a cac voice sip codec g711 sample-interval 20 
config switchconfig strong-pwd lockout attempts mgmtuser 3 
config switchconfig strong-pwd lockout time mgmtuser 5 
config interface dhcp management primary {{ wlc_dhcp_server_ip_address }}
config interface dhcp service-port enable 
config interface port management 1 
config interface address management {{ wlc_mgmt_ip_address }} {{ wlc_mgmt_ip_mask }} {{ wlc_mgmt_ip_gateway }}
config interface address virtual {{ wlc_virtual_ip_address }}
config database size 2048 
config mgmtuser add {{ wlc_admin_username }} {{ wlc_admin_password }} read-write 
config mobility group domain {{ wlc_mobility_group_name }} 
config certificate generate webadmin 
config advanced 802.11a channel add 36 
config advanced 802.11a channel add 40 
config advanced 802.11a channel add 44 
config advanced 802.11a channel add 48 
config advanced 802.11a channel add 52 
config advanced 802.11a channel add 56 
config advanced 802.11a channel add 60 
config advanced 802.11a channel add 64 
config advanced 802.11a channel add 149 
config advanced 802.11a channel add 153 
config advanced 802.11a channel add 157 
config advanced 802.11a channel add 161 
config advanced 802.11b channel add 1 
config advanced 802.11b channel add 6 
config advanced 802.11b channel add 11 
config sys-nas {{ wlc_name }} 
config network rf-network-name {{ wlc_rf_network_name }}
config network multicast l2mcast disable service-port 
config network multicast l2mcast disable virtual 
config time ntp interval {{ wlc_ntp_interval }} 
config time ntp server 1 {{ wlc_ntp_server }}
config sysname {{ wlc_name }} 
config country NZ 
config wlan exclusionlist 1 60 
config wlan security wpa enable 1 
config wlan security web-auth server-precedence 1 local radius ldap 
config wlan create 1 "{{ wlc_ssid }}" "{{ wlc_ssid }}" 
config wlan interface 1 management 
config wlan broadcast-ssid enable 1 
config wlan session-timeout 1 1800 
config wlan mfp client enable 1 
config wlan wmm allow 1 
config wlan enable 1 
config 802.11b 11gsupport enable 
config 802.11b cac voice sip bandwidth 64 sample-interval 20 
config 802.11b cac voice sip codec g711 sample-interval 20 

# WLC Config End

The template inserts various user configurable variables that are described in

1
/defaults/main.yml
:

# WLC configuration settings
wlc_name: wlc01
wlc_admin_username: admin
wlc_admin_password: Pass1234

wlc_mgmt_ip_address: 192.168.1.6
wlc_mgmt_ip_mask: 255.255.255.0
wlc_mgmt_ip_gateway: 192.168.1.254
wlc_dhcp_server_ip_address: 192.168.1.254 
wlc_virtual_ip_address: 1.1.1.1

wlc_mobility_group_name: "{{ wlc_name }}"
wlc_rf_network_name: "{{ wlc_name }}"
wlc_ntp_server: 64.99.80.30
wlc_ntp_interval: 3600
wlc_ssid: Test SSID

The task will deploy the configuration file to a file named

1
ciscowlc.cfg
- this name is used as it is one of the file names that the vWLC will attempt to download from the TFTP server as part of the AutoInstall feature (see here for more details).

Configuring the VMWare DHCP Server

The required network environment to support the AutoInstall feature is almost in place. All that remains is to configure the VMWare DHCP server as follows:

  • Create a DHCP reservation for the vWLC service port interface.
  • The DHCP reservation must include the BOOTP next server setting, which is used by the vWLC during AutoInstall to determine the IP address of the TFTP server to download its configuration from

Configuring a DHCP reservation is useful for the following reasons:

  • We know the IP address that will be assigned to the service port. We need this to configure the
    1
    /etc/hosts
    
    file on the host system and to determine when the vWLC has provisioned successfully.
  • We can constrain custom DHCP settings (i.e. BOOTP next server) to the vWLC virtual machine only. This avoids unforeseen side effects that might be caused by adding these settings globally.

This requires two tasks that are defined in the

1
dhcp.yml
file:

---
- name: Remove previous DHCP reservations
  blockinfile:
    dest: '{{ wlc_dhcpd_conf_path }}'
    marker: "# {mark} ANSIBLE MANAGED BLOCK - {{ wlc_vm_name }} {{ wlc_vm_ip_address }}"
    content: ""
  become: yes
- name: Add DHCP reservation
  blockinfile:
    dest: '{{ wlc_dhcpd_conf_path }}'
    marker: "# {mark} ANSIBLE MANAGED BLOCK - {{ wlc_vm_name }} {{ wlc_vm_ip_address }}"
    insertafter: EOF
    content: |
      host {{ wlc_vm_name }} {
        hardware ethernet {{ wlc_vm_mac_address }};
        fixed-address {{ wlc_vm_ip_address }};
        option domain-name-servers {{ wlc_vm_ip_gateway }};
        option domain-name localdomain;
        default-lease-time 1200;
        max-lease-time 1200;  
        option routers {{ wlc_vm_ip_gateway }};
        next-server {{ wlc_host_ip_address }};
      }
  become: yes
  notify:
    - stop vmware networking
    - start vmware networking
    - start vm
- meta: flush_handlers

First any previous DHCP reservations are removed. The VMWare DHCP configuration is controlled by the

1
/Library/Preferences/VMware Fusion/vmnet8/dhcpd.conf
file and an unmodified example is shown below:

# Configuration file for ISC 2.0 vmnet-dhcpd operating on vmnet8.
#
# This file was automatically generated by the VMware configuration program.
# See Instructions below if you want to modify it.
#
# We set domain-name-servers to make some DHCP clients happy
# (dhclient as configured in SuSE, TurboLinux, etc.).
# We also supply a domain name to make pump (Red Hat 6.x) happy.
#

###### VMNET DHCP Configuration. Start of "DO NOT MODIFY SECTION" #####
# Modification Instructions: This section of the configuration file contains
# information generated by the configuration program. Do not modify this
# section.
# You are free to modify everything else. Also, this section must start 
# on a new line 
# This file will get backed up with a different name in the same directory 
# if this section is edited and you try to configure DHCP again.

# Written at: 09/28/2015 19:46:42
allow unknown-clients;
default-lease-time 1800;                # default is 30 minutes
max-lease-time 7200;                    # default is 2 hours

subnet 192.168.232.0 netmask 255.255.255.0 {
	range 192.168.232.128 192.168.232.254;
	option broadcast-address 192.168.232.255;
	option domain-name-servers 192.168.232.2;
	option domain-name localdomain;
	default-lease-time 1800;                # default is 30 minutes
	max-lease-time 7200;                    # default is 2 hours
	option netbios-name-servers 192.168.232.2;
	option routers 192.168.232.2;
}
host vmnet8 {
	hardware ethernet 00:50:56:C0:00:08;
	fixed-address 192.168.232.1;
	option domain-name-servers 0.0.0.0;
	option domain-name "";
	option routers 0.0.0.0;
}
####### VMNET DHCP Configuration. End of "DO NOT MODIFY SECTION" #######

If you are familiar with the ISC DHCPD server, you’ll notice this is exactly what VMWare is using for the DHCP service. This makes it very easy to configure the DHCP server for our needs.

We add a DHCP reservation for the vWLC virtual machine to the bottom of the DHCP configuration file as defined in the

1
name: Add  DHCP reservation
task.

This allows us to control the IP address allocated to the vWLC virtual machine and set the BOOTP next server (TFTP Server) that is required for AutoInstall. This option set using the

1
next-server
directive in the reservation and specifying the IP address of the host vmnet8 adapter (
1
wlc_host_ip_address
).

VMWare Fusion uses a default DHCP range of x.x.x.128 - x.x.x.254, so you can reserve any IP address between 3 - 126 (x.x.x.1 and x.x.x.2 are used by VMWare). Recall that this value is controlled by the

1
wlc_vm_svc_ip_octet
setting.

The DHCP reservation also needs to adopt the various other settings defined in the standard DHCP scope for the vmnet8 interface.

To deploy the necessary configuration for the reservation, I’m using a third-party community module called yaegashi.blockinfile.

Here is an example of the DHCP configuration file with the DHCP reservation configuration appended to the end of the file:

# Configuration file for ISC 2.0 vmnet-dhcpd operating on vmnet8.
#
# This file was automatically generated by the VMware configuration program.
# See Instructions below if you want to modify it.
#
# We set domain-name-servers to make some DHCP clients happy
# (dhclient as configured in SuSE, TurboLinux, etc.).
# We also supply a domain name to make pump (Red Hat 6.x) happy.
#

###### VMNET DHCP Configuration. Start of "DO NOT MODIFY SECTION" #####
# Modification Instructions: This section of the configuration file contains
# information generated by the configuration program. Do not modify this
# section.
# You are free to modify everything else. Also, this section must start 
# on a new line 
# This file will get backed up with a different name in the same directory 
# if this section is edited and you try to configure DHCP again.

# Written at: 09/28/2015 19:46:42
allow unknown-clients;
default-lease-time 1800;                # default is 30 minutes
max-lease-time 7200;                    # default is 2 hours

subnet 192.168.232.0 netmask 255.255.255.0 {
	range 192.168.232.128 192.168.232.254;
	option broadcast-address 192.168.232.255;
	option domain-name-servers 192.168.232.2;
	option domain-name localdomain;
	default-lease-time 1800;                # default is 30 minutes
	max-lease-time 7200;                    # default is 2 hours
	option netbios-name-servers 192.168.232.2;
	option routers 192.168.232.2;
}
host vmnet8 {
	hardware ethernet 00:50:56:C0:00:08;
	fixed-address 192.168.232.1;
	option domain-name-servers 0.0.0.0;
	option domain-name "";
	option routers 0.0.0.0;
}
####### VMNET DHCP Configuration. End of "DO NOT MODIFY SECTION" #######

# BEGIN ANSIBLE MANAGED BLOCK wlc01 192.168.232.127
host wlc01 {
  hardware ethernet 00:0c:29:0d:ec:56;
  fixed-address 192.168.232.127;
  option domain-name-servers 192.168.232.2;
  option domain-name localdomain;
  default-lease-time 1200;
  max-lease-time 1200;  
  option routers 192.168.232.2;
  next-server 192.168.232.1;
}
# END ANSIBLE MANAGED BLOCK wlc01 192.168.232.127

The

1
blockinfile
module includes begin and end marker lines, which are useful for removing the inserted block later during cleanup.

With the modifications made to the DHCP configuration, the

1
name: Add DHCP reservation
task notifies several handlers, defined in
1
handlers/main.yml
:

- name: stop vmware networking
  command: '"{{ wlc_vmnet_path }}" --stop'
  become: yes
- name: start vmware networking
  command: '"{{ wlc_vmnet_path }}" --start'
  become: yes
- name: start vm
  command: '"{{ wlc_vmrun_path }}" start "{{ wlc_vm_vmx_path }}" nogui'
  become: no

These handlers restart VMWare networking using the

1
vmnet-cli
command (included with VMWare Fusion), allowing the DHCP configuration changes to take effect.

With the DHCP configuration in place, the virtual machine is then started to begin the AutoInstall process.

The order of handlers as defined in the handler file in Ansible is important. I have noticed the order of execution follows the order specified in the handler file, rather than the order specified in the

1
notify
action of the calling task (as one might expect).

UPDATE: At this point I have also added provisioning of the local

1
/etc/hosts
file with the vWLC name (as defined by
1
wlc_vm_name
) and service port IP address (as defined by
1
wlc_vm_ip_address
). This occurs by default but can be disabled by setting
1
wlc_vm_persist_dhcp_reservation
to
1
no
.

Virtual Machine AutoInstall and Cleanup

At this point, everything is in place for the (recently booted) vWLC virtual machine to use the AutoInstall feature:

  • TFTP daemon is enabled and configured with a vWLC configuration file
  • VMWare DHCP server is configured to advertise the TFTP server IP address and issue a DHCP reservation to vWLC virtual machine

vWLC AutoInstall Process

The following screen shots show the various stages of vWLC AutoInstall.

First time boot of the vWLC virtual machine. The OVA includes an ISO installer that images the virtual machine hard disk:

vWLC Installation Start

After approximately one minute the hard disk imaging is complete and the appliance reboots:

vWLC Imaging Complete

One quirk of the vWLC virtual machine is that you have to explicitly enable console output at boot. This is only needed if you need to access the console for any reason (or take screenshots of the install process :):

vWLC press any key

After approximately two and a half minutes since first boot the setup wizard will be displayed. This allows you to manually configure the vWLC, which we obviously don’t do in this case:

vWLC Setup Wizard

After 30 seconds the AutoInstall process will start automatically. About 30 seconds into the AutoInstall process, you will see the vWLC virtual machine download the

1
ciscowlc.cfg
configuration file and reboot:

vWLC Config File Download

After rebooting, the installation will be complete. The entire process takes just a shade over five minutes.

vWLC Setup Wizard

Cleanup

After installation is complete, the following tasks in the

1
vwlc.yml
file will be executed:

---
- name: Wait for vWLC to provision
  local_action: wait_for host={{ wlc_vm_ip_address }} port=443 delay=10 timeout=600
  sudo: false
- name: Remove DHCP reservation
  blockinfile:
    dest: '{{ wlc_dhcpd_conf_path }}'
    marker: "# {mark} ANSIBLE MANAGED BLOCK - {{ wlc_vm_name }} {{ wlc_vm_ip_address }}"
    content: ""
  become: yes
  when: not wlc_vm_persist_dhcp_reservation
  notify:
    - stop vmware networking
    - start vmware networking
- name: Remove TFTP plist file
  file: >
    path="{{ wlc_tftp_plist }}"
    state=absent
  notify:
    - stop system tftp daemon
    - restore previous system tftp daemon
  become: yes
- name: Remove TFTP config file
  file: >
    path="{{ wlc_tftp_path }}/ciscowlc.cfg"
    state=absent
  notify:
    - stop system tftp daemon
    - restore previous system tftp daemon
  become: yes

The playbook is configured wait for the vWLC virtual machine to come online with the

1
name: Wait for WLC to provision
task. This task attempts to establish a TCP connection to port 443 on the vWLC virtual machine service port IP address.

You might be tempted to use port 22 to determine if the vWLC virtual machine is fully provisioned. The vWLC leaves port 22 open during the AutoInstall process allowing the task to establish a TCP connection, hence using port 22 would mean playbook would think provisioning has finished at a much earlier time. Port 443 (HTTPS) is only activated once provisioning is fully complete, hence gives a more reliable indication that provisioning is complete.

Once the provisioning is completed, a series of cleanup tasks takes place.

This cleanup removes the DHCP reservation (by default this task is skipped unless

1
wlc_vm_persist_dhcp_reservation
is set to
1
no
), the TFTP plist file, and the TFTP WLC configuration file.

Wrap Up

Well this has been a very long article, but hopefully you have some good insights into how you Ansible can automate deployment of the Cisco Virtual Wireless Controller.

If you are like me and regularly need a vWLC instance running in a lab, development or demonstration environment, this playbook should save you a lot of time.

Along the way I’ve also shown you a few of the internals that support VMWare Fusion and how you can use those to automate certain tasks. Hopefully you’ve also picked up a few Ansible tricks that you can apply to your own automation/deployment scenarios.

Deploying the Cisco CSR 1000v using Ansible

How to deploy Cisco's CSR 1000v using Ansible and VMWare Fusion Continue reading

Seastar DPDK Web Framework Showdown

Published on September 01, 2015

DPDK on an Intel NUC

Published on August 28, 2015