Ansible and JUNOS - Managing Juniper Devices with SSH and NETCONF

I’m a long time fan of Juniper networking devices, there seems to be a snobbery against them for reasons I’ve never understood. Imagine my disappointment to find that this was reflected somewhat in the Ansible documentation where JUNOS seems to be the black sheep and the guides are a little patchy.

Recently I’ve been having some fun integrating Ansible with JUNOS and this post is going to look at both how to configure JUNOS to allow connections from Ansible and then the little gotchas that are needed to configure Ansible itself.

A Quick Overview

Ansible manages network devices using two connection methods which we do not usually see when working with servers and cloud systems, these are network_cli and netconf. The former of these; network_cli is the more familiar option which establishes an SSH connection to a network device, however it’s function is very limited and really all we use it for is to ensure the NETCONF service is enabled on our remote devices.

In a nutshell, NETCONF allows for the transport of XML over RPC in order to manage the configuration of network devices. XML isn’t exactly anyone’s favourite way to handle data but Ansible lets us simplify this process by inputting our values as YAML, establishing a local connection to the controller and sending the RPC calls for us using an SSH subsystem.

An excellent breakdown of the execution process from the Juniper documentation is shown below:

Source: https://www.juniper.net/documentation/us/en/software/junos-ansible/ansible/topics/concept/junos-ansible-modules-overview.html#understanding-the-ansible-for-junos-os-collections-roles-and-modules__d51e156

Preparing JUNOS For Use With Ansible

First of all we’ll want to get JUNOS configured properly to allow our Ansible Controller to make a secure connection. The first thing we’ll want of course is a Service Account on the JUNOS device and Public Key Authentication configured.

What we’re working with:

  • An Ansible Controller named mc-mgmt.
  • A Service Account on the Controller named svc_ansible with an existing SSH key-pair.
  • A Juniper SRX Device named mcfw.madcaplaughs.co.uk, with SSH enabled on it’s trust Zone and the trust Zone bound to physical interface ge-0/0/1.
  • Our SRX device is using local authentication and accounts, with no external authentication or authorisation mechanisms.

To configure our Service Account

#--Transfer Public Key from Ansible Controller to JUNOS Device
svc_ansible@mc-mgmt:~ $ scp .ssh/id_rsa.pub welsh@mcfw.madcaplaughs.co.uk:/tmp
welsh@mcfw.madcaplaughs.co.uks password: *********************************************
# id_rsa.pub        100%  401    86.6KB/s   00:00 

#--Connect to the JUNOS Device via SSH
svc_ansible@mc-mgmt:~ $ ssh welsh@mcfw.madcaplaughs.co.uk
welsh@mcfw.madcaplaughs.co.uks password: *********************************************

#--Enter JUNOS Shell
welsh@mcfw> start shell
%

Now that we’re in the shell, it’s worth running a quick cat /etc/passwd and looking at the uids of your users currently on the device. We’re going to need to a set a UID on the next account that we create and we don’t want to set two the same. In our example we’re going to use UID 3000 but your usage may differ! Anyway, that’s all we really need to do on the shell.

#--Exit JUNOS Shell
% exit

#-- Create and configure Service Account
welsh@mcfw> configure
# Entering configuration mode
welsh@mcfw# set system login user svc_ansible authentication load-key-file /tmp/id_rsa.pub
welsh@mcfw# set system login user svc_ansible class super-user
welsh@mcfw# set system login user svc_ansible uid 3000

#--Commit Changes and disconect from JUNOS device
welsh@mcfw# commit
# commit complete
welsh@mcfw# exit
# Exiting configuration mode
welsh@mcfw> exit

#--Test Connection
svc_ansible@mc-mgmt:~ $ ssh svc_ansible@mcfw.madcaplaughs.co.uk
svc_ansible@mcfw> 

As we see, our Service Account now authenticates using Public Key Authentication, without a password.

Ensuring The NETCONF Server Is Accessible On JUNOS

Whilst we’re on the JUNOS device, we’ll also need to make sure that the NETCONF service is accessible via the Interface associated with our trust Zone.

#--Allow NETCONF Service on trust Zone interface (ge-0/0/)
svc_ansible@mcfw> configure
# Entering configuration mode
svc_ansible@mcfw# set security zones security-zone trust interfaces ge-0/0/1 host-inbound-traffic system-services netconf
svc_ansible@mcfw# commit
# commit complete
svc_ansible@mcfw# exit
# Exiting configuration mode
svc_ansible@mcfw> exit

#--Test that NETCONF Server is available, -p specificies TCP port and -s specificies to use the SSH subsystem
svc_ansible@mc-mgmt:~ $ ssh svc_ansible@mcfw.madcaplaughs.co.uk -p 830 -s netconf
# <!-- No zombies were killed during the creation of this user interface -->
# <!-- user svc_ansible, class j-super-user -->
# <hello>
#   <capabilities>
#     <capability>urn:ietf:params:xml:ns:netconf:base:1.0</capability>
#     <capability>urn:ietf:params:xml:ns:netconf:capability:candidate:1.0</capability>
#     <capability>urn:ietf:params:xml:ns:netconf:capability:confirmed-commit:1.0</capability>
#     <capability>urn:ietf:params:xml:ns:netconf:capability:validate:1.0</capability>
#     <capability>urn:ietf:params:xml:ns:netconf:capability:url:1.0?protocol=http,ftp,file</capability>
#     <capability>http://xml.juniper.net/netconf/junos/1.0</capability>
#     <capability>http://xml.juniper.net/dmi/system/1.0</capability>
#   </capabilities>
#   <session-id>59756</session-id>
# </hello>

As we can see, we’re getting a good connection and returning a test XML payload (with maybe the world’s best heading).

Configuring The Ansible Controller

Before we use Ansible we’ll need to install a Collection to give us access to some both the Ansible Modules and a new ansible_network_os methods for managing JUNOS.

A couple of options exist and the Juniper Documentation gives a good breakdown of their differences and available support. I favour the junipernetworks.junos Collection which is supported and maintained by Ansible. To install this from Ansible Galaxy we can run:

svc_ansible@mc-mgmt:~ $ ansible-galaxy collection install junipernetworks.junos
# Starting galaxy collection install process
# Process install dependency map
# Starting collection install process
# junipernetworks.junos (2.4.0) was installed successfully
# Skipping 'ansible.netcommon' as it is already installed
# Skipping 'ansible.utils' as it is already installed

Next, let’s install a couple of essential Python libraries, I’m going to use Python 3 (though if you’re forced in to using Python 2 for legacy reasons you can rest assured that both the Collection and these libraries are still supported):

#--Python 3
pip3 install ncclient
pip3 install jxmlease

#--Python 2
pip install ncclient
pip install jxmlease

The ncclient library is used to establish NETCONF connections, whilst the jxmlease library allows for the parsing and transformation of XML (this library is incidentally not mentioned in the setup guide though it is mentioned in the individual module documents, you won’t get very far without it though as you can’t use NETCONF and Ansible without the ability to weave XML and a lot of people seem to get really stuck here as very messy XML errors will be thrown inside JSON errors).

Now, let’s set up an Inventory with some essential configuration. We’ll name this file inventory.ini:

[junos_devices]
mcfw.madcaplaughs.co.uk

[junos_devices:vars]
ansible_user=svc_ansible
remote_user=svc_ansible
ansible_netconf_username=svc_ansible
ansible_netconf_password=Sup3S3cr3Pa55
ansible_network_os=junipernetworks.junos.junos

The two “ansible_netconf” variables above are essential to establish a connection and are interestingly absent from the majority of the Ansible Documentation, it’s possible that I’ve encountered a bug but I suspect this could be an oversight in the docs.

In a real deployment, the password should not go in the inventory! This is just an illustrative example, a better solution here is to use Ansible Vault as we’ve looked at previously.

Ensuring NETCONF Remains Active On The JUNOS Device

Now that we’re set up we can start using Ansible, we want to make sure that as much as possible is configured idempotenly and that should include even the configuration of NETCONF itself.

Luckily, the Ansible docs provide us with a very cool ready-baked Play to enable NETCONF on JUNOS:

#--netconf_enable_example.yaml

---
- name: Firewall - Ensure NETCONF
  hosts: junos_devices
  connection: ansible.netcommon.network_cli #--This connection method needs to be explicitly defined
  gather_facts: false #--Fact gathering must be explicitly disabled, attempting to gather facts will fail
  tasks:

  - name: Enable - JUNOS NETCONF
    junipernetworks.junos.junos_netconf:
...

Running this will ensure that the other required configurations for NETCONF remain enabled:

svc_ansible@mc-mgmt:~ $ ansible-playbook -i inventory.ini netconf_enable_example.yaml

# PLAY [Firewall - Ensure NETCONF] ***********************************************
# 
# TASK [Enable - JUNOS NETCONF] **************************************************
# ok: [mcfw.madcaplaughs.co.uk]
# 
# PLAY RECAP *********************************************************************
# mcfw.madcaplaughs.co.uk   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

With this in place, we should finally be able to execute tasks using a NETCONF example, which we’ll see with a simple Play to fetch some facts from JUNOS:

#--netconf_facts_example.yaml

---
- name: Firewalls - NETCONF Configurations
  hosts: junos_devices
  connection: ansible.netcommon.netconf
  gather_facts: false #--Must be explicitly disabled, attempting to gather facts via this method will fail
  tasks:

  - name: Firewalls - Collect Firewall Facts
    junipernetworks.junos.junos_facts:
      gather_subset: config
    register: junos_facts

  - name: Firewall - Output Firewall Hostname
    debug:
      var: junos_facts.ansible_facts.ansible_net_hostname
...

Finally, we can see that the Play executes, and the return data is parsed in to JSON:

# PLAY [Firewalls - NETCONF Configurations] **************************************
# 
# TASK [Firewalls - Collect Firewall Facts] **************************************
# ok: [mcfw.madcaplaughs.co.uk]
# 
# TASK [Firewall - Output Firewall Hostname] *************************************
# ok: [mcfw.madcaplaughs.co.uk] => {
#     "junos_facts.ansible_facts.ansible_net_hostname": "mcfw"
# }
# 
# PLAY RECAP *********************************************************************
# mcfw.madcaplaughs.co.uk   : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Not too painful, but it could be clearer!

Written on July 29, 2021