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:
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 |
|
r2 |
|
r3 |
|
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.
#
# 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:
The directories
lab/ceos-rX.flash
are required to store the saved configuration of cEOS.Before running cEOS, prepare the
startup-config
with the management IP address and a local userannet:annet
.The
depends_on
section is added to each cEOS service to avoid overloading resources on weaker hardware.The docker-compose file specifies the cEOS version. If you use a different version, update the file accordingly.
If you use your own Netbox, you need to:
Create a directory
netbox-docker
;Change
docker-compose.override.yml
todocker-compose.yml
;Remove the
services/netbox
section from the docker-compose file;Remove the
depends_on
section from the cEOS services.
# 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 <<EOF
no aaa root
aaa authorization serial-console
aaa authorization exec default local
aaa authorization exec console none
username annet privilege 15 role network-admin secret 0 annet
switchport default mode routed
no service interface inactive port-id allocation disabled
transceiver qsfp default-mode 4x10G
service routing protocols model multi-agent
logging console informational
spanning-tree mode mstp
interface Ethernet1
no switchport
interface Ethernet2
no switchport
interface Ethernet3
no switchport
interface Management0
ip address 172.20.0.101/24
ip routing
end
EOF
#
cat > lab/ceos-r2.flash/startup-config <<EOF
no aaa root
aaa authorization serial-console
aaa authorization exec default local
aaa authorization exec console none
username annet privilege 15 role network-admin secret 0 annet
switchport default mode routed
no service interface inactive port-id allocation disabled
transceiver qsfp default-mode 4x10G
service routing protocols model multi-agent
logging console informational
spanning-tree mode mstp
interface Ethernet1
no switchport
interface Ethernet2
no switchport
interface Ethernet3
no switchport
interface Management0
ip address 172.20.0.102/24
ip routing
end
EOF
#
cat > lab/ceos-r3.flash/startup-config <<EOF
no aaa root
aaa authorization serial-console
aaa authorization exec default local
aaa authorization exec console none
username annet privilege 15 role network-admin secret 0 annet
switchport default mode routed
no service interface inactive port-id allocation disabled
transceiver qsfp default-mode 4x10G
service routing protocols model multi-agent
logging console informational
spanning-tree mode mstp
interface Ethernet1
no switchport
interface Ethernet2
no switchport
interface Ethernet3
no switchport
interface Management0
ip address 172.20.0.103/24
ip routing
end
EOF
#
# create docker-compose override file
cd netbox-docker
cat > docker-compose.override.yml <<EOF
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.20.0.0/24
gateway: 172.20.0.1
r1r2_net:
name: r1r2_net
driver: bridge
internal: true
ipam:
config:
- subnet: 10.1.2.0/24
r1r3_net:
name: r1r3_net
driver: bridge
internal: true
ipam:
config:
- subnet: 10.1.3.0/24
r2r3_net:
name: r2r3_net
driver: bridge
internal: true
ipam:
config:
- subnet: 10.2.3.0/24
x-ceos-defaults: &ceos-defaults
image: arista-ceos:4.33.1F
platform: linux/amd64
environment:
- INTFTYPE=eth
- MGMT_INTF=eth0
- MAPETH0=1
- ETBA=1
- SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT=1
- CEOS=1
- EOS_PLATFORM=ceoslab
- container=docker
privileged: true
command: >
/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:
docker compose up -d
Ensure Netbox is accessible at http://localhost:8000/.
Create a superuser using the script:
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:
docker exec -it r1 Cli
Try connecting to cEOS via SSH using annet:annet
:
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.
In Organisation/Site, add a Site - name:
lab
, slug:lab
.In Devices/Manufacturers, add a Manufacturer - name:
Arista
, slug:arista
.In Devices/Device Types, add a Device Type - Manufacturer:
Arista
, name:cEOS
, slug:ceos
.In Devices/Device Roles, add a Device Role - name:
switch
, slug:switch
, color: choose any.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
.
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
.
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
.
For each device, assign a Primary IPv4 address. In edit mode, assign Primary IPv4 to
172.20.0.101
,172.20.0.102
, and172.20.0.103
respectively.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.
# 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.
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:
Device credentials. For the lab environment, we use
annet:annet
.A Netbox token. Open Netbox, go to Admins/API Tokens, and add a new token for the user
annet
.
#
# create folder for future annet generators
mkdir generators
touch generators/__init__.py
#
# create configuration file:
cat > annet_config.yaml <<EOF
fetcher:
default:
adapter: gnetcli
params: &gnetcli_params
dev_login: annet
dev_password: annet
deployer:
default:
adapter: gnetcli
params:
<<: *gnetcli_params
generators:
default:
- generators/__init__.py
storage:
netbox:
adapter: netbox
params:
url: http://127.0.0.1:8000
token: 0217a15128a1f8f66bac4b84b3edc5261ba33863
context:
default:
fetcher: default
deployer: default
generators: default
storage: netbox
selected_context: default
EOF
#
# define path to configuration file:
export ANN_CONTEXT_CONFIG_PATH=annet_config.yaml
Let’s check if everything works!
Try to get the Netbox device model:
> 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:
> 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
:
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_<vendor name>
# - run_<vendor name>
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
:
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:
> 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:
> 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:
> 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:
> annet deploy -g description r1.lab r2.lab r3.lab
Verify the result:
> 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
:
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
:
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
:
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
:
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
:
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
:
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:
> 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:
> 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:
> 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:
> annet deploy r2.lab r3.lab
Again look at the diff:
> 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:
> 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
:
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
:
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
:
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
:
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:
> 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:
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:
> annet deploy r1.lab r2.lab r3.lab
Check the result:
> 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 |
|
r2 |
|
r3 |
|
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
:
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
:
# 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
:
from annet.rpl_generators import CommunityList
COMMUNITIES = [
CommunityList(name="ADVERTISE", members=["65000:1"])
]
generators/rpl_views/prefix_list.py
:
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
:
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
:
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
:
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
:
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
:
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:
> 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:
> 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:
annet deploy r1.lab r2.lab r3.lab
Check the result:
> 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
:
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
:
# 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 |
|
r3 |
|
Here’s the ISIS generator and updated init file:
generators/isis.py
:
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
:
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:
> 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
> 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:
annet deploy r1.lab r2.lab r3.lab
And check the result:
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#