Annet Tutorial
==============
This tutorial has been tested on MacOS but should also work on Linux. While it's unclear if Windows WSL supports it, there's no harm in trying!
Prepare Environment
-------------------
We'll use Docker Compose to set up the lab environment. You're welcome to use any other virtualization tool, but Docker Compose is straightforward and works across Linux, macOS, and Windows.
For MacOS, we recommend using `Docker Desktop for Mac `__ or `orbstack `__.
Arista cEOS
^^^^^^^^^^^
We've chosen to use Containerized Arista EOS because Arista EOS is widely used and has a Cisco-like interface that's easy to understand. The key advantage is that the image can be `downloaded from the official site `__ for free by any registered user.
.. note:: Use your own domain or corporate email for registration, as Arista doesn't allow common email providers like Gmail.
Download the image and import it into Docker:
.. code:: bash
docker image import cEOS64-lab-4.33.1F.tar.xz arista-ceos:4.33.1F --platform linux/amd64
Topology
^^^^^^^^
::
╔════════╗
Eth1 ║AS 65001║ Eth2
┌─────────║ r1 ║──────────┐
│ .11 ║ ║ .11 │
│ ╚════════╝ │
│ │
10.1.2.0/24 10.1.3.0/24
│ │
│ │
Eth1 │ .12 .13 │ Eth1
╔════════╗ ╔════════╗
║AS 65002║ Eth2 Eth2 ║AS 65003║
║ r2 ║────────────────────║ r3 ║
║ ║.12 10.2.3.0/24 .13║ ║
╚════════╝ ╚════════╝
The network consists of three routers directly connected to each other.
Out-of-band management IP addresses are:
+--------+------------------+
| Router | MGMT |
+========+==================+
| r1 | ``172.20.0.101`` |
+--------+------------------+
| r2 | ``172.20.0.102`` |
+--------+------------------+
| r3 | ``172.20.0.103`` |
+--------+------------------+
Netbox
^^^^^^
.. note:: Currently, version 3.7 is supported (2025q1). Support for newer versions will be added soon.
If you prefer to use your own Netbox installation, you can skip this section. However, make sure to read the notes at the beginning of the next section.
The easiest way to install Netbox is to use the dockerized version.
.. code:: bash
#
# clone repo with dockerized version of netbox
git clone https://github.com/netbox-community/netbox-docker.git
#
# got into directory
cd netbox-docker
#
# change version to 3.7, you can do it in you favorite editor instead,
# just replace "VERSION-v4.1-3.0.2" to "VERSION-v3.7" in ./netbox-docker/docker-compose.yml, or use sed:
# NOTE: be careful, in the tutorial version 3.0.2 of netbox docker is using,
# may be you face with newer version and it requires to change something else too,
# to checkout the correct version use "git fetch --tags && git checkout tags/3.0.2"
sed -i.bak 's/VERSION-v4.1-3.0.2/VERSION-v3.7/g' docker-compose.yml
#
# if you run netbox on weak hardware you can change timeouts in docker-compose.yml,
# e.g. multiply all the timeouts by 10 in your favorite editor, or use sed:
sed -i.bak 's/0s/00s/g' docker-compose.yml
Docker Compose Override File
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some important notes:
1. The directories ``lab/ceos-rX.flash`` are required to store the saved configuration of cEOS.
2. Before running cEOS, prepare the ``startup-config`` with the management IP address and a local user ``annet:annet``.
3. The ``depends_on`` section is added to each cEOS service to avoid overloading resources on weaker hardware.
4. The docker-compose file specifies the cEOS version. If you use a different version, update the file accordingly.
5. If you use your own Netbox, you need to:
- Create a directory ``netbox-docker``;
- Change ``docker-compose.override.yml`` to ``docker-compose.yml``;
- Remove the ``services/netbox`` section from the docker-compose file;
- Remove the ``depends_on`` section from the cEOS services.
.. code:: bash
# go to root of your folder
cd ..
#
# create folders for cEOS configuration files
mkdir -p lab/ceos-r1.flash lab/ceos-r2.flash lab/ceos-r3.flash
#
# create configuration files for cEOS
cat > lab/ceos-r1.flash/startup-config < lab/ceos-r2.flash/startup-config < lab/ceos-r3.flash/startup-config < docker-compose.override.yml <
/sbin/init
systemd.setenv=INTFTYPE=eth
systemd.setenv=MGMT_INTF=eth0
systemd.setenv=MAPETH0=1
systemd.setenv=ETBA=1
systemd.setenv=SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1
systemd.setenv=CEOS=1
systemd.setenv=EOS_PLATFORM=ceoslab
systemd.setenv=container=docker
services:
netbox:
container_name: netbox
hostname: netbox
ports:
- 8000:8080
r1:
<<: *ceos-defaults
hostname: r1
container_name: r1
depends_on:
netbox:
condition: service_healthy
networks:
default:
ipv4_address: 172.20.0.101
r1r2_net:
ipv4_address: 10.1.2.11
r1r3_net:
ipv4_address: 10.1.3.11
volumes:
- ../lab/ceos-r1.flash:/mnt/flash/
r2:
<<: *ceos-defaults
hostname: r2
container_name: r2
depends_on:
netbox:
condition: service_healthy
networks:
default:
ipv4_address: 172.20.0.102
r1r2_net:
ipv4_address: 10.1.2.12
r2r3_net:
ipv4_address: 10.2.3.12
volumes:
- ../lab/ceos-r2.flash:/mnt/flash/
r3:
<<: *ceos-defaults
hostname: r3
container_name: r3
depends_on:
netbox:
condition: service_healthy
networks:
default:
ipv4_address: 172.20.0.103
r1r3_net:
ipv4_address: 10.1.3.13
r2r3_net:
ipv4_address: 10.2.3.13
volumes:
- ../lab/ceos-r3.flash:/mnt/flash/
EOF
Run Environment
^^^^^^^^^^^^^^^
Now, let's run Netbox and the lab:
.. code:: none
docker compose up -d
Ensure Netbox is accessible at http://localhost:8000/.
Create a superuser using the script:
.. code:: none
docker-compose run netbox python manage.py createsuperuser
For consistency, use ``annet`` for both the login and password. You can change these later if needed.
Try connecting to the cEOS CLI:
.. code:: none
docker exec -it r1 Cli
Try connecting to cEOS via SSH using ``annet:annet``:
.. code:: none
ssh annet@172.20.0.101
Update Netbox Database
^^^^^^^^^^^^^^^^^^^^^^
Annet uses data from Netbox to generate configurations. Ensure the data is in place before working with Annet.
1. In **Organisation/Site**, add a Site - name: ``lab``, slug: ``lab``.
2. In **Devices/Manufacturers**, add a Manufacturer - name: ``Arista``, slug: ``arista``.
3. In **Devices/Device Types**, add a Device Type - Manufacturer: ``Arista``, name: ``cEOS``, slug: ``ceos``.
4. In **Devices/Device Roles**, add a Device Role - name: ``switch``, slug: ``switch``, color: choose any.
5. In **Devices/Devices**, add three Devices:
- name: ``r1.lab``, device role: ``switch``, device type: ``cEOS``, site: ``lab``;
- name: ``r2.lab``, device role: ``switch``, device type: ``cEOS``, site: ``lab``;
- name: ``r3.lab``, device role: ``switch``, device type: ``cEOS``, site: ``lab``.
6. For each device, add interfaces in **Add Components/Interfaces**:
- name: ``Ethernet1``, type: ``1000Base-T``;
- name: ``Ethernet2``, type: ``1000Base-T``;
- name: ``Ethernet3``, type: ``1000Base-T``;
- name: ``Management0``, type: ``1000Base-T``, Management only: ``True``.
7. For each device, add an IP address in the **Interfaces** tab:
- device: ``r1.lab``, interface: ``Management0``, IP address: ``172.20.0.101/24``;
- device: ``r2.lab``, interface: ``Management0``, IP address: ``172.20.0.102/24``;
- device: ``r3.lab``, interface: ``Management0``, IP address: ``172.20.0.103/24``.
8. For each device, assign a **Primary IPv4** address. In edit mode, assign **Primary IPv4** to ``172.20.0.101``, ``172.20.0.102``, and ``172.20.0.103`` respectively.
9. Finally, create connections between devices following the topology. In the **Interfaces** tab, add cables between:
- device: ``r1.lab``, interface: ``Ethernet1``, connected to device: ``r2.lab``, interface: ``Ethernet1``;
- device: ``r1.lab``, interface: ``Ethernet2``, connected to device: ``r3.lab``, interface: ``Ethernet1``;
- device: ``r2.lab``, interface: ``Ethernet2``, connected to device: ``r3.lab``, interface: ``Ethernet2``.
Annet Installation
----------------------
Create a virtual environment and install Annet along with the required packages. We recommend using Python 3.12 or later.
.. code:: bash
# go to root of your folder
cd ..
#
# create and activate venv
python3 -m venv .venv
source .venv/bin/activate
#
# install packages
pip install "annet[netbox]" gnetcli_adapter gnetclisdk
gnetcli
^^^^^^^
Before we start, we need to install the gnetcli server binary.
.. note:: This step requires Golang to be installed. Alternatively, you can download the binary for your platform from https://github.com/annetutil/gnetcli/releases. Annet will use this binary, so ensure the folder containing it is added to your PATH environment variable.
.. code:: bash
export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin
go install github.com/annetutil/gnetcli/cmd/gnetcli_server@latest
Annet Configuration
-------------------
Annet interacts with devices and Netbox, so we need to define:
1. Device credentials. For the lab environment, we use ``annet:annet``.
2. A Netbox token. Open Netbox, go to **Admins/API Tokens**, and add a new token for the user ``annet``.
.. code:: bash
#
# create folder for future annet generators
mkdir generators
touch generators/__init__.py
#
# create configuration file:
cat > annet_config.yaml < annet show device-dump r1.lab
device.asset_tag = None
device.breed = 'eos4'
device.created = datetime.datetime(2025, 1, 26, 12, 0, 14, 63670, tzinfo=tzutc())
device.device_role.id = 1
device.device_role.name = 'switch'
device.device_type = DeviceType(id=1, manufacturer=Entity(id=1, name='Arista'), model='cEOS')
device.display = 'r1.lab'
device.face = None
device.fqdn = 'r1.lab'
device.hostname = 'r1.lab'
device.hw.model = 'Arista cEOS'
device.hw.soft = ''
device.hw.vendor = 'arista'
device.hw.Arista = True
...
Try to get the current configuration of a device:
.. code:: none
> annet show current r1.lab
# -------------------- r1.lab.cfg --------------------
! Command: show running-config
! device: r1 (cEOSLab, EOS-4.33.1F-39879738.4331F (engineering build))
!
no aaa root
!
username annet privilege 15 role network-admin secret sha512 $6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP.
!
switchport default mode routed
!
no service interface inactive port-id allocation disabled
!
transceiver qsfp default-mode 4x10G
!
service routing protocols model multi-agent
!
hostname r1
!
spanning-tree mode mstp
!
system l1
unsupported speed action error
unsupported error-correction action error
!
aaa authorization serial-console
aaa authorization exec default local
aaa authorization exec console none
!
interface Ethernet1
no switchport
!
interface Ethernet2
no switchport
!
interface Ethernet3
no switchport
!
interface Management0
ip address 172.20.0.101/24
!
ip routing
!
router multicast
ipv4
software-forwarding kernel
!
ipv6
software-forwarding kernel
!
end
Let's Play with Annet
----------------------
Create First Generator
^^^^^^^^^^^^^^^^^^^^^^
For now, let's create a generator for interface descriptions.
Create a file ``generators/description.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
class Description(PartialGenerator):
"""Generator of description on interfaces"""
# tags allow more usefully execute set of generators
TAGS = ["description", "iface"]
# for partial generators there are two methods for each vendors should be:
# - acl_
# - run_
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
interface
description
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
for interface in device.interfaces:
if interface.connected_endpoints:
with self.block(f"interface {interface.name}"):
remote_device = interface.connected_endpoints[0].device.name.split(".")[0]
remote_iface = interface.connected_endpoints[0].name
yield f"description {remote_device}@{remote_iface}"
And update the file ``generators/__init__.py``:
.. code:: python
from annet.generators import BaseGenerator
from annet.storage import Storage
from . import description
def get_generators(store: Storage) -> list[BaseGenerator]:
"""All the generators should be returned by the function"""
return [
description.Description(store),
]
Check the list of generators:
.. code:: none
> annet show generators
| PARTIAL-Class | Tags | Module | Description |
|-----------------+--------------------+-----------------------------------------------------+----------------------------------------|
| Description | description, iface | Users_gslv_annet_generators___init___py.description | Generator of description on interfaces |
Get the generated configuration for all three devices:
.. code:: none
> annet gen -g description r1.lab r2.lab r3.lab
# -------------------- r1.lab.cfg --------------------
interface Ethernet1
description r2@Ethernet1
interface Ethernet2
description r3@Ethernet1
# -------------------- r2.lab.cfg --------------------
interface Ethernet1
description r1@Ethernet1
interface Ethernet2
description r3@Ethernet2
# -------------------- r3.lab.cfg --------------------
interface Ethernet1
description r1@Ethernet2
interface Ethernet2
description r2@Ethernet2
Look at the diff:
.. code:: diff
> annet diff -g description r1.lab r2.lab r3.lab
# -------------------- r2.lab.cfg --------------------
interface Ethernet1
+ description r1@Ethernet1
interface Ethernet2
+ description r3@Ethernet2
# -------------------- r3.lab.cfg --------------------
interface Ethernet1
+ description r1@Ethernet2
interface Ethernet2
+ description r2@Ethernet2
# -------------------- r1.lab.cfg --------------------
interface Ethernet1
+ description r2@Ethernet1
interface Ethernet2
+ description r3@Ethernet1
And deploy it:
.. code:: none
> annet deploy -g description r1.lab r2.lab r3.lab
Verify the result:
.. code:: none
> ssh annet@172.20.0.101
(annet@172.20.0.101) Password:
Last login: Sun Jan 26 15:29:33 2025 from 172.20.0.0
r1#sh int desc
Interface Status Protocol Description
Et1 up up r2@Ethernet1
Et2 up up r3@Ethernet1
Ma0 up up
r1#
Extend Coverage
^^^^^^^^^^^^^^^
Thanks to ACL, we can add new configuration parts to Annet step by step without affecting other parts of the configuration.
Add generators for AAA, hostname, IP address, routing, and STP.
Create the following files:
``generators/aaa.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
LOCAL_USERS = {
"annet": {
"privilege": 15,
"role": "network-admin",
"secret sha512": "$6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP."
}
}
class Aaa(PartialGenerator):
"""Generator of AAA"""
TAGS = ["aaa"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
aaa
username
"""
def run_arista(self, _: Device):
"""Generator for Arista devices"""
yield "no aaa root"
yield "aaa authorization serial-console"
yield "aaa authorization exec default local"
yield "aaa authorization exec console none"
for username, attributes in LOCAL_USERS.items():
attributes_line = " ".join(f"{key} {value}" for key, value in attributes.items())
yield f"username {username} {attributes_line}"
``generators/hostname.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
class Hostname(PartialGenerator):
"""Generator of Hostname"""
TAGS = ["hostname"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
hostname
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
yield f"hostname {device.hostname.split('.')[0]}"
``generators/ip_address.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
class IpAddress(PartialGenerator):
"""Generator of IP addresses"""
TAGS = ["routing", "iface"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
interface
ip address
no switchport
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
for interface in device.interfaces:
with self.block(f"interface {interface.name}"):
for ip_address in interface.ip_addresses:
yield f"ip address {ip_address.address}"
if interface.name.startswith("Ethernet"):
yield "no switchport"
``generators/routing.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
class Routing(PartialGenerator):
"""Generator of Routing"""
TAGS = ["routing"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
service routing
ip routing
"""
def run_arista(self, _: Device):
"""Generator for Arista devices"""
yield "service routing protocols model multi-agent"
yield "ip routing"
``generators/stp.py``:
.. code:: python
from annet.generators import PartialGenerator
from annet.storage import Device
class Stp(PartialGenerator):
"""Generator of STP"""
TAGS = ["stp"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
spanning-tree
"""
def run_arista(self, _: Device):
"""Generator for Arista devices"""
yield "spanning-tree mode mstp"
Again, update ``generators/__init__.py``:
.. code:: python
from annet.generators import BaseGenerator
from annet.storage import Storage
from . import aaa, description, hostname, ip_address, routing, stp
def get_generators(store: Storage) -> list[BaseGenerator]:
"""All the generators should be returned by the function"""
return [
aaa.Aaa(store),
description.Description(store),
hostname.Hostname(store),
ip_address.IpAddress(store),
routing.Routing(store),
stp.Stp(store),
]
Look at the list of generators:
.. code:: none
> annet show generators
| PARTIAL-Class | Tags | Module | Description |
|-----------------+--------------------+---------------------------------------------------------+------------------------------------------------------|
| Aaa | aaa | Users_gslv_dev_annet_generators___init___py.aaa | Generator of AAA |
| Description | description, iface | Users_gslv_dev_annet_generators___init___py.description | Generator of description on interfaces |
| Hostname | hostname | Users_gslv_dev_annet_generators___init___py.hostname | Generator of Hostname |
| IpAddress | routing, iface | Users_gslv_dev_annet_generators___init___py.ip_address | Generator of IP addresses |
| Routing | routing | Users_gslv_dev_annet_generators___init___py.routing | Generator of Routing |
| Stp | stp | Users_gslv_dev_annet_generators___init___py.stp | Generator of STP |
Look at the diff:
.. code:: diff
> annet diff r1.lab r2.lab r3.lab
# -------------------- r3.lab.cfg --------------------
- username annet privilege 15 role network-admin secret sha512 $6$s7NCIG5Rocu3FSK0$018nDOmgJctLO7qGVotvo9OOD1qyKVMTwaURO8sh7YaoCitMIE2HRWePYq2T5aGqEsa2Y0ukqe5/PKlNV43zc0
+ username annet privilege 15 role network-admin secret sha512 $6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP.
# -------------------- r2.lab.cfg --------------------
- username annet privilege 15 role network-admin secret sha512 $6$ycnCXwDzpQPU6WqS$6u0MD.hyOKaRh6r8Tnb97S8zFQVYeXaQuo8nkFHCez7VlBxeJmGsbbgeTePg.k23hEdK.LN1TB5sCjfkS7Mdu.
+ username annet privilege 15 role network-admin secret sha512 $6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP.
We notice that the user ``annet`` has a different hash on ``r2`` and ``r3``. This is fine because we created the user ``annet`` with a plain text password in the default configuration.
Look at the patch:
.. code:: none
> annet patch r2.lab r3.lab
# -------------------- r2.lab.patch --------------------
username annet privilege 15 role network-admin secret sha512 $6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP.
# -------------------- r3.lab.patch --------------------
username annet privilege 15 role network-admin secret sha512 $6$i5LaTWzHeAJx/vLu$rYUKKATawfpjItHKJJie3Fgsa2EqkMyH0XYY2.1Dl/2G.uNVzuntS5poblWuf6urafiurknH2/NotkUHiamoP.
And deploy it:
.. code:: none
> annet deploy r2.lab r3.lab
Again look at the diff:
.. code:: none
> annet diff r1.lab r2.lab r3.lab
No diff found - everything is ok for now.
Look at the diff without ACL to check what's configurations lines is still not covered by annet:
.. code:: diff
> annet diff r1.lab r2.lab r3.lab --no-acl
# -------------------- r1.lab.cfg, r2.lab.cfg, r3.lab.cfg --------------------
- switchport default mode routed
- transceiver qsfp default-mode 4x10G
- system l1
- unsupported speed action error
- unsupported error-correction action error
- router multicast
- ipv4
- software-forwarding kernel
- ipv6
- software-forwarding kernel
- end
- no service interface inactive port-id allocation disabled
We've skipped them, but if you want, you can create new generators to add them to Annet.
BGP Configuration
^^^^^^^^^^^^^^^^^
Annet has a brilliant tool for creating BGP peers — mesh. It allows us to create templates for BGP peers and apply them to Netbox devices. Annet takes connections between devices from Netbox and passes them through templates. As a result, we get a list of local and remote peer pairs. This list can be used in generators.
Some people call mesh templates "network design in Python code!"
Imagine we need to have BGP sessions between ``r1``, ``r2``, and ``r3`` over direct links to exchange IPv4 routes.
Create a mesh template ``generators/mesh_views/routers.py``:
.. code:: python
from annet.mesh import DirectPeer, GlobalOptions, MeshRulesRegistry, MeshSession
# create registry, short name allows skip domain parts in templates
registry = MeshRulesRegistry(match_short_name=True)
# define base asnum
BASE_ASNUM = 65000
# define global options of the host
@registry.device("r{num}")
def global_options(global_opts: GlobalOptions):
"""Define global options"""
global_opts.router_id = f"1.1.1.{global_opts.match.num}"
# define peering between routers, we use different names for num, because if they have the same names they have to be with the same value
# e.g. ("r{num}", "r{num}") means the only peering between r1 and r1, r2 and r2 and r3 and r3 passed though templates
@registry.direct("r{num1}", "r{num2}")
def routers_peerings(router1: DirectPeer, router2: DirectPeer, session: MeshSession):
"""Define peering between routers for IPv4 unicast family"""
# find minimal and maximum numbers of routers
min_num = min(router1.match.num1, router2.match.num2)
max_num = max(router1.match.num1, router2.match.num2)
# define first router params
router1.asnum = BASE_ASNUM + router1.match.num1
router1.addr = f"10.{min_num}.{max_num}.1{router1.match.num1}/24"
router1.families = {"ipv4_unicast"}
router1.group_name = "ROUTERS"
# define second router params
router2.asnum = BASE_ASNUM + router2.match.num2
router2.addr = f"10.{min_num}.{max_num}.1{router2.match.num2}/24"
router2.families = {"ipv4_unicast"}
router2.group_name = "ROUTERS"
Create an init file ``generators/mesh_views/__init__.py``:
.. code:: python
from annet.mesh import MeshRulesRegistry
from . import routers
registry = MeshRulesRegistry(match_short_name=True)
registry.include(routers.registry)
Now, we should use mesh data in generators. First, update the L3Addresses generator ``generators/l3_addresses.py``:
.. code:: python
from annet.generators import PartialGenerator
# import mesh executor to get access to mesh data
from annet.mesh import MeshExecutor
from annet.storage import Device
# import mesh registry
from .mesh_views import registry
class IpAddress(PartialGenerator):
"""Generator of IP addresses"""
TAGS = ["routing", "iface"]
def acl_arista(self, _: Device):
"""ACL for Arista devices"""
return """
interface
ip address
no switchport
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
# update device storage with mesh data
executor: MeshExecutor = MeshExecutor(registry, device.storage)
executor.execute_for(device)
for interface in device.interfaces:
with self.block(f"interface {interface.name}"):
for ip_address in interface.ip_addresses:
yield f"ip address {ip_address.address}"
if interface.name.startswith("Ethernet"):
yield "no switchport"
Add a new generator for BGP configuration — ``generators/bgp.py``:
.. code:: python
from typing import Optional
from annet.bgp_models import ASN, BgpConfig
from annet.generators import PartialGenerator
from annet.mesh.executor import MeshExecutor
from annet.storage import Device
from .mesh_views import registry
def bgp_asnum(mesh_data: BgpConfig) -> Optional[ASN]:
"""Return AS number parse mesh bgp peers"""
if not mesh_data:
return None
# AS can be defined in global options
if mesh_data.global_options.local_as:
return mesh_data.global_options.local_as
# If AS is not defined in global options, look for it in peers
asnum: set[ASN] = set()
for peer in mesh_data.peers:
asnum.add(peer.options.local_as)
if len(asnum) == 1:
return asnum.pop()
elif len(asnum) > 1:
raise RuntimeError(f"AutonomusSystemIsNotDefined: {str(asnum)}")
return None
class Bgp(PartialGenerator):
"""Generator of BGP process and neighbors"""
TAGS = ["bgp", "routing"]
def acl_arista(self, _: Device) -> str:
"""ACL for Arista devices"""
return """
router bgp
router-id
neighbor
redistribute connected
maximum-paths
address-family
neighbor
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
executor: MeshExecutor = MeshExecutor(registry, device.storage)
mesh_data: BgpConfig = executor.execute_for(device)
rid: Optional[str] = mesh_data.global_options.router_id if mesh_data.global_options.router_id else None
asnum: Optional[ASN] = bgp_asnum(mesh_data)
if not asnum or not rid:
return
with self.block("router bgp", asnum):
yield "router-id", rid
# group configuration
for peer in mesh_data.peers:
yield "neighbor", peer.group_name, "peer group"
# use conditional context for group configuration
with self.block_if("address-family ipv4", condition=("ipv4_unicast" in peer.families)):
yield "neighbor", peer.group_name, "activate"
# peer configuration
for peer in mesh_data.peers:
yield "neighbor", peer.addr, "peer group", peer.group_name
yield "neighbor", peer.addr, "remote-as", peer.remote_as
Check the list of generators:
.. code:: none
> annet show generators
| PARTIAL-Class | Tags | Module | Description |
|-----------------+--------------------+---------------------------------------------------------+------------------------------------------------------|
| Aaa | aaa | Users_gslv_dev_annet_generators___init___py.aaa | Generator of AAA |
| Bgp | bgp, routing | Users_gslv_dev_annet_generators___init___py.bgp | Generator of BGP process and neighbors |
| Description | description, iface | Users_gslv_dev_annet_generators___init___py.description | Generator of description on interfaces |
| Hostname | hostname | Users_gslv_dev_annet_generators___init___py.hostname | Generator of Hostname |
| IpAddress | routing, iface | Users_gslv_dev_annet_generators___init___py.ip_address | Generator of IP addresses |
| Routing | routing | Users_gslv_dev_annet_generators___init___py.routing | Generator of Routing |
| Stp | stp | Users_gslv_dev_annet_generators___init___py.stp | Generator of STP |
Check the diff:
.. code:: diff
annet diff r1.lab r2.lab r3.lab
# -------------------- r1.lab.cfg --------------------
interface Ethernet1
+ ip address 10.1.2.11/24
interface Ethernet2
+ ip address 10.1.3.11/24
+ router bgp 65001
+ router-id 1.1.1.1
+ neighbor ROUTERS peer group
+ address-family ipv4
+ neighbor ROUTERS activate
+ neighbor 10.1.2.12 peer group ROUTERS
+ neighbor 10.1.2.12 remote-as 65002
+ neighbor 10.1.3.13 peer group ROUTERS
+ neighbor 10.1.3.13 remote-as 65003
# -------------------- r2.lab.cfg --------------------
interface Ethernet1
+ ip address 10.1.2.12/24
interface Ethernet2
+ ip address 10.2.3.12/24
+ router bgp 65002
+ router-id 1.1.1.2
+ neighbor ROUTERS peer group
+ address-family ipv4
+ neighbor ROUTERS activate
+ neighbor 10.1.2.11 peer group ROUTERS
+ neighbor 10.1.2.11 remote-as 65001
+ neighbor 10.2.3.13 peer group ROUTERS
+ neighbor 10.2.3.13 remote-as 65003
# -------------------- r3.lab.cfg --------------------
interface Ethernet1
+ ip address 10.1.3.13/24
interface Ethernet2
+ ip address 10.2.3.13/24
+ router bgp 65003
+ router-id 1.1.1.3
+ neighbor ROUTERS peer group
+ address-family ipv4
+ neighbor ROUTERS activate
+ neighbor 10.1.3.11 peer group ROUTERS
+ neighbor 10.1.3.11 remote-as 65001
+ neighbor 10.2.3.12 peer group ROUTERS
+ neighbor 10.2.3.12 remote-as 65002
Looks great! Deploy it to the devices:
.. code:: none
> annet deploy r1.lab r2.lab r3.lab
Check the result:
.. code:: none
> ssh annet@172.20.0.101
(annet@172.20.0.101) Password:
Last login: Tue Feb 4 05:27:51 2025 from 172.20.0.0
r1#sh ip bgp sum
BGP summary information for VRF default
Router identifier 1.1.1.1, local AS number 65001
Neighbor Status Codes: m - Under maintenance
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
10.1.2.12 4 65002 4 4 0 0 00:00:05 Estab 0 0
10.1.3.13 4 65003 4 4 0 0 00:00:05 Estab 0 0
r1#
Redistribute Connected
^^^^^^^^^^^^^^^^^^^^^^
Let's go deeper. The task now is to configure the redistribution of connected networks into BGP.
Create a ``loopback10`` interface on each router with an address in Netbox, following the table:
+--------+--------------------+
| Router | Loopback10 address |
+========+====================+
| r1 | ``192.168.1.1/24`` |
+--------+--------------------+
| r2 | ``192.168.2.1/24`` |
+--------+--------------------+
| r3 | ``192.168.3.1/24`` |
+--------+--------------------+
Go to the router page, click **Add Components**, and choose **Interfaces**. Use the name ``loopback10`` and type ``Virtual``. Next, add an IP address to the interface following the table.
Now, we need to add the redistribution of connected networks to BGP in the mesh. Additionally, we want to filter prefixes between routers!
To do this, update the file ``generators/mesh_views/routers.py``:
.. code:: python
from annet.bgp_models import Redistribute
from annet.mesh import DirectPeer, GlobalOptions, MeshRulesRegistry, MeshSession
# create registry, short name allows skip domain parts in templates
registry = MeshRulesRegistry(match_short_name=True)
# define base asnum
BASE_ASNUM = 65000
# define global options of the host
@registry.device("r{num}")
def global_options(global_opts: GlobalOptions):
"""Define global options"""
global_opts.router_id = f"1.1.1.{global_opts.match.num}"
# define redistribute
global_opts.ipv4_unicast.redistributes = (
Redistribute(protocol="connected", policy="IMPORT_CONNECTED"),
)
# define peering between routers, we use different names for num, because if they have the same names they have to be with the same value
# e.g. ("r{num}", "r{num}") means the only peering between r1 and r1, r2 and r2 and r3 and r3 passed though templates
# pylint: disable=unused-argument
@registry.direct("r{num1}", "r{num2}")
def routers_peerings(router1: DirectPeer, router2: DirectPeer, session: MeshSession):
"""Define peering between routers for IPv4 unicast family"""
# find minimal and maximum numbers of routers
min_num = min(router1.match.num1, router2.match.num2)
max_num = max(router1.match.num1, router2.match.num2)
# define first router params
router1.asnum = BASE_ASNUM + router1.match.num1
router1.addr = f"10.{min_num}.{max_num}.1{router1.match.num1}/24"
router1.families = {"ipv4_unicast"}
router1.group_name = "ROUTERS"
router1.import_policy = "ROUTERS_IMPORT"
router1.export_policy = "ROUTERS_EXPORT"
router1.send_community = True
# define second router params
router2.asnum = BASE_ASNUM + router2.match.num2
router2.addr = f"10.{min_num}.{max_num}.1{router2.match.num2}/24"
router2.families = {"ipv4_unicast"}
router2.group_name = "ROUTERS"
router2.import_policy = "ROUTERS_IMPORT"
router2.export_policy = "ROUTERS_EXPORT"
router2.send_community = True
You'll notice that the redistribution has a link to the policy ``IMPORT_CONNECTED``. This can be defined by a new generator as plain config, but Annet has a special tool for working with policies. Currently, only Huawei VRP, Arista EOS, and FRR (2025q1) are supported, but we expect this to be updated soon.
First, create a new module by creating an empty file ``generators/rpl_views/__init__.py``. This module will contain policies and their elements.
Create a Python file with the policies—``generators/rpl_views/routemap.py``:
.. code:: python
# pylint: disable=missing-function-docstring
from annet.adapters.netbox.common.models import NetboxDevice
from annet.rpl import R, Route, RouteMap
# create routemap decorator
routemap = RouteMap[NetboxDevice]()
# define redistribute policy
@routemap
def IMPORT_CONNECTED(_: NetboxDevice, route: Route):
with route(
R.protocol == "connected",
R.match_v4("LOCAL_NETS"),
number=10
) as rule:
rule.community.set("ADVERTISE")
rule.allow()
with route(number=20) as rule:
rule.deny()
@routemap
def ROUTERS_IMPORT(_: NetboxDevice, route: Route):
with route(
R.match_v4("LOCAL_NETS", or_longer=(16, 24)), # custom ge/le
R.community.has("ADVERTISE"),
number=10
) as rule:
rule.allow()
with route(number=20) as rule:
rule.deny()
@routemap
def ROUTERS_EXPORT(_: NetboxDevice, route: Route):
with route(
R.community.has("ADVERTISE"),
number=10
) as rule:
rule.allow()
with route(number=20) as rule:
rule.deny()
For more details on how to use RPL, refer to the `documentation `__.
The next two files contain community and prefix list definitions.
``generators/rpl_views/community.py``:
.. code:: python
from annet.rpl_generators import CommunityList
COMMUNITIES = [
CommunityList(name="ADVERTISE", members=["65000:1"])
]
``generators/rpl_views/prefix_list.py``:
.. code:: python
from annet.rpl_generators import IpPrefixList
PREFIX_LISTS = [
IpPrefixList(name="LOCAL_NETS", members=["192.168.0.0/16"])
]
This doesn't look too difficult, but we need to create three generators for:
- Policy
- Community
- Prefix list
Policy generator — ``generators/route_map.py``:
.. code:: python
from typing import Any
from annet.mesh import MeshExecutor
from annet.rpl import RoutingPolicy
from annet.rpl_generators import (
AsPathFilter,
CommunityList,
IpPrefixList,
RDFilter,
RoutingPolicyGenerator,
get_policies,
)
from .mesh_views import registry
from .rpl_views import community, prefix_list, route_map
# the class inherited from RoutingPolicyGenerator which has already has generators for some vendors,
# but we should define some required methods
class RouteMap(RoutingPolicyGenerator):
"""Generator of Routing Policy"""
# mandatory method to get policies, in our case it takes policies mentioned in mesh
def get_policies(self, device: Any) -> list[RoutingPolicy]:
"""Get mentioned in mesh policies"""
return get_policies(
routemap=route_map.routemap,
device=device,
mesh_executor=MeshExecutor(
registry,
self.storage,
),
)
# mandatory method to get communities
def get_community_lists(self, device: Any) -> list[CommunityList]:
"""Get community lists"""
return community.COMMUNITIES
# mandatory method to get prefix list
def get_prefix_lists(self, _: Any) -> list[IpPrefixList]:
"""Get prefix lists, not used right now"""
return prefix_list.PREFIX_LISTS
# mandatory method which not used right now
def get_as_path_filters(self, _: Any) -> list[AsPathFilter]:
"""Get as-path filters, not used right now"""
return []
# mandatory method which not used right now
def get_rd_filters(self, _: Any) -> list[RDFilter]:
"""Get rd filters, not used right now"""
return []
Community generator — ``generators/community.py``:
.. code:: python
from typing import Any
from annet.mesh import MeshExecutor
from annet.rpl import RoutingPolicy
from annet.rpl_generators import CommunityList, CommunityListGenerator, get_policies
from .mesh_views import registry
from .rpl_views import community, route_map
class Community(CommunityListGenerator):
"""Generator of Community Lists"""
# mandatory method to get policies, in our case it takes policies mentioned in mesh
def get_policies(self, device: Any) -> list[RoutingPolicy]:
"""Get mentioned in mesh policies"""
return get_policies(
routemap=route_map.routemap,
device=device,
mesh_executor=MeshExecutor(
registry,
self.storage,
),
)
# mandatory method to get communities
def get_community_lists(self, _: Any) -> list[CommunityList]:
"""Get community lists"""
return community.COMMUNITIES
Prefix list generator — ``generators/prefix_list.py``:
.. code:: python
from typing import Any
from annet.mesh import MeshExecutor
from annet.rpl import RoutingPolicy
from annet.rpl_generators import IpPrefixList, PrefixListFilterGenerator, get_policies
from .mesh_views import registry
from .rpl_views import prefix_list, route_map
class PrefixList(PrefixListFilterGenerator):
"""Generator of Community Lists"""
# mandatory method to get policies, in our case it takes policies mentioned in mesh
def get_policies(self, device: Any) -> list[RoutingPolicy]:
"""Get mentioned in mesh policies"""
return get_policies(
routemap=route_map.routemap,
device=device,
mesh_executor=MeshExecutor(
registry,
self.storage,
),
)
# mandatory method to get communities
def get_prefix_lists(self, _: Any) -> list[IpPrefixList]:
"""Get prefix lists, not used right now"""
return prefix_list.PREFIX_LISTS
Again, update ``generators/__init__.py``:
.. code:: python
from annet.generators import BaseGenerator
from annet.storage import Storage
from . import (
aaa,
bgp,
community,
description,
hostname,
ip_address,
prefix_list,
route_map,
routing,
stp,
)
def get_generators(store: Storage) -> list[BaseGenerator]:
"""All the generators should be returned by the function"""
return [
aaa.Aaa(store),
bgp.Bgp(store),
community.Community(store),
description.Description(store),
hostname.Hostname(store),
ip_address.IpAddress(store),
prefix_list.PrefixList(store),
route_map.RouteMap(store),
routing.Routing(store),
stp.Stp(store),
]
Don't forget to update the BGP generator to support import/export policies and send communities — ``generators/bgp.py``:
.. code:: python
from typing import Optional
from annet.bgp_models import ASN, BgpConfig
from annet.generators import PartialGenerator
from annet.mesh.executor import MeshExecutor
from annet.storage import Device
from .mesh_views import registry
def bgp_asnum(mesh_data: BgpConfig) -> Optional[ASN]:
"""Return AS number parse mesh bgp peers"""
if not mesh_data:
return None
# AS can be defined in global options
if mesh_data.global_options.local_as:
return mesh_data.global_options.local_as
# If AS is not defined in global options, look for it in peers
asnum: set[ASN] = set()
for peer in mesh_data.peers:
asnum.add(peer.options.local_as)
if len(asnum) == 1:
return asnum.pop()
elif len(asnum) > 1:
raise RuntimeError(f"AutonomusSystemIsNotDefined: {str(asnum)}")
return None
class Bgp(PartialGenerator):
"""Partial generator class of BGP process and neighbors"""
TAGS = ["bgp", "routing"]
def acl_arista(self, _: Device) -> str:
"""ACL for Arista devices"""
return """
router bgp
router-id
neighbor
maximum-paths
address-family
redistribute
neighbor
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
executor: MeshExecutor = MeshExecutor(registry, device.storage)
mesh_data: BgpConfig = executor.execute_for(device)
rid: Optional[str] = mesh_data.global_options.router_id if mesh_data.global_options.router_id else None
asnum: Optional[ASN] = bgp_asnum(mesh_data)
if not asnum or not rid:
return
with self.block("router bgp", asnum):
yield "router-id", rid
# redistribute
with self.block("address-family ipv4"):
if mesh_data.global_options and mesh_data.global_options.ipv4_unicast and \
mesh_data.global_options.ipv4_unicast.redistributes:
for redistribute in mesh_data.global_options.ipv4_unicast.redistributes:
yield "redistribute", redistribute.protocol, "" \
if not redistribute.policy else f"route-map {redistribute.policy}"
# group configuration
for peer in mesh_data.peers:
yield "neighbor", peer.group_name, "peer group"
# import/export policies
if peer.import_policy:
yield "neighbor", peer.group_name, "route-map", peer.import_policy, "in"
if peer.export_policy:
yield "neighbor", peer.group_name, "route-map", peer.export_policy, "out"
if peer.options.send_community:
yield "neighbor", peer.group_name, "send-community"
# use conditional context for group configuration
with self.block_if("address-family ipv4", condition=("ipv4_unicast" in peer.families)):
yield "neighbor", peer.group_name, "activate"
# peer configuration
for peer in mesh_data.peers:
yield "neighbor", peer.addr, "peer group", peer.group_name
yield "neighbor", peer.addr, "remote-as", peer.remote_as
Let's check the diff:
.. code:: diff
> annet diff r1.lab
# -------------------- r1.lab.cfg --------------------
+ interface Loopback10
+ ip address 192.168.1.1/24
+ ip community-list ADVERTISE permit 65000:1
+ ip prefix-list LOCAL_NETS
+ seq 10 permit 192.168.0.0/16 ge 16 le 32
+ ip prefix-list LOCAL_NETS_16_24
+ seq 10 permit 192.168.0.0/16 ge 16 le 24
+ route-map IMPORT_CONNECTED permit 10
+ match source-protocol connected
+ match ip address prefix-list LOCAL_NETS
+ set community community-list ADVERTISE
+ route-map IMPORT_CONNECTED deny 20
+ route-map ROUTERS_IMPORT permit 10
+ match ip address prefix-list LOCAL_NETS_16_24
+ match community ADVERTISE
+ route-map ROUTERS_IMPORT deny 20
+ route-map ROUTERS_EXPORT permit 10
+ match community ADVERTISE
+ route-map ROUTERS_EXPORT deny 20
router bgp 65001
address-family ipv4
+ redistribute connected route-map IMPORT_CONNECTED
+ neighbor ROUTERS route-map ROUTERS_IMPORT in
+ neighbor ROUTERS route-map ROUTERS_EXPORT out
+ neighbor ROUTERS send-community
And the patch:
.. code:: none
> annet patch r1.lab
# -------------------- r1.lab.patch --------------------
interface Loopback10
ip address 192.168.1.1/24
exit
ip community-list ADVERTISE permit 65000:1
ip prefix-list LOCAL_NETS
seq 10 permit 192.168.0.0/16 ge 16 le 32
exit
ip prefix-list LOCAL_NETS_16_24
seq 10 permit 192.168.0.0/16 ge 16 le 24
exit
route-map IMPORT_CONNECTED permit 10
match source-protocol connected
match ip address prefix-list LOCAL_NETS
set community community-list ADVERTISE
exit
route-map IMPORT_CONNECTED deny 20
exit
route-map ROUTERS_IMPORT permit 10
match ip address prefix-list LOCAL_NETS_16_24
match community ADVERTISE
exit
route-map ROUTERS_IMPORT deny 20
exit
route-map ROUTERS_EXPORT permit 10
match community ADVERTISE
exit
route-map ROUTERS_EXPORT deny 20
exit
router bgp 65001
address-family ipv4
redistribute connected route-map IMPORT_CONNECTED
exit
neighbor ROUTERS route-map ROUTERS_IMPORT in
neighbor ROUTERS route-map ROUTERS_EXPORT out
neighbor ROUTERS send-community
exit
Deploy it on all three routers:
.. code:: bash
annet deploy r1.lab r2.lab r3.lab
Check the result:
.. code:: none
> ssh annet@172.20.0.101
(annet@172.20.0.101) Password:
Last login: Wed Feb 5 19:44:08 2025 from 172.20.0.0
r1#sh ip bgp
BGP routing table information for VRF default
Router identifier 1.1.1.1, local AS number 65001
Route status codes: s - suppressed contributor, * - valid, > - active, E - ECMP head, e - ECMP
S - Stale, c - Contributing to ECMP, b - backup, L - labeled-unicast
% - Pending best path selection
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI Origin Validation codes: V - valid, I - invalid, U - unknown
AS Path Attributes: Or-ID - Originator ID, C-LST - Cluster List, LL Nexthop - Link Local Nexthop
Network Next Hop Metric AIGP LocPref Weight Path
* > 192.168.1.0/24 - - - - 0 i
* > 192.168.2.0/24 10.1.2.12 0 - 100 0 65002 i
* 192.168.2.0/24 10.1.3.13 0 - 100 0 65003 65002 i
* > 192.168.3.0/24 10.1.3.13 0 - 100 0 65003 i
* 192.168.3.0/24 10.1.2.12 0 - 100 0 65002 65003 i
r1#
Indirect BGP
^^^^^^^^^^^^
We're going to create IS-IS peering between ``r2`` and ``r3`` to exchange ``Loopback0`` addresses. After that, we'll establish indirect BGP peering between ``r2`` and ``r3`` instead of direct peering. We'll also change the ASN on ``r2`` and ``r3`` to ``65004``.
The details are presented in the diagram:
::
╔════════╗
Eth1 ║AS 65001║ Eth2
┌─────────║ r1 ║──────────┐
│ .11 ║ ║ .11 │
│ ╚════════╝ │
│ │
10.1.2.0/24 10.1.3.0/24
│ │
│ │
Eth1 │ .12 .13 │ Eth1
╔════════╗ ╔════════╗
║AS 65004║ Eth2 IS-IS Eth2 ║AS 65004║
║ r2 ║────────────────────║ r3 ║
║ ║.12 10.2.3.0/24 .13║ ║
╚════════╝ ╚════════╝
Lo0 Lo0
1.1.1.2/32 1.1.1.3/32
| |
| |
+------------iBGP-------------+
First, we need to change the mesh. Here are the steps:
1. Add a ``Loopback0`` interface with IP addresses to ``r2`` and ``r3``, following the diagram.
2. Disable direct peering between ``r2`` and ``r3``.
3. Create a simple policy ``PERMIT_ANY`` for indirect peering.
4. Create indirect peering between ``r2`` and ``r3`` using the ``Loopback0`` interfaces.
To add a new loopback interface, repeat the steps from the **Redistribute Connected** section.
Disabling direct peering is easy — just add an additional condition that returns nothing. Configuring indirect peering requires using the ``@registry.indirect`` decorator. Here's the updated mesh—``generators/mesh_views/routers.py``:
.. code:: python
from annet.bgp_models import Redistribute
from annet.mesh import (
DirectPeer,
GlobalOptions,
IndirectPeer,
MeshRulesRegistry,
MeshSession,
)
# create registry, short name allows skip domain parts in templates
registry = MeshRulesRegistry(match_short_name=True)
# define base asnum
BASE_ASNUM = 65000
# define global options of the host
@registry.device("r{num}")
def global_options(global_opts: GlobalOptions):
"""Define global options"""
global_opts.router_id = f"1.1.1.{global_opts.match.num}"
# define redistribute
global_opts.ipv4_unicast.redistributes = (
Redistribute(protocol="connected", policy="IMPORT_CONNECTED"),
)
# define peering between routers, we use different names for num, because if they have the same names they have to be with the same value
# e.g. ("r{num}", "r{num}") means the only peering between r1 and r1, r2 and r2 and r3 and r3 passed though templates
@registry.direct("r{num1}", "r{num2}")
def routers_peerings(router1: DirectPeer, router2: DirectPeer, _: MeshSession):
"""Define peering between routers for IPv4 unicast family"""
# disable direct peering between r2 and r3
if (router1.match.num1 == 2 and router2.match.num2 == 3
or router1.match.num1 == 3 and router2.match.num2 == 2):
return
# find minimal and maximum numbers of routers
min_num = min(router1.match.num1, router2.match.num2)
max_num = max(router1.match.num1, router2.match.num2)
# define first router params
router1.asnum = BASE_ASNUM + 4 if router1.match.num1 in (2, 3) else BASE_ASNUM + router1.match.num1
router1.addr = f"10.{min_num}.{max_num}.1{router1.match.num1}/24"
router1.families = {"ipv4_unicast"}
router1.group_name = "ROUTERS"
router1.import_policy = "ROUTERS_IMPORT"
router1.export_policy = "ROUTERS_EXPORT"
router1.send_community = True
# define second router params
router2.asnum = BASE_ASNUM + 4 if router2.match.num2 in (2, 3) else BASE_ASNUM + router2.match.num2
router2.addr = f"10.{min_num}.{max_num}.1{router2.match.num2}/24"
router2.families = {"ipv4_unicast"}
router2.group_name = "ROUTERS"
router2.import_policy = "ROUTERS_IMPORT"
router2.export_policy = "ROUTERS_EXPORT"
router2.send_community = True
# define indirect between routers r2 and r3, note that we use colon after match name.
# it means that after colum follow regex, by default regex is any digit - '\d+',
# but for now we want to set specific numbers. also indirect peering do not relies on connection in netbox,
# since we should define ifname and addr from exited interface
@registry.indirect("r{num1:2}", "r{num2:3}")
def routers_indirect_peerings(router1: IndirectPeer, router2: IndirectPeer, _: MeshSession):
"""Define indirect peering between routers r2 and r3 for IPv4 unicast family"""
for device in (router1, router2):
for iface in device.device.interfaces:
if iface.name == "Loopback0" and iface.type.value == "virtual" and iface.ip_addresses:
device.ifname = iface.name
device.addr = iface.ip_addresses[0].address
# define first router params
router1.asnum = BASE_ASNUM + 4
router1.families = {"ipv4_unicast"}
router1.group_name = "INTERNAL"
router1.import_policy = "PERMIT_ANY"
router1.export_policy = "PERMIT_ANY"
router1.send_community = True
router1.update_source = device.ifname
# define second router params
router2.asnum = BASE_ASNUM + 4
router2.families = {"ipv4_unicast"}
router2.group_name = "INTERNAL"
router2.import_policy = "PERMIT_ANY"
router2.export_policy = "PERMIT_ANY"
router2.send_community = True
router2.update_source = device.ifname
We also updated the policy view—``generators/rpl_views/route_map.py``:
.. code:: python
# pylint: disable=missing-function-docstring
from annet.adapters.netbox.common.models import NetboxDevice
from annet.rpl import R, Route, RouteMap
# create routemap decorator
routemap = RouteMap[NetboxDevice]()
# define redistribute policy
@routemap
def IMPORT_CONNECTED(_: NetboxDevice, route: Route):
with route(
R.protocol == "connected",
R.match_v4("LOCAL_NETS", or_longer=(24, 32)),
number=10
) as rule:
rule.community.set("ADVERTISE")
rule.allow()
with route(number=20) as rule:
rule.deny()
@routemap
def ROUTERS_IMPORT(_: NetboxDevice, route: Route):
with route(
R.community.has("ADVERTISE"),
number=10
) as rule:
rule.allow()
with route(number=20) as rule:
rule.deny()
@routemap
def ROUTERS_EXPORT(_: NetboxDevice, route: Route):
with route(
R.community.has("ADVERTISE"),
number=10
) as rule:
rule.allow()
with route(number=20) as rule:
rule.deny()
@routemap
def PERMIT_ANY(_: NetboxDevice, route: Route):
with route(number=10) as rule:
rule.allow()
What else? We need to configure an IGP to provide connectivity between loopbacks! Unfortunately, the mesh doesn't support any protocols except BGP for now (2025q1). We need to assign IP addresses to interfaces and create a new generator for the ISIS protocol.
Let's assign IP addresses following the table:
+--------+------------------+
| Router | Eth2 address |
+========+==================+
| r2 | ``10.2.3.12/24`` |
+--------+------------------+
| r3 | ``10.2.3.13/24`` |
+--------+------------------+
Here's the ISIS generator and updated init file:
``generators/isis.py``:
.. code:: python
from typing import Optional
from annet.bgp_models import BgpConfig
from annet.generators import PartialGenerator
from annet.mesh.executor import MeshExecutor
from annet.storage import Device
from .mesh_views import registry
def _get_isis_net(area: str, ip_address: str) -> str:
"""Generate ISIS net address from IPv4 address"""
padded_octets = [str(int(octet)).zfill(3) for octet in ip_address.split(".")]
combined = "".join(padded_octets)
return area + ".".join([combined[i:i+4] for i in range(0, len(combined), 4)]) + ".00"
class Isis(PartialGenerator):
"""Partial generator class of ISIS process"""
TAGS = ["isis", "routing"]
def acl_arista(self, _: Device) -> str:
"""ACL for Arista devices"""
return """
router isis
~ %global
interface %cant_delete
isis
"""
def run_arista(self, device: Device):
"""Generator for Arista devices"""
ISIS_NEIGHBORS = {
"r2.lab": "r3.lab",
"r3.lab": "r2.lab"
}
executor: MeshExecutor = MeshExecutor(registry, device.storage)
mesh_data: BgpConfig = executor.execute_for(device)
rid: Optional[str] = mesh_data.global_options.router_id if mesh_data.global_options.router_id else None
if device.hostname not in ISIS_NEIGHBORS or not rid:
return
with self.block("router isis 1"):
yield "net", _get_isis_net("49.0001.", rid)
yield "router-id ipv4 ", rid
yield "is-type level-2"
yield "address-family ipv4 unicast"
for interface in device.interfaces:
if interface.name == "Loopback0" and interface.type.value == "virtual" and interface.ip_addresses:
with self.block(f"interface {interface.name}"):
yield "isis enable 1"
if interface.connected_endpoints:
for endpoint in interface.connected_endpoints:
if device.hostname in ISIS_NEIGHBORS and ISIS_NEIGHBORS[device.hostname] == endpoint.device.name:
with self.block(f"interface {interface.name}"):
yield "isis enable 1"
``generators/__init__.py``:
.. code:: python
from annet.generators import BaseGenerator
from annet.storage import Storage
from . import (
aaa,
bgp,
community,
description,
hostname,
ip_address,
isis,
prefix_list,
route_map,
routing,
stp,
)
def get_generators(store: Storage) -> list[BaseGenerator]:
"""All the generators should be returned by the function"""
return [
aaa.Aaa(store),
bgp.Bgp(store),
community.Community(store),
description.Description(store),
hostname.Hostname(store),
ip_address.IpAddress(store),
isis.Isis(store),
prefix_list.PrefixList(store),
route_map.RouteMap(store),
routing.Routing(store),
stp.Stp(store),
]
Look at the diff and patch:
.. code:: diff
> annet diff r1.lab r2.lab r3.lab
# -------------------- r1.lab.cfg --------------------
router bgp 65001
- neighbor 10.1.2.12 remote-as 65002
+ neighbor 10.1.2.12 remote-as 65004
- neighbor 10.1.3.13 remote-as 65003
+ neighbor 10.1.3.13 remote-as 65004
# -------------------- r2.lab.cfg --------------------
+ router isis 1
+ net 49.0001.0010.0100.1002.00
+ router-id ipv4 1.1.1.2
+ is-type level-2
+ address-family ipv4 unicast
interface Ethernet2
+ isis enable 1
+ interface Loopback0
+ ip address 1.1.1.2/32
+ isis enable 1
- router bgp 65002
- router-id 1.1.1.2
- neighbor ROUTERS peer group
- neighbor ROUTERS route-map ROUTERS_IMPORT in
- neighbor ROUTERS route-map ROUTERS_EXPORT out
- neighbor ROUTERS send-community
- neighbor 10.1.2.11 peer group ROUTERS
- neighbor 10.1.2.11 remote-as 65001
- neighbor 10.2.3.13 peer group ROUTERS
- neighbor 10.2.3.13 remote-as 65003
- address-family ipv4
- neighbor ROUTERS activate
- redistribute connected route-map IMPORT_CONNECTED
+ router bgp 65004
+ router-id 1.1.1.2
+ address-family ipv4
+ redistribute connected route-map IMPORT_CONNECTED
+ neighbor ROUTERS activate
+ neighbor INTERNAL activate
+ neighbor ROUTERS peer group
+ neighbor ROUTERS route-map ROUTERS_IMPORT in
+ neighbor ROUTERS route-map ROUTERS_EXPORT out
+ neighbor ROUTERS send-community
+ neighbor INTERNAL peer group
+ neighbor INTERNAL route-map PERMIT_ANY in
+ neighbor INTERNAL route-map PERMIT_ANY out
+ neighbor INTERNAL send-community
+ neighbor INTERNAL update-source Loopback0
+ neighbor 10.1.2.11 peer group ROUTERS
+ neighbor 10.1.2.11 remote-as 65001
+ neighbor 1.1.1.3 peer group INTERNAL
+ neighbor 1.1.1.3 remote-as 65004
+ route-map PERMIT_ANY permit 10
# -------------------- r3.lab.cfg --------------------
+ router isis 1
+ net 49.0001.0010.0100.1003.00
+ router-id ipv4 1.1.1.3
+ is-type level-2
+ address-family ipv4 unicast
interface Ethernet2
+ isis enable 1
+ interface Loopback0
+ ip address 1.1.1.3/32
+ isis enable 1
- router bgp 65003
- router-id 1.1.1.3
- neighbor ROUTERS peer group
- neighbor ROUTERS route-map ROUTERS_IMPORT in
- neighbor ROUTERS route-map ROUTERS_EXPORT out
- neighbor ROUTERS send-community
- neighbor 10.1.3.11 peer group ROUTERS
- neighbor 10.1.3.11 remote-as 65001
- neighbor 10.2.3.12 peer group ROUTERS
- neighbor 10.2.3.12 remote-as 65002
- address-family ipv4
- neighbor ROUTERS activate
- redistribute connected route-map IMPORT_CONNECTED
+ router bgp 65004
+ router-id 1.1.1.3
+ address-family ipv4
+ redistribute connected route-map IMPORT_CONNECTED
+ neighbor ROUTERS activate
+ neighbor INTERNAL activate
+ neighbor ROUTERS peer group
+ neighbor ROUTERS route-map ROUTERS_IMPORT in
+ neighbor ROUTERS route-map ROUTERS_EXPORT out
+ neighbor ROUTERS send-community
+ neighbor INTERNAL peer group
+ neighbor INTERNAL route-map PERMIT_ANY in
+ neighbor INTERNAL route-map PERMIT_ANY out
+ neighbor INTERNAL send-community
+ neighbor INTERNAL update-source Loopback0
+ neighbor 10.1.3.11 peer group ROUTERS
+ neighbor 10.1.3.11 remote-as 65001
+ neighbor 1.1.1.2 peer group INTERNAL
+ neighbor 1.1.1.2 remote-as 65004
+ route-map PERMIT_ANY permit 10
.. code:: none
> annet patch r1.lab r2.lab r3.lab
# -------------------- r1.lab.patch --------------------
router bgp 65001
no neighbor 10.1.2.12 remote-as 65002
no neighbor 10.1.3.13 remote-as 65003
neighbor 10.1.2.12 remote-as 65004
neighbor 10.1.3.13 remote-as 65004
exit
# -------------------- r2.lab.patch --------------------
no router bgp 65002
router isis 1
net 49.0001.0010.0100.1002.00
router-id ipv4 1.1.1.2
is-type level-2
address-family ipv4 unicast
exit
interface Ethernet2
isis enable 1
exit
interface Loopback0
ip address 1.1.1.2/32
isis enable 1
exit
route-map PERMIT_ANY permit 10
exit
router bgp 65004
router-id 1.1.1.2
address-family ipv4
redistribute connected route-map IMPORT_CONNECTED
neighbor ROUTERS activate
neighbor INTERNAL activate
exit
neighbor ROUTERS peer group
neighbor ROUTERS route-map ROUTERS_IMPORT in
neighbor ROUTERS route-map ROUTERS_EXPORT out
neighbor ROUTERS send-community
neighbor INTERNAL peer group
neighbor INTERNAL route-map PERMIT_ANY in
neighbor INTERNAL route-map PERMIT_ANY out
neighbor INTERNAL send-community
neighbor INTERNAL update-source Loopback0
neighbor 10.1.2.11 peer group ROUTERS
neighbor 10.1.2.11 remote-as 65001
neighbor 1.1.1.3 peer group INTERNAL
neighbor 1.1.1.3 remote-as 65004
exit
# -------------------- r3.lab.patch --------------------
no router bgp 65003
router isis 1
net 49.0001.0010.0100.1003.00
router-id ipv4 1.1.1.3
is-type level-2
address-family ipv4 unicast
exit
interface Ethernet2
isis enable 1
exit
interface Loopback0
ip address 1.1.1.3/32
isis enable 1
exit
route-map PERMIT_ANY permit 10
exit
router bgp 65004
router-id 1.1.1.3
address-family ipv4
redistribute connected route-map IMPORT_CONNECTED
neighbor ROUTERS activate
neighbor INTERNAL activate
exit
neighbor ROUTERS peer group
neighbor ROUTERS route-map ROUTERS_IMPORT in
neighbor ROUTERS route-map ROUTERS_EXPORT out
neighbor ROUTERS send-community
neighbor INTERNAL peer group
neighbor INTERNAL route-map PERMIT_ANY in
neighbor INTERNAL route-map PERMIT_ANY out
neighbor INTERNAL send-community
neighbor INTERNAL update-source Loopback0
neighbor 10.1.3.11 peer group ROUTERS
neighbor 10.1.3.11 remote-as 65001
neighbor 1.1.1.2 peer group INTERNAL
neighbor 1.1.1.2 remote-as 65004
exit
Deploy it:
.. code:: none
annet deploy r1.lab r2.lab r3.lab
And check the result:
.. code:: none
ssh annet@172.20.0.102
(annet@172.20.0.102) Password:
Last login: Fri Feb 7 08:34:22 2025 from 172.20.0.0
r2#sh ip bgp sum
BGP summary information for VRF default
Router identifier 1.1.1.2, local AS number 65004
Neighbor Status Codes: m - Under maintenance
Neighbor V AS MsgRcvd MsgSent InQ OutQ Up/Down State PfxRcd PfxAcc
1.1.1.3 4 65004 6 7 0 0 00:01:00 Estab 2 2
10.1.2.11 4 65001 2656 2652 0 0 00:01:33 Estab 1 1
r2#