Getting Started with Juniper and Ansible

Overview

In this blog post, we will go through how to create a simple Ansible Playbook to configure a Juniper SRX Firewall. This blog post assumes prior knowledge of Ansible and Junos OS. If you are not familiar with Ansible, please check out my other blog post here

Ansible can help create and manage configuration changes centrally by pushing them out to all/or some devices and requires no extra software to be installed on the devices. One of Ansible's greatest strengths is idempotence where the same configuration is maintained even if we run the playbook multiple times.

Our goal here is to configure a Juniper SRX Firewall using Ansible. The configuration includes time zone, hostname, L3 interfaces, static route, security zones and NAT.

Prerequisite

Initial Configurations

Before we start using Ansible, we need to ensure that Ansible can successfully establish an SSH connection to the device. Configuring username, password and management IP are some of the prerequisites before running Ansible successfully.

The following are the minimum required configurations on the SRX.

root# show | compare  
[edit system]
+  root-authentication {
+      encrypted-password "$6$vbMCkv58$.hoAjcSyOvn.3qT0ardbMBhqcTRXnKGgLambzUeVl7748ItC9UPpO6VGFumn1sUaCbqNn6Y0PYWVAp..ph9Zr."; ## SECRET-DATA
+  }
+  domain-name packet.lan;
[edit interfaces fxp0 unit 0]
+     family inet {
+         address 10.10.50.16/16;
+     }

Junos Ansible Collection

Please ensure that you install the Junos ansible collection by using the following command.

sureshv@mac:~/Documents|⇒  ansible-galaxy collection install junipernetworks.junos

Starting galaxy collection install process
Process install dependency map
Starting collection install process
Downloading https://galaxy.ansible.com/download/junipernetworks-junos-4.1.0.tar.gz to /Users/sureshv/.ansible/tmp/ansible-local-703e7nkjwm9/tmp0hb84cjz/junipernetworks-junos-4.1.0-lbwqptx9
Installing 'junipernetworks.junos:4.1.0' to '/Users/sureshv/.ansible/collections/ansible_collections/junipernetworks/junos'
junipernetworks.junos:4.1.0 was installed successfully
Skipping 'ansible.netcommon:4.1.0' as it is already installed
Skipping 'ansible.utils:2.7.0' as it is already installed

Enable netconf SSH Subsystem

When you look at Junos Ansible modules, you will notice that one of the requirements is defined as "this module requires the netconf system service be enabled on the remote device being managed"

To enable access to the netconf ssh subsystem using the default SSH port (22), configure the following.

[edit]
root@srx# set system services netconf ssh

File Structure

I've created the following directories and files in my project folder called ansible. We will break them down one by one shortly.

sureshv@mac:~/Documents/ansible|⇒  tree
.
├── ansible.cfg
├── inventory
│   ├── group_vars
│   │   └── srx.yml
│   ├── host_file.ini
│   └── host_vars
│       ├── branch_srx.yml
└── playbook.yml

ansible.cfg

Certain settings in Ansible are adjustable via a configuration file ansible.cfg I always prefer to create a new ansible.cfg file on the directory that I'm working on rather than editing the default file which is located at /etc/ansible/. The configuration file that is located in the current working directory always takes precedence over the default file.

I've made the following changes to the config file to suit my lab environment.

  • host_key_checking - I set the host key check to false, otherwise, Ansible will complain that the host key is not trusted.
  • inventory - Referencing the location of the inventory file
[defaults]
host_key_checking = False
inventory = /Users/sureshv/Documents/ansible/inventory/host_file.ini

Ansible Inventory File host_file.ini

An inventory file is a configuration file that defines the hosts and the mapping of hosts into groups. The hosts are the devices that we want Ansible to manage. The default inventory file is located at /etc/ansible/hosts.

You can specify a different inventory file at the command line using the -i <path> option or reference it in the ansible.cfg file as we did in this example.

In our example, I've added the SRX to a group called srx and added its IP and name to the inventory file host_file.ini

#host_file.ini
[srx]
branch_srx ansible_host=10.10.50.16

Variables

Now that we have defined our inventory, where do we specify the variables such as interface names, IP addresses, usernames, passwords etc? Although you can store variables directly in the main inventory file, storing them on a separate host and group variables files may help you organize your variables more easily.

Ansible loads host and group variable files by searching paths relative to the inventory file or the playbook file.

For example, SRX firewall belongs to the group srx so, if our inventory file is located at /Documents/ansible/inventory it will use variables in YAML files at the following locations.

/Documents/ansible/inventory/host_vars/branch_srx.yml #host_vars
/Documents/ansible/inventory/grouop_vars/srx.yml #group_vars

So, we specify variables specific to a particular device under host_vars such as IP address, hostname etc. We can also specify variables that are common to a group of devices under group_vars such as DNS servers, NTP servers, banners, time-zones etc.

💡
Please note that host and group variable files must use YAML syntax. Valid file extensions include ‘.yml’, ‘.yaml’, ‘.json’, or no file extension.

host_vars branch_srx.yml

Here we specify variables that are unique to branch_srx only

---
interfaces:
  - name: ge-0/0/0
    ip: 116.85.10.1/29
    zone: wan
    description: WAN
  - name: ge-0/0/1
    ip: 10.16.1.1/24
    zone: users
    description: Users
  - name: ge-0/0/2
    ip: 10.16.2.1/24
    zone: applications
    description: Apps and Servers
  - name: ge-0/0/3
    ip: 10.16.9.1/24
    zone: dmz
    description: DMZ

group_vars srx.yml

Here we specify variables that are common to a group of devices. For example, if you were to add another SRX to this group, it will inherit the variables automatically.

---
ansible_connection: ansible.netcommon.netconf
ansible_network_os: junipernetworks.junos.junos
ansible_user: admin
ansible_password: Cisco123
  • ansible_connection - Ansible uses the ansible-connection setting to determine how to connect to a remote device. When working with network devices, we need to set this to an appropriate network connection option, so Ansible treats the remote node as a network device with a limited execution environment. Without this setting, Ansible would attempt to use ssh to connect to the remote and execute the Python script on the network device, which would fail because Python generally isn’t available on network devices.
  • ansible_network_os - Informs Ansible which Network platform these hosts correspond to. This is required when using the ansible.netcommon.* connection options.
💡
Please note that it is recommended to encrypt the passwords in production environments. You can use Ansible Vault to easily encrypt passwords and any confidential information.

Playbook

The final piece to the puzzle is the actual playbook. You can find all the available Junos Ansible modules here - https://docs.ansible.com/ansible/latest/collections/junipernetworks/junos/index.html

---
- name: vSRX Playbook
  hosts: srx
  gather_facts: no

  collections:
    - junipernetworks.junos

  tasks:
    - name: Time Zone
      junipernetworks.junos.junos_config:
        lines:
        - set system time-zone Europe/London
      tags: ntp

    - name: Hostname
      junipernetworks.junos.junos_hostname:
        config:
          hostname: 'branch_srx'
      tags:
      - system

    - name: L3 Interfaces
      junipernetworks.junos.junos_l3_interfaces:
        config:
        - name: "{{ item.name }}"
          ipv4:
          - address: "{{ item.ip }}"
      with_items: "{{ interfaces }}"
      tags:
      - l3
    
    - name: Static Routes
      junipernetworks.junos.junos_static_routes:
        config:
        - address_families:
          - afi: ipv4
            routes:
            - dest: 0.0.0.0/0
              next_hop:
              - forward_router_address: 116.85.10.6
      tags:
      - route
    
    - name: Security Zones and Allow Ping
      junipernetworks.junos.junos_config:
        lines:
        - set security zones security-zone "{{ item.zone }}" interfaces "{{ item.name }}" host-inbound-traffic system-services ping
      with_items: "{{ interfaces }}"
      tags:
      - l3

    - name: Source NAT
      junipernetworks.junos.junos_config:
        lines:
        - set security nat source rule-set users_to_wan from zone users
        - set security nat source rule-set users_to_wan to zone wan
        - set security nat source rule-set users_to_wan rule snat_pat match source-address 10.16.1.0/24
        - set security nat source rule-set users_to_wan rule snat_pat match destination-address 0.0.0.0/0
        - set security nat source rule-set users_to_wan rule snat_pat then source-nat interface
      tags:
      - nat

"{{ ........ }}"

Any values that you see inside the "{{ }}" curly braces are variables. When you run the playbook, Ansible will obtain the value of the variables from the host and group var files.

When you run the playbook, Ansible will obtain the value of the variables from the host and group var files. We have four interfaces under the parent variable interfaces.

On the first iteration, "{{ item.name }}" becomes ge-0/0/0 and "{{ item.ip }}" becomes 116.85.10.1/29

On the second iteration, "{{ item.name }}" becomes ge-0/0/1 and "{{ item.ip }}" becomes 10.16.1.1/24 and so on and so forth.

The benefit of this is, if you were to add a new interface, all you have to do is add the interface name and IP to the host_vars file.

tags

If you have a large playbook, it may be useful to run only specific parts of it instead of running the entire playbook. You can do this with Ansible tags. You can use tags to execute or skip selected tasks.

  1. Add tags to your tasks
  2. Select or skip tags when you run your playbook by using the --tags argument.

For example, let's say we've added another static route and want to run the playbook to push the new static route to the devices. If you run the playbook without any tags, it will run all of your tasks. It is not a big deal with just a few devices, but if you have a larger playbook, it will take a long time to run the entire playbook.

Running the playbook will result in the following commands being sent out to the device.

set system time-zone Europe/London

set system host-name branch_srx

set interfaces ge-0/0/0 unit 0 family inet address 116.85.10.1/29
set interfaces ge-0/0/1 unit 0 family inet address 10.16.1.1/24
set interfaces ge-0/0/2 unit 0 family inet address 10.16.2.1/24
set interfaces ge-0/0/3 unit 0 family inet address 10.16.9.1/24

set security zones security-zone wan interfaces ge-0/0/0.0 host-inbound-traffic system-services ping
set security zones security-zone users interfaces ge-0/0/1.0 host-inbound-traffic system-services ping
set security zones security-zone applications interfaces ge-0/0/2.0 host-inbound-traffic system-services ping
set security zones security-zone dmz interfaces ge-0/0/3.0 host-inbound-traffic system-services ping

set security nat source rule-set users_to_wan from zone users
set security nat source rule-set users_to_wan to zone wan
set security nat source rule-set users_to_wan rule snat_pat match source-address 10.16.1.0/24
set security nat source rule-set users_to_wan rule snat_pat match destination-address 0.0.0.0/0
set security nat source rule-set users_to_wan rule snat_pat then source-nat interface

Unsupported Modules

You must have noticed that I've used set commands to configure both Security Zones and NAT. Even though Ansible has a module to create Security Zones, I couldn't get it to work hence using the set commands. Please let me know in the comments if you managed to create the zones via the Ansible module.

Ansible on the other hand doesn't have a module to configure NAT so, I had to manually configure them via the set commands.

References

https://docs.ansible.com/ansible/latest/collections/junipernetworks/junos/index.html