Network Automation     Archive

Grabbing L3 Firewall Policy Information from Meraki

Had a friend of mine from Cisco ask about how/if Ansible can work with Cisco Meraki. As with most answers with “can Ansible do this” my initial response was, of course!

Quick background: Cisco Meraki is one of the largest LAN SDN infrastructures today. Using their open APIs, we sat down and wrote an Ansible playbook around checking and provisioning policies on a single access point. The goal of this exercise is to eventually tie together other security products and define specific security policies to deploy across a Cisco Meraki Infrastructure.

Here’s a look at the playbook in its entirety:


---
- name: GRAB MERAKI SECURITY POLICY OF US AP DEPOT IN CISCO DEVNET
  hosts: localhost
  connection: local
  gather_facts: no

  vars_files:
    - ./vars.yml

  tasks:
    - name: CHECK CONNECTION
      uri:
        url: https://api.meraki.com/api/v0/organizations
        method: GET
        headers:
          X-Cisco-Meraki-API-Key: "{{ MERAKI_API_KEY }}"
      register: meraki_org

    - name: CHANGE MERAKI_ORG TO LOWER
      set_fact:
        meraki_org_lower: "{{ meraki_org.json | lower }}"

    - name: GRAB DEVNET SANDBOX ORGANIZATION ID
      set_fact:
        meraki_org_id: "{{item.id}}"
      with_items: "{{meraki_org_lower}}"
      when: item.name == 'devnet sandbox'

    - name: GRAB ORGANIZATION'S LIST OF NETWORKS
      uri:
        url: https://api.meraki.com/api/v0/organizations/{{meraki_org_id}}/networks
        method: GET
        headers:
          X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
      register: meraki_network

    - name: GRAB ID OF WIRELESS TYPE AND NAME OF US AP DEPOT
      set_fact:
        meraki_network_id: "{{ item.id }}"
      with_items: "{{ meraki_network.json }}"
      when: item.type == 'wireless' and item.name == 'US AP Depot'

    - name: GRAB SSID OF NETWORK
      uri:
        url: https://api.meraki.com/api/v0/networks/{{meraki_network_id}}/ssids
        method: GET
        headers:
          X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
      register: meraki_ssid

    - name: GRAB ID OF US AP DEPOT WIFI
      set_fact:
        meraki_ssid_number: "{{item.number}}"
      with_items: "{{ meraki_ssid.json}}"
      when: item.name == 'US AP Depot WiFi'

    - name: GRAB L3 FIREWALL POLICIES
      uri:
        url: https://api.meraki.com/api/v0/networks/{{meraki_network_id}}/ssids/{{meraki_ssid_number}}/l3FirewallRules
        method: GET
        headers:
          X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
      register: meraki_l3firewallrules

    - debug:
        msg: "{{meraki_l3firewallrules}}"

Now, let’s step through the tasks!

The first task is to grab the ID of the “DevNet SandBox” organization which is a Cisco provided sandbox environment.


- name: CHECK CONNECTION
  uri:
    url: https://api.meraki.com/api/v0/organizations
    method: GET
    headers:
      X-Cisco-Meraki-API-Key: "{{ MERAKI_API_KEY }}"
  register: meraki_org

- name: CHANGE MERAKI_ORG TO LOWER
  set_fact:
    meraki_org_lower: "{{ meraki_org.json | lower }}"

- name: GRAB DEVNET SANDBOX ORGANIZATION ID
  set_fact:
    meraki_org_id: "{{item.id}}"
  with_items: "{{meraki_org_lower}}"
  when: item.name == 'devnet sandbox'

Here we use the uri module to query the Cisco Meraki API in order to, first, grab the organizations that exist, and from there, grab the organization ID that has the name of “DevNet SandBox.” We have an additional task to utilize the set_fact module to alter the registered variable to be lowercase so we don’t miss the name due to a capitalized letter here and there. With this task, we now have the organization ID, which we will use in the next task to point to the new API endpoint.

NOTE: Many debug statements were used to figure out the key value pair, no animals were harmed.


- name: GRAB ORGANIZATION'S LIST OF NETWORKS
  uri:
    url: https://api.meraki.com/api/v0/organizations/{{meraki_org_id}}/networks
    method: GET
    headers:
      X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
  register: meraki_network

- name: GRAB ID OF TYPE WIRELESS AND NAME OF US AP DEPOT
  set_fact:
    meraki_network_id: "{{ item.id }}"
  with_items: "{{ meraki_network.json }}"
  when: item.type == 'wireless' and item.name == 'US AP Depot'

Now, an organization contains a list of networks. For our example, we want to grab a network that is of type “wireless,” but also having the name of “US AP Depot.” The type and name used in this example is specific to the Cisco DevNet Sandbox, but could easily be applied to any Cisco Meraki environment.

We now have the network ID of a specific wireless network named “US AP Depot,” which we will use to get the SSID of a specific Access Point (AP).


- name: GRAB SSID OF NETWORK
  uri:
    url: https://api.meraki.com/api/v0/networks/{{meraki_network_id}}/ssids
    method: GET
    headers:
      X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
  register: meraki_ssid

- name: GRAB ID OF US AP DEPOT WIFI
  set_fact:
    meraki_ssid_number: "{{item.number}}"
  with_items: "{{ meraki_ssid.json}}"
  when: item.name == 'US AP Depot WiFi'

Next, we use the meraki_network_id from the previous task to grab the SSID’s for the US AP Depot network. We also add a conditional when statement to ensure that we are grabbing the correct SSID, with the conditional being that the name equals US AP Depot WiFi


- name: GRAB L3 FIREWALL POLICIES
  uri:
    url: https://api.meraki.com/api/v0/networks/{{meraki_network_id}}/ssids/{{meraki_ssid_number}}/l3FirewallRules
    method: GET
    headers:
      X-Cisco-Meraki-API-Key: "{{MERAKI_API_KEY}}"
  register: meraki_l3firewallrules

- debug:
    msg: "{{meraki_l3firewallrules}}"

Lastly, we use the SSID number from the previous task to grab the L3 Firewall Policies from our Cisco Meraki Infrastructure. In summary, we were able to dive down from the organization to the access point and retrieve a security policy. As an extra step, if we change the URI request to a ‘PUT’ method and add a body, we can push/change the security policies.

PLAY [GRAB MERAKI SECURITY POLICY OF US AP DEPOT IN CISCO DEVNET] *****************************************************************

TASK [CHECK CONNECTION] ***********************************************************************************************************
ok: [localhost]

TASK [debug] **********************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "cache_control": "no-cache, no-store, max-age=0, must-revalidate",
        "changed": false,
        "connection": "close",
        "content_type": "application/json; charset=utf-8",
        "cookies": {},
        "cookies_string": "",
        "date": "Mon, 16 Jul 2018 12:50:41 GMT",
        "expires": "Fri, 01 Jan 1990 00:00:00 GMT",
        "failed": false,
        "json": [
            {
                "id": 537758,
                "name": "mCloud Demo",
                "samlConsumerUrl": "https://n149.meraki.com/saml/login/d029Cc/rtFn7bJtIRna",
                "samlConsumerUrls": [
                    "https://n149.meraki.com/saml/login/d029Cc/rtFn7bJtIRna"
                ]
            },
            {
                "id": 549236,
                "name": "DevNet Sandbox"
            },
            {
                "id": 646829496481089033,
                "name": "Test ENV"
            }
        ],
        "msg": "OK (unknown bytes)",
        "pragma": "no-cache",
        "redirected": true,
        "server": "nginx",
        "status": 200,
        "strict_transport_security": "max-age=15552000; includeSubDomains",
        "transfer_encoding": "chunked",
        "url": "https://n149.meraki.com/api/v0/organizations",
        "vary": "Accept-Encoding",
        "x_frame_options": "sameorigin",
        "x_request_id": "ce0f2dbfa4e1bf2a192191b4c652d6a2",
        "x_runtime": "0.206179",
        "x_ua_compatible": "IE=Edge,chrome=1"
    }
}

TASK [CHANGE MERAKI_ORG TO LOWER] *************************************************************************************************
ok: [localhost]

TASK [GRAB DEVNET SANDBOX ORGANIZATION ID] ****************************************************************************************
skipping: [localhost] => (item={u'samlconsumerurls': [u'https://n149.meraki.com/saml/login/d029cc/rtfn7bjtirna'], u'samlconsumerurl': u'https://n149.meraki.com/saml/login/d029cc/rtfn7bjtirna', u'name': u'mcloud demo', u'id': 537758})
ok: [localhost] => (item={u'id': 549236, u'name': u'devnet sandbox'})
skipping: [localhost] => (item={u'id': 646829496481089033, u'name': u'test env'})

TASK [GRAB ORGANIZATION'S LIST OF NETWORKS] ***************************************************************************************
ok: [localhost]

TASK [GRAB ID OF WIRELESS TYPE AND NAME OF US AP DEPOT] ***************************************************************************
skipping: [localhost] => (item={u'name': u'SM - Corp', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'systems manager', u'id': u'N_646829496481145308'})
skipping: [localhost] => (item={u'name': u'SM - Branch', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'systems manager', u'id': u'N_646829496481145309'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Always On Read Only', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099586'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Small Business Reservable 1', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099587'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Small Business Reservable 2', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099589'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Small Business Reservable 3', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099590'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Small Business Reservable 4', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099592'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'DevNet Small Business Reservable 5', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099593'})
skipping: [localhost] => (item={u'name': u'Camera Depot', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'camera', u'id': u'N_646829496481145361'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'EU AP Depot', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'wireless', u'id': u'N_646829496481145363'})
ok: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'US AP Depot', u'tags': u' Sandbox ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'wireless', u'id': u'N_646829496481145366'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'PythonChallenge', u'tags': u' tag1 tag2 ', u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'combined', u'id': u'L_646829496481099579'})
skipping: [localhost] => (item={u'disableMyMerakiCom': False, u'name': u'WW AP Depot', u'tags': None, u'organizationId': u'549236', u'timeZone': u'America/Los_Angeles', u'type': u'wireless', u'id': u'N_646829496481146258'})

TASK [GRAB SSID OF NETWORK] *******************************************************************************************************
ok: [localhost]

TASK [GRAB ID OF US AP DEPOT WIFI] ************************************************************************************************
ok: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': True, u'number': 0, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'US AP Depot WiFi'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 1, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 2'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 2, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 3'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 3, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 4'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 4, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 5'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 5, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 6'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 6, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 7'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 7, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 8'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 8, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 9'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 9, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 10'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 10, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 11'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 11, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 12'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 12, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 13'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 13, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 14'})
skipping: [localhost] => (item={u'ipAssignmentMode': u'NAT mode', u'splashPage': u'None', u'perClientBandwidthLimitDown': 0, u'enabled': False, u'number': 14, u'perClientBandwidthLimitUp': 0, u'ssidAdminAccessible': False, u'bandSelection': u'Dual band operation', u'minBitrate': 11, u'authMode': u'open', u'name': u'Unconfigured SSID 15'})

TASK [GRAB L3 FIREWALL POLICIES] **************************************************************************************************
ok: [localhost]

TASK [debug] **********************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "cache_control": "no-cache, no-store, max-age=0, must-revalidate",
        "changed": false,
        "connection": "close",
        "content_type": "application/json; charset=utf-8",
        "cookies": {},
        "cookies_string": "",
        "date": "Mon, 16 Jul 2018 12:50:47 GMT",
        "expires": "Fri, 01 Jan 1990 00:00:00 GMT",
        "failed": false,
        "json": [
            {
                "comment": "Wireless clients accessing LAN",
                "destCidr": "Local LAN",
                "destPort": "Any",
                "policy": "deny",
                "protocol": "Any"
            },
            {
                "comment": "Default rule",
                "destCidr": "Any",
                "destPort": "Any",
                "policy": "allow",
                "protocol": "Any"
            }
        ],
        "msg": "OK (unknown bytes)",
        "pragma": "no-cache",
        "redirected": true,
        "server": "nginx",
        "status": 200,
        "strict_transport_security": "max-age=15552000; includeSubDomains",
        "transfer_encoding": "chunked",
        "url": "https://n149.meraki.com/api/v0/networks/N_646829496481145366/ssids/0/l3FirewallRules",
        "vary": "Accept-Encoding",
        "x_frame_options": "sameorigin",
        "x_request_id": "d9ce7b26523317d4eb7f5be62a06f3d0",
        "x_runtime": "0.354606",
        "x_ua_compatible": "IE=Edge,chrome=1"
    }
}

PLAY RECAP ************************************************************************************************************************
localhost                  : ok=10   changed=0    unreachable=0    failed=0

Hopefully this was helpful and it opens your imagination to other things that you can accomplish via Ansible!

Read more

Upgrading Cisco IOS Devices with Ansible

Upgrading network images to stable and or later versions is nothing new in the networking world. A recent discussion with a customer, however, encouraged the creation of a simple, yet effective playbook to help automate this process.

The two use cases were around:

  1. CVE. A vulnerability is reported and an image upgrade is required to resolve it.

  2. Compliance checking. Ensure that the networking devices are at the compliant and approved version of code, and if not, make sure that it is.

At the time, the customer would SSH into each device to upgrade the image, wait for it to boot back up, check to make sure the image was upgraded to the correct version, then move onto the next device. In the case of the vulnerability, this became an all hands on deck activity that resulted in many hours from multiple engineers to patch the network devices.

Now, let’s take a look at an Ansible Playbook that can automate this process.


---
- name: UPGRADE ROUTER FIRMWARE
  hosts: routers
  connection: network_cli
  gather_facts: no

  vars:
    compliant_ios_version: 16.08.01a

  tasks:
    - name: GATHER ROUTER FACTS
      ios_facts:

    - name: UPGRADE IOS IMAGE IF NOT COMPLIANT
      block:
      - name: COPY OVER IOS IMAGE
        command: "scp system-image-filename.bin {{inventory_hostname}}:/system-image-filename.bin"

      - name: SET BOOT SYSTEM FLASH
        ios_config:
          commands:
            - "boot system flash:system-image-filename.bin"

      - name: REBOOT ROUTER
        ios_command:
          commands:
            - "reload\n"

      - name: WAIT FOR ROUTER TO RETURN
        wait_for:
          host: "{{inventory_hostname}}"
          port: 22
          delay: 60
        delegate_to: localhost

      when: ansible_net_version != compliant_ios_version

    - name: GATHER ROUTER FACTS FOR VERIFICATION
      ios_facts:

    - name: ASSERT THAT THE IOS VERSION IS CORRECT
      assert:
        that:
          - compliant_ios_version == ansible_net_version

Let’s step through the components!

First, the playbook level variables:

---
- name: UPGRADE ROUTER FIRMWARE
  hosts: routers
  connection: network_cli
  gather_facts: no

  vars:
    compliant_ios_version: 16.08.01a

  tasks:

We’re passing in the following:
name - Naming our playbook.
hosts - What nodes are we targeting? In this case, we are targeting a group called routers.
connection - Defining the connection type.
gather_facts - This is a server specific task and does not apply to us.
vars - Defining a variable that we will use to compare IOS versions.
tasks - Where we will list the tasks we want the playbook to accomplish.

The first task:

- name: GATHER ROUTER FACTS
  ios_facts:

The above tasks runs the ios_facts module which collects facts from remote devices running Cisco IOS.
ios_facts_module

The ios_facts module provides us with the ansible_net_version which defines the operating system version running on the remote device. We’ll be using this as conditional logic in our proceeding tasks.

Next, we create a block which contains a list of tasks. We put a conditional on this block with a when statement near the bottom of the playbook - or at the end of the block. The when statement does a check to see if the current version of the remote device is NOT equal to the version we specify. If it’s NOT equal, the tasks in the block will execute and proceed to upgrade the remote device to the version of code that’s required.


- name: UPGRADE IOS IMAGE IF NOT COMPLIANT
  block:
  - name: COPY OVER IOS IMAGE
    command: "scp system-image-filename.bin {{inventory_hostname}}:/system-image-filename.bin"

  - name: SET BOOT SYSTEM FLASH
    ios_config:
      commands:
        - "boot system flash:system-image-filename.bin"

  - name: REBOOT ROUTER
    ios_command:
      commands:
        - "reload\n"

  - name: WAIT FOR ROUTER TO RETURN
    wait_for:
      host: "{{inventory_hostname}}"
      port: 22
      delay: 60
    delegate_to: localhost

  when: ansible_net_version != compliant_ios_version

The first task within the block is to copy over the Cisco IOS image to the remote device. We utilize the SCP command to do so.

Next, we utilize the ios_config module to set the boot system of the remote device so it’ll use the image the new image if the version is not compliant or what we want it to be.

We are now ready to reboot the remote device so it boots into the correct image. We pass in “reload\n” into the ios_command to perform the reload.

The next task, as the name suggests, waits for the remote device to reachable again. We specify the port we want to test reachability against and an initial delay before we begin to poll for reachability. Once the remote device is reachable, we do a final verification of the version.


- name: GATHER ROUTER FACTS FOR VERIFICATION
  ios_facts:

- name: ASSERT THAT THE IOS VERSION IS CORRECT
  assert:
    that:
      - compliant_ios_version == ansible_net_version

With the remote device having been rebooted, we execute the ios_facts module again to capture the new ansible_net_version.

Finally, we use the assert module to ensure that the upgrade was successful and the remote device is running the compliant IOS version. By using the assert module, we make the playbook fail if it is NOT the correct version.

Read more