---
date: "2024-01-27T10:01:14Z"
title: VPP Python API
aliases:
- /s/articles/2024/01/27/vpp-papi.html
params:
  asciinema: true
---

{{< image width="200px" float="right" src="/assets/vpp/fdio-color.svg" alt="VPP" >}}

# About this series

Ever since I first saw VPP - the Vector Packet Processor - I have been deeply impressed with its
performance and versatility. For those of us who have used Cisco IOS/XR devices, like the classic
_ASR_ (aggregation service router), VPP will look and feel quite familiar as many of the approaches
are shared between the two.

You'll hear me talk about VPP being API centric, with no configuration persistence, and that's by
design. However, there is this also a CLI utility called `vppctl`, right, so what gives? In truth,
the CLI is used a lot by folks to configure their dataplane, but it really was always meant to be
a debug utility. There's a whole wealth of programmability that is _not_ exposed via the CLI at all,
and the VPP community develops and maintains an elaborate set of tools to allow external programs
to (re)configure the dataplane. One such tool is my own [[vppcfg]({{< ref "2022-04-02-vppcfg-2" >}})] which takes a YAML specification that describes the dataplane configuration, and applies it
safely to a running VPP instance.

## Introduction

In case you're interested in writing your own automation, this article is for you! I'll provide a
deep dive into the Python API which ships with VPP. It's actually very easy to use once you get used
it it -- assuming you know a little bit of Python of course :)

### VPP API: Anatomy

When developers write their VPP features, they'll add an API definition file that describes
control-plane messages that are typically called via shared memory interface, which explains why
these things are called _memclnt_ in VPP. Certain API _types_ can be created, resembling their
underlying C structures, and these types are passed along in _messages_. Finally a _service_ is a
**Request/Reply** pair of messages. When requests are received, VPP executes a _handler_ whose job it
is to parse the request and send either a singular reply, or a _stream_ of replies (like a list of
interfaces).

Clients connect to a unix domain socket, typically `/run/vpp/api.sock`. A TCP port can also
be used, with the caveat that there is no access control provided. Messages are exchanged over this
channel _asynchronously_. A common pattern of async API design is to have a client identifier
(called a _client_index_) and some random number (called a _context_) with which the client
identifies their request. Using these two things, VPP will issue a callback using (a) the
_client_index_ to send the reply to and (b) the client knows which _context_ the reply is meant for.

By the way, this _asynchronous_ design pattern gives programmers one really cool benefit out of the
box: events that are not explicitly requested, like say, link-state change on an interface, can now
be implemented by simply registering a standing callback for a certain message type - I'll show how
that works at the end of this article. As a result, any number of clients, their requests and even
arbitrary VPP initiated events can be in flight at the same time, which is pretty slick!

#### API Types

Most APIs requests pass along datastructures, which follow their internal representation in VPP. I'll start
by taking a look at a simple example -- the VPE itself. It defines a few things in
`src/vpp/vpe_types.api`, notably a few type definitions and one enum:

```
typedef version
{
  u32 major;
  u32 minor;
  u32 patch;

  /* since we can't guarantee that only fixed length args will follow the typedef,
     string type not supported for typedef for now. */
  u8 pre_release[17];           /* 16 + "\0" */
  u8 build_metadata[17];        /* 16 + "\0" */
};

typedef f64 timestamp;
typedef f64 timedelta;

enum log_level {
  VPE_API_LOG_LEVEL_EMERG = 0,    /* emerg */
  VPE_API_LOG_LEVEL_ALERT = 1,    /* alert */
  VPE_API_LOG_LEVEL_CRIT = 2,     /* crit */
  VPE_API_LOG_LEVEL_ERR = 3,      /* err  */
  VPE_API_LOG_LEVEL_WARNING = 4,  /* warn */
  VPE_API_LOG_LEVEL_NOTICE = 5,   /* notice */
  VPE_API_LOG_LEVEL_INFO = 6,     /* info */
  VPE_API_LOG_LEVEL_DEBUG = 7,    /* debug */
  VPE_API_LOG_LEVEL_DISABLED = 8, /* disabled */
};
```

By doing this, API requests and replies can start referring to these types in. When reading this, it
feels a bit like a C header file, showing me the structure. For example, I know that if I ever need
to pass along an argument called `log_level`, I know which values I can provide, together with their
meaning.

#### API Messages

I now take a look at `src/vpp/api/vpe.api` itself, this is where the VPE API is defined. It includes
the former `vpe_types.api` file, so it can reference these typedefs and the enum. Here, I see a few
messages defined that constitute a **Request/Reply** pair:

```
define show_version
{
  u32 client_index;
  u32 context;
};

define show_version_reply
{
  u32 context;
  i32 retval;
  string program[32];
  string version[32];
  string build_date[32];
  string build_directory[256];
};
```

There's one small surprise here out of the gate. I would've expected that beautiful typedef called
`version` from the `vpe_types.api` file to make an appearance, but it's conspicuously missing from
the `show_version_reply` message. Ha! But the rest of it seems reasonably self-explanatory -- as I
already know about the _client_index_ and _context_ fields, I now know that this request does not
carry any arguments, and that the reply has a _retval_ for application errors, similar to how most
libC functions return 0 on success, and some negative value error number defined in
[[errno.h](https://en.wikipedia.org/wiki/Errno.h)]. Then, there are four strings of the given
length, which I should be able to consume.

#### API Services

The VPP API defines three types of message exchanges:

1. **Request/Reply** - The client sends a request message and the server replies with a single reply
   message. The convention is that the reply message is named as `method_name + _reply`.

1. **Dump/Detail** - The client sends a “bulk” request message to the server, and the server replies
   with a set of detail messages. These messages may be of different type. The method name must end
   with `method + _dump`, the reply message should be named `method + _details`. These Dump/Detail
   methods are typically used for acquiring bulk information, like the complete FIB table.

1. **Events** - The client can register for getting asynchronous notifications from the server. This
   is useful for getting interface state changes, and so on. The method name for requesting
   notifications is conventionally prefixed with `want_`, for example `want_interface_events`.

If the convention is kept, the API machinery will correlate the `foo` and `foo_reply` messages into
RPC services. But it's also possible to be explicit about these, by defining _service_ scopes in the
`*.api` files. I'll take two examples, the first one is from the Linux Control Plane plugin (which
I've [[written about]({{< ref "2021-08-12-vpp-1" >}})] a lot while I was contributing to it back in
2021).

**Dump/Detail (example)**: When enumerating _Linux Interface Pairs_, the service definition looks like
this:

```
service {
  rpc lcp_itf_pair_get returns lcp_itf_pair_get_reply
    stream lcp_itf_pair_details;
};
```

To puzzle this together, the request called `lcp_itf_pair_get` is paired up with a reply called
`lcp_itf_pair_get_reply` followed by a stream of zero-or-more `lcp_itf_pair_details` messages. Note
the use of the pattern _rpc_ X _returns_ Y _stream_ Z.


**Events (example)**: I also take a look at an event handler like the one in the interface API that
made an appearance in my list of API message types, above:

```
service {
  rpc want_interface_events returns want_interface_events_reply
    events sw_interface_event;
};
```

Here, the request is `want_interface_events` which returns a `want_interface_events_reply` followed
by zero or more `sw_interface_event` messages, which is very  similar to the streaming (dump/detail)
pattern. The semantic difference is that streams are lists of things and events are asynchronously
happening things in the dataplane -- in other words the _stream_ is meant to end whlie the _events_
messages are generated by VPP when the event occurs. In this case, if an interface is created or
deleted, or the link state of an interface changes, one of these is sent from VPP to the client(s)
that registered an interested in it by calling the `want_interface_events` RPC.

#### JSON Representation

VPP comes with an internal API compiler that scans the source code for these `*.api` files and
assembles them into a few output formats. I take a look at the Python implementation of it in
`src/tools/vppapigen/` and see that it generates C, Go and JSON. As an aside, I chuckle a little bit
on a Python script generating Go and C, but I quickly get over myself. I'm not that funny.

The `vppapigen` tool outputs a bunch of JSON files, one per API specification, which wraps up all of
the information from the _types_, _unions_ and _enums_, the _message_ and _service_ definitions,
together with a few other bits and bobs, and when VPP is installed, these end up in
`/usr/share/vpp/api/`. As of the upcoming VPP 24.02 release, there's about 50 of these _core_ APIs
and an additional 80 or so APIs defined by _plugins_ like the Linux Control Plane.

Implementing APIs is pretty user friendly, largely due to the `vppapigen` tool taking so much of the
boilerplate and autogenerating things. As an example, I need to be able to enumerate the interfaces
that are MPLS enabled, so that I can use my [[vppcfg]({{< ref "2022-03-27-vppcfg-1" >}})] utility to
configure MPLS. I contributed an API called `mpls_interface_dump` which returns a
stream of `mpls_interface_details` messages. You can see that small contribution in merged [[Gerrit
39022](https://gerrit.fd.io/r/c/vpp/+/39022)].

## VPP Python API

The VPP API has been ported to many languages (C, C++, Go, Lua, Rust, Python, probably a few others).
I am primarily a user of the Python API, which ships alongside VPP in a separate Debian package. The
source code lives in `src/vpp-api/python/` which doesn't have any dependencies other than Python's
own `setuptools`. Its implementation canonically called `vpp_papi`, which, I cannot tell a lie,
reminds me of spanish rap music. But, if you're still reading, maybe now is a good time to depart
from the fundamental, and get to the practical!

### Example: Hello World

Without further ado, I dive right in with this tiny program:

```python
from vpp_papi import VPPApiClient, VPPApiJSONFiles

vpp_api_dir = VPPApiJSONFiles.find_api_dir([])
vpp_api_files = VPPApiJSONFiles.find_api_files(api_dir=vpp_api_dir)
vpp = VPPApiClient(apifiles=vpp_api_files, server_address="/run/vpp/api.sock")

vpp.connect("ipng-client")

api_reply = vpp.api.show_version()
print(api_reply)
```

The first thing this program does is construct a so-called `VPPApiClient` object. To do this, I need
to feed it a list of JSON definitions, so that it knows what types of APIs are available.
As I
mentioned above, those
to create the list of files, but there are two handy helpers here:
1.  **find_api_dir()** - This is a helper that finds the location of the API files. Normally, the JSON
    files get installed in `/usr/share/vpp/api/`, but when I'm writing code, it's more likely that the
    files are in `/home/pim/src/vpp/` somewhere. This helper function tries to do the right thing and
    detect if I'm in a client or if I'm using a production install, and will return the correct
    directory.
1.  **find_api_files()** - Now, I could rummage through that directory and find the JSON files, but
    there's another handy helper that does that for me, given a directory (like the one I just got
    handed to me). Life is easy.

Once I have the JSON files in hand, I can construct a client by specifying the _server_address_
location to connect to -- this is typically a unix domain socket in `/run/vpp/api.sock` but it can
also be a TCP endpoint. As a quick aside: If you, like me, stumbled over the socket being owned by
`root:vpp` but not writable by the group, that finally got fixed by Georgy in
[[Gerrit 39862](https://gerrit.fd.io/r/c/vpp/+/39862)].

Once I'm connected, I can start calling arbitrary API methods, like `show_version()` which does not
take any arguments. Its reply is a named tuple, and it looks like this:

```bash
pim@vpp0-0:~/vpp_papi_examples$ ./00-version.py
show_version_reply(_0=1415, context=1,
  retval=0, program='vpe', version='24.02-rc0~46-ga16463610',
  build_date='2023-10-15T14:50:49', build_directory='/home/pim/src/vpp')
```

And here is my beautiful hello world in seven (!) lines of code. All that reading and preparing
finally starts paying off. Neat-oh!

### Example: Listing Interfaces

From here on out, it's just incremental learning. Here's an example of how to extend the hello world
example above and make it list the dataplane interfaces and their IPv4/IPv6 addresses:

```python
api_reply = vpp.api.sw_interface_dump()
for iface in api_reply:
  str = f"[{iface.sw_if_index}] {iface.interface_name}"
  ipr = vpp.api.ip_address_dump(sw_if_index=iface.sw_if_index, is_ipv6=False)
  for addr in ipr:
    str += f" {addr.prefix}"
  ipr = vpp.api.ip_address_dump(sw_if_index=iface.sw_if_index, is_ipv6=True)
  for addr in ipr:
    str += f" {addr.prefix}"
  print(str)
```

The API method `sw_interface_dump()` can take a few optional arguments. Notably, if `sw_if_index` is
set, the call will dump that exact interface. If it's not set, it will default to -1 which will dump
all interfaces, and this is how I use it here. For completeness, the method also has an optional
string `name_filter`, which will dump all interfaces which contain a given substring. For example
passing `name_filter='loop'` and `name_filter_value=True` as arguments, would enumerate all interfaces
that have the word 'loop' in them.

Now, the definition of the `sw_interface_dump` method suggests that it returns a stream (remember
the **Dump/Detail** pattern above), so I can predict that the messages I will receive are of type
`sw_interface_details`. There's *lots* of cool information in here, like the MAC address, MTU,
encapsulation (if this is a sub-interface), but for now I'll only make note of the `sw_if_index` and
`interface_name`.

Using this interface index, I then call the `ip_address_dump()` method, which looks like this:

```
define ip_address_dump
{
  u32 client_index;
  u32 context;
  vl_api_interface_index_t sw_if_index;
  bool is_ipv6;
};

define ip_address_details
{
  u32 context;
  vl_api_interface_index_t sw_if_index;
  vl_api_address_with_prefix_t prefix;
};
```

Allright then! If I want the IPv4 addresses for a given interface (referred to not by its name, but
by its index), I can call it with argument `is_ipv6=False`. The return is zero or more messages that
contain the index again, and a prefix the precise type of which can be looked up in `ip_types.api`.
After doing a form of layer-one traceroute through the API specification files, it turns out, that
this prefix is cast to an instance of the `IPv4Interface()` class in Python. I won't bore you with
it, but the second call sets `is_ipv6=True` and, unsurprisingly, returns a bunch of
`IPv6Interface()` objects.

To put it all together, the output of my little script:
```
pim@vpp0-0:~/vpp_papi_examples$ ./01-interface.py
VPP version is 24.02-rc0~46-ga16463610
[0] local0
[1] GigabitEthernet10/0/0 192.168.10.5/31 2001:678:d78:201::fffe/112
[2] GigabitEthernet10/0/1 192.168.10.6/31 2001:678:d78:201::1:0/112
[3] GigabitEthernet10/0/2
[4] GigabitEthernet10/0/3
[5] loop0 192.168.10.0/32 2001:678:d78:200::/128
```

### Example: Linux Control Plane

Normally, services of are either a **Request/Reply** or a **Dump/Detail** type. But careful readers may
have noticed that the Linux Control Plane does a little bit of both. It has a **Request/Reply/Detail**
triplet, because for request `lcp_itf_pair_get`, it will return a `lcp_itf_pair_get_reply` AND a
_stream_ of `lcp_itf_pair_details`. Perhaps in hindsight a more idiomatic way to do this was to have
created simply a `lcp_itf_pair_dump`, but considering this is what we ended up with, I can use it as
a good example case -- how might I handle such a response?

```python
api_reply = vpp.api.lcp_itf_pair_get()
if isinstance(api_reply, tuple) and api_reply[0].retval == 0:
  for lcp in api_reply[1]:
    str = f"[{lcp.vif_index}] {lcp.host_if_name}"
    api_reply2 = vpp.api.sw_interface_dump(sw_if_index=lcp.host_sw_if_index)
    tap_iface = api_reply2[0]
    api_reply2 = vpp.api.sw_interface_dump(sw_if_index=lcp.phy_sw_if_index)
    phy_iface = api_reply2[0]
    str += f" tap {tap_iface.interface_name} phy {phy_iface.interface_name} mtu {phy_iface.link_mtu}"
    print(str)
```

This particular API first sends its _reply_ and then its _stream_, so I can expect it to be a tuple
with the first element being a namedtuple and the second element being a list of details messages. A
good way to ensure that is to check for the reply's _retval_ field to be 0 (success) before trying
to enumerate the _Linux Interface Pairs_. These consist of a VPP interface (say
`GigabitEthernet10/0/0`), which corresponds to a TUN/TAP device which in turn has a VPP name (eg
`tap1`) and a Linux name (eg. `e0`).

The Linux Control Plane call will return these dataplane objects as numerical interface indexes,
not names. However, I can resolve them to names by calling the `sw_interface_dump()` method and
specifying the index as an argument. Because this is a **Dump/Detail** type API call, the return will
be a _stream_ (a list), which will have either zero (if the index didn't exist), or one element
(if it did).

Using this I can puzzle together the following output:
```
pim@vpp0-0:~/vpp_papi_examples$ ./02-lcp.py
VPP version is 24.02-rc0~46-ga16463610
[2] loop0 tap tap0 phy loop0 mtu 9000
[3] e0 tap tap1 phy GigabitEthernet10/0/0 mtu 9000
[4] e1 tap tap2 phy GigabitEthernet10/0/1 mtu 9000
[5] e2 tap tap3 phy GigabitEthernet10/0/2 mtu 9000
[6] e3 tap tap4 phy GigabitEthernet10/0/3 mtu 9000
```

### VPP's Python API objects

The objects in the VPP dataplane can be arbitrarily complex. They can have nested objects, enums,
unions, repeated fields and so on. To illustrate a more complete example, I will take a look at an
MPLS tunnel object in the dataplane. I first create the MPLS tunnel using the CLI, as follows:

```
vpp# mpls tunnel l2-only via 192.168.10.3 GigabitEthernet10/0/1 out-labels 8298 100 200
vpp# mpls local-label add 8298 eos via l2-input-on mpls-tunnel0
```
The first command creates an interface called `mpls-tunnel0` which, if it receives an ethernet frame, will
encapsulate it into an MPLS datagram with a labelstack of `8298.100.200`, and then forward it on to
the router at 192.168.10.3. The second command adds a FIB entry to the MPLS table, upon receipt of a
datagram with the label `8298`, unwrap it and present the resulting datagram contents as an ethernet
frame into `mpls-tunnel0`. By cross connecting this MPLS tunnel with any other dataplane interface
(for example, `HundredGigabitEthernet10/0/1.1234`), this would be an elegant way to configure a
classic L2VPN ethernet-over-MPLS transport. Which is hella cool, but I digress :)

I want to inspect this tunnel using the API, and I find an `mpls_tunnel_dump()` method. As we
know well by now, this is a **Dump/Detail** type method, so the return value will be a list of
zero or more `mpls_tunnel_details` messages.

The `mpls_tunnel_details` message is simply a wrapper around an `mpls_tunnel` type as can be seen in
`mpls.api`, and it references the `fib_path` type as well. Here they are:

```
typedef fib_path
{
  u32 sw_if_index;
  u32 table_id;
  u32 rpf_id;
  u8 weight;
  u8 preference;

  vl_api_fib_path_type_t type;
  vl_api_fib_path_flags_t flags;
  vl_api_fib_path_nh_proto_t proto;
  vl_api_fib_path_nh_t nh;
  u8 n_labels;
  vl_api_fib_mpls_label_t label_stack[16];
};

typedef mpls_tunnel
{
  vl_api_interface_index_t mt_sw_if_index;
  u32 mt_tunnel_index;
  bool mt_l2_only;
  bool mt_is_multicast;
  string mt_tag[64];
  u8 mt_n_paths;
  vl_api_fib_path_t mt_paths[mt_n_paths];
};

define mpls_tunnel_details
{
  u32 context;
  vl_api_mpls_tunnel_t mt_tunnel;
};
```

Taking a closer look, the `mpls_tunnel` message consists of an index, then an `mt_tunnel_index`
which corresponds to the tunnel number (ie. interface mpls-tunnel**N**), some boolean flags, and
then a vector of N FIB paths. Incidentally, you'll find FIB paths all over the place in VPP: in
routes, tunnels like this one, ACLs, and so on, so it's good to get to know them a bit.

Remember when I created the tunnel, I specified something like `.. via ..`? That's a tell-tale
sign that what follows is a FIB path. I specified a nexhop (192.168.10.3 GigabitEthernet10/0/3) and
a list of three out-labels (8298, 100 and 200), all of which VPP has tucked them away in this
`mt_paths` field.

Although it's a bit verbose, I'll paste the complete object for this tunnel, including the FIB path.
You know, for science:

```
mpls_tunnel_details(_0=1185, context=5,
  mt_tunnel=vl_api_mpls_tunnel_t(
    mt_sw_if_index=17,
    mt_tunnel_index=0,
    mt_l2_only=True,
    mt_is_multicast=False,
    mt_tag='',
    mt_n_paths=1,
    mt_paths=[
      vl_api_fib_path_t(sw_if_index=2, table_id=0,rpf_id=0, weight=1, preference=0,
        type=<vl_api_fib_path_type_t.FIB_API_PATH_TYPE_NORMAL: 0>,
        flags=<vl_api_fib_path_flags_t.FIB_API_PATH_FLAG_NONE: 0>,
        proto=<vl_api_fib_path_nh_proto_t.FIB_API_PATH_NH_PROTO_IP4: 0>,
        nh=vl_api_fib_path_nh_t(
          address=vl_api_address_union_t(
            ip4=IPv4Address('192.168.10.3'), ip6=IPv6Address('c0a8:a03::')),
            via_label=0, obj_id=0, classify_table_index=0),
          n_labels=3,
          label_stack=[
            vl_api_fib_mpls_label_t(is_uniform=0, label=8298, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=100, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=200, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0),
            vl_api_fib_mpls_label_t(is_uniform=0, label=0, ttl=0, exp=0)
          ]
        )
      ]
    )
  )
```

This `mt_paths` is really interesting, and I'd like to make a few observations:
*   **type**, **flags** and **proto** are ENUMs which I can find in `fib_types.api`
*   **nh** is the nexthop - there is only one nexthop specified per path entry, so when things like
    ECMP multipath are in play, this will be a vector of N _paths_ each with one _nh_. Good to know.
    This nexthop specifies an address which is a _union_ just like in C. It can be either an
    _ip4_ or an _ip6_. I will know which to choose due to the _proto_ field above.
*   **n_labels** and **label_stack**: The MPLS label stack has a fixed size. VPP reveals here (in
    the API definition but also in the response) that the label-stack can be at most 16 labels deep.
    I feel like this is an interview question at Cisco, somehow. I know how many labels are relevant
    because of the _n_labels_ field above. Their type is of `fib_mpls_label` which can be found in
    `mpls.api`.

After having consumed all of this, I am ready to write a program that wheels over these message
types and prints something a little bit more compact. The final program, in all of its glory --

```python
from vpp_papi import VPPApiClient, VPPApiJSONFiles, VppEnum

def format_path(path):
  str = ""
  if path.proto == VppEnum.vl_api_fib_path_nh_proto_t.FIB_API_PATH_NH_PROTO_IP4:
    str += f" ipv4 via {path.nh.address.ip4}"
  elif path.proto == VppEnum.vl_api_fib_path_nh_proto_t.FIB_API_PATH_NH_PROTO_IP6:
    str += f" ipv6 via {path.nh.address.ip6}"
  elif path.proto == VppEnum.vl_api_fib_path_nh_proto_t.FIB_API_PATH_NH_PROTO_MPLS:
    str += f" mpls"
  elif path.proto == VppEnum.vl_api_fib_path_nh_proto_t.FIB_API_PATH_NH_PROTO_ETHERNET:
    api_reply2 = vpp.api.sw_interface_dump(sw_if_index=path.sw_if_index)
    iface = api_reply2[0]
    str += f" ethernet to {iface.interface_name}"
  else:
    print(path)
  if path.n_labels > 0:
     str += " label"
  for i in range(path.n_labels):
     str += f" {path.label_stack[i].label}"
  return str

api_reply = vpp.api.mpls_tunnel_dump()
for tunnel in api_reply:
  str = f"Tunnel [{tunnel.mt_tunnel.mt_sw_if_index}] mpls-tunnel{tunnel.mt_tunnel.mt_tunnel_index}"
  for path in tunnel.mt_tunnel.mt_paths:
    str += format_path(path)
  print(str)

api_reply = vpp.api.mpls_table_dump()
for table in api_reply:
  print(f"Table [{table.mt_table.mt_table_id}] {table.mt_table.mt_name}")
  api_reply2 = vpp.api.mpls_route_dump(table=table.mt_table.mt_table_id)
  for route in api_reply2:
    str = f"  label {route.mr_route.mr_label} eos {route.mr_route.mr_eos}"
    for path in route.mr_route.mr_paths:
      str += format_path(path)
    print(str)
```

{{< image width="50px" float="left" src="/assets/shared/warning.png" alt="Warning" >}}

Funny detail - it took me almost two years to discover `VppEnum`, which contains all of these
symbols. If you end up reading this after a Bing, Yahoo or DuckDuckGo search, feel free to buy
me a bottle of Glenmorangie - sl&aacute;inte!

The `format_path()` method here has the smarts. Depending on the _proto_ field, I print either
an IPv4 path, an IPv6 path, an internal MPLS path (for example for the reserved labels 0..15), or an
Ethernet path, which is the case in the FIB entry above that diverts incoming packets with label 8298 to be
presented as ethernet datagrams into the intererface `mpls-tunnel0`. If it is an Ethernet proto, I
can use the _sw_if_index_ field to figure out which interface, and retrieve its details to find its
name.

The `format_path()` method finally adds the stack of labels to the returned string, if the _n_labels_
field is non-zero.

My program's output:

```
pim@vpp0-0:~/vpp_papi_examples$ ./03-mpls.py
VPP version is 24.02-rc0~46-ga16463610
Tunnel [17] mpls-tunnel0 ipv4 via 192.168.10.3 label 8298 100 200
Table [0] MPLS-VRF:0
  label 0 eos 0 mpls
  label 0 eos 1 mpls
  label 1 eos 0 mpls
  label 1 eos 1 mpls
  label 2 eos 0 mpls
  label 2 eos 1 mpls
  label 8298 eos 1 ethernet to mpls-tunnel0
```

### Creating VxLAN Tunnels

Until now, all I've done is _inspect_ the dataplane, in other words I've called a bunch of APIs
that do not change state. Of course, many of VPP's API methods _change_ state as well. I'll turn to
another example API -- The VxLAN tunnel API is defined in `plugins/vxlan/vxlan.api` and it's gone
through a few iterations. The VPP community tries to keep backwards compatibility, and a simple way
of doing this is to create new versions of the methods by tagging them with suffixes such as `_v2`,
while eventually marking the older versions as deprecated by setting the `option deprecated;` field
in the definition. In this API specification I can see that we're already at version 3 of the
**Request/Reply** method in `vxlan_add_del_tunnel_v3` and version 2 of the **Dump/Detail** method in
`vxlan_tunnel_v2_dump`.

Once again, using these `*.api` defintions, finding an incantion to create a unicast VxLAN tunnel
with a given VNI, then listing the tunnels, and finally deleting the tunnel I just created, would
look like this:

```python
api_reply = vpp.api.vxlan_add_del_tunnel_v3(is_add=True, instance=100, vni=8298,
  src_address="192.0.2.1", dst_address="192.0.2.254", decap_next_index=1)
if api_reply.retval == 0:
  print(f"Created VXLAN tunnel with sw_if_index={api_reply.sw_if_index}")

api_reply = vpp.api.vxlan_tunnel_v2_dump()
for vxlan in api_reply:
  str = f"[{vxlan.sw_if_index}] instance {vxlan.instance} vni {vxlan.vni}"
  str += " src {vxlan.src_address}:{vxlan.src_port} dst {vxlan.dst_address}:{vxlan.dst_port}")
  print(str)

api_reply = vpp.api.vxlan_add_del_tunnel_v3(is_add=False, instance=100, vni=8298,
  src_address="192.0.2.1", dst_address="192.0.2.254", decap_next_index=1)
if api_reply.retval == 0:
  print(f"Deleted VXLAN tunnel with sw_if_index={api_reply.sw_if_index}")
```

Many of the APIs in VPP will have create and delete in the same method, mostly by specifying the
operation with an `is_add` argument like here. I think it's kind of nice because it makes the
creation and deletion symmetric, even though the deletion needs to specify a fair bit more than
strictly necessary: the _instance_ uniquely identifies the tunnel and should have been enough.

The output of this [[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)] sequence
(which stands for **C**reate, **R**ead, **U**pdate, **D**elete, in case you haven't come across that
acronym yet) then looks like this:

```
pim@vpp0-0:~/vpp_papi_examples$ ./04-vxlan.py
VPP version is 24.02-rc0~46-ga16463610
Created VXLAN tunnel with sw_if_index=18
[18] instance 100 vni 8298 src 192.0.2.1:4789 dst 192.0.2.254:4789
Deleted VXLAN tunnel with sw_if_index=18
```

### Listening to Events

But wait, there's more! Just one more thing, I promise. Way in the beginning of this article, I
mentioned that there is a special variant of the **Dump/Detail** pattern, and that's the **Events**
pattern. With the VPP API client, first I register a single callback function, and then I can
enable/disable events to trigger this callback.

{{< image width="80px" float="left" src="/assets/vpp-papi/warning.png" alt="Warning" >}}
One important note to this: enabling this callback will spawn a new (Python) thread so that the main
program can continue to execute. Because of this, all the standard care has to be taken to make the
program thread-aware. Make sure to pass information from the events-thread to the main-thread in a
safe way!

Let me demonstrate this powerful functionality with a program that listens on
`want_interface_events` which is defined in `interface.api`:

```
service {
  rpc want_interface_events returns want_interface_events_reply
    events sw_interface_event;
};

define sw_interface_event
{
  u32 client_index;
  u32 pid;
  vl_api_interface_index_t sw_if_index;
  vl_api_if_status_flags_t flags;
  bool deleted;
};
```

Here's a complete program, shebang and all, that accomplishes this in a minimalistic way:

```python
#!/usr/bin/env python3

import time
from vpp_papi import VPPApiClient, VPPApiJSONFiles, VppEnum

def sw_interface_event(msg):
  print(msg)

def vpp_event_callback(msg_name, msg):
  if msg_name == "sw_interface_event":
    sw_interface_event(msg)
  else:
    print(f"Received unknown callback: {msg_name} => {msg}")

vpp_api_dir = VPPApiJSONFiles.find_api_dir([])
vpp_api_files = VPPApiJSONFiles.find_api_files(api_dir=vpp_api_dir)
vpp = VPPApiClient(apifiles=vpp_api_files, server_address="/run/vpp/api.sock")

vpp.connect("ipng-client")
vpp.register_event_callback(vpp_event_callback)
vpp.api.want_interface_events(enable_disable=True, pid=8298)

api_reply = vpp.api.show_version()
print(f"VPP version is {api_reply.version}")

try:
  while True:
    time.sleep(1)
except KeyboardInterrupt:
  pass
```

## Results

After all of this deep-diving, all that's left is for me to demonstrate the API by means of this
little screencast [[asciinema](/assets/vpp-papi/vpp_papi_clean.cast),
[gif](/assets/vpp-papi/vpp_papi_clean.gif)] - I hope you enjoy it as much as I enjoyed creating it:

{{< asciinema src="/assets/vpp-papi/vpp_papi_clean.cast" >}}

Note to self:

```
$ asciinema-edit quantize --range 0.18,0.8 --range 0.5,1.5 --range 1.5 \
  vpp_papi.cast > clean.cast
$ Insert the ANSI colorcodes from the mac's terminal into clean.cast's header:
"theme":{"fg": "#ffffff","bg":"#000000",
         "palette":"#000000:#990000:#00A600:#999900:#0000B3:#B300B3:#999900:#BFBFBF:
                    #666666:#F60000:#00F600:#F6F600:#0000F6:#F600F6:#00F6F6:#F6F6F6"}
$ agg --font-size 18 clean.cast clean.gif
$ gifsicle --lossy=80 -k 128 -O2 -Okeep-empty clean.gif -o vpp_papi_clean.gif
```
