Jerikan+Ansible: a configuration management system for network

ex-Blade Network Team

There are many re­sources for net­work au­to­ma­tion with An­si­ble. Most of them only ex­pose the first steps or limit them­selves to a nar­row scope. They give no clue on how to ex­pand from that. Real net­work en­vi­ron­ments may be large, ver­sa­tile, het­ero­ge­neous, and filled with ex­cep­tions. The lack of real-world ex­am­ples for An­si­ble de­ploy­ments, un­like Pup­pet and SaltStack, leads many teams to build brit­tle and in­com­plete au­to­ma­tion so­lutions.

We have re­leased un­der an open-source li­cense our at­tempt to tackle this problem:

  • Jerikan, a tool to build con­fig­u­ra­tion files from a sin­gle source of truth and Jinja2 tem­plates, along with its in­te­gra­tion into the GitLab CI system;
  • an An­si­ble play­book to de­ploy these con­fig­u­ra­tion files on net­work de­vices; and
  • the con­fig­u­ra­tion data and the tem­plates for our, now de­funct, dat­a­cen­ters in San Fran­cisco and South Ko­rea, cov­er­ing many ven­dors (Face­book Wedge 100, Dell S4048 and S6010, Ju­niper QFX 5110, Ju­niper QFX 10002, Cisco ASR 9001, Cisco Cat­a­lyst 2960, Opengear con­sole servers, and Linux), and many func­tion­al­i­ties (pro­vi­sion­ing, BGP-to-the-host rout­ing, edge rout­ing, out-of-band net­work, DNS con­fig­u­ra­tion, in­te­gra­tion with NetBox and IRRs).

Here is a quick demo to con­fig­ure a new peering:

This work is the col­lec­tive ef­fort of Cé­dric Has­coët, Jean-Christophe Legatte, Loïc Pail­has, Sébastien Hur­tel, Tchadel Icard, and Vin­cent Bernat. We are the net­work team of Blade, a French com­pany op­er­at­ing Shadow, a cloud-com­put­ing prod­uct. In May 2021, our com­pany was bought by Oc­tave Klaba and the in­fra­struc­ture is be­ing trans­ferred to OVHcloud, sav­ing Shadow as a prod­uct, but mak­ing our team re­dundant.

Our net­work was around 800 de­vices, span­ning over 10 dat­a­cen­ters with more than 2.5 Tbps of avail­able egress band­width. The re­leased ma­te­rial is there­fore a sub­stan­tial ex­am­ple of man­ag­ing a medium-scale net­work us­ing An­si­ble. We have left out the han­dling of our legacy dat­a­cen­ters to make the fi­nal re­sult more read­able while keep­ing enough ma­te­rial to not turn it into a triv­ial example.

Jerikan#

The first com­po­nent is Jerikan. As in­put, it takes a list of de­vices, con­fig­u­ra­tion data, tem­plates, and val­i­da­tion scripts. It gen­er­ates a set of con­fig­u­ra­tion files for each de­vice. An­si­ble could cover this task, but it has the fol­low­ing lim­i­tations:

  • it is slow;
  • er­rors are dif­fi­cult to de­bug;1 and
  • the hi­er­ar­chy to look up a vari­able is rigid.
Jerikan inputs and outputs
Jerikan inputs and outputs

If you want to fol­low the ex­am­ples, you only need to have Docker and Docker Com­pose in­stalled. Clone the repos­i­tory and you are ready!

Source of truth#

We use YAML files, ver­sioned with Git, as the sin­gle source of truth in­stead of us­ing a data­base, like NetBox, or a mix of a data­base and text files. This pro­vides many ad­vantages:

  • any­one can use their pre­ferred text editor;
  • the team pre­pares changes in branches;
  • the team re­views changes us­ing merge requests;
  • the merge re­quests ex­pose the changes to the gen­er­ated con­fig­u­ra­tion files;
  • roll­back to a pre­vi­ous state is easy; and
  • it is fast.

Up­date (2021-11)

See “Git as a source of truth for net­work au­to­ma­tion” for more de­tailed ar­gu­ments about choos­ing Git in­stead of NetBox.

The first file is devices.​yaml. It con­tains the de­vice list. The sec­ond file is classifier.​yaml. It de­fines a scope for each de­vice. A scope is a set of keys and val­ues. It is used in tem­plates and to look up data as­so­ci­ated with a device.

$ ./run-jerikan scope to1-p1.sk1.blade-group.net
continent: apac
environment: prod
groups:
- tor
- tor-bgp
- tor-bgp-compute
host: to1-p1.sk1
location: sk1
member: '1'
model: dell-s4048
os: cumulus
pod: '1'
shorthost: to1-p1

The de­vice name is matched against a list of reg­u­lar ex­pres­sions and the scope is ex­tended by the re­sult of each match. For to1-p1.​sk1.​blade-group.​net, the fol­low­ing sub­set of classifier.​yaml de­fines its scope:

matchers:
  - '^(([^.]*)\..*)\.blade-group\.net':
      environment: prod
      host: '\1'
      shorthost: '\2'
  - '\.(sk1)\.':
      location: '\1'
      continent: apac
  - '^to([12])-[as]?p(\d+)\.':
      member: '\1'
      pod: '\2'
  - '^to[12]-p\d+\.':
      groups:
        - tor
        - tor-bgp
        - tor-bgp-compute
  - '^to[12]-(p|ap)\d+\.sk1\.':
      os: cumulus
      model: dell-s4048

The third file is searchpaths.py. It de­scribes which di­rec­to­ries to search for a vari­able. A Python func­tion pro­vides a list of paths to look up in data/ for a given scope. Here is a sim­pli­fied version:2

def searchpaths(scope):
    paths = [
        "host/{scope[location]}/{scope[shorthost]}",
        "location/{scope[location]}",
        "os/{scope[os]}-{scope[model]}",
        "os/{scope[os]}",
        'common'
    ]
    for idx in range(len(paths)):
        try:
            paths[idx] = paths[idx].format(scope=scope)
        except KeyError:
            paths[idx] = None
    return [path for path in paths if path]

With this def­i­ni­tion, the data for to1-p1.​sk1.​blade-group.​net is looked up in the fol­low­ing paths:

$ ./run-jerikan scope to1-p1.sk1.blade-group.net
[…]
Search paths:
  host/sk1/to1-p1
  location/sk1
  os/cumulus-dell-s4048
  os/cumulus
  common

Vari­ables are scoped us­ing a names­pace that should be spec­i­fied when do­ing a lookup. We use the fol­low­ing ones:

  • system for ac­counts, DNS, sys­log servers, …
  • topology for ports, in­ter­faces, IP ad­dresses, subnets, …
  • bgp for BGP con­figuration
  • build for tem­plates and val­i­da­tion scripts
  • apps for ap­pli­ca­tion variables

When look­ing up for a vari­able in a given names­pace, Jerikan looks for a YAML file named af­ter the names­pace in each di­rec­tory in the search paths. For ex­am­ple, if we look up a vari­able for to1-p1.​sk1.​blade-group.​net in the bgp names­pace, the fol­low­ing YAML files are pro­cessed: host/​sk1/​to1-p1/​bgp.​yaml, location/​sk1/​bgp.​yaml, os/​cumulus-dell-s4048/​bgp.​yaml, os/​cumulus/​bgp.​yaml, and common/​bgp.​yaml. The search stops at the first match.

The schema.​yaml file al­lows us to over­ride this be­hav­ior by ask­ing to merge dic­tio­nar­ies and ar­rays across all match­ing files.3 Here is an ex­cerpt of this file for the topology namespace:

system:
  users:
    merge: hash
  sampling:
    merge: hash
  ansible-vars:
    merge: hash
  netbox:
    merge: hash

The last fea­ture of the source of truth is the abil­ity to use Jinja2 tem­plates for keys and val­ues by pre­fix­ing them with “~”:

# In data/os/junos/system.yaml
netbox:
  manufacturer: Juniper
  model: "~{{ model|upper }}"

# In data/groups/tor-bgp-compute/system.yaml
netbox:
  role: net_tor_gpu_switch

Look­ing up for netbox in the system names­pace for to1-p2.​ussfo03.​blade-group.​net yields the fol­low­ing result:

$ ./run-jerikan scope to1-p2.ussfo03.blade-group.net
continent: us
environment: prod
groups:
- tor
- tor-bgp
- tor-bgp-compute
host: to1-p2.ussfo03
location: ussfo03
member: '1'
model: qfx5110-48s
os: junos
pod: '2'
shorthost: to1-p2
[…]
Search paths:
[…]
  groups/tor-bgp-compute
[…]
  os/junos
  common
$ ./run-jerikan lookup to1-p2.ussfo03.blade-group.net system netbox
manufacturer: Juniper
model: QFX5110-48S
role: net_tor_gpu_switch

This also works for struc­tured data:

# In groups/adm-gateway/topology.yaml
interface-rescue:
  address: "~{{ lookup('topology', 'addresses').rescue }}"
  up:
    - "~ip route add default via {{ lookup('topology', 'addresses').rescue|ipaddr('first_usable') }} table rescue"
    - "~ip rule add from {{ lookup('topology', 'addresses').rescue|ipaddr('address') }} table rescue priority 10"

# In groups/adm-gateway-sk1/topology.yaml
interfaces:
  ens1f0: "~{{ lookup('topology', 'interface-rescue') }}"

This yields the fol­low­ing result:

$ ./run-jerikan lookup gateway1.sk1.blade-group.net topology interfaces
[…]
ens1f0:
  address: 121.78.242.10/29
  up:
  - ip route add default via 121.78.242.9 table rescue
  - ip rule add from 121.78.242.10 table rescue priority 10

When putting data in the source of truth, we use the fol­low­ing rules:

  1. Don’t re­peat yourself.
  2. Put the data in the most spe­cific place with­out break­ing the first rule.
  3. Use tem­plates with par­si­mony, mostly to help with the pre­vi­ous rules.
  4. Re­strict the data model to what is needed for your use case.

The first rule is im­por­tant. For ex­am­ple, when spec­i­fy­ing IP ad­dresses for a point-to-point link, only spec­ify one side and de­duce the other value in the tem­plates. The last rule means you do not need to mimic a BGP YANG model to spec­ify BGP peers and policies:

peers:
  transit:
    cogent:
      asn: 174
      remote:
        - 38.140.30.233
        - 2001:550:2:B::1F9:1
      specific-import:
        - name: ATT-US
          as-path: ".*7018$"
          lp-delta: 50
  ix-sfmix:
    rs-sfmix:
      monitored: true
      asn: 63055
      remote:
        - 206.197.187.253
        - 206.197.187.254
        - 2001:504:30::ba06:3055:1
        - 2001:504:30::ba06:3055:2
    blizzard:
      asn: 57976
      remote:
        - 206.197.187.42
        - 2001:504:30::ba05:7976:1
      irr: AS-BLIZZARD

Templates#

The list of tem­plates to com­pile for each de­vice is stored in the source of truth, un­der the build namespace:

$ ./run-jerikan lookup edge1.ussfo03.blade-group.net build templates
data.yaml: data.j2
config.txt: junos/main.j2
config-base.txt: junos/base.j2
config-irr.txt: junos/irr.j2
$ ./run-jerikan lookup to1-p1.ussfo03.blade-group.net build templates
data.yaml: data.j2
config.txt: cumulus/main.j2
frr.conf: cumulus/frr.j2
interfaces.conf: cumulus/interfaces.j2
ports.conf: cumulus/ports.j2
dhcpd.conf: cumulus/dhcp.j2
default-isc-dhcp: cumulus/default-isc-dhcp.j2
authorized_keys: cumulus/authorized-keys.j2
motd: linux/motd.j2
acl.rules: cumulus/acl.j2
rsyslog.conf: cumulus/rsyslog.conf.j2

Tem­plates are us­ing Jinja2. This is the same en­gine used in An­si­ble. Jerikan ships some cus­tom fil­ters but also reuses some of the use­ful fil­ters from An­si­ble, no­tably ipaddr. Here is an ex­cerpt of templates/​junos/​base.j2 to con­fig­ure DNS and NTP servers on Ju­niper devices:

system {
  ntp {
{% for ntp in lookup("system", "ntp") %}
    server {{ ntp }};
{% endfor %}
  }
  name-server {
{% for dns in lookup("system", "dns") %}
    {{ dns }};
{% endfor %}
  }
}

The equiv­a­lent tem­plate for Cisco IOS XR is:

{% for dns in lookup('system', 'dns') %}
domain vrf VRF-MANAGEMENT name-server {{ dns }}
{% endfor %}
!
{% for syslog in lookup('system', 'syslog') %}
logging {{ syslog }} vrf VRF-MANAGEMENT
{% endfor %}
!

There are three helper func­tions provided:

  • devices() re­turns the list of de­vices match­ing a set of con­di­tions on the scope. For ex­am­ple, devices("location==​ussfo03", "groups==​tor-bgp") re­turns the list of de­vices in San Fran­cisco in the tor-bgp group. You can also omit the op­er­a­tor if you want the spec­i­fied value to be equal to the one in the lo­cal scope. For ex­am­ple, devices("location") re­turns de­vices in the cur­rent location.
  • lookup() does a key lookup. It takes the names­pace, the key, and op­tion­ally, a de­vice name. If not pro­vided, the cur­rent de­vice is assumed.
  • scope() re­turns the scope of the pro­vided device.

Here is how you would de­fine iBGP ses­sions be­tween edge de­vices in the same location:

{% for neighbor in devices("location", "groups==edge") if neighbor != device %}
  {% for address in lookup("topology", "addresses", neighbor).loopback|tolist %}
protocols bgp group IPV{{ address|ipv }}-EDGES-IBGP {
  neighbor {{ address }} {
    description "IPv{{ address|ipv }}: iBGP to {{ neighbor }}";
  }
}
  {% endfor %}
{% endfor %}

We also have a global key-value store to save in­for­ma­tion to be reused in an­other tem­plate or de­vice. This is quite use­ful to au­to­mat­i­cally build DNS records. First, “cap­ture” the IP ad­dress in­serted into a tem­plate with store() as a filter:

interface Loopback0
 description 'Loopback:'
 {% for address in lookup('topology', 'addresses').loopback|tolist %}
 ipv{{ address|ipv }} address {{ address|store('addresses', 'Loopback0')|ipaddr('cidr') }}
 {% endfor %}
!

Then, re­use it later to build DNS records by it­er­at­ing over store():5

{% for device, ip, interface in store('addresses') %}
  {% set interface = interface|replace('/', '-')|replace('.', '-')|replace(':', '-') %}
  {% set name = '{}.{}'.format(interface|lower, device) %}
{{ name }}. IN {{ 'A' if ip|ipv4 else 'AAAA' }} {{ ip|ipaddr('address') }}
{% endfor %}

Tem­plates are com­piled lo­cally with ./run-jerikan build. The --limit ar­gu­ment re­stricts the de­vices to gen­er­ate con­fig­u­ra­tion files for. Build is not done in par­al­lel be­cause a tem­plate may de­pend on the data col­lected by an­other tem­plate. Cur­rently, it takes 1 minute to com­pile around 3000 files span­ning over 800 devices.

Jerikan outputs when building
templates
Output of Jerikan after building configuration files for six devices

When an er­ror oc­curs, a de­tailed trace­back is dis­played, in­clud­ing the tem­plate name, the line num­ber and the value of all vis­i­ble vari­ables. This is a ma­jor time-saver com­pared to Ansible!

templates/opengear/config.j2:15: in top-level template code
    config.interfaces.{{ interface }}.netmask {{ infos.adddress | ipaddr("netmask") }}
        continent  = 'us'
        device     = 'con1-ag2.ussfo03.blade-group.net'
        environment = 'prod'
        host       = 'con1-ag2.ussfo03'
        infos      = {'address': '172.30.24.19/21'}
        interface  = 'wan'
        location   = 'ussfo03'
        loop       = <LoopContext 1/2>
        member     = '2'
        model      = 'cm7132-2-dac'
        os         = 'opengear'
        shorthost  = 'con1-ag2'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

value = JerkianUndefined, query = 'netmask', version = False, alias = 'ipaddr'

[…]
        # Check if value is a list and parse each element
        if isinstance(value, (list, tuple, types.GeneratorType)):
            _ret = [ipaddr(element, str(query), version) for element in value]
            return [item for item in _ret if item]

>       elif not value or value is True:
E       jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'adddress'

We don’t have gen­eral-pur­pose rules when writ­ing tem­plates. Like for the source of truth, there is no need to cre­ate generic tem­plates able to pro­duce any BGP con­fig­u­ra­tion. There is a bal­ance to be found be­tween read­abil­ity and avoid­ing du­pli­ca­tion. Tem­plates can be­come scary and com­plex: some­times, it’s bet­ter to write a fil­ter or a func­tion in jerikan/​jinja.py. Mas­ter­ing Jinja2 is a good in­vest­ment. Take time to browse through our tem­plates as some of them show in­ter­est­ing features.

Checks#

Op­tion­ally, each con­fig­u­ra­tion file can be val­i­dated by a script in the checks/ di­rec­tory. Jerikan looks up the key checks in the build names­pace to know which checks to run:

$ ./run-jerikan lookup edge1.ussfo03.blade-group.net build checks
- description: Juniper configuration file syntax check
  script: checks/junoser
  cache:
    input: config.txt
    output: config-set.txt
- description: check YAML data
  script: checks/data.yaml
  cache: data.yaml

In the above ex­am­ple, checks/​junoser is ex­e­cuted if there is a change to the gen­er­ated config.​txt file. It also out­puts a trans­formed ver­sion of the con­fig­u­ra­tion file which is eas­ier to un­der­stand when us­ing diff. Junoser checks a Junos con­fig­u­ra­tion file us­ing Ju­niper’s XML schema def­i­ni­tion for Net­conf.6 On er­ror, Jerikan displays:

jerikan/build.py:127: RuntimeError
-------------- Captured syntax check with Junoser call --------------
P: checks/junoser edge2.ussfo03.blade-group.net
C: /app/jerikan
O:
E: Invalid syntax:  set system syslog archive size 10m files 10 word-readable
S: 1

Integration into GitLab CI#

The next step is to com­pile the tem­plates us­ing a CI. As we are us­ing GitLab, Jerikan ships with a .gitlab-ci.​yml file. When we need to make a change, we cre­ate a ded­i­cated branch and a merge re­quest. GitLab com­piles the tem­plates us­ing the same en­vi­ron­ment we use on our lap­tops and stores them as an artifact.

Merge requests in GitLab for a
change
Merge request to add a new port in USSFO03. The templates were compiled successfully but approval from another team member is still required to merge.

Be­fore ap­prov­ing the merge re­quest, an­other team mem­ber looks at the changes in data and tem­plates but also the dif­fer­ences for the gen­er­ated con­fig­u­ra­tion files:

Differences for generated configuration
files
The change configures a port on the Juniper device, adds records to DNS, and updates NetBox with the new IP addresses (not shown).

Ansible#

Af­ter Jerikan has built the con­fig­u­ra­tion files, An­si­ble takes over. It is also pack­aged as a Docker im­age to avoid the trou­ble of main­tain­ing the right Python vir­tual en­vi­ron­ment and en­sure ev­ery­one is us­ing the same versions.

Inventory#

Jerikan has gen­er­ated an in­ven­tory file. It con­tains all the man­aged de­vices, the vari­ables de­fined for each of them and the groups con­verted to An­si­ble groups:

ob1-n1.sk1.blade-group.net ansible_host=172.29.15.12 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
ob2-n1.sk1.blade-group.net ansible_host=172.29.15.13 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
ob1-n1.ussfo03.blade-group.net ansible_host=172.29.15.12 ansible_user=blade ansible_connection=network_cli ansible_network_os=ios
none ansible_connection=local

[oob]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[os-ios]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[model-c2960s]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net

[location-sk1]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net

[location-ussfo03]
ob1-n1.ussfo03.blade-group.net

[in-sync]
ob1-n1.sk1.blade-group.net
ob2-n1.sk1.blade-group.net
ob1-n1.ussfo03.blade-group.net
none

in-sync is a spe­cial group for de­vices whose con­fig­u­ra­tion should match the golden con­fig­u­ra­tion. Daily and unat­tended, An­si­ble should be able to push con­fig­u­ra­tions to this group. The mid-term goal is to cover all devices.

none is a spe­cial de­vice for tasks not re­lated to a spe­cific host. This in­cludes syn­chro­niz­ing NetBox, IRR ob­jects, and the DNS, up­dat­ing the RPKI, and build­ing the ge­ofeed files.

Playbook#

We use a sin­gle play­book for all de­vices. It is de­scribed in the ansible/​playbooks/​site.​yaml file. Here is a short­ened version:

- hosts: adm-gateway:!done
  strategy: mitogen_linear
  roles:
    - blade.linux
    - blade.adm-gateway
    - done

- hosts: os-linux:!done
  strategy: mitogen_linear
  roles:
    - blade.linux
    - done

- hosts: os-junos:!done
  gather_facts: false
  roles:
    - blade.junos
    - done

- hosts: os-opengear:!done
  gather_facts: false
  roles:
    - blade.opengear
    - done

- hosts: none:!done
  gather_facts: false
  roles:
    - blade.none
    - done

A host ex­e­cutes only one of the plays. For ex­am­ple, a Junos de­vice ex­e­cutes the blade.​junos role. Once a play has been ex­e­cuted, the de­vice is added to the done group and the other plays are skipped.

The play­book can be ex­e­cuted with the con­fig­u­ra­tion files gen­er­ated by the GitLab CI us­ing the ./run-ansible-gitlab com­mand. This is a wrap­per around Docker and the ansible-playbook com­mand and it ac­cepts the same ar­gu­ments. To de­ploy the con­fig­u­ra­tion on the edge de­vices for the SK1 dat­a­cen­ter in “check” mode, we use:

$ ./run-ansible-gitlab playbooks/site.yaml --limit='edge:&location-sk1' --diff --check
[…]
PLAY RECAP *************************************************************
edge1.sk1.blade-group.net  : ok=6    changed=0    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0
edge2.sk1.blade-group.net  : ok=5    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

We have some rules when writ­ing roles:

  • --check must de­tect if a change is needed;
  • --diff must pro­vide a vi­su­al­iza­tion of the planned changes;
  • --check and --diff must not dis­play any­thing if there is noth­ing to change;
  • writ­ing a cus­tom mod­ule tai­lored to our needs is a valid solution;
  • the whole de­vice con­fig­u­ra­tion is managed;7
  • se­crets must be stored in Vault;
  • tem­plates should be avoided as we have Jerikan for that; and
  • avoid du­pli­ca­tion and re­use tasks.8

We avoid us­ing col­lec­tions from An­si­ble Gal­axy, the ex­cep­tion be­ing col­lec­tions to con­nect and in­ter­act with ven­dor de­vices, like cisco.​iosxr col­lec­tion. The qual­ity of An­si­ble Gal­axy col­lec­tions is quite ran­dom and it is an ad­di­tional main­te­nance bur­den. It seems bet­ter to write roles tai­lored to our needs. The col­lec­tions we use are in ci/​ansible/​ansible-galaxy.​yaml. We use Mi­to­gen to get a 10× speedup on An­si­ble ex­e­cu­tions on Linux hosts.

We also have a few play­books for op­er­a­tional pur­poses: up­grad­ing the OS ver­sion, iso­lat­ing an edge router, etc. We were also plan­ning on how to add op­er­a­tional checks in roles: are all the BGP ses­sions up? They could have been used to val­i­date a de­ploy­ment—and roll­back if there is an issue.

Cur­rently, our play­books are run from our lap­tops. To keep tabs, we are us­ing ARA. A weekly dry-run on de­vices in the in-sync group also pro­vides a dash­board on which de­vices we need to run An­sible on.

Configuration data and templates#

Jerikan ships with pre-pop­u­lated data and tem­plates match­ing the con­fig­u­ra­tion of our USSFO03 and SK1 dat­a­cen­ters. They do not ex­ist any­more but, we prom­ise, all this was used in pro­duc­tion back in the day! 😢

Network architecture for Blade
datacenter
The latest iteration of our network infrastructure for SK1, USSFO03, and future data centers. The production network is using BGPttH using a spine-leaf fabric. The out-of-band network is using a simple L2 design, using the spanning tree protocol, as well as a set of console servers.

No­tably, you can find the con­fig­u­ra­tion for:

Our edge routers
Some are run­ning on Junos, like edge2.ussfo03, the oth­ers on IOS XR, like edge1.sk1. The im­ple­mented func­tion­al­i­ties are sim­i­lar in both cases and we could swap one for the other. It in­cludes the BGP con­fig­u­ra­tion for tran­sits, peer­ings, and IX as well as the as­so­ci­ated BGP poli­cies. PeeringDB is queried to get the max­i­mum num­ber of pre­fixes to ac­cept for peer­ings. bgpq3 and a con­tainer­ized IRRd help to fil­ter re­ceived routes. A fire­wall is added to pro­tect the rout­ing en­gine. Both IPv4 and IPv6 are con­figured.
Our BGP-based fab­ric
BGP is used in­side the dat­a­cen­ter9 and is ex­tended on bare-metal hosts. The con­fig­u­ra­tion is au­to­mat­i­cally de­rived from the de­vice lo­ca­tion and the port num­ber.10 Top-of-the-rack de­vices are us­ing pas­sive BGP ses­sions for ports to­wards servers. They are also serv­ing a pro­vi­sion­ing net­work to let them boot us­ing DHCP and PXE. They also act as a DHCP server. The de­sign is mul­ti­ven­dor. Some de­vices are run­ning Cu­mu­lus Linux, like to1-p1.ussfo03, while some oth­ers are run­ning Junos, like to1-p2.ussfo03.
Our out-of-band fab­ric
We are us­ing Cisco Cat­a­lyst 2960 switches to build an L2 out-of-band net­work. To pro­vide re­dun­dancy and save a few bucks on wiring, we build small loops and run the span­ning-tree pro­to­col. See ob1-p1.ussfo03. It is re­dun­dantly con­nected to our gate­way servers. We also use OpenGear de­vices for con­sole ac­cess. See con1-n1.ussfo03
Our ad­min­is­tra­tive gate­ways
These Linux servers have mul­ti­ple pur­poses: SSH jump boxes, res­cue con­nec­tion, di­rect ac­cess to the out-of-band net­work, zero-touch pro­vi­sion­ing of net­work de­vices,11 In­ter­net ac­cess for man­age­ment flows, cen­tral­iza­tion of the con­sole servers us­ing Con­server, and API for au­to­con­fig­u­ra­tion of BGP ses­sions for bare-metal servers. They are the first servers in­stalled in a new dat­a­cen­ter and are used to pro­vi­sion ev­ery­thing else. Check both the gen­er­ated files and the as­so­ci­ated An­si­ble tasks.

Up­date (2021-10)

I gave a talk about Jerikan and An­si­ble dur­ing FRnOG #34. It is es­sen­tially the same as this article.

Up­date (2026-10)

This post was briefly dis­cussed on Hacker News.