Automating Juniper vMX BGP Configuration with PyEZ

  • September 19, 2016

Objective: Configure BGP between Three Sites

This tutorial describes how to configure BGP on three Juniper vMX routers using the Juniper PyEZ Python library. The Juniper PyEZ library is a micro-framework for Python that enables you to remotely manage and automate devices running Junos OS. The intention of this tutorial is to show how a deployment process can be simplified using some lightweight scripting.

Using Juniper documents as a guideline, we'll be configuring BGP between three sites Example: Configuring External BGP Point-to-Point Peer Sessions.

Topology

This tutorial was written using the Juniper vMX 5-node topology.

Design

Please use this diagram for reference as we walk through configuring BGP using PyEZ on the three Juniper vMX routers.

juniper_3_node

Verify Device Connectivity using PyEZ

The process of configuring everything needed to get the BGP network will be broken into incremental sections. The first step is to import the required Python modules and establish basic connectivity. Once you instantiate a device object, you can check to see if it's connected by using the connected property. Subsequently, you can view device facts by using the facts property.

ntc@ntc:~$ python
Python 2.7.10 (default, Oct 14 2015, 16:09:02)
[GCC 5.2.1 20151010] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from jnpr.junos.device import Device as JUNIPER
>>> from jnpr.junos.utils.config import Config
>>>
>>>
>>> vmx1 = JUNIPER(host='vmx1', user='ntc', password='ntc123')
>>>
>>> vmx1.open()
Device(vmx1)
>>> vmx1.connected
True
>>> vmx1.facts
{'domain': 'ntc.com', 'hostname': 'vmx1', 'ifd_style': 'CLASSIC', 'version_info': junos.version_info(major=(15, 1), type=F, minor=4, build=15), 'version_RE0': '15.1F4.15', '2RE': False, 'serialnumber': 'VMX29', 'fqdn': 'vmx1.ntc.com', 'virtual': True, 'switch_style': 'BRIDGE_DOMAIN', 'version': '15.1F4.15', 'master': 'RE0', 'HOME': '/var/home/ntc', 'model': 'VMX', 'RE0': {'status': 'OK', 'last_reboot_reason': '0x200:normal shutdown ', 'model': 'RE-VMX', 'up_time': '6 hours, 18 minutes, 25 seconds', 'mastership_state': 'master'}, 'vc_capable': False, 'personality': 'MX'}
>>>

For more information on what methods and attributes are available, you can use help and dir on the vmx1 object.

>>> dir(vmx1)
[ 'auto_probe', 'bind', 'cli', 'close', 'connected', 'display_xml_rpc', 'execute', 'facts', 'facts_refresh', 'hostname', 'logfile', 'manages', 'open', 'password', 'probe', 'rpc', 'timeout', 'transform', 'user']
>>>
>>>
>>> help(vmx1)
Help on Device in module jnpr.junos.device object:

class Device(__builtin__.object)
 |  Junos Device class.
 |
 |  :attr:`ON_JUNOS`:
 |      **READ-ONLY** -
 |      Auto-set to ``True`` when this code is running on a Junos device,
 |      vs. running on a local-server remotely connecting to a device.
 |
 |  :attr:`auto_probe`:
 |      When non-zero the call to :meth:`open` will probe for NETCONF
 |      reachability before proceeding with the NETCONF session establishment.
 |      If you want to enable this behavior by default, you could do the
 |      following in your code::
 |
 |          from jnpr.junos import Device
 |
 |          # set all device open to auto-probe with timeout of 10 sec
 |          Device.auto_probe = 10
 |
 |          dev = Device( ... )
 |          dev.open()   # this will probe before attempting NETCONF connect
 |

Create Interfaces Configuration Template

We'll have a few main configurations to get BGP peers established - the first one we'll look at is the interface configurations for one of the vMX routers.

The PyEZ library can send configurations via a string, a static file, or automatically render a Jinja2 template with variables. In our example, we'll be using the Jinja2 template as we need to configure multiple interfaces. This provides the most flexibility.

In order to construct the Jinja template for interfaces, we'll first pull the interfaces configuration section from the device.

Note: It is helpful to break the configuration into configuration stanzas, and we'll do this based on the sections shown in a show config on the vMX devices. In this tutorial, we focus on a few sections, namely interfaces, policy, and routing configurations.

Here is a sample output of what the interfaces output looks like.

ntc@vmx1# show interfaces
ge-0/0/0 {
    description to_VMX03;
    unit 0 {
        family inet {
            address 10.10.30.1/24;
        }
    }
}
ge-0/0/1 {
    unit 0 {
        family inet;
    }
}

If we pull out the unique configuration data (IP address, mask, etc.), we can easily come up with the following Jinja2 template:

interfaces {
{% for int in interfaces %}
    {{ int.physical_interface }} {
        description {{ int.description }};
        unit 0 {
{% if int.ip_address is defined %}
            family inet {
                address {{ int.ip_address }};
        }
{% else %}
            family inet;
{% endif %}
        }
    } {% endfor %}
}

Note that this template is using a list of dictionaries as it's data model. We'll show how to insert variables into the template in a little bit.

This is the first of our three Jinja2 templates that we'll be using.

We'll save this template in the home directory and call it bgp_template.conf.

Insert Data (Variables) into Template

Since now the template is created, we now need a way to insert variables into the template to re-build the configuration. In order to do this, we'll use the Jinja2 Python module.

We simply import jinja2 and then load (access) the directory where the template is stored.

>>> import jinja2
>>>
>>> templateLoader = jinja2.FileSystemLoader(searchpath="/")
>>> templateEnv = jinja2.Environment(loader=templateLoader)
>>>
>>> TEMPLATE_FILE = "/home/ntc/bgp_template.conf"
>>> template = templateEnv.get_template(TEMPLATE_FILE)
>>>

At this point, we need to create the data model that will work with our template. In our case, we have a dictionary called config_vars that contains a single key-value pair. The key is called interfaces and it's value is a list of dictionaries. Take note of the for loop in the template, i.e. for int in interfaces - the interfaces list in the dictionary is the interfaces object we are iterating through in the template.

>>> config_vars = {
...          'interfaces' : [
...             { 'physical_interface' : 'ge-0/0/2', 'description' : 'to_VMX02', 'ip_address' : '10.10.10.1/24' } ,
...             { 'physical_interface' : 'ge-0/0/0', 'description' : 'to_VMX03', 'ip_address' : '10.10.30.1/24' }
...           ]
... }
>>> 

We can now insert, or render, the variables directly into the template as shown here:

>>> outputText = template.render( config_vars )
>>> print outputText
interfaces {

    ge-0/0/2 {
        description to_VMX02;
        unit 0 {

            family inet {
                address 10.10.10.1/24;
        }

        }
    }
    ge-0/0/0 {
        description to_VMX03;
        unit 0 {

            family inet {
                address 10.10.30.1/24;
        }

        }
    }
}

Thus far, we've see how to de-couple a basic interface configuration and create two separate software artifacts - a Jinja2 template and a variables (data) file.

Load and Commit the Interfaces Configuration

At this point, we have a config that can be sent to the device. To do this, we'll use the Config object we imported from PyEZ earlier.

>>> config = Config(vmx1)
>>> config.load(template_path=conf_file, template_vars=config_vars, merge=True)
<Element load-configuration-results at 0x7f96662c8518>
>>> config.commit()
True
>>>

Note: that you need to load the config and commit it the config for it to take effect as when the config is loaded, it's loaded as part of the candidate configuration.

You can SSH to the device to verify the configuration.

ntc@ntc:~$ ssh vmx1
Password:
Last login: Mon Aug  8 20:25:40 2016 from 10.0.0.5
--- JUNOS 15.1F4.15 built 2015-12-23 20:22:39 UTC
ntc@vmx1> show configuration interfaces
ge-0/0/0 {
    description to_VMX03;
    unit 0 {
        family inet {
            address 10.10.30.1/24;
        }
    }
}
ge-0/0/1 {
    unit 0 {
        family inet;
    }
}
ge-0/0/2 {
    description to_VMX02;
    unit 0 {
        family inet {
            address 10.10.10.1/24;
        }
    }
}

Juniper's use of stanza's works well with Jinja templates as you can more easily break each stanza into it's own unique template or template section, and incrementally build your config. However, while we are showing different templates here, we are building on the original, e.g. bgp_template.conf.

We have the basic interfaces configured for a single router, so we'll now look at adding BGP configuration to the bgp_template.conf file.

Build BGP Configuration Templates

Using the same process as before, we'll create a template and a variables file which will yeild the BGP configuration.

protocols {
    bgp {
        export export_bgp;
{% for peer in bgp %}
        group {{ peer.remote_description }} {
            description {{ peer.remote_description }};
            local-address {{ peer.local_ip }};
            neighbor {{ peer.remote_peer }};
            peer-as {{ peer.remote_as }};
        }
{% endfor %}
    }
}

Using the same process again, we'll create yet another template for the global BGP configuration.

routing-options {
    autonomous-system {{ bgpasn }};
    router-id {{ bgp_router_id }};
}
policy-options {
    policy-statement export_bgp {
        term 1 {
            from {
                route-filter 10.0.0.0/8 prefix-length-range /24-/24;
            }
            then accept;
        }
        term END {
            then reject;
        }
    }
}

Take note that the route-filter is hard coded into the template. As you work on the configuration, it's up to you to determine which part of the device configuration must be modeled in YAML, and which can be embedded in the template.

Inserting BGP Variables into the Template

Remember earlier, we created config_vars, which was the variable we used to render with the template. We are going to expand that now adding in all data required to configure all three vMX routers, including each router's interfaces and BGP configuration.

We'll do this by adding more key-value pairs to the config_vars dictionary. We'll break it up by device.

>>> config_vars = {
...     'vmx1': {
...          'interfaces' : [
...             { 'physical_interface' : 'ge-0/0/2', 'description' : 'to_VMX02', 'ip_address' : '10.10.10.1/24' } ,
...             { 'physical_interface' : 'ge-0/0/0', 'description' : 'to_VMX03', 'ip_address' : '10.10.30.1/24' }
...           ],
...          'bgp' : [
...             { 'remote_as' : '65222', 'remote_peer' : '10.10.10.2', 'remote_description' : 'VMX02', 'local_ip' : '10.10.10.1' } ,
...             { 'remote_as' : '65333', 'remote_peer' : '10.10.30.3', 'remote_description' : 'VMX03', 'local_ip' : '10.10.30.1'  }
...           ],
...           'bgp_router_id' : '10.10.10.1',
...           'bgpasn' : '65111'
...     },
...     'vmx2': {
...         'interfaces' : [
...             { 'physical_interface' : 'ge-0/0/2', 'description' : 'to_VMX01', 'ip_address' : '10.10.10.2/24' } ,
...             { 'physical_interface' : 'ge-0/0/1', 'description' : 'to_VMX03', 'ip_address' : '10.10.20.2/24' }
...           ],
...           'bgp' : [
...             { 'remote_as' : '65111', 'remote_peer' : '10.10.10.1', 'remote_description' : 'VMX01', 'local_ip' : '10.10.10.2'  } ,
...             { 'remote_as' : '65333', 'remote_peer' : '10.10.20.3', 'remote_description' : 'VMX03', 'local_ip' : '10.10.20.2'  }
...           ],
...           'bgp_router_id' : '10.10.20.2',
...           'bgpasn' : '65222'
...
...     },
...      'vmx3': {
...         'interfaces' : [
...             { 'physical_interface' : 'ge-0/0/1', 'description' : 'to_VMX02', 'ip_address' : '10.10.20.3/24' } ,
...             { 'physical_interface' : 'ge-0/0/0', 'description' : 'to_VMX01', 'ip_address' : '10.10.30.3/24' }
...           ],
...          'bgp' : [
...             { 'remote_as' : '65111', 'remote_peer' : '10.10.30.1', 'remote_description' : 'VMX01', 'local_ip' : '10.10.30.3'  } ,
...             { 'remote_as' : '65222', 'remote_peer' : '10.10.20.2', 'remote_description' : 'VMX02', 'local_ip' : '10.10.20.3'  }
...           ],
...           'bgp_router_id' : '10.10.30.3',
...           'bgpasn' : '65333'
...     }
...  }
>>>

Rather than embed these variables in code, you may want to think about storing them as YAML files and directly loading them as dictionaries in Python. This can be done quite easily by using yaml.load(open(config.yml)).

Apply Device Configurations

In order to easily apply configurations to all three devices, we'll create a list of devices and variables for the credentials and file names, and then loop through the list, loading and committing the configuration for each device.

>>> device_list = ['vmx1', 'vmx2', 'vmx3' ]
>>> user = 'ntc'
>>> password = 'ntc123'
>>> conf_file = "bgp_template.conf"

We will now loop through the list of devices:

>>> for device_name in device_list:
...    device = JUNIPER(host=device_name, user=user, password=password)
...    device.open()
...    print device.connected
...    print device.hostname
...    device_vars = config_vars[device_name]
...    config = Config(device)
...    results = config.load(template_path=conf_file, template_vars=device_vars, merge=True)
...    config.commit()
...    device.close()
...
Device(vmx1)
True
vmx1
True
Device(vmx2)
True
vmx2
True
Device(vmx3)
True
vmx3
True
>>>

You can see that there are basic print statements that can be built-in to show the the current status.

Verify Configuration and Validate BGP Connectivity

At this point your BGP peers should be up, you can verify by SSH'ing to each device and issuing a show bgp summary command and/or running ping tests.

ntc@ntc:~$ ssh vmx1
Password:
Last login: Tue Aug  9 02:59:03 2016 from 10.0.0.5
--- JUNOS 15.1F4.15 built 2015-12-23 20:22:39 UTC
ntc@vmx1> show bgp summary
Groups: 2 Peers: 2 Down peers: 0
Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
inet.0
                       6          1          0          0          0          0
Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Accepted/Damped...
10.10.10.2            65222         85         87       0       0       37:49 1/3/3/0              0/0/0/0
10.10.30.3            65333         88         87       0       0       37:48 0/3/3/0              0/0/0/0

ntc@vmx1> ping 10.10.10.2
PING 10.10.10.2 (10.10.10.2): 56 data bytes
64 bytes from 10.10.10.2: icmp_seq=0 ttl=64 time=5.202 ms
^C
--- 10.10.10.2 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 5.202/5.202/5.202/0.000 ms

ntc@vmx1>

Take the time to verify connectivity between all devices. Issue any other commands that can be used to test the connectivity.

Configuration Files

bgp_template.conf

interfaces {
{% for int in interfaces %}
    {{ int.physical_interface }} {
        description {{ int.description }};
        unit 0 {
{% if int.ip_address is defined %}
            family inet {
                address {{ int.ip_address }};
        }
{% else %}
            family inet;
{% endif %}
        }
    } {% endfor %}
}
routing-options {
    autonomous-system {{ bgpasn }};
    router-id {{ bgp_router_id }};
}
protocols {
    bgp {
        export export_bgp;
{% for peer in bgp %}
        group {{ peer.remote_description }} {
            description {{ peer.remote_description }};
            local-address {{ peer.local_ip }};
            neighbor {{ peer.remote_peer }};
            peer-as {{ peer.remote_as }};
        }
{% endfor %}
    }
}
policy-options {
    policy-statement export_bgp {
        term 1 {
            from {
                route-filter 10.0.0.0/8 prefix-length-range /24-/24;
            }
            then accept;
        }
        term END {
            then reject;
        }
    }
}

build_push_bgp.py

from jnpr.junos.device import Device as JUNIPER
from jnpr.junos.utils.config import Config

device_list = ['vmx1', 'vmx2', 'vmx3' ]
user = 'ntc'
password = 'ntc123'
conf_file = "bgp_template.conf"

config_vars = { 
    'vmx1': {
         'interfaces' : [
            { 'physical_interface' : 'ge-0/0/2', 'description' : 'to_VMX02', 'ip_address' : '10.10.10.1/24' } ,
            { 'physical_interface' : 'ge-0/0/0', 'description' : 'to_VMX03', 'ip_address' : '10.10.30.1/24' }
          ],
         'bgp' : [
            { 'remote_as' : '65222', 'remote_peer' : '10.10.10.2', 'remote_description' : 'VMX02', 'local_ip' : '10.10.10.1' } ,
            { 'remote_as' : '65333', 'remote_peer' : '10.10.30.3', 'remote_description' : 'VMX03', 'local_ip' : '10.10.30.1'  } 
          ],
          'bgp_router_id' : '10.10.10.1',
          'bgpasn' : '65111'
    },
    'vmx2': {
        'interfaces' : [
            { 'physical_interface' : 'ge-0/0/2', 'description' : 'to_VMX01', 'ip_address' : '10.10.10.2/24' } ,
            { 'physical_interface' : 'ge-0/0/1', 'description' : 'to_VMX03', 'ip_address' : '10.10.20.2/24' }
          ],
          'bgp' : [
            { 'remote_as' : '65111', 'remote_peer' : '10.10.10.1', 'remote_description' : 'VMX01', 'local_ip' : '10.10.10.2'  } ,
            { 'remote_as' : '65333', 'remote_peer' : '10.10.20.3', 'remote_description' : 'VMX03', 'local_ip' : '10.10.20.2'  } 
          ],
          'bgp_router_id' : '10.10.20.2',
          'bgpasn' : '65222'

    },
     'vmx3': {
        'interfaces' : [
            { 'physical_interface' : 'ge-0/0/1', 'description' : 'to_VMX02', 'ip_address' : '10.10.20.3/24' } ,
            { 'physical_interface' : 'ge-0/0/0', 'description' : 'to_VMX01', 'ip_address' : '10.10.30.3/24' }
          ],
         'bgp' : [
            { 'remote_as' : '65111', 'remote_peer' : '10.10.30.1', 'remote_description' : 'VMX01', 'local_ip' : '10.10.30.3'  } ,
            { 'remote_as' : '65222', 'remote_peer' : '10.10.20.2', 'remote_description' : 'VMX02', 'local_ip' : '10.10.20.3'  } 
          ],
          'bgp_router_id' : '10.10.30.3',
          'bgpasn' : '65333'
    }
 }


for device_name in device_list:
   device = JUNIPER(host=device_name, user=user, password=password)
   device.open()
   print device.connected
   print device.hostname
   device_vars = config_vars[device_name]
   config = Config(device)
   results = config.load(template_path=conf_file, template_vars=device_vars, merge=True)
   config.commit()
   device.close()