Exploring Python’s args and kwargs in a Networking Context

Blog Detail

At a certain point in time, while working with Python, you might have seen code that uses *args and **kwargs as parameters in functions. This feature brings excellent flexibility when developing Python code. I’m going to go over some of the basics of packing arguments into a function, showing how to pass a variable amount of positional arguments and key-value pairs into a function. Feel free to follow along if you have access to a Python interpreter.

Note: Print functions are used for demonstration. These functions don’t create a connection to a device. The goal is to focus on *args and **kwargs.

Using args

The current function, device_connection, has four different required positional parameters and are printed out inside the function.


def device_connection(ip, device_type, username, password):
    print("ip: ", ip)
    print("device_type: ", device_type)
    print("username :", username)
    print("password: ", password)

This next step is calling the device_connection function and it’s passing string arguments at the exact number of parameters in the function. As expected, when calling the function, the data is printed.


>>>
>>> device_connection("10.10.10.2", "cisco_ios", "cisco", "cisco123")
ip:  10.10.10.2
device_type:  cisco_ios
username : cisco
password:  cisco123
>>>

As mentioned before, these are positional parameters and if the order in which they are called is incorrect, then the output is printed inaccurately. In this case, the device connection is not established if the function was built correctly for connectivity. The next example shows what it looks like when calling the function incorrectly.


>>>
>>> device_connection("cisco_ios", "10.10.10.2", "cisco", "cisco123")
ip:  cisco_ios
device_type:  10.10.10.2
username : cisco
password:  cisco123
>>>

Notice how in the above output, the IP value is now populated with the device type string while the device_type is populated with 10.10.10.2.

Another caveat could be if another argument is defined, because the connection requires a different SSH port to establish the connection. If added when calling the function, it fails because the current function was only built to take 4 arguments. Take a look at the example below. The new argument with the port number is added to the function.


>>>
>>> device_connection("cisco_ios", "10.10.10.2", "cisco", "cisco123", "8022")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: device_connection() takes 4 positional arguments but 5 were given
>>>

The next section is going to show how to solve some of these issues.

Using *args

The basics of packing with *-operator are compressing multiple values into an agument/parameter. The first line inside the function is using the type function inside a print statement to display the data type returned from the *args variable. The next few lines have print statements accessing each element of the tuple, which are accessed similar to a list by the index number.

Note: The asterisk (*) symbol is needed to pack the data into a tuple, or it fails and views the parameter as a single argument when the function call is initiated.


def device_connection(*args):
    print(type(args))
    print("tuple: ", args)
    print("ip: ", args[0])
    print("device_type: ", args[1])
    print("username: ", args[2])
    print("password: ", args[3])

The example below calls the device_connection function, except for this time, the parameter used when building the function is *args.


>>>
>>> device_connection("10.10.10.2", "cisco_ios", "cisco", "cisco123")
<class 'tuple'>
tuple:  ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123')
ip:  10.10.10.2
device_type:  cisco_ios
username:  cisco
password:  cisco123
>>>

Notice how the function output above printed each argument added to function call and it was “compressed” into the args variable.

Another way of accessing data from a tuple is to use a for loop like the example below.


def device_connection(*args):
    print(type(args))
    print("tuple: ", args)
    for arg in args:
        print(arg)

Data can also be assigned to variables and used as input when calling the function.


ip = "10.10.10.2"
device_type = "cisco_ios"
username = "cisco"
password = "cisco123"

Check out the output when the function is called.


>>>
>>> device_connection(ip, device_type, username, password)
<class 'tuple'>
tuple:  ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123')
10.10.10.2
cisco_ios
cisco
cisco123
>>>

Notice how each value was accessed using a for loop when called inside the function without having to provide the index.

Another great thing about using *args when creating a function is that it can take multiple inputs without having to specify it when building the function. So if another value needs to be processed, then it can be passed as another argument when calling the function.

Note: The * asterisk is doing all the “magic” when packing all the arguments; args can be any arbitrary variable.

In this step a new variable called port is created with a value of 22.

port = 22

The example below shows the output when calling the device_connection when passing all the same previous variables plus the new variable.


>>>
>>> device_connection(ip, device_type, username, password, port)
<class 'tuple'>
tuple:  ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123', 22)
10.10.10.2
cisco_ios
cisco
cisco123
22
>>>

Using **kwargs

Previously when passing parameters with no *-operator, it was by just passing a variable argument. If the same number of arguments aren’t passed when calling the function, it returns with an error. When using the single *-operator multiple arguments could be passed as options and the data is packed into a tuple.

Another way of using function parameters is to use the ** double asterisk operator and any arbitrary variable when packing key:value pairs as a parameter into a dictionary.

The function below is using **kwargs with a variable. Inside the function, it prints the data type and the data that is passed when calling the function.


def device_connection(**kwargs):
    print(type(kwargs))
    print(kwargs)

The example below shows the output when calling the function with two keyword arguments.


>>>
>>> device_connection(device_type="ios", ip="10.10.10.2")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2'}
>>>

Like mentioned before, the (**) asterisk operator is doing the work to pack the data into a variable. Users don’t have to use the variables named args and kwargs. They could use an arbitrary variable such as **connection_data instead of **kwargs, as shown in the example below.


def device_connection(**connection_data):
    print(type(connection_data))
    print(connection_data)

The example below shows the output when calling the function using **connection_data.


>>>
>>> device_connection(device_type="ios", ip="10.10.10.2")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2'}
>>>

This next example calls the function again, but with more keyword arguments.


>>>
>>> device_connection(device_type="ios", ip="10.10.10.2", username="cisco", password="cisc123")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisc123'}
>>>

Notice how the data type output is a dictionary and the input arguments are key:value pairs.

Something else to point out is that *args can also be used with **kwargs together in a single function. Using both *args and **kwargs together gives the function some more options to choose from in terms of how it can consume the data.

The example below is using a variable containing a dictionary of device data.


csr1 = {
    'device_type': 'ios',
    'ip':   '10.10.10.2',
    'username': 'cisco',
    'password': 'cisco123',
    'port': 8022,
    'secret': 'secret'}

The function below is built to take in both types of parameters.


def device_connection(*args, **kwargs):
    print("*args: ", type(args))
    print("**kwargs: ", type(kwargs))
    print("args[0]: ", args[0])
    print("args: ", args)
    print("kwargs: ", kwargs)

When calling the function, notice how the first argument is the variable. The second and third arguments are keyword arguments.

The first output prints out a tuple for *args, the second output is dict for **kwargs, the third output gets access to the dictionary by accessing index 0 in the tuple and the last two outputs print out the data “packed” in args and kwargs.


>>>
>>> device_connection(csr1, device_type="ios", host="csr1")
*args:  <class 'tuple'>
**kwargs:  <class 'dict'>
args[0]:  {'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisco123', 'port': 8022, 'secret': 'secret'}
args:  ({'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisco123', 'port': 8022, 'secret': 'secret'},)
kwargs:  {'device_type': 'ios', 'host': 'csr1'}
>>>


Conclusion

The examples shown in this blog use networking data to explore Python parameters in a function using *(for tuples) and **(for dictionaries). There are different ways of building Python functions, and they don’t always need to use * and ** operators for every function built. Sometimes functions can be basic and only need specific data. But when a function takes in variable amount or unknown amount of data, then * and ** operators can come in handy to build more flexible functions.

-Hector



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Using Ansible through a Bastion Host

Blog Detail

This blog is going to go over the concepts of using Ansible to interact with a private network through a bastion host. The use of bastion hosts is not new to the industry—they have been used for a while by many companies that need to give users access to private networks. Bastion hosts are typically public facing, hardened systems that work as an entrypoint to systems that are behind a firewall or any other restricted locations.

Set up SSH public key authentication with bastion host

When interacting with a bastion host, the recommended first step is to set up SSH public keys and authenticate with the bastion host.

If an SSH key doe not already exist, create it by issuing the command ssh-keygen -t rsa

  root@eb54369adc49:/ntc# ssh-keygen -t rsa
  Generating public/private rsa key pair.
  Enter file in which to save the key (/root/.ssh/id_rsa):
  Created directory '/root/.ssh'.
  Enter passphrase (empty for no passphrase):
  Enter same passphrase again:
  Your identification has been saved in /root/.ssh/id_rsa.
  Your public key has been saved in /root/.ssh/id_rsa.pub.
  The key fingerprint is:
  SHA256:T5YDoF91QrJk2ZN1b7OYq9Pfn3OWVW518iCJHGIQKeQ root@eb54369adc49
  The key's randomart image is:
  +---[RSA 2048]----+
  |   .. +++++oo .  |
  |   ....+++=o . . |
  |    E. .+o + . .o|
  |     . . .o.o =.*|
  |      . S =  + *+|
  |         + .  . =|
  |          . .. .o|
  |           ... o=|
  |           .. .+*|
  +----[SHA256]-----+
  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc#

Since this is just for demonstration, I’ve kept everything as default when creating the keys.

Note: Click here if you want to know more about how to set up an SSH public key with more details.

After creating the public key, the ssh-copy-id Linux command can be used to transfer the id_rsa.pub public key to the bastion host.

  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc# ssh-copy-id ntc@bastion
  /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"
  /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are   already installed
  /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to   install the new keys
  ntc@bastion password:
  
  Number of key(s) added: 1
  
  Now try logging into the machine, with:   "ssh 'ntc@bastion'"
  and check to make sure that only the key(s) you wanted were added.
  
  root@eb54369adc49:/ntc#

Finally, attempt to SSH into the bastion host and make sure it works as expected. The workstation should log in through SSH without having to provide a password.

  root@eb54369adc49:/ntc# ssh ntc@bastion
  Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-59-generic x86_64)
  
   * Documentation:  https://help.ubuntu.com
   * Management:     https://landscape.canonical.com
   * Support:        https://ubuntu.com/advantage
  
  466 packages can be updated.
  321 updates are security updates.
  
  New release '18.04.3 LTS' available.
  Run 'do-release-upgrade' to upgrade to it.
  
  Last login: Mon Jan 13 13:44:58 2020 from 71.52.24.123
  ntc@bastion:~$
  ntc@bastion:~$ exit
  logout
  Connection to bastion closed.
  root@eb54369adc49:/ntc#

There are many different ways of creating a public key and transferring it to the remote host. This transfer can also be handled via Ansible, as the below playbook shows:


---
    - name: Generate Key
      hosts: localhost
      connection: local
      gather_facts: no

      tasks: 
          - name: Generate an OpenSSH keypair with the default values (4096 bits, rsa)
            openssh_keypair:
                path: /root/.ssh/id_rsa
                owner: root
                group: root

          - name: Deploy public key to remote host
            shell: ssh-copy-id ntc@bastion

Setting up Ansible to Interact with a Cisco IOS Device through a Bastion Host

Now that it is possible to authenticate into the bastion host only using SSH keys and not require user/passwords, it’s time to build the Ansible inventory file with the devices that are going to be targeted by Ansible. Before creating the inventory file, notice how the only way to interact with the Cisco csr1 device is through the bastion host.

 # Topology
 ┏━━━━━━━━━━━━━━┓          ┏━━━━━━━━━━━━━━┓          ┏━━━━━━━━━━━━┓         
 ┃  Container   ┃──────────┃ Bastion host ┃──────────┃    csr1    ┃
 ┗━━━━━━━━━━━━━━┛          ┗━━━━━━━━━━━━━━┛          ┗━━━━━━━━━━━━┛

This first test is using the ping command to ping the csr1 device from the local container. The results should come back with 100% packet loss.

  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc# ping csr1 -c 5
  PING csr1 (54.84.81.49) 56(84) bytes of data.
  
  --- csr1 ping statistics ---
  5 packets transmitted, 0 received, 100% packet loss, time 4176ms

After using the SSH command to access the bastion host, the ping command is used again to show that there is IP connectivity between the bastion host and the networking device.

  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc# ssh ntc@bastion
  Welcome to Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-59-generic x86_64)
  
   * Documentation:  https://help.ubuntu.com
   * Management:     https://landscape.canonical.com
   * Support:        https://ubuntu.com/advantage
  
  466 packages can be updated.
  321 updates are security updates.
  
  New release '18.04.3 LTS' available.
  Run 'do-release-upgrade' to upgrade to it.
  
  Last login: Mon Jan 13 14:38:26 2020 from 71.52.24.123
  ntc@bastion:~$
  ntc@bastion:~$ ping csr1 -c 5
  PING csr1 (10.0.0.51) 56(84) bytes of data.
  64 bytes from csr1 (10.0.0.51): icmp_seq=2 ttl=255 time=2.34 ms
  64 bytes from csr1 (10.0.0.51): icmp_seq=3 ttl=255 time=2.26 ms
  64 bytes from csr1 (10.0.0.51): icmp_seq=4 ttl=255 time=1.99 ms
  64 bytes from csr1 (10.0.0.51): icmp_seq=5 ttl=255 time=2.23 ms
  
  --- csr1 ping statistics ---
  5 packets transmitted, 4 received, 20% packet loss, time 4013ms
  rtt min/avg/max/mdev = 1.994/2.209/2.342/0.138 ms
  ntc@bastion:~$

Now that you can verify direct connectivity from the container to the csr1 device is only possible through the bastion host, the ProxyCommand can be used to gain access to the Cisco device from the container.

Note: ProxyCommand is an SSH builtin command feature to provide support for proxy use cases.

  root@eb54369adc49:/ntc#
  root@eb54369adc49:/ntc# ssh -o ProxyCommand="ssh -W %h:%p ntc@bastion" ntc@csr1
  The authenticity of host 'csr1 (<no hostip for proxy command>)' can't be established.
  RSA key fingerprint is SHA256:BaRERnJrQ4vzALKQELc6lIs7Kujbe3UCPffPcAhcRKg.
  Are you sure you want to continue connecting (yes/no)? yes
  Warning: Permanently added 'csr1' (RSA) to the list of known hosts.
  Password:
  
  
  
  csr1#
  csr1#

Playbook

For this test the ios_command and ios_config will be used. The backup parameter has been enabled to show that the backup files are stored in the local machine and not on the bastion host.


---

    - name: Cisco IOS Playbook
      hosts: ios
      connection: network_cli
      gather_facts: no

      tasks:

        - name: SHOW VERSION
          ios_command:
            commands: show version

        - name: CONFIGURE SNMP COMMUNITY
          ios_config:
            commands: snmp-server community ntc-blog RW
            backup: true

Inventory

For now, only the basic credentials and device type are required to target the device.

  [all:vars]
  ansible_user=ntc
  ansible_ssh_password=ntc123
  
  [ios]
  csr1 
  
  [ios:vars]
  ansible_network_os=ios

Note: There is a dedicated group vars for IOS only for now, since a Juniper device will be tested later in this blog too.

First Execution of Playbook

Similar to the previous tests, the local machine should not be able to communicate with the Cisco device, since it does not have direct access to the private network.

  root@eb54369adc49:ntc$ ansible-playbook -i inventory bastion_playbook.yml
  
  PLAY [Cisco IOS Playbook] ******************************************************************
  
  TASK [SHOW VERSION] ************************************************************************
  fatal: [csr1]: FAILED! => {"changed": false, "msg": "timed out"}
  
  PLAY RECAP *********************************************************************************
  csr1 : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
  
  root@eb54369adc49:ntc$

Executing a Playbook against a Cisco IOS Device through a Bastion Host

To be able to communicate with the Cisco device the ProxyCommand needs to be executed when trying to SSH into the device. To start using the ProxyCommand with Ansible the ansible_ssh_common_args, a variable needs to be added to the inventory file.

  [all:vars]
  ansible_user=ntc
  ansible_ssh_password=ntc123
  
  [ios]
  csr1 
  
  [ios:vars]
  ansible_network_os=ios
  ansible_ssh_common_args=ProxyCommand="ssh -W %h:%p ntc@bastion"

More details can be found in the Ansible documentation about how to use the ansible_ssh_common_args and ProxyCommand. For now, I’ve added a device to the inventory file with the needed credentials, and the ProxyCommand used earlier and tested through the Linux terminal.

The Ansible playbook can now be executed normally using the standard ansible-playbook command. The only modification needed to interact with the networking device was enabling SSH keys and using the ansible_ssh_common_args in the inventory file.

  root@eb54369adc49:/ntc# ansible-playbook -i inventory bastion_playbook.yml -v
  Using /ntc/ansible.cfg as config file
  
  PLAY [Cisco IOS Playbook] ***********************************************************
  
  TASK [SHOW VERSION] ***************************************************************
  
  ok: [csr1] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python"}
  "changed": false, "stdout": ["Cisco IOS XE Software, 
  Version 16.06.02\nCisco IOS Software [Everest], Virtual XE Software
  (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.6.2, RELEASE
  SOFTWARE (fc2)\nTechnical Support: http://www.cisco.com
  techsupport\nCopyright (c) 1986-2017 by Cisco Systems, Inc
  \nCompiled Wed 01-Nov-17 07:24 by mcpre\n\n\nCisco IOS-X
  software, Copyright (c) 2005-2017 by cisco Systems, Inc
  \nAll rights reserved.  Certain components of Cisco IOS-XE
  software are\nlicensed under the GNU General Public License" ...omitted]]}
  
  TASK [CONFIGURE SNMP COMMUNITY] ***********************************************************
  changed: [csr1] => {"backup_path": "/ntc/backup/csr1_config.2020-01-13@22:02:16", "banners": 
  {}, "changed": true, "commands": ["snmp-server community ntc-blog RW"],
  "date": "2020-01-13", "filename": "csr1_config.2020-01-13@22:02:16", "shortname": "/ntc
  backup/csr1_config", "time": "22:02:16", "updates": ["snmp-server community ntc-blog RW"]}
  
  PLAY RECAP *********************************************************************************
  csr1    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Setting up Ansible to Interact with a Juniper vMX Device using NETCONF through a Bastion Host

To interact with a Juniper device via a bastion host that uses NETCONF, a different variable is required and a configuration file needs to be built. This link gives some details on different ways to enable the feature, but for this test I’ll be adding the ansible_netconf_ssh_config=./ntc/netconf_proxy_config.cfg variable with the path to my netconf_proxy_config.cfg file.

Note: Even thought the Ansible documentation is not entirely clear why ansible_ssh_common_args does not work with NETCONF devices, maybe this link can provide insight to that question.

  [all:vars]
  ansible_user=ntc
  ansible_ssh_password=ntc123
  
  [ios]
  csr1 
  
  [ios:vars]
  ansible_network_os=ios
  ansible_ssh_common_args=ProxyCommand="ssh -W %h:%p ntc@bastion"
  
  [vmx]
  vmx1
  
  [vmx:vars]
  ansible_network_os=junos
  ansible_netconf_ssh_config=./ntc/netconf_proxy_config.cfg

Setting up the netconf_proxy_config.cfg and ansible.cfg file

The Ansible documentation shows how to build a proper netconf_proxy_config.cfg file. However, since I’m only testing with a single device, the only requirement is the ProxyCommand that will establish the SSH connection with the bastion host and the Juniper device.

ProxyCommand ssh -W %h:%p ntc@bastion

The next step will be to add a second play using the correct modules to interact with a Juniper device.

Executing a Playbook against a Juniper vMX Device using NETCONF through a Bastion Host

The example playbook has been extended to include not only the original Cisco IOS tasks, but an additional play to run the equivalent on Juniper devices. The goal is to run a show version and make a configuration change to add an SNMP community to the device configuration.


---

    - name: Cisco IOS Playbook
      hosts: ios
      connection: network_cli
      gather_facts: no
      tags: cisco

      tasks:

        - name: SHOW VERSION
          ios_command:
            commands: show version

        - name: CONFIGURE SNMP COMMUNITY
          ios_config:
            commands: snmp-server community ntc-blog RW
            backup: true

    - name: Juniper vMX Playbook
      hosts: vmx
      connection: netconf
      gather_facts: no
      tags: juniper
      
      tasks:

        - name: SHOW VERSION
          junos_command:
            commands: show version

        - name: CONFIGURE SNMP COMMUNITY
          junos_config:
            commands: set snmp community ntc-blog authorization read-write

After building the playbook, it’s time to execute it and see the results. In this case I ran the playbook and added a tag so only the Juniper play gets executed.

  root@eb54369adc49:/ntc# ansible-playbook -i inventory bastion_playbook.yml --tags juniper -v
  Using /ntc/ansible.cfg as config file
  
  PLAY [Cisco IOS Playbook] *******************************************************************
  
  PLAY [Juniper IOS Playbook] *****************************************************************
  
  TASK [SHOW VERSION] *************************************************************************
  ok: [vmx1] => {"changed": false, "stdout": ["Hostname: vmx1\nModel: vmx\nJunos: 15.1F4.15
  \nJUNOS Base OS boot [15.1F4.15]\nJUNOS Base OS Software Suite [15.1F4.15]\nJUNOS Crypto 
  Software Suite [15.1F4.15]\nJUNOS OnlineDocumentation [15.1F4.15]\nJUNOS 64-bit Kernel Software 
  Suite [15.1F4.15]\nJUNOS Routing Software Suite [15.1F4.15]\nJUNOS Runtime Software Suite [15.1F4.  15]
  \nJUNOS 64-bit Runtime Software Suite [15.1F4.15] \nJUNOS Services AACL PIC package [15.1F4.15]  \nJUNOS 
  Services Application Level Gateway (xlp64) [15.1F4.15]\nJUNOS Services ..omitted"]]}
  
  TASK [CONFIGURE SNMP COMMUNITY] *************************************************************
  changed: [vmx1] => {"changed": true}
  
  PLAY RECAP **********************************************************************************
  vmx1  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Conclusion

In order to use Ansible through a bastion host with core modules, SSH keys are recommended. When interacting with devices that support NETCONF, the ansible_netconf_ssh_config variable is required to be enabled and for traditional SSH devices the ansible_ssh_common_args is required.

Other third party modules like ntc_show_command or NAPALM can also be used but will not be able to support ansible_ssh_common_args or ansible_netconf_ssh_config. In this case, the delegate_to argument will be needed to use a third party module with a jump-host/bastion-host.

-Hector



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Ansible gather_facts for Networking Devices

Blog Detail

The purpose of this blog is to show some differences in how Ansible handled gathering facts from networking devices prior to version 2.9 and how they can be gathered in version 2.9.2. A main point to show here is how previously the gather_facts key was disabled when working with networking devices and now it can be enabled to gather device facts.

Gathering *_facts the “Old” way

Ansible core comes included with vendor specific *_facts modules. So normally to collect facts from devices the playbook was built using the vendor specific facts module and the needed key was used for any other particular task like asserting device versions, building reports, etc. The example below is a playbook used to gather facts from a Cisco device.


---
 - name: GATHER FACTS FOR IOS
   hosts: csr1
   connection: network_cli
   gather_facts: no    #<---Gather Facts Disabled

   tasks:

     - name: GATHER FACTS FOR IOS IN A TASK
       ios_facts:

     - name: VIEW ALL ANSIBLE FACT KEYS
       debug:
         var: ansible_facts.keys()

     - name: VIEW HOSTNAME
       debug:
         var: ansible_net_hostname
     
     - name: VIEW OS VERSION
       debug:
         var: ansible_net_version

Note: The first task is using the ios_facts module to gather all the data, the other three tasks are used to display the data returned from the facts module and keys inside ansible_facts can be accessed directly.

Notice how in the play definition the key gather_facts is set to no. Originally, Ansible was designed to work with Linux servers. Ansible focuses on collecting as much data as possible from the remote Linux servers and copying Python modules to the servers for automation tasks so they could be executed on the remote machines.

When working with networking devices Ansible does not copy Python modules to the remote devices to run an automation task. The Python module is executed in the local Ansible workstation and things like sending show commands or configuration changes are wrapped in the Python module and sent over through SSH. The gather_facts key was not designed to know the difference between a Linux server and a networking device, so by enabling the feature would end up gathering facts from the local Ansible workstation and not the remote networking device.

Running the Playbook with gather_facts disabled

The output of the current playbook would look like this:

Note: The current Ansible version is 2.7.10

ntc@jump-host:~$ ansible-playbook -i inventory network_facts.yml

PLAY [GATHER FACTS FOR IOS] ********************************************************************************

TASK [GATHER FACTS FOR IOS] ********************************************************************************
ok: [csr1]

TASK [VIEW ALL ANSIBLE FACT KEYS] **************************************************************************
ok: [csr1] => {
    "ansible_facts.keys()": "dict_keys(['net_serialnum', 'net_all_ipv4_addresses', 
'net_model', 'net_hostname', 'net_gather_subset', 'net_filesystems_info', 'net_interfaces', 
'net_version', 'net_neighbors', 'net_all_ipv6_add resses', 'net_memtotal_mb', 'net_filesystems', 
'net_image', 'net_memfree_mb'])"
}

TASK [VIEW HOSTNAME] ***************************************************************************************
ok: [csr1] => {
    "ansible_net_hostname": "csr1"
}

TASK [VIEW OS VERSION] *************************************************************************************
ok: [csr1] => {
    "ansible_net_version": "16.08.01a"
}

PLAY RECAP *************************************************************************************************
csr1                        : ok=4    changed=0    unreachable=0    failed=0

The current output only displays facts that belong to the remote device. The keys seen in the output are the ones available from the ios_facts module.

Running the Playbook with gather_facts enabled

When the gather_facts key is enabled in the play definition (gather_facts: yes) the output will return not only data from the remote device but also from the Ansible workstation as seen below:

ntc@jump-host:~$ ansible-playbook -i inventory network_facts.yml

PLAY [GATHER FACTS FOR IOS] ********************************************************************************

TASK [Gathering Facts] *************************************************************************************
ok: [csr1]

TASK [GATHER FACTS FOR IOS] ********************************************************************************
ok: [csr1]

TASK [VIEW ALL ANSIBLE FACT KEYS] **************************************************************************
ok: [csr1] => {
    "ansible_facts.keys()": "dict_keys(['module_setup', 'distribution_version', 'distribution_file_variety', 'env',
    'userspace_bits', 'architecture', 'default_ipv4', 'swapfree_mb', 'default_ipv6', 'cmdline', 'selinux',
    'userspace_architecture', 'product_uuid', 'pkg_mgr', 'distribution', 'iscsi_iqn', 'all_ipv6_addresses', 
    'uptime_seconds', 'kernel', 'system_capabilities_enforced', 'python', 'is_chroot', 'user_shell', 'product_serial'
    'form_factor', 'distribution_file_parsed', 'fips', 'user_id', 'selinux_python_present', 'ansible_local',
    'processor_vcpus', 'processor', 'ssh_host_key_ecdsa_public', 'mounts', 'system_vendor', 'swaptotal_mb',
    'distribution_major_version', 'real_group_id', 'lsb', 'machine', 'ssh_host_key_rsa_public', 'user_gecos',
    'processor_threads_per_core', 'eth1', 'product_name', 'all_ipv4_addresses', 'python_version',
    'product_version', 'service_mgr', 'memory_mb', 'user_dir', 'gather_subset', 'real_user_id', 
    'virtualization_role', 'tunl0', 'dns', 'effective_group_id', 'lo', 'memtotal_mb', 'device_links', 
    'apparmor', 'memfree_mb', 'processor_count', 'hostname', 'interfaces', 'machine_id', 'fqdn', 'user_gid',
    'nodename', 'domain', 'distribution_file_path', 'virtualization_type', 'ssh_host_key_ed25519_public', 
    'processor_cores', 'bios_version', 'date_time', 'distribution_release', 'os_family', 'effective_user_id',
    'sit0', 'system', 'devices', 'user_uid', 'ssh_host_key_dsa_public', 'bios_date', 'system_capabilities',
    'ens3', 'net_serialnum', 'net_all_ipv4_addresses', 'net_model', 'net_hostname', 'net_gather_subset',
    'net_filesystems_info', 'net_interfaces', 'net_version', 'net_neighbors', 'net_all_ipv6_addresses',
    'net_memtotal_mb', 'net_filesystems', 'net_image', 'net_memfree_mb'])"
}

TASK [VIEW HOSTNAME] ***************************************************************************************
ok: [csr1] => {
    "ansible_net_hostname": "csr1"
}

TASK [VIEW OS VERSION] *************************************************************************************
ok: [csr1] => {
    "ansible_net_version": "16.08.01a"
}

PLAY RECAP *************************************************************************************************
csr1                        : ok=5    changed=0    unreachable=0    failed=0

Gathering Facts from different platforms

Another thing to point out is that when using the core *_facts modules if another vendor or platform needs to be added to the playbook another play would need to be built to gather facts from that vendor or platform. Without this addition, the module will fail because the ios_facts module is vendor specific and it will try to gather facts from any devices targeted in the scope of that play. The playbook would look something like this:

---
 - name: GATHER FACTS FOR IOS
   hosts: csr1
   connection: network_cli
   gather_facts: no

   tasks:

     - name: GATHER FACTS FOR IOS
       ios_facts:

     - name: VIEW ALL ANSIBLE FACT KEYS
       debug:
         var: ansible_facts.keys()

     - name: VIEW HOSTNAME
       debug:
         var: ansible_net_hostname
     
     - name: VIEW OS VERSION
       debug:
         var: ansible_net_version
  
 - name: GATHER FACTS FOR NXOS
   hosts: nxos
   connection: network_cli
   gather_facts: no

   tasks:

     - name: GATHER FACTS FOR NXOS
       nxos_facts:

     - name: VIEW ALL ANSIBLE FACT KEYS
       debug:
         var: ansible_facts.keys()

     - name: VIEW HOSTNAME
       debug:
         var: ansible_net_hostname
    
     - name: VIEW OS VERSION
       debug:
         var: ansible_net_version

Gather Facts the “New” way

There have been a lot of changes with Ansible focused on improving how to manage networking devices. More and more the platform has been used in multi-vendor environments, meaning that the tool needs to be more robust and vendor neutral. Recent features and improvements are being built to be more “vendor neutral” – such as cli_config and cli_command (added in Ansilble version 2.7).

In Ansible version 2.9, gathering facts from devices from the play definition is now possible without having to build it as a task, like shown in the previous playbook. This time the key gather_facts can be enabled and not gather facts from the local workstation but from the devices targeted in the hosts key.

Note: The modules ios_facts and nxos_fact are still being used but are now being executed in the play definition.

---

 - name: GATHER FACTS
   hosts: csr1,nxos
   connection: network_cli
   gather_facts: yes  #<----Gather Facts Enabled 

   tasks:
   
    #NO VENDOR SPECIFIC MODULE LIKE ios_facts OR nxos_facts ANYMORE

    - name: VIEW ALL ANSIBLE FACT KEYS
      debug:
       var: ansible_facts.keys()

    - name: VIEW HOSTNAME
      debug:
        var: ansible_net_hostname
    
    - name: VIEW OS VERSION
      debug:
        var: ansible_net_version

Take a look at the current playbook. The hosts key has two different operating systems which normally would require another play when trying to gather data from different platforms because of the vendor specific modules defined in the tasks. This time gathering facts is being done at the play definition level and at the tasks level only the debug modules are being used to display the available keys and variables to see hostname and version information.

Running the Playbook with gather_facts enabled the “New Way”

ntc@jump-host:~$ ansible-playbook -i inventory network_facts.yml

PLAY [GATHER FACTS] ***************************************************************************************

TASK [Gathering Facts] ************************************************************************************


ok: [csr1]
ok: [nxos]

TASK [VIEW ALL ANSIBLE FACT KEYS] *************************************************************************
ok: [csr1] => {
    "ansible_facts.keys()": "dict_keys(['network_resources', 'net_gather_network_resources', 
    'net_gather_subset', 'net_system', 'net_model', 'net_image', 'net_version', 'net_hostname', 
    'net_api', 'net_python_version', 'net_iostype', 'net_serialnum', 'net_filesystems', 
    'net_filesystems_info', 'net_memtotal_mb', 'net_memfree_mb', 'net_config', 'net_all_ipv4_addresses',
    'net_all_ipv6_addresses', 'net_neighbors', 'net_interfaces', '_facts_gathered'])"
}
ok: [nxos] => {
    "ansible_facts.keys()": "dict_keys(['network_resources', 'net_gather_network_resources', 
    'net_gather_subset', 'net__os', 'net__platform', 'net__hostname', 'net_interfaces_list', 
    'net_vlan_list', 'net_module', 'net_fan_info', 'net_power_supply_info', 'net_filesystems', 
    'net_memtotal_mb', 'net_memfree_mb', 'net_features_enabled', 'net_all_ipv4_addresses', 
    'net_all_ipv6_addresses', 'net_neighbors', 'net_interfaces', 'net_serialnum', 'net_license_hostid',
    'net_system', 'net_model', 'net_image', 'net_version', 'net_platform', 'net_hostname', 'net_api', 
    'net_python_version', 'net_config', '_facts_gathered'])"
}

TASK [VIEW HOSTNAME] ***************************************************************************************
ok: [csr1] => {
    "ansible_net_hostname": "csr1"
}
ok: [nxos] => {
    "ansible_net_hostname": "nxos-spine1"
}

TASK [VIEW OS VERSION] **************************************************************************************
ok: [csr1] => {
    "ansible_net_version": "16.08.01a"
}
ok: [nxos] => {
    "ansible_net_version": "7.0(3)I7(4)"
}

PLAY RECAP *************************************************************************************************
csr1                        : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
nxos                       : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The output above shows the results of the playbook ran in the new version. This time only the device facts are gathered in a single play for two different platforms, rather than running vendor specific modules that have to run in two different plays.

Note: Now that gather_facts can be used with networking devices there are some configurations that can be changed in the ansible.cfg file that will affect how the gathering could behave.

Changing the settings below in the ansible.cfg file will affect how gathering facts from devices will be gathered.

# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
# gathering = implicit

For more information on what other new changes exist in Ansible 2.9 check out the link below:

Ansible 2.9 Porting Guide


Conclusion

Before this new update in Ansible 2.9.2, when building playbooks for networking devices the gather_facts key was always disabled and it almost seemed like an extra key that needed to be there for no good reason unless facts from the local machine needed to be added. In this new version of Ansible gathering facts from networking devices really makes things easier and more flexible in a structured way. I’m looking forward to test this out with the new rersource modules and showing this feature to more of my students from now on.

-Hector



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!