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 ```