Replace dumper with an actual YAML emitter

This commit is contained in:
Pim van Pelt
2022-04-03 21:37:48 +00:00
parent 53a7935168
commit a4a91d1f5e
3 changed files with 150 additions and 96 deletions

View File

@ -110,15 +110,20 @@ fully valid! For a full write up of the syntax and semantic validation, see
### vppcfg dump ### vppcfg dump
The purpose of the **dump** module is to connect to the VPP dataplane, and retrieve its The purpose of the **dump** module is to connect to the VPP dataplane, and retrieve its
state, printing most information found in the INFO logs. Although it does contact VPP, it state, emitting the configuration as a YAML file. Although it does contact VPP, it
will perform *readonly* operations and never manipulate state in the dataplane, so it will perform *readonly* operations and never manipulate state in the dataplane, so it
should be safe to run. should be safe to run.
There are no flags to the dump command. It will return 0 if the connection to VPP was If the flag `-o/--output` is given, the resulting YAML is written to that filename, but
established and its state successfully dumped to the logs, and non-zero otherwise. if it is not given, the output will be written to stdout. It will return 0 if the connection
to VPP was established and its state successfully dumped to the logs, and non-zero otherwise.
Use of the **dump** command can be done even if the dataplane was configured outside of Use of the **dump** command can be done even if the dataplane was configured outside of
`vppcfg`, in other words, the following can be done: `vppcfg`, although some non-supported scenarios (for example, sub-interfaces on loopbacks)
will be flagged as warnings. If warnings or errors are reported, the YAML file cannot be
assumed safe. Conversely, if no warnings/errors are logged, the resulting YAML should be
a good representation of the dataplane state, as far as `vppcfg` is concerned. A good way
to confirm that is to subsequently run the output file back into `vppcfg check`.
``` ```
$ vppcfg dump || echo "Not a hoopy frood" $ vppcfg dump || echo "Not a hoopy frood"
@ -132,23 +137,47 @@ DBGvpp# set interface ip address GigabitEthernet3/0/0.100 2001:db8:1::1/64
DBGvpp# create bridge-domain 10 DBGvpp# create bridge-domain 10
DBGvpp# set interface l2 bridge HundredGigabitEthernet12/0/0 10 DBGvpp# set interface l2 bridge HundredGigabitEthernet12/0/0 10
$ vppcfg dump $ vppcfg dump -o vpp.yaml
[INFO ] vppcfg.vppapi.connect: VPP version is 22.06-rc0~320-g8f60318ac [INFO ] vppcfg.vppapi.connect: VPP version is 22.06-rc0~320-g8f60318ac
[INFO ] vppcfg.vppapi.dump_phys: GigabitEthernet3/0/0 idx=1 [INFO ] vppcfg.vppapi.write: Wrote YAML config to vpp.yaml
[INFO ] vppcfg.vppapi.dump_phys: GigabitEthernet3/0/1 idx=2
[INFO ] vppcfg.vppapi.dump_phys: HundredGigabitEthernet12/0/0 idx=3 $ cat vpp.yaml
[INFO ] vppcfg.vppapi.dump_phys: HundredGigabitEthernet12/0/1 idx=4 bondethernets: {}
[INFO ] vppcfg.vppapi.dump_interfaces: local0 idx=0 type=local mac=00:00:00:00:00:00 mtu=0 flags=0 bridgedomains:
[INFO ] vppcfg.vppapi.dump_interfaces: GigabitEthernet3/0/0 idx=1 type=dpdk mac=00:25:90:0c:05:00 mtu=9000 flags=2 bd10:
[INFO ] vppcfg.vppapi.dump_interfaces: GigabitEthernet3/0/1 idx=2 type=dpdk mac=00:25:90:0c:05:01 mtu=9000 flags=2 description: ''
[INFO ] vppcfg.vppapi.dump_interfaces: HundredGigabitEthernet12/0/0 idx=3 type=dpdk mac=b4:96:91:b3:b1:10 mtu=8996 flags=0 interfaces:
[INFO ] vppcfg.vppapi.dump_interfaces: HundredGigabitEthernet12/0/1 idx=4 type=dpdk mac=b4:96:91:b3:b1:11 mtu=8996 flags=0 - HundredGigabitEthernet12/0/0
[INFO ] vppcfg.vppapi.dump_interfaces: GigabitEthernet3/0/0.100 idx=5 type=dpdk mac=00:00:00:00:00:00 mtu=0 flags=2 mtu: 8996
[INFO ] vppcfg.vppapi.dump_interfaces: Encapsulation: dot1q 100 exact-match interfaces:
[INFO ] vppcfg.vppapi.dump_interfaces: L3: 2001:db8:1::1/64 GigabitEthernet3/0/0:
[INFO ] vppcfg.vppapi.dump_subints: GigabitEthernet3/0/0.100 tags=1 idx=5 encap=dot1q 100 exact-match description: ''
[INFO ] vppcfg.vppapi.dump_bridgedomains: BridgeDomain10 mtu: 9000
[INFO ] vppcfg.vppapi.dump_bridgedomains: Members: HundredGigabitEthernet12/0/0 sub-interfaces:
100:
addresses:
- 2001:db8:1::1/64
description: ''
encapsulation:
dot1q: 100
exact-match: true
mtu: 9000
GigabitEthernet3/0/1:
description: ''
mtu: 9000
HundredGigabitEthernet12/0/0:
description: ''
mtu: 8996
HundredGigabitEthernet12/0/1:
description: ''
mtu: 8996
loopbacks: {}
vxlan_tunnels: {}
$ vppcfg check -c vpp.yaml
[INFO ] root.main: Loading configfile vpp.yaml
[INFO ] vppcfg.config.valid_config: Configuration validated successfully
[INFO ] root.main: Configuration is valid
``` ```
### vppcfg plan ### vppcfg plan

View File

@ -5,9 +5,11 @@ interface metadata.
from vpp_papi import VPPApiClient from vpp_papi import VPPApiClient
import os import os
import sys
import fnmatch import fnmatch
import logging import logging
import socket import socket
import yaml
class VPPApi(): class VPPApi():
def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'): def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'):
@ -211,18 +213,6 @@ class VPPApi():
self.cache_read = True self.cache_read = True
return self.cache_read return self.cache_read
def get_encapsulation(self, iface):
""" Return a string with the encapsulation of a subint """
encap = "dot1q"
if iface.sub_if_flags & 8:
encap = "dot1ad"
encap += " %d" % iface.sub_outer_vlan_id
if iface.sub_inner_vlan_id> 0:
encap += " inner-dot1q %d" % iface.sub_inner_vlan_id
if iface.sub_if_flags & 16:
encap += " exact-match"
return encap
def phys_exist(self, ifname_list): def phys_exist(self, ifname_list):
""" Return True if all interfaces in the `ifname_list` exist as physical interface names """ Return True if all interfaces in the `ifname_list` exist as physical interface names
in VPP. Return False otherwise.""" in VPP. Return False otherwise."""
@ -271,73 +261,107 @@ class VPPApiDumper(VPPApi):
def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'): def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'):
VPPApi.__init__(self, address, clientname) VPPApi.__init__(self, address, clientname)
def dump(self): def write(self, outfile):
self.dump_phys() if outfile and outfile == '-':
self.dump_interfaces() fh = sys.stdout
self.dump_subints() outfile = "(stdout)"
self.dump_bridgedomains() else:
fh = open(outfile, 'w')
def dump_phys(self): config = self.cache_to_config()
phys = self.get_phys()
for ifname in phys:
iface = self.cache['interface_names'][ifname]
self.logger.info("%s idx=%d" % (iface.interface_name, iface.sw_if_index))
def dump_subints(self): print(yaml.dump(config), file=fh)
subints = self.get_qinx_interfaces()
for ifname in subints:
iface = self.cache['interface_names'][ifname]
self.logger.info("%s tags=2 idx=%d encap=%s" % (iface.interface_name, iface.sw_if_index, self.get_encapsulation(iface)))
subints = self.get_dot1x_interfaces() if fh is not sys.stdout:
for ifname in subints: fh.close()
iface = self.cache['interface_names'][ifname] self.logger.info("Wrote YAML config to %s" % (outfile))
self.logger.info("%s tags=1 idx=%d encap=%s" % (iface.interface_name, iface.sw_if_index, self.get_encapsulation(iface)))
def dump_bridgedomains(self): def cache_to_config(self):
for bd_id, bridge in self.cache['bridgedomains'].items(): config = {"loopbacks": {}, "bondethernets": {}, "interfaces": {}, "bridgedomains": {}, "vxlan_tunnels": {} }
self.logger.info("BridgeDomain%d" % (bridge.bd_id)) for idx, iface in self.cache['bondethernets'].items():
if bridge.bvi_sw_if_index > 0 and bridge.bvi_sw_if_index < 2**32-1 : bond = {"description": ""}
self.logger.info(" BVI: " + self.cache['interfaces'][bridge.bvi_sw_if_index].interface_name) if iface.sw_if_index in self.cache['bondethernet_members']:
bond['interfaces'] = [self.cache['interfaces'][x].interface_name for x in self.cache['bondethernet_members'][iface.sw_if_index]]
config['bondethernets'][iface.interface_name] = bond
members = [] for numtags in [ 0, 1, 2 ]:
for member in bridge.sw_if_details:
members.append(self.cache['interfaces'][member.sw_if_index].interface_name)
if len(members) > 0:
self.logger.info(" Members: " + ' '.join(members))
def dump_interfaces(self):
for idx, iface in self.cache['interfaces'].items(): for idx, iface in self.cache['interfaces'].items():
self.logger.info("%s idx=%d type=%s mac=%s mtu=%d flags=%d" % (iface.interface_name, if iface.sub_number_of_tags != numtags:
iface.sw_if_index, iface.interface_dev_type, iface.l2_address, continue
iface.mtu[0], iface.flags))
if iface.interface_dev_type=='bond' and iface.sub_id == 0 and iface.sw_if_index in self.cache['bondethernet_members']:
members = [self.cache['interfaces'][x].interface_name for x in self.cache['bondethernet_members'][iface.sw_if_index]]
self.logger.info(" Members: %s" % ' '.join(members))
if iface.interface_dev_type=="VXLAN":
vxlan = self.cache['vxlan_tunnels'][iface.sw_if_index]
self.logger.info(" VXLAN: %s:%d -> %s:%d VNI %d" % (vxlan.src_address, vxlan.src_port,
vxlan.dst_address, vxlan.dst_port, vxlan.vni))
if iface.interface_dev_type=='Loopback':
if iface.sub_id > 0: if iface.sub_id > 0:
self.logger.info(" Encapsulation: %s" % (self.get_encapsulation(iface))) self.logger.warning("Refusing to export sub-interfaces of loopback devices (%s)" % iface.interface_name)
continue
loop = {"description": ""}
loop['mtu'] = iface.mtu[0]
if iface.sw_if_index in self.cache['lcps']: if iface.sw_if_index in self.cache['lcps']:
lcp = self.cache['lcps'][iface.sw_if_index] loop['lcp'] = self.cache['lcps'][iface.sw_if_index].host_if_name
tap_name = self.cache['interfaces'][lcp.host_sw_if_index].interface_name if iface.sw_if_index in self.cache['interface_addresses']:
tap_idx = lcp.host_sw_if_index
self.logger.info(" LCP: %s (tap=%s idx=%d netns=%s)" % (lcp.host_if_name, tap_name, tap_idx, lcp.namespace))
if len(self.cache['interface_addresses'][iface.sw_if_index]) > 0: if len(self.cache['interface_addresses'][iface.sw_if_index]) > 0:
self.logger.info(" L3: %s" % ' '.join(self.cache['interface_addresses'][iface.sw_if_index])) loop['addresses'] = self.cache['interface_addresses'][iface.sw_if_index]
config['loopbacks'][iface.interface_name] = loop
elif iface.interface_dev_type in ['bond', 'VXLAN', 'dpdk']:
i = {"description": "" }
if iface.sw_if_index in self.cache['lcps']:
i['lcp'] = self.cache['lcps'][iface.sw_if_index].host_if_name
if iface.sw_if_index in self.cache['interface_addresses']:
if len(self.cache['interface_addresses'][iface.sw_if_index]) > 0:
i['addresses'] = self.cache['interface_addresses'][iface.sw_if_index]
if iface.sw_if_index in self.cache['l2xcs']: if iface.sw_if_index in self.cache['l2xcs']:
l2xc = self.cache['l2xcs'][iface.sw_if_index] l2xc = self.cache['l2xcs'][iface.sw_if_index]
self.logger.info(" L2XC: %s" % self.cache['interfaces'][l2xc.tx_sw_if_index].interface_name) i['l2xc'] = self.cache['interfaces'][l2xc.tx_sw_if_index].interface_name
i['mtu'] = iface.mtu[0]
if iface.sub_number_of_tags == 0:
config['interfaces'][iface.interface_name] = i
continue
for bd_id, bridge in self.cache['bridgedomains'].items(): encap = {}
if bridge.bvi_sw_if_index == iface.sw_if_index: if iface.sub_if_flags&8:
self.logger.info(" BVI: BridgeDomain%d" % (bd_id)) encap['dot1ad'] = iface.sub_outer_vlan_id
else:
encap['dot1q'] = iface.sub_outer_vlan_id
if iface.sub_inner_vlan_id > 0:
encap['inner-dot1q'] = iface.sub_inner_vlan_id
encap['exact-match'] = bool(iface.sub_if_flags&16)
i['encapsulation'] = encap
pass sup_iface = self.cache['interfaces'][iface.sup_sw_if_index]
if iface.mtu[0] > 0:
i['mtu'] = iface.mtu[0]
else:
i['mtu'] = sup_iface.mtu[0]
if not 'sub-interfaces' in config['interfaces'][sup_iface.interface_name]:
config['interfaces'][sup_iface.interface_name]['sub-interfaces'] = {}
config['interfaces'][sup_iface.interface_name]['sub-interfaces'][iface.sub_id] = i
for idx, iface in self.cache['vxlan_tunnels'].items():
vpp_iface = self.cache['interfaces'][iface.sw_if_index]
vxlan = { "description": "",
"vni": int(iface.vni),
"local": str(iface.src_address),
"remote": str(iface.dst_address) }
config['vxlan_tunnels'][vpp_iface.interface_name] = vxlan
for idx, iface in self.cache['bridgedomains'].items():
# self.logger.info("%d: %s" % (idx, iface))
bridge_name = "bd%d" % idx
mtu = 1500
bridge = {"description": ""}
bvi = None
if iface.bvi_sw_if_index != 2**32-1:
bvi = self.cache['interfaces'][iface.bvi_sw_if_index]
mtu = bvi.mtu[0]
bridge['bvi'] = bvi.interface_name
members = []
for member in iface.sw_if_details:
if bvi and bvi.interface_name == self.cache['interfaces'][member.sw_if_index].interface_name == bvi.interface_name:
continue
members.append(self.cache['interfaces'][member.sw_if_index].interface_name)
mtu = self.cache['interfaces'][member.sw_if_index].mtu[0]
if len(members) > 0:
bridge['interfaces'] = members
bridge['mtu'] = mtu
config['bridgedomains'][bridge_name] = bridge
return config

3
vppcfg
View File

@ -40,6 +40,7 @@ def main():
check_p.add_argument('-c', '--config', dest='config', required=True, type=str, help="""YAML configuration file for vppcfg""") check_p.add_argument('-c', '--config', dest='config', required=True, type=str, help="""YAML configuration file for vppcfg""")
dump_p = subparsers.add_parser('dump', help="dump current running VPP configuration (VPP readonly)") dump_p = subparsers.add_parser('dump', help="dump current running VPP configuration (VPP readonly)")
dump_p.add_argument('-o', '--output', dest='outfile', required=False, default='-', type=str, help="""Output file for YAML config, default stdout""")
plan_p = subparsers.add_parser('plan', help="plan changes from current VPP dataplane to target config (VPP readonly)") plan_p = subparsers.add_parser('plan', help="plan changes from current VPP dataplane to target config (VPP readonly)")
plan_p.add_argument('-s', '--schema', dest='schema', type=str, help="""YAML schema validation file, default to use built-in""") plan_p.add_argument('-s', '--schema', dest='schema', type=str, help="""YAML schema validation file, default to use built-in""")
@ -68,7 +69,7 @@ def main():
if not d.readconfig(): if not d.readconfig():
logging.error("Could not retrieve config from VPP") logging.error("Could not retrieve config from VPP")
sys.exit(-7) sys.exit(-7)
d.dump() d.write(args.outfile)
sys.exit(0) sys.exit(0)
try: try: