---
# System prepended metadata

title: FRR Management Framwork
tags: [SONiC]

---

FRR Management Framwork
====
###### tags: `SONiC`
# Agenda 2021/05/27
How to use the .j2 file
How to use protocols interact with FRR daemon
How a config_db.json be load into the FRR daemons.

[FRR Management Framework HLD](https://github.com/Azure/SONiC/blob/master/doc/mgmt/SONiC_Design_Doc_Unified_FRR_Mgmt_Interface.md)
[FRRouting](/4D69f2fjR6GrCaTuhrRJ9A) my note

# FRR System Architecture
FRR is a suite of daemons that work together to build the routing table. Each major protocol is implemented in its own daemon, and these daemons talk to a middleman daemon (zebra), which is responsible for coordinating routing decisions and talking to the dataplane.
![](https://i.imgur.com/QNRzyjB.png)
FRR daemons can be managed through a single integrated user interface shell called vtysh. vtysh connects to each daemon through a UNIX domain socket and then works as a proxy for user input. 

Daemon start up config in SONiC
```
dockers/docker-fpm-frr/frr/supervisord/supervisord.conf.j2
platform/vs/docker-sonic-vs/supervisord.conf
```
FRR Daemon start up parameter
```
src/sonic-frr/frr/tools/etc/frr/daemons
```



# Communication Protocol to FRR daemons
VTYSH communicates with FRR daemons by way of domain socket. Each daemon creates its own socket, typically in `/var/run/frr/<daemon>.vty`. 
We could check in GDB like following:
```bash=
b vtysh_client_run
(gdb) p *vclient
$2 = {fd = 7, name = 0x55ce74661299 "staticd", flag = 32768, path = "/var/run/frr/staticd.vty", '\000' <repeats 4071 times>, next = 0x0}
```
In the VTYSH to daemon direction, messages are simply NUL-terminated strings whose content are CLI commands. Here is a typical message from VTYSH to a daemon:
```c=
Request
ip route 10.77.77.0/24 10.44.44.2\0
```
The response format has some more data in it. First is a NUL-terminated string containing the **plaintext response, which is just the output of the command that was sent in the request**. The plaintext response is followed by 3 null marker bytes, followed by a 1-byte status code that indicates whether the command was successful or not.
```
Response

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Plaintext Response                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Marker (0x00)                 |  Status Code  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```

Our `frrcfgd`  will use same protocol to communicate with FRR daemons.


# Advance Introduction of frrcfgd.py
- New FRR config deamon (frrcfgd) fully based on config-DB events, the legacy bgpcfgd daemon(src/sonic-bgpcfgd/bgpcfgd/main.py) will be replace.

# FRR Daemon startup config file
```mermaid
    graph TD
        docker_init.sh([docker_init.sh])
        sonic-cfggen[sonic-cfggen]
        jinja2["staticd.conf.j2/staticd.db.conf.j2"]
        config["/etc/frr/staticd.conf"]
        docker_init.sh-->sonic-cfggen
        jinja2-->sonic-cfggen
        sonic-cfggen-->config
```
## Templates
- if `frr_mgmt_framework_config` enable, sonic-cfggen will use `templates/frr/staticd.conf.j2` to generate `/etc/sonic/frr/staticd.conf` (individual config file for staticd daemon).

```
templates/
├── bfdd
│   └── bfdd.conf.j2
├── bgpd
│   ├── bgpd.conf.db.addr_family.evpn.j2
│   ├── bgpd.conf.db.addr_family.j2
│   ├── bgpd.conf.db.comm_list.j2
│   ├── bgpd.conf.db.j2
│   ├── bgpd.conf.db.nbr_af.j2
│   ├── bgpd.conf.db.nbr_or_peer.j2
│   ├── bgpd.conf.db.pref_list.j2
│   ├── bgpd.conf.db.route_map.j2
│   └── bgpd.conf.j2
├── frr
│   └── frr.conf.j2
├── ospfd
│   ├── ospfd.conf.db.area.j2
│   ├── ospfd.conf.db.comm_list.j2
│   ├── ospfd.conf.db.distributeroute.j2
│   ├── ospfd.conf.db.interface.j2
│   ├── ospfd.conf.db.policyrange.j2
│   ├── ospfd.conf.db.vlink.j2
│   └── ospfd.conf.j2
└── staticd
    ├── staticd.conf.j2
    ├── staticd.db.conf.j2
    └── staticd.db.default_route.conf.j2
```
`*.conf.j2` are responsible include `*.db.conf.j2` which is template file of  FRR daemon config file format.
`*.db.conf.j2` describe how the CONFIG_DB data translate to FRR daemon config.

`sonic-cfggen` use CONFIG_DB and `*.db.conf.j2` as input, then generate the FRR daemons config.

## sonic-cfggen

- Render template with json:
    `sonic-cfggen -d -t /usr/share/template/bgpd.conf.j2`
- Dump config DB content into json file:
    `sonic-cfggen -d --print-data > db_dump.json`
- Load content of json file into config DB:
    `sonic-cfggen -j db_dump.json --write-to-db`
### docker_init.sh trigger sonic-cfggen
When the bgp  container startup, the docker_init.sh execuate.
```bash
CFGGEN_PARAMS=" \
    -d \
    -y /etc/sonic/constants.yml \
    -t /usr/share/sonic/templates/frr_vars.j2 \
    -t /usr/share/sonic/templates/supervisord/supervisord.conf.j2,/etc/supervisor/conf.d/supervisord.conf \
    -t /usr/share/sonic/templates/bgpd/gen_bgpd.conf.j2,/etc/frr/bgpd.conf \
    -t /usr/share/sonic/templates/supervisord/critical_processes.j2,/etc/supervisor/critical_processes \
    -t /usr/share/sonic/templates/zebra/zebra.conf.j2,/etc/frr/zebra.conf \
    -t /usr/share/sonic/templates/staticd/gen_staticd.conf.j2,/etc/frr/staticd.conf \
    -t /usr/share/sonic/templates/gen_frr.conf.j2,/etc/frr/frr.conf \
    -t /usr/share/sonic/templates/isolate.j2,/usr/sbin/bgp-isolate \
    -t /usr/share/sonic/templates/unisolate.j2,/usr/sbin/bgp-unisolate \
    -t /usr/local/sonic/frrcfgd/bfdd.conf.j2,/etc/frr/bfdd.conf \
    -t /usr/local/sonic/frrcfgd/ospfd.conf.j2,/etc/frr/ospfd.conf \
"
```

## jinja2

[jinja Document](https://jinja.palletsprojects.com/en/3.0.x/)

- sonic-cfggen handle jinja2

paths:  `['/', '/usr/share/sonic/templates', '/usr/local/sonic/frrcfgd']`
```python=
def _get_jinja2_env(paths):
    """
    Retreive Jinj2 env used to render configuration templates
    """
    loader = jinja2.FileSystemLoader(paths)
    redis_bcc = RedisBytecodeCache(SonicV2Connector(host='127.0.0.1'))
    env = jinja2.Environment(loader=loader, trim_blocks=True, bytecode_cache=redis_bcc)
    env.filters['sort_by_port_index'] = sort_by_port_index
    env.filters['ipv4'] = is_ipv4
    env.filters['ipv6'] = is_ipv6
    env.filters['unique_name'] = unique_name
    env.filters['pfx_filter'] = pfx_filter
    env.filters['ip_network'] = ip_network
    for attr in ['ip', 'network', 'prefixlen', 'netmask', 'broadcast']:
        env.filters[attr] = partial(prefix_attr, attr)
    # Pass the is_multi_asic function as global
    env.globals['multi_asic'] = is_multi_asic

    return env
```
- Custom Filters
templates/staticd/staticd.db.conf.j2
```
{%- set ip_addr = ip_prefix.split('/')[0] %}
{%- if ip_addr|ipv4 %}
    {%- set ns.str = 'ip route' %}
{%- else %}
    {%- set ns.str = 'ipv6 route' %}
{%- endif %}
```
Filter python implementation
```python
env.filters['ipv4'] = is_ipv4
def is_ipv4(value):
    if not value:
        return False
    if isinstance(value, netaddr.IPNetwork):
        addr = value
    else:
        try:
            addr = netaddr.IPNetwork(str(value))
        except:
            return False
    return addr.version == 4
```
[build-in-filter](https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters)
- macro
It is like a function implement.
```
macro iproute(ip_prefix, nh_blackhole, nh_ip, nh_intf, nh_dist, nh_tag, nh_vrf)
```


Render the data
```python
env = _get_jinja2_env(paths)
for template_file, dest_file in args.template:
    template = env.get_template(os.path.basename(template_file))
    template_data = template.render(data)
    if dest_file == "config-db":
        deep_update(data, FormatConverter.to_deserialized(json.loads(template_data)))
    else:
        with smart_open(dest_file, 'w') as df:
            print(template_data, file=df)

```



- FRR daemon config Format output
```bash
docker exec -it bgp bash
root@sonic:/# sonic-cfggen -d -t /usr/local/sonic/frrcfgd/staticd.db.conf.j2
! template: staticd/staticd.db.conf.j2
!
! Static Route configuration using config DB static route table
!

ip route 10.1.1.0/24 192.168.3.2 Ethernet4
ipv6 route FC00:0:1:cc::/64 FC00:0:1:bb:0:0:0:2 Ethernet4
!
vrf Vrf01
  ip route 10.1.1.0/24 192.168.3.2 Ethernet4
  exit-vrf
!

```



### /etc/frr/staticd.conf 
Set the log or FRR options appear here.
```json=
! =========== Managed by sonic-cfggen DO NOT edit manually! ====================
! generated by templates/frr/staticd.conf.j2 using config DB data
! file: staticd.conf
!
!
! template: common/daemons.common.conf.j2
!
hostname sonic
password zebra
enable password zebra
!
log syslog informational
log facility local4
!
! end of template: common/daemons.common.conf.j2!
!
! set static default route to mgmt gateway as a backup to learned default
!!
! template: staticd/staticd.db.conf.j2
!
! Static Route configuration using config DB static route table
!
!
```

# `BgpdClientMgr`: Interact with FRR daemon
- TABLE_DAEMON:
    - Bind the config_db table name with FRR dameon.
```python
'STATIC_ROUTE': ['staticd']
'DEVICE_METADATA': ['bgpd']
```
- ALL_DAEMONS: all running FRR daemons.
```python
ALL_DAEMONS = ['bgpd', 'zebra', 'staticd', 'bfdd', 'ospfd', 'pimd']
```

- __create_frr_client:
        Create the client socket by ALL_DAEMONS and connect by address `/run/frr/staticd.vty (/var/run/frr/<daemon>.vty)`
- VTYSH_CMD_DAEMON:
    - Define which daemon will handle the corespond command

```python
bgpd_client.run_vtysh_command(table, command, daemons)
self.__send_data(sock, command + '\0')
self.__get_reply(sock)
```

# `BGPConfigDaemon`: Handle with CONFIG_DB

Get the table from config db.
```python=
nbr_table = self.config_db.get_table('BGP_NEIGHBOR')
sroute_table = self.config_db.get_table('STATIC_ROUTE')
```

### Subscribe and Listen
```python   
def subscribe_all(self):
        for table, hdlr in self.table_handler_list:
            self.config_db.subscribe(table, hdlr)
```
Start listen Redis keyspace events and will trigger corresponding handlers when content of a table changes.
```python
def listen(self):
    """Start listen Redis keyspace events and will trigger corresponding handlers when content of a table changes.
    """
    self.pubsub = self.get_redis_client(self.db_name).pubsub()
    self.pubsub.psubscribe(**{"__keyspace@{}__:*".format(self.get_dbid(self.db_name)): self.sub_msg_handler})
    self.sub_thread = self.pubsub.run_in_thread(sleep_time = 0.01)
```
### Binding table and handler list:
```python
self.table_handler_list = 
('BGP_NEIGHBOR', self.bgp_neighbor_handler),
('STATIC_ROUTE', self.bgp_table_handler_common)
```
#### bgp_table_handler_common handler
```bash
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: ----------------------------------
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:  BGP table handling
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: ----------------------------------
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: table : STATIC_ROUTE
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: key   : 10.1.1.0/24
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: op    : SET
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: data  :
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:         blackhole  - False
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:         ifname     - Ethernet4
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:         nexthop    - 192.168.3.2
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:         vrf        - default
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd:
```

### self.__add_op_to_data
Prepare the CONFIG_DB data:
```python
{'blackhole': (False, ADD), 
 'ifname': (Ethernet4, ADD), 
 'nexthop': (192.168.3.2, ADD), 
 'vrf': (default, ADD)}
```


### self.__update_bgp:
Data get from Redis DB:
```python
STATIC_ROUTE, 
{'blackhole': (False, ADD), 'ifname': (Ethernet4, ADD), 'nexthop': (192.168.3.2, ADD), 'vrf': (default, ADD), 'ip_prefix': (10.1.1.0/24, UPDATE)},
['configure terminal', 'vrf default'], 
default
```
- 

### Define global keymap data:
- Input data field value:
    - ++ or +: optional value
    - no prefix: Mandatory field
    - field&field: Key list
- Output command print format


```python
static_route_map = [
(['ip_prefix|ipv4', '++blackhole', '++nexthop', '++ifname', '++track', '++tag', '++distance', '++nexthop-vrf'],
'{no:no-prefix}ip route {} {:blackhole} {} {} {:track} {:nh-tag} {} {:nh-vrf}',
hdl_static_route, socket.AF_INET),

(['ip_prefix|ipv6', '++blackhole', '++nexthop', '++ifname', '++track', '++tag', '++distance', '++nexthop-vrf'],
'{no:no-prefix}ipv6 route {} {:blackhole} {} {} {:track} {:nh-tag} {} {:nh-vrf} ',
hdl_static_route, socket.AF_INET6)
]
```
- Table to key map
```python
tbl_to_key_map = {
'STATIC_ROUTE':     static_route_map,
'BGP_NEIGHBOR':     cmn_key_map[0:2] + nbr_key_map + cmn_key_map[2:],
}
```

### Find the keymap/db_field by table and tbl_key
```python
key_map = BGPKeyMapList(self.tbl_to_key_map[table], table, tbl_key)
```
- table: STATIC_ROUT 
- tbl_key: {'ip_prefix': 'ipv6'}
- self.tbl_to_key_map[table]
```python=
[(['ip_prefix|ipv4', '++blackhole', '++nexthop', '++ifname', '++track', '++tag', '++distance', '++nexthop-vrf'], '{no:no-prefix}ip route {} {:blackhole} {} {} {:track} {:nh-tag} {} {:nh-vrf}', <function hdl_static_route at 0x7f2d9fe69048>, <AddressFamily.AF_INET: 2>), (['ip_prefix|ipv6', '++blackhole', '++nexthop', '++ifname', '++track', '++tag', '++distance', '++nexthop-vrf'], '{no:no-prefix}ipv6 route {} {:blackhole} {} {} {:track} {:nh-tag} {} {:nh-vrf} ', <function hdl_static_route at 0x7f2d9fe69048>, <AddressFamily.AF_INET6: 10>)]
```

Looks Like:
```python
db_field: ['ip_prefix', '++blackhole', '++nexthop', '++ifname', '++track', '++tag', '++distance', '++nexthop-vrf'], 
key_map: [CMD: {no:no-prefix}ip route {} {:blackhole} {} {} {:track} {:nh-tag} {} {:nh-vrf}]
````

### key_map.run_command(self, table, data, cmd_prefix, vrf)
Take key_map and data, then construct the real vtysh command.

Parameter value
```bash
data: {'blackhole': (False, ADD), 
'ifname': (Ethernet4, ADD), 
'nexthop': (FC00:0:1:bb:0:0:0:2, ADD), 
'vrf': (default, ADD), 
'ip_prefix': (fc00:0:1:cc::/64, UPDATE)}
cmd_prefix: ['configure terminal', 'vrf default']
vrf: default
```

1. classfied the db_field to following catgory
    - opt_idx_list
    - req_idx_list
    - upd_id_list
    - del_id_list
    - no_chg_id_list

2. Get the field value we care:
    - **self.get_cmd_data**(key_list, req_idx_list, opt_idx_list, data, del_id_list, no_chg_id_list, merge_vals, True)
3. Construct the vtysh command by keymap
    - **key_map.get_command**(daemon, data_val_op[1], start_idx, *(upper_vals + data_val_op[0]))



### Last step: g_run_command
```python
g_run_command(table, cmd_prefix + "-c '%s'" % cmd, True, key_map.daemons, ignore_fail)
```

```bash
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: VTYSH CMD: configure terminal daemons: ['staticd']
May 24 13:52:15.139524 sonic DEBUG bgp#/frrcfgd: VTYSH CMD: vrf default daemons: ['staticd']
May 24 13:52:15.146124 sonic DEBUG bgp#/frrcfgd: VTYSH CMD: ip route 10.1.1.0/24 False 192.168.3.2 Ethernet4 daemons: ['staticd']
```
Finally, it send to the `BgpdClientMgr` class.


# Appendix
----
BGP_NEIGHBOR table:
```json=
"BGP_NEIGHBOR": {
        "10.0.0.1": {
            "rrclient": 0,
            "name": "ARISTA01T2",
            "local_addr": "10.0.0.0",
            "nhopself": 0,
            "holdtime": "180",
            "asn": "65200",
            "keepalive": "60"
        },
 }
```
STATIC_ROUTE table:
```json
# config_db.json
"STATIC_ROUTE":{
    "10.1.1.0/24": {
        "vrf":"default",
        "nexthop":"192.168.3.2",
        "blackhole":"False",
        "ifname":"Ethernet4"
    },
    "FC00:0:1:cc::/64": {
        "vrf":"default",
        "nexthop":"FC00:0:1:bb:0:0:0:2",
        "blackhole":"False",
        "ifname":"Ethernet4"
    }
},
```

# [FRR Management HLD from SONiC](https://github.com/Azure/SONiC/blob/master/doc/mgmt/SONiC_Design_Doc_Unified_FRR_Mgmt_Interface.md)

:::info
- Notation: docker-fpm-frr (bgp container, sonic-frr)
:::
# Configuration to CONFIG_DB
SONiC have many configuration method to `CONFIG_DB(4)`
- [SONiC Configuration Database Manual](https://github.com/Azure/sonic-swss/blob/master/doc/Configuration.md)
- [SONiC Configuration Setup Service](https://github.com/Azure/SONiC/blob/b16791f3ca16398153556f40b11831cfbb6aba79/doc/ztp/SONiC-config-setup.md)

For now, it is using the `FRR vtysh` to set the `ROUTE_TABLE` which is in the `APPL_DB`. There is no method will sync the configuration to `CONFIG_DB`.

There are few major method to setting the `CONFIG_DB`

| Terms | Definition | comment |
| -------- | -------- | -------- |
| config-setup     | config-setup service (/usr/bin/config-setup)    | `files/image_config/config-setup/config-setup`     |
| /etc/sonic/config_db.json     | startup-config     | Text     |
| minigraph    | use on testbed    | Text     |
| ztp     | Zero Touch Provisioning     | Text     |
| sonic-mgmt-framework     | RESTFUL/Openapi/swagger/yang     | Text     |
| sonic-utilities     | CLI interfaces     | src/sonic-utilities/config/main.py     |

Each type have difference purpose.

On the other hand, SONiC also have 3 different management frameworks and 1 management framework for testbed.
They are all make people confuse and using on different function.

### sonic-buildimage (for config_db)
- [src/sonic-mgmt-framework](https://github.com/Azure/sonic-mgmt-framework)
![](https://i.imgur.com/1bPe21G.jpg)
- **[src/sonic-frr-mgmt-framework](https://github.com/Azure/sonic-buildimage/tree/master/src/sonic-frr-mgmt-framework)**
    - It is for convert the CONFIG_DB ROUTE_TABLE setting to APPL_DB
:::danger
Now, we setting the route table `APPL_DB` from `FRR vtysh`.
In the future it maybe different
:::
- [src/sonic-utilities](https://github.com/Azure/sonic-utilities)
    - CLI method, such as command `config interface ip add Ethernet0 192.168.112.254/24 `
### Testbed (management the testbed)
- [sonic-mgmt](https://github.com/Azure/sonic-mgmt)
This is another story.

# Redis DB choice
[SONiC Configuration Database Manual](https://github.com/Azure/sonic-swss/blob/master/doc/Configuration.md)
:::success
Chose the right ddatabase to find the keys.
```bash
redis-cli
127.0.0.1:6379[4]> select 1
# or 
redis-cli -n <database index>
```
The database index mapping is in `src/sonic-swss-common/common/schema.h`
:::
```

#define APPL_DB         0
#define ASIC_DB         1
#define COUNTERS_DB     2
#define LOGLEVEL_DB     3
#define CONFIG_DB       4
#define PFC_WD_DB       5
#define FLEX_COUNTER_DB 5
#define STATE_DB        6
#define SNMP_OVERLAY_DB 7
#define RESTAPI_DB      8
#define GB_ASIC_DB      9
#define GB_COUNTERS_DB  10
#define GB_FLEX_COUNTER_DB  11
#define CHASSIS_APP_DB      12
#define CHASSIS_STATE_DB    13
```
# sonic-utilities
source code : `src/sonic-utilities/config/main.py`





# Open `frr_mgmt_framework_config`
14:06
src/sonic-mgmt-framework


clue1: /usr/bin/docker_init.sh
It will load the flag value from config_db.json

clue2: dockers/docker-fpm-frr/frr/frr_vars.j2

```json=
{
    "frr_mgmt_framework_config":
        {% if "frr_mgmt_framework_config" in DEVICE_METADATA["localhost"].keys() %}
            "{{ DEVICE_METADATA["localhost"]["frr_mgmt_framework_config"] }}"
        {% else %}
            ""
}
```

The DEVICE_METADATA is a key.

So, we could add `"frr_mgmt_framework_config": "true"` to config_db.json like following.

![](https://i.imgur.com/08huXZK.png)

```bash=
config reload

docker exec -it bgp bash
root        82  1.0  0.8 171612 16924 pts/0    Sl   08:10   0:00 /usr/bin/python3 /usr/local/bin/frrcfgd

#or use it without enter containr bash
docker exec -i bgp ps -aux

root        59  0.6  0.4 171612 17260 pts/0    Sl   08:36   0:02 /usr/bin/python3 /usr/local/bin/frrcfgd
```

[FRR_Mgmt_Interface](https://github.com/Azure/SONiC/blob/7747e2f77ca34a7e7539f4f572d5657c42a957dc/doc/mgmt/SONiC_Design_Doc_Unified_FRR_Mgmt_Interface.md)

# Create client socket and connect with damon server
`BgpdClientMgr.__create_frr_client`
Connect to frr daemon:
following are server address.
```bash=
root@sonic:/# ls /run/frr/
bgpd.pid  bgpd.vty  staticd.pid  staticd.vty  zebra.pid  zebra.vty  zserv.api
```

Debug Log: `/var/log/frr`


```bash=
docker cp sonic-frr-mgmt-framework.tgz bgp:/
gdb python3
r -m pdb frrcfgd.py
```


