To begin with, this guide explores how to build a multihomed EVPN-VXLAN fabric using Arista CloudVision Portal (CVP). There are two common design models: MLAG and non-MLAG. As a result, many engineers prefer the non-MLAG design engineers prefer the non-MLAG multihoming model due to better interoperability, reduced cabling costs, and adherence to open standards. We’ll use a custom Studio workflow in CVP to automate this configuration, since no built-in solution exists for this specific use case.

What is Studio?

Studio is a tool within CloudVision Portal that lets you automate switch configuration. It uses forms and Python/Mako scripts to generate and push configurations. You choose a template, fill in the required fields, and Studio applies the configuration to your devices.

When built-in Studios aren’t enough—like for Active-Active EVPN-VXLAN Multihoming—you can build a custom Studio to fit your needs.

Multihoming EVPN-VXLAN

Multihoming EVPN-VXLAN increases redundancy without using MLAG. It works with EVPN route-type 1 (Ethernet Auto-Discovery) and route-type 4 (Ethernet Segment). Leaf switches connect directly to multiple spines and operate independently. You create logical port-channels using Ethernet Segment Identifiers (ESI). Hence, the ESI is a 10-byte value that defines which links belong to the same multihomed segment.

Configuration Requirements for Active-Active EVPN-VXLAN Multihoming

To automate Active-Active Multihoming with EVPN-VXLAN, you need:

  • Point-to-point interfaces with /31 subnets between leaf and spine switches
  • Loopback0 IPs on each device (used for BGP EVPN peering and VXLAN source IP)
  • BGP AS numbers (leaves use different ASNs; spines can share one)
  • ESI values on interfaces that form port-channels
  • VLAN-to-VNI mapping for VXLAN and advertisement over EVPN

So, we’ll automate all of this using a custom Studio.

Design of the Custom Studio

The custom Studio form lets you define everything you need. Devices are listed automatically. So, you assign each one a role (leaf or spine) and ID. The form also accepts values for BGP AS numbers, loopback IPs, subnets, ESI values, and other parameters. When submitted, the Studio creates a configuration tailored to your topology.

The Code of the Form: Python and Mako

We use Python and Mako templates inside the Studio to dynamically generate the configuration. So, It processes interface information, assigns IP addresses, calculates loopbacks, and builds BGP neighbor relationships based on device roles.

<% 

#GETTING CURRENT DEVICE INFO ( Module is context, Class is Context, creating device object, use its getDevice function)
device= ctx.getDevice()

device_int_list=list(device.getInterfaces().mapping.keys())

#GETTING Tag Label and values of current device, values are in list.
role_of_switch=device.getTags(ctx,'Role')
ID_of_switch=device.getTags(ctx,'ID')

role_value =[tag.value for tag in role_of_switch]
id_value=[id.value for id in ID_of_switch] 

# this is a list, value will come from form by mako
p2PSubnet=[ ]
spine_int_subnet= " "
peer_int_list= []
spine_loopback_list= []
interface_index=0

def spliting(subnet):

    ip_part, subnet_part = subnet.split('/')
    octets = ip_part.split('.')
    first_three_octet = ".".join(octets[:3])
    last_octet = int(octets[3])

    return first_three_octet,last_octet

def generating_IP(subnet,cidr,id_value,interface_index=None, role=None):

    if isinstance(subnet, list) and role == "leaf":
        subnet=subnet[interface_index]
        first_three_octet,last_octet=spliting(subnet)
        generated_last_octet=(2*int(id_value[0]))-1
        ip=first_three_octet+'.'+str(generated_last_octet)+'/'+str(cidr)
        return ip

    if isinstance(subnet, list):

        subnet=subnet[int(id_value[0]) - 1]
        first_three_octet,last_octet=spliting(subnet)
        generated_last_octet=int(last_octet + interface_index)
        ip=first_three_octet+'.'+str(generated_last_octet)+'/'+str(cidr)
        return ip
   
    else:
        
        first_three_octet,last_octet=spliting(subnet)
        generated_last_octet=int(id_value[0])-1
        ip=first_three_octet+'.'+str(generated_last_octet)+'/'+str(cidr)
        return ip


def get_peer_info(peer_int_obj, ctx, device):
   
    if peer_int_obj.getPeerDevice() and peer_int_obj.getPeerDevice().hostName:
        peer_device = peer_int_obj.getPeerDevice()
        role_of_peer = peer_device.getTags(ctx, 'Role')
        ID_of_peer = peer_device.getTags(ctx, 'ID')
        peer_role_value = [tag.value for tag in role_of_peer]
        peer_id_value = [tag.value for tag in ID_of_peer]
        description = device.hostName + '-' + peer_int_obj.getPeerDevice().hostName
    else:
        peer_role_value = [False]
        description = False
        peer_id_value =False

    return peer_role_value, description, peer_id_value


%>
  
% if dc:
   
    
    <% spine_loopback0=dc.resolve()["dcgroup"]["spineandleafDetails"]["spineLoopback0"] %>
    <% spine_as=dc.resolve()["dcgroup"]["spineandleafDetails"]["spineAsNumber"] %>
    <% leaf_loopback0=dc.resolve()["dcgroup"]["spineandleafDetails"]["leafLoopback0"] %>
    <% leaf_as=dc.resolve()["dcgroup"]["spineandleafDetails"]["leafAsNumber"] %>
    <% maximum_path=dc.resolve()["dcgroup"]["spineandleafDetails"]["maximumPath"] %>
    <% ecmp=dc.resolve()["dcgroup"]["spineandleafDetails"]["ecmpValue"] %>
    <% howManyLeaf=dc.resolve()["dcgroup"]["spineandleafDetails"]["howManyLeaf"] %>
    <% howManySpine=dc.resolve()["dcgroup"]["spineandleafDetails"]["howManySpine"] %>
    <% multihop=dc.resolve()["dcgroup"]["spineandleafDetails"]["ebgpMultihop"] %>

    %for i in dc.resolve()["dcgroup"]["p2PSubnets"]:
            
           <% p2PSubnet.append(i["p2PSubnet"])  %>
            
    % endfor

% endif

%if role_value[0] == "spine":

    % for i in device_int_list:
    <%
    peer_int_obj=device.getInterfaces().mapping.values().mapping[i]
    peer_role_value, description,peer_id_value = get_peer_info(peer_int_obj, ctx, device)
    %>              
    % if peer_role_value[0] == "leaf":
                   
    <% 
    interface_ip =generating_IP(p2PSubnet,31,id_value,interface_index)
    interface_index +=2 
    %>
         
    interface ${i}
    mtu ${dc.resolve()["dcgroup"]["spineandleafDetails"]["mtu"]}
    description ${description}
    no switchport 
    ip address ${interface_ip}
    !

    % endif

    %endfor

    <% loopback_ip= generating_IP(spine_loopback0,32,id_value) %>
    interface loopback0
    ip address ${loopback_ip}
    !

    peer-filter LEAF-AS
    match as-range ${leaf_as}-${int(leaf_as)+int(howManyLeaf)-1 } result accept

    !
    ip routing
    !
    router bgp ${spine_as}
    router-id ${loopback_ip.split('/')[0]}
    distance bgp 20 200 200
    maximum-paths ${maximum_path} ecmp ${ecmp}
    no bgp default ipv4-unicast
    bgp listen range ${p2PSubnet[int(id_value[0]) - 1]} peer-group UNDERLAY peer-filter LEAF-AS
    bgp listen range ${leaf_loopback0} peer-group OVERLAY peer-filter LEAF-AS
    neighbor UNDERLAY peer group 
    neighbor OVERLAY peer group
    neighbor OVERLAY update-source loopback  0
    neighbor OVERLAY ebgp-multihop ${multihop}
    neighbor OVERLAY send-community
    neighbor OVERLAY bfd
    !
    address-family ipv4 
    neighbor UNDERLAY activate
    redistribute connected 
    !
    address-family evpn
    neighbor OVERLAY activate 
    !
    interface vxlan 1
    vxlan source-interface loopback 0


%endif

%if role_value[0] == "leaf":

    % for i in device_int_list:
    <%
    peer_int_obj=device.getInterfaces().mapping.values().mapping[i]
    peer_role_value, description,peer_id_value = get_peer_info(peer_int_obj, ctx, device)
    %> 

    % if peer_role_value[0] == "spine":
                   
    <% 
    interface_ip =generating_IP(p2PSubnet,31,id_value,interface_index,role="leaf")
    first_three_octet,last_octet =spliting(interface_ip)
    peer_int_ip = f"{first_three_octet}.{last_octet - 1}"
    peer_int_list.append(peer_int_ip)

    spine_loopback_ip= generating_IP(spine_loopback0,32,peer_id_value) 
    spine_loopback_list.append(spine_loopback_ip.split('/')[0])


    interface_index +=1
    %>

    interface ${i}
    mtu ${dc.resolve()["dcgroup"]["spineandleafDetails"]["mtu"]}
    description ${description}
    no switchport 
    ip address ${interface_ip}
    !

    % endif

    %endfor

    <% 
    loopback_ip= generating_IP(leaf_loopback0,32,id_value)
    
    
    %>
    interface loopback0
    ip address ${loopback_ip}
    !

    ip routing
    !
    router bgp ${int(leaf_as)+int(id_value[0])-1}
    router-id ${loopback_ip.split('/')[0]}
    distance bgp 20 200 200
    maximum-paths ${maximum_path} ecmp ${ecmp}
    no bgp default ipv4-unicast
    
    neighbor UNDERLAY peer group
    neighbor UNDERLAY remote-as ${spine_as}
     
    %for i in peer_int_list:
    neighbor  ${i} peer group  UNDERLAY
    %endfor

    neighbor OVERLAY peer group
    neighbor OVERLAY remote-as ${spine_as}

    %for i in spine_loopback_list:
    neighbor  ${i} peer group  OVERLAY
    %endfor

    neighbor OVERLAY update-source loopback  0
    neighbor OVERLAY ebgp-multihop ${multihop}
    neighbor OVERLAY send-community
    neighbor OVERLAY bfd
    !
    address-family ipv4 
    neighbor UNDERLAY activate
    redistribute connected 
    !
    address-family evpn
    neighbor OVERLAY activate 
    !
    interface vxlan 1
    vxlan source-interface loopback 0


%endif

 

Configuration Output Example

After submitting the form and reviewing the workspace in CVP, the system creates full configurations for all devices. For example, below is a sample output for spine-1, part of a topology with 2 spines and 6 leaf switches. Hence, the config includes interface settings, loopbacks, BGP, and VXLAN tunnel setup—generated automatically by the custom Studio.

By Mahmut Aydin

CCIE R&S #63405

Leave a Reply

Your email address will not be published.