Skip to content

exabgp migration walkthrough

Thomas Mangin edited this page Apr 11, 2026 · 3 revisions

Pre-Alpha. This page describes behavior that may change. Not for production use.

A complete worked migration of a non-trivial ExaBGP setup to Ze. The scenario: an existing ExaBGP deployment with two eBGP sessions, a Python process script for programmatic route injection, and a watchdog setup for deferred announcements. The goal: run the same workload on Ze without rewriting the Python script.

The migration goes in three passes. Convert the config, run the existing script through the bridge, and only then port the script to a native Ze plugin once the rest is stable.

Starting point: the ExaBGP config

# exabgp.conf (v2 syntax)
neighbor 10.0.0.1 {
    router-id 10.0.0.100;
    local-address 10.0.0.100;
    local-as 65000;
    peer-as 65001;
    hold-time 90;

    family {
        ipv4 unicast;
    }

    api {
        processes [ inject-routes ];
    }

    static {
        route 192.0.2.0/24 next-hop self;
    }
}

neighbor 10.0.0.2 {
    router-id 10.0.0.100;
    local-address 10.0.0.100;
    local-as 65000;
    peer-as 65002;
    hold-time 90;

    family {
        ipv4 unicast;
    }

    api {
        processes [ inject-routes ];
    }
}

process inject-routes {
    run python3 /opt/scripts/inject.py;
    encoder json;
    receive {
        parsed;
        update;
    }
}

A standard ExaBGP setup: two peers, one process script binding both, one static route. The script reads JSON events from stdin and writes text commands to stdout.

Pass 1: convert the config

ze config migrate --dry-run exabgp.conf > /dev/null
ze config migrate exabgp.conf -o ze.conf
ze config validate ze.conf

The --dry-run pass shows every transformation the migrator would apply. For this config:

Transformation analysis:
  [pending] neighbor->peer
  [pending] api->new-format
  [pending] static->announce

Result: 3 transformation(s) would apply. All would succeed.

After running the migrate command without --dry-run, the output in ze.conf looks like:

bgp {
    router-id 10.0.0.100;
    session { asn { local 65000; } }

    peer upstream1 {
        connection {
            remote { ip 10.0.0.1; }
            local  { ip 10.0.0.100; }
        }
        session {
            asn { remote 65001; }
            family { ipv4/unicast { prefix { maximum 10000; } } }
        }
        timer { hold-time 90; }

        process inject-routes {
            receive [ parsed update ]
        }

        update {
            attribute { origin igp; next-hop self; }
            nlri      { ipv4/unicast add 192.0.2.0/24; }
        }
    }

    peer upstream2 {
        connection {
            remote { ip 10.0.0.2; }
            local  { ip 10.0.0.100; }
        }
        session {
            asn { remote 65002; }
            family { ipv4/unicast { prefix { maximum 10000; } } }
        }
        timer { hold-time 90; }

        process inject-routes {
            receive [ parsed update ]
        }
    }
}

plugin {
    external inject-routes {
        run "ze exabgp plugin python3 /opt/scripts/inject.py";
        encoder json;
    }
}

Notice what changed:

  • neighbor became peer with a name, and the IP/AS moved into remote { }.
  • The static route became an update { } block under the peer.
  • The process block moved to plugin { external }, with ze exabgp plugin prepended to the run line.
  • Every family got an automatic prefix { maximum 10000 } (the migrator's default).

The prefix limit needs attention. 10,000 is a conservative default. If upstream1 is a full-table peer, bump it to 1,000,000 or more. If it is a single-route customer, tighten it to what you actually expect.

Pass 2: run the script through the bridge

The ze exabgp plugin bridge translates between Ze JSON and ExaBGP JSON in one direction, and between ExaBGP text commands and Ze commands in the other. The Python script does not know it is talking to Ze.

Start the daemon:

ze init
ze ze.conf

Watch the peers come up:

ze cli -c "peer list"

If both peers are ESTABLISHED, the bridge is working. The script is running, the JSON events are flowing, and the static route is being advertised.

Monitor the events the script is seeing:

ze cli monitor event peer upstream1 include update

You should see the same JSON envelope the script would have received under ExaBGP. The field names and shapes are ExaBGP-compatible, not Ze-native, because the bridge translates on the way out.

If the script writes route commands to stdout, watch them take effect:

ze cli -c "rib routes sent upstream1 ipv4/unicast"

Known divergences

A few things you will hit on the way. Most are documented on the ExaBGP migration page and in the in-tree exabgp-differences.md.

  • Neighbor qualifier syntax. ExaBGP supports neighbor <ip> local-as <asn> announce route ... for targeting specific sessions when multiple sessions to the same peer exist. Ze does not. Commands apply to every session matching the peer IP.
  • Attribute ordering in UPDATE messages. ExaBGP sorts attributes by type code. Ze uses RFC 4271 section 5 order. Both are RFC-compliant; wire captures will differ but semantics do not.
  • Unsupported capabilities. multi-session and operational are parsed and ignored.

None of these block migration in practice.

Pass 3: port the script to native Ze

Once the setup is stable with the bridge, you can port the script to a native Ze plugin when you actually need the reasons to do so. The reasons:

  • Performance. The bridge translates between two JSON dialects on every event. A native plugin skips the translation.
  • Access to RPKI events. If the script should react to RPKI validation results, the bridge does not expose them. Native plugins subscribe to rpki and update-rpki events.
  • The commit workflow. If the script wants atomic multi-route updates, the commit window commands are not in the ExaBGP text protocol. Native plugins use them through the SDK.
  • The cache commands. cache list, cache forward, cache release are Ze-specific.

The porting path depends on the script's language. For Python, the ze_api module in the test scripts is a starting point. For Go, pkg/plugin/sdk is the SDK. The plugin development section covers both.

The nice property of Ze's model is that you can do this porting one script at a time. The bridge keeps the unported scripts running. There is no flag day.

Verification checklist

Before you call the migration done:

  1. ze config validate ze.conf passes.
  2. Every peer reaches ESTABLISHED.
  3. The prefix count under peer <name> show matches the count you saw under ExaBGP.
  4. The existing script's expected behaviour (route announcements, withdrawals, event reactions) still works through the bridge.
  5. ze show warnings is clean.
  6. The operational reports show nothing unexpected.

If all six are true, you are running on Ze.

What could go wrong

Default prefix limit too low. The migrator fills in 10,000. Forgetting to bump it for full-table peers trips the prefix-limit teardown the moment the peer starts sending routes. Fix it before you start the daemon, not after.

Per-peer process bindings. If the ExaBGP config bound the same script to multiple peers, the migrator creates the process binding on each peer. The bridge runs one script per process, not one per peer, so the script receives events from every bound peer in the same JSON stream. Make sure the script can distinguish which peer each event came from (the ExaBGP JSON envelope carries that information).

Scripts that parse ExaBGP syntax. A script that reads the ExaBGP config file directly (to discover neighbours, for example) will not find a matching file after migration. Either migrate the file-reading logic to talk to Ze's introspection surface (ze schema, ze env list) or leave the old file in place as a read-only reference.

Capability differences. ExaBGP by default advertises a narrower capability set than Ze. Peers that used to see no Graceful Restart capability will now see one, which in some cases changes their behaviour. If that is a problem, disable the capability explicitly per peer.

See also

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally