Blogs

Simplifying L2VPN config with custom YANG

By mwiget posted 04-19-2017 07:53

  

Summary

There are many ways to provision pseudowires over a MPLS network. Some require knowledge about both PE addresses, like FEC 129 (https://tools.ietf.org/html/rfc4379). Others, like Kompella L2VPN (https://tools.ietf.org/html/rfc6624, assume pseudowires to belong to the same customer across a MPLS network and hence require a routing instance per customer per PE. What if a pseudowire must be established between customers?

 

If the goal is to support millions of pseudwires across thousands of PE routers, the planning and provisioning can become overwhelming and scaling limitations, like number of routing instances per PE, become limiting factors.

 

The solution outlined below is taking a novel approach to Kompella L2VPN by defining an abstract service model for pseudowires, requiring just the bare minimum data for a given endpoint: A site wide unique line Id and the attachment circuit (interface and VLAN). The remote end is automatically discovered via BGP and the label allocated via LDP.

 

A custom YANG model in combination with an on-device translation script hides the complexity and protocol and device related configuration from the OSS/BSS layer: the operator simply specifies the Line Id and attachment point and the network provisions itself !

 

An example of a single pseudowire between two PE’s using custom YANG is shown in the following picture:

Custom YANG based pseudwire

 

The solution allows the provisioning of each endpoints without any knowledge of the remote attachment point, allowing for isolated and dynamic provisioning of either endpoint. Once the second endpoint is provisioned, BGP signaling will allow the pseudowire to be established.

 

A short overview video walks thru the process:

 

Theory

Before going into the model, a quick recap on how Kompella L2VPN according to RFC 6624 works. A more detailed description can be found in IP-MPLS Forum BGP Autodiscovery and Signaling for
VPWS-Based VPN Services
.

 

L2 Frames are transported with two MPLS labels, an outer RSVP/LDP transport label (to reach the remote PE) and an inner, BGP based, Virtual Circuit (VC) label:

 

MPLS outer label MPLS inner label Payload
RSVP/LDP transport BGP VC L2 Frame

 

BGP is used to signal the inner label of the circuit endpoint together with the address of the PE router and the outer label is learned either via LDP or RSVP to reach the PE’s IP address.

Each site within a L2VPN has a unique, 16-bit id, that is signaled via BGP NLRI (Network Layer Reachability Information). LSP’s are formed between PE’s by calculating the inner VC label based on local and remote site Id.

 

A brief comparison between Kompella and Martini L2VPN can be found in Darren’s blog at [L2VPN on Junos using CCC/Martini/Kompella](L2VPN on Junos using CCC/Martini/Kompella).

The purpose of our custom YANG model is for point-to-point E-Lines and hence a method must be found to calculate route target, route distinguisher, local and remote site id per E-Line.

Instead of assigning unique routing instances per customers, the solution shares pseudowires between customers and maximizes the number of pseudowires per routing instance. This is achieved by generating the rotue target plus local and remote site id with a simple calculation.

 

The constant Max_PW_per_RT defines how many pseudowires can be assigned per route target. Keep in mind, that the site Id is a 16 bit number and each pseudowire is defined by 2 unique numbers (within the same route target). Typical values for max_pw_per_rt are 4096 and maximum is 8192.

Input variable is the Line_id, which is an unsigned integer, identifying the pseudowire across the network:

 

  • Route_target = Base_RT + int(Line_id / Max_PW_per_RT)

The local and remote site Id’s are calculated:

 

  • Site_Id_1 = (2 * (Line_id + 1)) % (2 * Max_PW_per_RT) + 1
  • Site_Id_2 = Site_Id_1 + 1

The PE configuration for a given pseudowire differs on either side by swapping local and remote site id’s. The solution requires the operator to treat each pseudowire as a graph and define which endpoint is the beginning and which one the end. The current solution uses the notion of ‘hub’ and ‘spoke’, labelling one end of the stick as hub and the other as remote.

 

YANG Data Model

What do we absolutely need to define a pseudowire endpoint based on the formula above? we need:

  1. Line Id
  2. Is this the start or endpoint (hub or spoke)
  3. Physical Interface the circuit is attached to
  4. VLAN id
  5. Base route target

The Line Id defines the same pseudowire at both PE endpoints and the hub or spoke helps picking the values for local and remote site Id. The physical interface name together with the VLAN id defines the endpoint of the pseudowire from which packets are received and sent to. The base route target is added to the calculated route target in order to get a network wide unique range of route targets used for this pseudowire service.

module l2vpn {

  yang-version "1";
  namespace "http://yang.juniper.net/customyang/l2vpn";
  prefix "l2vpn";

  organization
    "Juniper Networks";

  description
    "A demo custom Yang module for L2VPN service";

  revision "2017-01-21" {
    description
      "Initial revision";
  }

  container l2vpn {
    description "Configure L2VPN service";
    container global {
      leaf as-number  {
        type string;
        mandatory true;
      }
      leaf side {
        description "Local connection point must either be hub or spoke";
        type enumeration {
          enum hub;
          enum spoke;
        }
        mandatory true;
      }
    }
    container lines {
      list line {
        description "L2VPN p2p links";
        key "line-id";
        leaf line-id {
          description "Unique Line Identifier";
          type uint32;
          mandatory true;
        }
        leaf interface {
          description "Local termination logical interface";
          type string;
        }
        leaf vlan {
          description "Local VLAN Id";
          type uint16 {
            range 0..4094;
          }
          mandatory true;
        }
        leaf side {
          description "Local connection point must either be hub or spoke";
          type enumeration {
            enum hub;
            enum spoke;
          }
        }
        leaf name {
          description "Line name/description";
          type string;
        }
      }
    }
  }
}

 

Starting with 16.1, Junos can consume custom YANG models at runtime and expand the configuration storage accordingly. To load just the YANG model, transfer the file above onto a Junos device running 16.1 or newer, then load it with request system yang add

mwiget@pe1> request system yang add package l2vpn module l2vpn.yang
YANG modules validation : START
YANG modules validation : SUCCESS
TLV generation: START
TLV generation: SUCCESS
Building schema and reloading /config/juniper.conf.gz ...
Setting up Virtual platform specific options
kenv: unable to get vmtype
mgd: commit complete
Restarting mgd ...

WARNING: cli has been replaced by an updated version:
CLI release 16.1R4.7 built by builder on 2017-02-22 12:29:01 UTC
Restart cli using the new version ? [yes,no] (yes)

Restarting cli ...
mwiget@pe1>

Verify the package is successfully loaded with:

mwiget@pe1> show system yang package
Package ID            :l2vpn
YANG Module(s)        :l2vpn.yang
Translation Script(s) :*
Translation script status is disabled
mwiget@pe1>

 

At this point, we can start configuring endpoints in Junos with the new YANG model. Though Junos has no clue what to do with these entries, because we haven’t specified a translation script just yet. But we can explore the new configuration CLI anyway.

 

Junos has a built-in, context sensitive help system. Maybe it also learned something new about l2vpn? Lets find out (output shortened): 

mwiget@pe1> configure
Entering configuration mode

[edit]
mwiget@pe1# help apropos l2vpn
set dynamic-profiles <profile-name> protocols oam ethernet connectivity-fault-management action-profile
. . .
set fabric protocols bgp group <group_name> neighbor <address> family l2vpn
    MPLS-based Layer 2 VPN and VPLS NLRI parameters
set l2vpn:l2vpn
    Configure L2VPN service
set l2vpn:l2vpn lines line
    L2VPN p2p links

[edit]
mwiget@pe1#

 

Yes! There is a new top level configuration stanza called l2vpn:l2vpn. The name l2vpn after the ‘:’ is what we defined in the custom YANG module and the l2vpn name is the namespace. Every custom YANG model must have a namespace. This ensures the uniqueness when multiple custom YANG models are loaded and makes sure, they don’t interfere with the native Junos YANG models.

 

Lets add now some pseudowires using the custom YANG model:

[edit]
mwiget@pe1# load merge terminal relative
[Type ^D at a new line to end input]
l2vpn:l2vpn {
        global {
          as-number 65000;
          side hub;
        }
    lines {
        line 900 {
            interface ge-0/0/1;
            vlan 100;
            name "Link HQ Berlin";
        }
        line 1000 {
            interface ge-0/0/1;
            vlan 1;
            name "Link Zurich";
        }
        line 5879004 {
            interface ge-0/0/1;
            vlan 88;
            name "Link Bangalore";
        }
    }
}
load complete

[edit]
mwiget@pe1# commit

There is also context sensitive help for each new element:

[edit l2vpn:l2vpn]
mwiget@pe1# set lines line 1000 ?
Possible completions:
  <[Enter]>            Execute this command
  interface            Local termination logical interface
  name                 Line name/description
  side                 Local connection point must either be hub or spoke
  vlan                 Local VLAN Id (0..4094)
  |                    Pipe through a command
[edit l2vpn:l2vpn]

The new configuration is now in Junos and will survive reboots just fine. It can be retrieved in all the different formats known to Junos: text, set, JSON, XML:

mwiget@pe1> show configuration l2vpn:l2vpn |display set
set l2vpn:l2vpn global as-number 65000
set l2vpn:l2vpn global side hub
set l2vpn:l2vpn lines line 900 interface ge-0/0/1
set l2vpn:l2vpn lines line 900 vlan 100
set l2vpn:l2vpn lines line 900 name "Link HQ Berlin"
set l2vpn:l2vpn lines line 1000 interface ge-0/0/1
set l2vpn:l2vpn lines line 1000 vlan 1
set l2vpn:l2vpn lines line 1000 name "Link Zurich"
set l2vpn:l2vpn lines line 5879004 interface ge-0/0/1
set l2vpn:l2vpn lines line 5879004 vlan 88
set l2vpn:l2vpn lines line 5879004 name "Link Bangalore"

mwiget@pe1> show configuration l2vpn:l2vpn |display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/16.1R4/junos">
    <l2vpn xmlns="http://yang.juniper.net/customyang/l2vpn">
            <global>
                <as-number>65000</as-number>
                <side>hub</side>
            </global>
            <lines>
                <line>
                    <line-id>900</line-id>
                    <interface>ge-0/0/1</interface>
                    <vlan>100</vlan>
                    <name>Link HQ Berlin</name>
                </line>
                <line>
                    <line-id>1000</line-id>
                    <interface>ge-0/0/1</interface>
                    <vlan>1</vlan>
                    <name>Link Zurich</name>
                </line>
                <line>
                    <line-id>5879004</line-id>
                    <interface>ge-0/0/1</interface>
                    <vlan>88</vlan>
                    <name>Link Bangalore</name>
                </line>
            </lines>
    </l2vpn>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

mwiget@pe1> show configuration l2vpn:l2vpn |display json
{
    "l2vpn:l2vpn" : {
        "global" : {
            "as-number" : "65000",
            "side" : "hub"
        },
        "lines" : {
            "line" : [
            {
                "line-id" : 900,
                "interface" : "ge-0/0/1",
                "vlan" : 100,
                "name" : "Link HQ Berlin"
            },
            {
                "line-id" : 1000,
                "interface" : "ge-0/0/1",
                "vlan" : 1,
                "name" : "Link Zurich"
            },
            {
                "line-id" : 5879004,
                "interface" : "ge-0/0/1",
                "vlan" : 88,
                "name" : "Link Bangalore"
            }
            ]
        }
    }
}

The next step is to create and load a translation script that transforms the new configuration into native Junos configuration.

 

Translation script

Junos translation scripts can be written in Python or SLAX.
I've picked SLAX for this example:

version 1.0;

ns junos = "http://xml.juniper.net/junos/*/junos";
ns xnm = "http://xml.juniper.net/xnm/1.1/xnm";
ns jcs = "http://xml.juniper.net/junos/commit-scripts/1.0";

ns l2vpn_p = "http://yang.juniper.net/customyang/l2vpn";

import "/var/db/scripts/import/junos.xsl";


match configuration {


  if (l2vpn_p:l2vpn) {

    var $connection = jcs:open();
    var $config-rpc = <get-configuration junos-model="junos-model"> {
      <configuration> {
        <l2vpn xmlns="http://yang.juniper.net/customyang/l2vpn"> {
          <global>;
        }
      }
    }
    var $global = jcs:execute($connection, $config-rpc);
    var $asnr = $global/l2vpn_p:l2vpn/l2vpn_p:global/l2vpn_p:as-number;
    var $side = $global/l2vpn_p:l2vpn/l2vpn_p:global/l2vpn_p:side;

    /*
    <xnm:warning> {
      <message> jcs:printf("asnr=%d, side=%s", $asnr, $side);
    }
    */

    for-each (l2vpn_p:l2vpn/l2vpn_p:lines/l2vpn_p:line) {

      var $lineid = l2vpn_p:line-id;
      var $maxpw = 8192;
      var $lsid = ( 2 * ($lineid + 1)) mod (2 * $maxpw) + 1;
      var $rsid = $lsid + 1;
      var $hubspoke = {
        if (l2vpn_p:side) {
          expr l2vpn_p:side;
        } else {
          expr $side;
        }
      }

      /*
      <xnm:warning> {
        <message> jcs:printf("lineid=%d", $lineid);
      }
      */

      var $line-rpc = <get-configuration junos-model="junos-model"> {
        <configuration> {
          <l2vpn xmlns="http://yang.juniper.net/customyang/l2vpn"> {
            <lines> {
              <line> {
                <line-id>$lineid;
              }
            }
          }
        }
      }

      var $line = jcs:execute($connection, $line-rpc);

      var $vlan = $line//l2vpn_p:vlan;
      /*
      <xnm:warning> {
        <message> jcs:printf("lineid=%d vlan=%d", $lineid, $vlan);
      }
      */

      var $localsite = {
        if ($hubspoke == "hub") {
          expr $lsid;
        } else {
          expr $rsid;
        }
      }
      var $remotesite = {
        if ($hubspoke  == "hub") {
          expr $rsid;
        } else {
          expr $lsid;
        }
      }
      var $rtn = floor ( $lineid div $maxpw );

      if ($line//l2vpn_p:interface) {
        <transient-change> {
          <interfaces> {
            <interface> {
              <name> $line//l2vpn_p:interface;
              <flexible-vlan-tagging>;
              <encapsulation> "flexible-ethernet-services";
              <description> "l2vpn " _ $asnr;
              <unit> {
                <name> $vlan;
                <vlan-id> $vlan;
                <encapsulation> "vlan-ccc";
                <family> {
                  <ccc>;
                }
              }
            }
          }
        }
     
      <transient-change> {
        <routing-instances> {
          <instance> {
            <name> "l2vpn-rt-" _ $rtn;
            <description> $asnr;
            <instance-type> "l2vpn";
            <interface> {
              <name> $line//l2vpn_p:interface _ "." _ $line//l2vpn_p:vlan;
            }
            <route-distinguisher> {
              <rd-type> $asnr _ ":" _ $rtn;
            }
            <vrf-target> {
              <community> "target:" _ $asnr _ ":" _ $rtn;
            }
            <protocols> {
              <l2vpn> {
                <encapsulation-type> "ethernet-vlan";
                <site> {
                  <name> "LINE-" _ $lineid;
                  <site-identifier> $localsite;
                  <interface> {
                    <name> $line//l2vpn_p:interface _ "." _ $line//l2vpn_p:vlan;
                    <remote-site-id> $remotesite;
                  }
                }
              }
            }
          }
        }
      } 
    }
  }
 }
}

We can add it to the already loaded custom YANG module:

mwiget@pe1> request system yang update l2vpn translation-script l2vpn.slax
Scripts syntax validation : START
l2vpn.slax: script check succeeds
Scripts syntax validation : SUCCESS

Or the script can be loaded together with the YANG module:

request system yang update l2vpn module custom-l2vpn.yang translation-script custom-l2vpn.slax

 

Translation scripts are very similar to commit script: They act upon configuration data whenever a commit is issued. There is however an important, performance related difference: The script is only given configuration delta, not the entire configuration. Imagine the configuration is updated with a new line on top of thousands of existing lines, or the VLAN id changes for one of the entries. There is no need to process all entries.

This performance optimization adds some complexity to the translation script: it might need to use RPC commands to retrieve relevant data not provided as input.

The example script queries the global l2vpn container once followed by the line container for a given change (add, delete, modify).

 

With the translation script loaded, we can check what the script generated: 

[edit]
mwiget@pe1# show |display translation-scripts translated-config
interfaces {
    ge-0/0/1 {
        description "l2vpn 65000";
        flexible-vlan-tagging;
        encapsulation flexible-ethernet-services;
        unit 100 {
            encapsulation vlan-ccc;
            vlan-id 100;
            family ccc;
        }
        unit 897 {
            encapsulation vlan-ccc;
            vlan-id 897;
            family ccc;
        }
        unit 88 {
            encapsulation vlan-ccc;
            vlan-id 88;
            family ccc;
        }
    }
}
routing-instances {
    l2vpn-rt-0 {
        description 65000;
        instance-type l2vpn;
        interface ge-0/0/1.100;
        interface ge-0/0/1.897;
        route-distinguisher 65000:0;
        vrf-target target:65000:0;
        protocols {
            l2vpn {
                encapsulation-type ethernet-vlan;
                site LINE-900 {
                    site-identifier 1803;
                    interface ge-0/0/1.100 {
                        remote-site-id 1804;
                    }
                }
                site LINE-1000 {
                    site-identifier 2003;
                    interface ge-0/0/1.897 {
                        remote-site-id 2004;
                    }
                }
            }
        }
    }
    l2vpn-rt-717 {
        description 65000;
        instance-type l2vpn;
        interface ge-0/0/1.88;
        route-distinguisher 65000:717;
        vrf-target target:65000:717;
        protocols {
            l2vpn {
                encapsulation-type ethernet-vlan;
                site LINE-5879004 {
                    site-identifier 10683;
                    interface ge-0/0/1.88 {
                        remote-site-id 10684;
                    }
                }
            }
        }
    }
}

[edit]
mwiget@pe1#
[edit]
mwiget@pe1# commit
commit complete

[edit]
mwiget@pe1# exit
Exiting configuration mode

With remote systems also being configured, the pseudowires become active:

mwiget@pe1> show l2vpn connections up
Layer-2 VPN connections:

Legend for connection status (St)
EI -- encapsulation invalid      NC -- interface encapsulation not CCC/TCC/VPLS
EM -- encapsulation mismatch     WE -- interface and instance encaps not same
VC-Dn -- Virtual circuit down    NP -- interface hardware not present
CM -- control-word mismatch      -> -- only outbound connection is up
CN -- circuit not provisioned    <- -- only inbound connection is up
OR -- out of range               Up -- operational
OL -- no outgoing label          Dn -- down
LD -- local site signaled down   CF -- call admission control failure
RD -- remote site signaled down  SC -- local and remote site ID collision
LN -- local site not designated  LM -- local site ID not minimum designated
RN -- remote site not designated RM -- remote site ID not minimum designated
XX -- unknown connection status  IL -- no incoming label
MM -- MTU mismatch               MI -- Mesh-Group ID not available
BK -- Backup connection          ST -- Standby connection
PF -- Profile parse failure      PB -- Profile busy
RS -- remote site standby    SN -- Static Neighbor
LB -- Local site not best-site   RB -- Remote site not best-site
VM -- VLAN ID mismatch           HS -- Hot-standby Connection

Legend for interface status
Up -- operational
Dn -- down

Instance: l2vpn-rt-0
Edge protection: Not-Primary
  Local site: LINE-900 (1803)
    connection-site           Type  St     Time last up          # Up trans
    1804                      rmt   Up     Apr 16 08:31:17 2017           1
      Remote PE: 2.2.2.2, Negotiated control-word: Yes (Null)
      Incoming label: 800005, Outgoing label: 800004
      Local interface: ge-0/0/1.100, Status: Up, Encapsulation: VLAN
      Flow Label Transmit: No, Flow Label Receive: No
  Local site: LINE-1000 (2003)
    connection-site           Type  St     Time last up          # Up trans
    2004                      rmt   Up     Apr 16 10:37:47 2017           1
      Remote PE: 2.2.2.2, Negotiated control-word: Yes (Null)
      Incoming label: 800001, Outgoing label: 800000
      Local interface: ge-0/0/1.897, Status: Up, Encapsulation: VLAN
      Flow Label Transmit: No, Flow Label Receive: No

Instance: l2vpn-rt-717
Edge protection: Not-Primary
  Local site: LINE-5879004 (10683)
    connection-site           Type  St     Time last up          # Up trans
    10684                     rmt   Up     Apr 16 08:31:17 2017           1
      Remote PE: 2.2.2.2, Negotiated control-word: Yes (Null)
      Incoming label: 800003, Outgoing label: 800002
      Local interface: ge-0/0/1.88, Status: Up, Encapsulation: VLAN
      Flow Label Transmit: No, Flow Label Receive: No

 

BGP, LDP and OSPF must be configured for the example to work: 

mwiget@pe1> show configuration protocols bgp
group internal {
    type internal;
    multihop;
    local-address 1.1.1.1;
    family l2vpn {
        signaling;
    }
    neighbor 2.2.2.2;
}

mwiget@pe1> show configuration protocols ldp
interface ge-0/0/0.0;

mwiget@pe1> show configuration protocols ospf
area 0.0.0.0 {
    interface all;
}

Summary

Custom YANG allows a level of abstraction on a device that simplifies operation by telling a device what to configure without the “how”. This is how intent based configuration works on Junos since 16.1.

 

References

Creating Translation Scripts for YANG Configuration Models


#l2vpn
#YANG
#ExpertAdvice