Ansible and Juniper Junos – Interfaces

Previously we had made our first incursions connecting an Ansible control node with a Juniper router. In this post, we’ll see how to retrieve information about the router interfaces, both layer 2 and layer 3, and configure new interfaces.

The official Ansible modules reference will be your main guide for any additional information.
https://docs.ansible.com/ansible/latest/modules/list_of_network_modules.html#junos

If you are interested on this subject, but don’t have access to physical gear, most of it should work on virtual appliances like vMX, vQFX, which you can operate on a stand-alone mode or on a network environment like GNS3 or EVE-NG.

Juniper vLabs will also give you an introduction to the Juniper platform.
https://jlabs.juniper.net/vlabs/portal/index.page

Layer 2 Interfaces

A basic layer 2 interface configuration in Junos looks like this:

ge-0/0/1 {
     description "L2 interface";
     speed 1g;
     unit 0 {
         family ethernet-switching {
             interface-mode access;
             vlan {
                 members vlan30;
             }
         }
     }
 }

This configuration can be written as an Ansible playbook like this:

- name: "Replace provided configuration with device configuration"
  junos_l2_interfaces:
    config:
      - name: ge-0/0/1
        access:
          vlan: v30
    state: merged

Currently, I do not have any EX series or QFX series to decomission and run tests against it, so stay tuned for any updates on this.

The official module documentation is on https://docs.ansible.com/ansible/latest/modules/junos_l2_interfaces_module.html.

Layer 3 Interfaces

A basic layer 3 interface configuration in Junos looks like this:

ge-0/0/1 {
     unit 0 {
         family inet {
             address 192.168.1.10/24;
         }
     }
 }

This configuration can be written as an Ansible playbook like the following, using the same format as the last post.

---
- hosts: all
  gather_facts: no

  tasks:

  - name: Config ge-0/0/1
    junos_l3_interfaces:
      config:
        - name: ge-0/0/1
          ipv4:
            - address: 192.168.1.10/24
      state: merged

Let’s run it and check the result.

$ ansible-playbook juniper.yml -i juniper-hosts.yml

PLAY [all] ********************************************************************************************************************

TASK [Config ge-0/0/1] ********************************************************************************************************
[WARNING]: Platform linux on host 192.168.15.220 is using the discovered Python interpreter at /usr/bin/python, but future
installation of another Python interpreter could change this. See
https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.

changed: [192.168.15.220]

PLAY RECAP ********************************************************************************************************************
192.168.15.220             : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Did you noticed the changed output?
What about the configuration on the router now?

admin> show configuration interfaces
ge-0/0/1 {
    unit 0 {
        family inet {
            address 192.168.1.10/24;
        }
    }
}
fxp0 {
    unit 0 {
        family inet {
            address 192.168.15.220/24;
        }
        family inet6;
    }
}

That’s awesome! We just configured and IP address on ge-0/0/1.

How does Ansible knows what to replace, what to override, and what to delete?

If you take a closer look to the playbook, you will see a line with state: merged. This is a module parameter that specifies the state of the router configuration after the module finishes its job.

The possible values are:

  • merged
  • replaced
  • overriden
  • deleted

In fact, the module matches whatever configuration you build on its parameters, applies a configuration action, and commits the result.

The official module documentation is on https://docs.ansible.com/ansible/latest/modules/junos_l3_interfaces_module.html.

Ansible and Juniper Junos – First Steps

On the previous post, I proposed a quick and dirty method to provision an Ansible control node, using Vagrant and VirtualBox. But, if you really want to spin the volume up to 11, the best is to work in a dedicated Linux machine.

On this lab, I will be using Linux Mint, which is a Ubuntu derivate, but most examples will work on any Linux distribution.

First, install Ansible if already don’t have it. There are two easy methods.

  • Using pip to install the ansible Python module. You’ll need a working installation of Python2 (altough the latests releases of Ansible works with Python 3 too). Also, pip has to be installed and on path.

    $ pip install ansible
  • Using apt by adding the Ansible ppa repository and offload all the work to the system package manager.

    $ sudo apt-add-repository ppa:ansible/ansible
    $ sudo apt-get update
    $ sudo apt install ansible

I preffer the last one and leave apt handle all the job, because the ppa repository is usually up-to-date with the latest release.

Once installed, check your setup with ansible --version or ansible localhost -m setup.

Ansible manages Junos using NETFCONF over SSH. In order to be able to connect to Junos via Ansible, both SSH and NETCONF services has to be enabled on the remote host.

SSH can be used for sending raw commands using the junos_command module, but NETCONF is definetly more versatile and supports the whose set of Ansible modules, which you can see here.

To use NETCONF, you will need an additional Python module in your Ansible control node.

$ pip install ncclient

And to enable SSH and NETCONF in your Juniper host , do the following. Probably you already have SSH enabled on your router so you can ignore that setting.

admin> edit  
Entering configuration mode
[edit]
admin# set system services ssh 
admin# set system services netconf ssh 
[edit]
admin# commit 
commit complete

Building the Ansible Inventory

Ansible inventory files can use many formats, depending on the plugins you have. The two main formats are INI files, and YAML.

INI-style Inventory

[junos]
192.168.15.194

[junos:vars]
ansible_connection=netconf
ansible_network_os=junos
ansible_user=admin
ansible_password=Password$1

YAML Inventory

all:
  hosts:
    "192.168.15.194":
  vars:
    ansible_connection: netconf
    ansible_network_os: junos
    ansible_user: admin
    ansible_password: Password$1

Both files represent the same set of information on a different style. Personally, I preffer the YAML notation because it is more human-friendly and readeable, and it allows you to learn YAML which is used on many other automation and orchestration tools.

What does this means

all:
  hosts:
    "192.168.15.194":

all: stands for all hosts and its always required. All hosts on an Ansible inventory belongs to at least two groups, all and ungrouped. All groups of course, all hosts, and ungrouped contains all hosts which don’t belong to another specific group besides all.

hosts: specifies the start position for the hosts. Hosts groups can be then written like:

mendoza:
   hosts:
     "192.168.15.194":
     core.thisnetwork.net:

Hosts groups can share a set of variables, which provide information for the Ansible modules. For this example, as the vars section is a direct children of the allgroup, these variables will apply to all hosts in the inventory.

vars:
     ansible_connection: netconf
     ansible_network_os: junos
     ansible_user: admin
     ansible_password: Password$1

The Junos modules for Ansible use the netconf connection, which uses SSH and NETCONF, so ensure to allow TCP/22 and TCP/830 on your rules.

Although a fixed inventory file with manually added hosts can be enough for some users, on future entries we’ll set up a dynamic inventory calling a source of truth like Netbox, or a monitoring system like Zabbix.

Testing our setup

The real fun with Ansible is on ansible-playbook, but first let’s fire up an Ansible module to warm up your Ansible-fu.

$ ansible -m junos_facts -i juniper-hosts.yml all
  • -m junos_facts instructs Ansible to use the junos_facts module
  • juniper-hosts.yml is our previously configured inventory file
  • all tells Ansible the group of hosts inside the inventory to use

If everythig works allright, this will output a long JSON with a bunch of information about your device.

192.168.15.195 | SUCCESS => {
    "ansible_facts": {
        "ansible_net_api": "netconf", 
        "ansible_net_filesystems": [
            "/dev/md0.uzip", 
            "devfs", 
            "/dev/gpt/junos", 
            "devfs", 
            "/dev/md1.uzip", 
            "/dev/md2.uzip", 
            "tmpfs", 
            "procfs", 
            "/dev/ada1s1e", 
            "/dev/ada1s1f", 
            "/dev/md3.uzip", 
            "/dev/md4.uzip", 
            "/dev/md5.uzip", 
            "/dev/md6.uzip", 
            "/dev/md7.uzip", 
            "/dev/md8.uzip", 
            "/dev/md9.uzip", 
            "/dev/md10.uzip", 
            "/dev/md11.uzip", 
            "/packages/mnt/junos-libs-compat32/usr/lib32", 
            "/packages/mnt/os-libs-compat32-10/usr/lib32", 
            "/packages/mnt/os-compat32/libexec", 
            "/var/jails/rest-api", 
            "/dev/md12", 
            "/dev/md13.uzip", 
            "/dev/md14.uzip", 
            "/dev/md15.uzip", 
            "/dev/md16.uzip", 
            "/dev/md17.uzip", 
            "/dev/md18.uzip", 
            "/dev/md19.uzip", 
            "/dev/md20.uzip", 
            "/dev/md21.uzip", 
            "/dev/md22.uzip", 
            "/dev/md23.uzip", 
            "/dev/md24.uzip", 
            "/dev/md25.uzip", 
            "/dev/md26.uzip", 
            "/dev/md27.uzip", 
            "/dev/md28.uzip", 
            "tmpfs", 
            "junosprocfs"
        ], 
        "ansible_net_gather_network_resources": [], 
        "ansible_net_gather_subset": [
            "hardware", 
            "default", 
            "interfaces"
        ], 
        "ansible_net_has_2RE": false, 
        "ansible_net_hostname": "None", 
        "ansible_net_interfaces": {
            ".local.": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Loopback"
            }, 
            "cbp0": {
                "admin-status": "up", 
                "macaddress": "00:05:86:cc:c1:11", 
                "mtu": "9192", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Ethernet"
            }, 
            "demux0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "9192", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Software-Pseudo"
            }, 
            "dsc": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Software-Pseudo"
            }, 
            "em1": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:cf:9b:01", 
                "mtu": "1514", 
                "oper-status": "up", 
                "speed": "1000mbps", 
                "type": "Ethernet"
            }, 
            "esi": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Software-Pseudo"
            }, 
            "fxp0": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:cf:9b:00", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "Unspecified", 
                "type": "Ethernet"
            }, 
            "ge-0/0/0": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:02", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/1": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:03", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/2": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:04", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/3": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:05", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/4": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:06", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/5": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:07", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/6": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:08", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/7": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:09", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/8": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:0a", 
                "mtu": "1514", 
                "oper-status": "down", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "ge-0/0/9": {
                "admin-status": "up", 
                "macaddress": "0c:b8:15:f6:47:0b", 
                "mtu": "1514", 
                "oper-status": "up", 
                "speed": "1000mbps", 
                "type": null
            }, 
            "gre": {
                "admin-status": "up", 
                "macaddress": null, 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "GRE"
            }, 
            "ipip": {
                "admin-status": "up", 
                "macaddress": null, 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "IPIP"
            }, 
            "irb": {
                "admin-status": "up", 
                "macaddress": "00:05:86:cc:c8:f0", 
                "mtu": "1514", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Ethernet"
            }, 
            "jsrv": {
                "admin-status": "up", 
                "macaddress": "00:05:86:cc:c8:c0", 
                "mtu": "1514", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Ethernet"
            }, 
            "lc-0/0/0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "0", 
                "oper-status": "up", 
                "speed": "800mbps", 
                "type": "Unspecified"
            }, 
            "lo0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Loopback"
            }, 
            "lsi": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Software-Pseudo"
            }, 
            "mtun": {
                "admin-status": "up", 
                "macaddress": null, 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Multicast-GRE"
            }, 
            "pfe-0/0/0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "0", 
                "oper-status": "up", 
                "speed": "800mbps", 
                "type": "Unspecified"
            }, 
            "pfh-0/0/0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "0", 
                "oper-status": "up", 
                "speed": "800mbps", 
                "type": "Unspecified"
            }, 
            "pimd": {
                "admin-status": "up", 
                "macaddress": null, 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "PIMD"
            }, 
            "pime": {
                "admin-status": "up", 
                "macaddress": null, 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "PIME"
            }, 
            "pip0": {
                "admin-status": "up", 
                "macaddress": "00:05:86:cc:c8:b0", 
                "mtu": "9192", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "Ethernet"
            }, 
            "pp0": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "1532", 
                "oper-status": "up", 
                "speed": "Unspecified", 
                "type": "PPPoE"
            }, 
            "rbeb": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Software-Pseudo"
            }, 
            "tap": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Software-Pseudo"
            }, 
            "vtep": {
                "admin-status": "up", 
                "macaddress": "Unspecified", 
                "mtu": "Unlimited", 
                "oper-status": "up", 
                "speed": "Unlimited", 
                "type": "Software-Pseudo"
            }
        }, 
        "ansible_net_memfree_mb": 179384, 
        "ansible_net_memtotal_mb": 2058336, 
        "ansible_net_model": "vmx", 
        "ansible_net_modules": [
            {
                "name": "Midplane"
            }, 
            {
                "description": "RE-VMX", 
                "name": "Routing Engine 0"
            }, 
            {
                "description": "VMX SCB", 
                "name": "CB 0"
            }, 
            {
                "description": "VMX SCB", 
                "name": "CB 1"
            }, 
            {
                "chassis_sub_module": null, 
                "description": "Virtual FPC", 
                "name": "FPC 0"
            }
        ], 
        "ansible_net_python_version": "2.7.15+", 
        "ansible_net_routing_engines": {
            "0": {
                "cpu_background": "0", 
                "cpu_background1": "0", 
                "cpu_background2": "0", 
                "cpu_background3": "0", 
                "cpu_idle": "61", 
                "cpu_idle1": "94", 
                "cpu_idle2": "95", 
                "cpu_idle3": "95", 
                "cpu_interrupt": "2", 
                "cpu_interrupt1": "1", 
                "cpu_interrupt2": "1", 
                "cpu_interrupt3": "1", 
                "cpu_system": "27", 
                "cpu_system1": "4", 
                "cpu_system2": "3", 
                "cpu_system3": "3", 
                "cpu_user": "10", 
                "cpu_user1": "1", 
                "cpu_user2": "1", 
                "cpu_user3": "1", 
                "last_reboot_reason": "Router rebooted after a normal shutdown.", 
                "load_average_fifteen": "0.62", 
                "load_average_five": "0.73", 
                "load_average_one": "0.75", 
                "mastership_priority": "master (default)", 
                "mastership_state": "master", 
                "memory_buffer_utilization": "13", 
                "memory_dram_size": "2010 MB", 
                "memory_installed_size": "(2048 MB installed)", 
                "model": "RE-VMX", 
                "slot": "0", 
                "start_time": "2019-11-26 12:06:10 UTC", 
                "status": "OK", 
                "up_time": "11 hours, 46 minutes, 19 seconds"
            }
        }, 
        "ansible_net_serialnum": "VM5DDBEA932E", 
        "ansible_net_system": "junos", 
        "ansible_net_version": "17.1R1.8", 
        "ansible_network_resources": {}, 
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": false
}

If you take a closer look, you’ll find out that I am using a vMX appliance, can you spot where is that information?

What if we could use this and more information to gather more useful data about our router, and execute actions of provision new configuration depending on the gathered data?

If Ansible modules are the tools in your workshop, playbooks are your instruction manuals, and your inventory of hosts are your raw material.

The First Playbook

Playbooks are expressed in YAML and are composed of one or more ‘plays’ in a list. The goal of a play is to map a group of hosts to some well defined roles, represented by things ansible calls tasks. At a basic level, a task is nothing more than a call to an ansible module.

Go ahead and create a new file, juniper.yml or whatever you like, and put the following inside:

---
- hosts: all
  gather_facts: no

  tasks:

  - name: Get Configuration
    junos_command:
      commands:
        - show configuration

Going from top to bottom this playbook tells Ansible to:

  • hosts: all, use the all hosts from the inventory
  • gather_facts: no, don’t gather any facts for now. More on this later.
  • tasks:, this is the list of all the tasks I want you to do.
  • - name:, this is the name of the task. It starts with a - because it’s a list, even if it has only a single entry
  • junos_command:use this module for this task, like -m junos_command. This module, like most, supports a set of parameters which you can see here, and they are below!
  • commands: this is the list of commands to execute
  • - show configuration: this is one of the commands

Now, run ansible-playbook juniper.yml -i juniper-hosts.yml. This will run your playbook, using all the hosts on the previously defined juniper-hosts.yml inventory.

$ ansible-playbook juniper.yml -i juniper-hosts.yml

PLAY [all] ***********************************************************************************************************************************************************************

TASK [Get Configuration] *********************************************************************************************************************************************************
[WARNING]: Platform linux on host 192.168.15.195 is using the discovered Python interpreter at /usr/bin/python, but future installation of another Python interpreter could
change this. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information.

ok: [192.168.15.195]

PLAY RECAP ***********************************************************************************************************************************************************************
192.168.15.195             : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Well, that was fun but.. there is nothing useful here, besides a warning! In fact, Ansible connected sucessfully to the router, and retrieved the configuration. We didn’t instructed it to show the configuration to us.

Modify your playbook so it looks like this:

---
- hosts: all
  gather_facts: no

  tasks:

  - name: Get Configuration
    junos_command:
      commands:
        - show configuration
    register: config

  - name: Show Config
    debug: var=config

We added an additional line to the first task, register: config, which saves the result of the task in a variable called config. This name can be whatever you like.

There is also an additional task named Show Config, with a debug: var=config instruction, which outputs the config variable.

Run the playbook once again like you did before, and check the result. Of course it will depend on your configuration. On my vMX, which is almost blank, and just using DHCP for a couple interfaces, the result was this.

$ ansible-playbook juniper.yml -i juniper-hosts.yml

PLAY [all] *****************************************************************************

TASK [Get Configuration] ***************************************************************
[WARNING]: Platform linux on host 192.168.15.195 is using the discovered Python
interpreter at /usr/bin/python, but future installation of another Python interpreter
could change this. See
https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html
for more information.

ok: [192.168.15.195]

TASK [Show Config] *********************************************************************
ok: [192.168.15.195] => {
    "config": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "failed": false, 
        "stdout": [
            "## Last changed: 2019-11-26 19:02:46 UTC\nversion 17.1R1.8;\nsystem {\n    root-authentication {\n        encrypted-password \"$6$5LBS/EfQ$tL9utW2Aj4T56SfJUxdnVaF/.RIbaZ65keFn1SbCgOTi6r.LDiGt3FvvoP2WuxuuosVtx0RobNk67obTMeNgF.\";\n    }\n    scripts {\n        inactive: language python;\n    }\n    login {\n        user admin {\n            uid 2000;\n            class super-user;\n            authentication {\n                encrypted-password \"$6$9YynK9hD$Is6rEd7WNnEYGF7q2MqQJoRU/9vGjkQv7Qig.V2WT1905ShVlow4LXKeATM5HR8F1vTwROz2gUpF7z7eCJruo1\";\n            }\n        }\n    }\n    services {\n        ssh;\n        netconf {\n            ssh;\n        }\n    }\n    syslog {\n        user * {\n            any emergency;\n        }\n        file messages {\n            any notice;\n            authorization info;\n        }\n        file interactive-commands {\n            interactive-commands any;\n        }\n    }\n}\ninterfaces {\n    ge-0/0/1 {\n        unit 0 {\n            family inet {\n                dhcp;\n            }\n        }\n    }\n    ge-0/0/9 {\n        unit 0 {\n            family inet {\n                dhcp;\n            }\n        }\n    }\n}"
        ], 
        "stdout_lines": [
            [
                "## Last changed: 2019-11-26 19:02:46 UTC", 
                "version 17.1R1.8;", 
                "system {", 
                "    root-authentication {", 
                "        encrypted-password \"$6$5LBS/EfQ$tL9utW2Aj4T56SfJUxdnVaF/.RIbaZ65keFn1SbCgOTi6r.LDiGt3FvvoP2WuxuuosVtx0RobNk67obTMeNgF.\";", 
                "    }", 
                "    scripts {", 
                "        inactive: language python;", 
                "    }", 
                "    login {", 
                "        user admin {", 
                "            uid 2000;", 
                "            class super-user;", 
                "            authentication {", 
                "                encrypted-password \"$6$9YynK9hD$Is6rEd7WNnEYGF7q2MqQJoRU/9vGjkQv7Qig.V2WT1905ShVlow4LXKeATM5HR8F1vTwROz2gUpF7z7eCJruo1\";", 
                "            }", 
                "        }", 
                "    }", 
                "    services {", 
                "        ssh;", 
                "        netconf {", 
                "            ssh;", 
                "        }", 
                "    }", 
                "    syslog {", 
                "        user * {", 
                "            any emergency;", 
                "        }", 
                "        file messages {", 
                "            any notice;", 
                "            authorization info;", 
                "        }", 
                "        file interactive-commands {", 
                "            interactive-commands any;", 
                "        }", 
                "    }", 
                "}", 
                "interfaces {", 
                "    ge-0/0/1 {", 
                "        unit 0 {", 
                "            family inet {", 
                "                dhcp;", 
                "            }", 
                "        }", 
                "    }", 
                "    ge-0/0/9 {", 
                "        unit 0 {", 
                "            family inet {", 
                "                dhcp;", 
                "            }", 
                "        }", 
                "    }", 
                "}"
            ]
        ], 
        "warnings": [
            "Platform linux on host 192.168.15.195 is using the discovered Python interpreter at /usr/bin/python, but future installation of another Python interpreter could change this. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information."
        ]
    }
}

PLAY RECAP *****************************************************************************
192.168.15.195             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

I know we all love show config | display set. Try adding display: set as an additional parameter of junos_command. It should look like this.

    junos_command:
      commands:
        - show configuration
      display: set

Run the playbook again, and your output should be as expected if you ran it on the Junos CLI.

Stay tuned for more posts to learn how to configure Juniper Junos using Ansible.

Quick Ansible control node with Vagrant

Ansible is an IT automation tools which can configure systems, deploy files and software and orchestrate almost every possible task you can imagine.

Unlike other configuration management tools like Chef or Puppet, Ansible is agent-less, and does all its magic over SSH. Forget about keeping up client daemons updated and additional certificates. Just use your existing SSH keys, or usernames and passwords.

Due to it agent-less nature, we just need a host to initiate SSH sessions towards the managed hosts. This node is called a control node in the Ansible jargon.

On this post, we’ll catch up with the basics of Vagrant and Ansible. The repository for this post is on https://github.com/baldoarturo/ansible-control-node.

Download VirtualBox

VirtualBox is a general-purpose full virtualizer for x86 hardware, targeted at server, desktop and embedded use. Get it from here.

Get Vagrant

We’ll use Vagrant to quickly spin up a control node, based on a ubuntu/xenial64 box. If you are not familiar with Vagrant, you just need to know it is a tool capable to provision VMs on different hypervisors, using a Vagrantfile a VM template.

First, get Vagrant for your system here, https://www.vagrantup.com/downloads.html, and install it.

Use Git

In order to make everything easier, we’ll init a new git repository to keep track of all the configuration changes. Also, we can push this repository to a remote and clone from wherever we need it, making a portable Ansible control node.

C:\Users\Arturo\Desktop\ansible-control-node>git init
Initialized empty Git repository in C:/Users/Arturo/Desktop/ansible-control-node/.git/

C:\Users\Arturo\Desktop\ansible-control-node>git config user.name "Arturo Baldo"

C:\Users\Arturo\Desktop\ansible-control-node>git config user.email "baldoarturo@gmail.com"

C:\Users\Arturo\Desktop\ansible-control-node>

Although everything can be done from a shell, I prefer to use the integrated terminal on VSCode, and make use of the universe of plugins. There is support for Ansible and Ansible Playbooks, Vagrant, a lot more!

Also, even if this post uses a Windows system, everything is reproducible on Linux platforms because all the tools are multiplatform.

Init the Vagrant environment

The Vagrant CLI is self explanatory and easy to understand.

C:\Users\Arturo\Desktop\ansible-control-node>vagrant
Usage: vagrant [options] <command> [<args>]

    -v, --version                    Print the version and exit.
    -h, --help                       Print this help.

Common commands:
     box             manages boxes: installation, removal, etc.
     cloud           manages everything related to Vagrant Cloud
     destroy         stops and deletes all traces of the vagrant machine
     global-status   outputs status Vagrant environments for this user
     halt            stops the vagrant machine
     help            shows the help for a subcommand
     init            initializes a new Vagrant environment by creating a Vagrantfile
     login
     package         packages a running vagrant environment into a box
     plugin          manages plugins: install, uninstall, update, etc.
     port            displays information about guest port mappings
     powershell      connects to machine via powershell remoting
     provision       provisions the vagrant machine
     push            deploys code in this environment to a configured destination
     rdp             connects to machine via RDP
     reload          restarts vagrant machine, loads new Vagrantfile configuration
     resume          resume a suspended vagrant machine
     snapshot        manages snapshots: saving, restoring, etc.
     ssh             connects to machine via SSH
     ssh-config      outputs OpenSSH valid configuration to connect to the machine
     status          outputs status of the vagrant machine
     suspend         suspends the machine
     up              starts and provisions the vagrant environment
     upload          upload to machine via communicator
     validate        validates the Vagrantfile
     version         prints current and latest Vagrant version
     winrm           executes commands on a machine via WinRM
     winrm-config    outputs WinRM configuration to connect to the machine

First, run vagrant intit to initialize a new environment.

C:\Users\Arturo\Desktop\ansible-control-node>vagrant init
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

Wait! You have a new file on your git repository. Make it the first commit.

C:\Users\Arturo\Desktop\ansible-control-node>git add .

C:\Users\Arturo\Desktop\ansible-control-node>git commit -m "First commit"
[master (root-commit) 1416f75] First commit
 1 file changed, 70 insertions(+)
 create mode 100644 Vagrantfile

C:\Users\Arturo\Desktop\ansible-control-node>

On the newly create Vagrantfile, we can set configuration and provisioning options for the VM.

First, replace config.vm.box = "base" with config.vm.box = "ubuntu/xenial64" to use this box.

If you want to see the VM, config vb.gui = true. By default this setting is commented out, so the VM will start in headless mode.

Feel free to customize the VM memory by commenting out the vb.memory = "1024" setting and adjusting it to your needs.

Once you are done, save your changes, commit, execute vagrant up, and watch Vagrant create a brand new VM for you.

C:\Users\Arturo\Desktop\ansible-control-node>vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/xenial64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/xenial64' version '20191114.0.0' is up to date...
==> default: Setting the name of the VM: ansible-control-node_default_1574353915423_60685
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default: Warning: Connection reset. Retrying...
    default: Warning: Connection aborted. Retrying...
    default: Warning: Remote connection disconnect. Retrying...
    default: 
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default: 
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
    default: The guest additions on this VM do not match the installed version of
    default: VirtualBox! In most cases this is fine, but in rare cases it can
    default: prevent things such as shared folders from working properly. If you see
    default: shared folder errors, please make sure the guest additions within the
    default: virtual machine match the version of VirtualBox you have installed on
    default: your host and reload your VM.
    default:
    default: Guest Additions Version: 5.1.38
    default: VirtualBox Version: 6.0
==> default: Mounting shared folders...
    default: /vagrant => C:/Users/Arturo/Desktop/ansible-control-node

Well, how do we login into the new VM? Try vagrant ssh

C:\Users\Arturo\Desktop\ansible-control-node>vagrant ssh
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-169-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage


0 packages can be updated.
0 updates are security updates.

New release '18.04.3 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


vagrant@ubuntu-xenial:~$ whoami
vagrant
vagrant@ubuntu-xenial:~$ 

Type exit and you will return to your system prompt.

You’ll see that you have new items on your directory. A .log file, with the output from Vagrant, and a .vagrant/ directory.

To exclude them from the repository, create a .gitignore, with the following contents.

.vagrant/
*.log

Save and commit.

Provisioning the VM

Return to the Vagrantfile, and add a section like this:

  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install software-properties-common -y
    apt-add-repository --yes --update ppa:ansible/ansible
    apt-get install ansible -y
  SHELL

This block instructs Vagrant to execute commands on the shell. First, the package list is updated, then the software-properties-common package is installed. To install Ansible, the ppa:ansible/ansible repository is added, and then Ansible is installed.

Notice the usage of -y to avoid interactive prompts.

Save the file, commit, and run vagrant provision. This will re-provision the VM with the new instructions from the Vagrantfile.

Once Vagrant finishes, connect once again with vagrant ssh, and run ansible --version.

vagrant@ubuntu-xenial:~$ ansible --version
ansible 2.9.1
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/vagrant/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.12 (default, Oct  8 2019, 14:14:10) [GCC 5.4.0 20160609]

That’s great! We have just installed Ansible on our new VM.

Seek and Destroy

You might wonder, where is this VM? Head to the VirtualBox Manager, and you’ll find it inside.

Here it is!

Without asking anyone.. kill the machine and delete it.

Hard power off
Delete all files

Head back to the console, and you will see that the SSH session has died. Of course, this makes perfect sense as the VM no longer exists.

vagrant@ubuntu-xenial:~$ Connection to 127.0.0.1 closed by remote host.
 Connection to 127.0.0.1 closed.

If you try to run vagrant provision, it will fail, as there is no VM to provision.

C:\Users\Arturo\Desktop\ansible-control-node>vagrant provision
==> default: VM not created. Moving on...

But what happens if you run vagrant up once again? Surprise! The VM will be recreated with all the previous configuration settings.

Using Ansible

Well, the VM is ready, Ansible is installed.. now what? Let’s write an inventory file and see what can we do.

I’ll create a simple file to connect to a testing docker instance on 192.168.85.253. No need to worry about ssh keys now, as we are testing, username and password will do the trick.

vagrant@ubuntu-xenial:~$ cat docker
[all:vars]
ansible_connection=ssh
ansible_user=MY_USERNAME
ansible_password=MY_PASSWORD

[docker]
192.168.85.253

The [all:vars] section configures the connection settings for all hosts in the file. The [docker] section contains the lists of hosts, in this case just 192.168.85.253.

Let’s run: ansible -m ping -i docker all, where -m is the module to run, -i is the inventory file, and all is the section of the inventory file which contains the hosts. Notice this is a very special ping.

vagrant@ubuntu-xenial:~$ ansible -m ping -i docker all
[DEPRECATION WARNING]: Distribution Ubuntu 16.04 on host 192.168.15.253 should use /usr/bin/python3, but is using /usr/bin/python for backward compatibility with prior Ansible releases. A future Ansible release
 will default to using the discovered platform python for this host. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information. This feature will be removed   
in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
192.168.15.253 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": false, 
    "ping": "pong"
}

Whoa! That definetly doesn’t look like a ICMP ping. First, this is an old box about to be decomissioned, so it has been very unmantained. Ansible warns us about an old version of Python, and a old version of Ansible which was installed previously on the remote hosts.

Now, try ansible -m setup -i docker all. Be aware this will throw a ton of data to you. The setup module gathers data about the host.

What it does and how does it, is documented here.