diff --git a/docs/user-guide.md b/docs/user-guide.md index 190589e..813edcb 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -110,15 +110,20 @@ fully valid! For a full write up of the syntax and semantic validation, see ### vppcfg dump 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 should be safe to run. -There are no flags to the dump command. It will return 0 if the connection to VPP was -established and its state successfully dumped to the logs, and non-zero otherwise. +If the flag `-o/--output` is given, the resulting YAML is written to that filename, but +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 -`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" @@ -132,23 +137,47 @@ DBGvpp# set interface ip address GigabitEthernet3/0/0.100 2001:db8:1::1/64 DBGvpp# create bridge-domain 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.dump_phys: GigabitEthernet3/0/0 idx=1 -[INFO ] vppcfg.vppapi.dump_phys: GigabitEthernet3/0/1 idx=2 -[INFO ] vppcfg.vppapi.dump_phys: HundredGigabitEthernet12/0/0 idx=3 -[INFO ] vppcfg.vppapi.dump_phys: HundredGigabitEthernet12/0/1 idx=4 -[INFO ] vppcfg.vppapi.dump_interfaces: local0 idx=0 type=local mac=00:00:00:00:00:00 mtu=0 flags=0 -[INFO ] vppcfg.vppapi.dump_interfaces: GigabitEthernet3/0/0 idx=1 type=dpdk mac=00:25:90:0c:05:00 mtu=9000 flags=2 -[INFO ] vppcfg.vppapi.dump_interfaces: GigabitEthernet3/0/1 idx=2 type=dpdk mac=00:25:90:0c:05:01 mtu=9000 flags=2 -[INFO ] vppcfg.vppapi.dump_interfaces: HundredGigabitEthernet12/0/0 idx=3 type=dpdk mac=b4:96:91:b3:b1:10 mtu=8996 flags=0 -[INFO ] vppcfg.vppapi.dump_interfaces: HundredGigabitEthernet12/0/1 idx=4 type=dpdk mac=b4:96:91:b3:b1:11 mtu=8996 flags=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 -[INFO ] vppcfg.vppapi.dump_interfaces: Encapsulation: dot1q 100 exact-match -[INFO ] vppcfg.vppapi.dump_interfaces: L3: 2001:db8:1::1/64 -[INFO ] vppcfg.vppapi.dump_subints: GigabitEthernet3/0/0.100 tags=1 idx=5 encap=dot1q 100 exact-match -[INFO ] vppcfg.vppapi.dump_bridgedomains: BridgeDomain10 -[INFO ] vppcfg.vppapi.dump_bridgedomains: Members: HundredGigabitEthernet12/0/0 +[INFO ] vppcfg.vppapi.write: Wrote YAML config to vpp.yaml + +$ cat vpp.yaml +bondethernets: {} +bridgedomains: + bd10: + description: '' + interfaces: + - HundredGigabitEthernet12/0/0 + mtu: 8996 +interfaces: + GigabitEthernet3/0/0: + description: '' + mtu: 9000 + 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 diff --git a/vpp/vppapi.py b/vpp/vppapi.py index 729c329..d019439 100644 --- a/vpp/vppapi.py +++ b/vpp/vppapi.py @@ -5,9 +5,11 @@ interface metadata. from vpp_papi import VPPApiClient import os +import sys import fnmatch import logging import socket +import yaml class VPPApi(): def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'): @@ -211,18 +213,6 @@ class VPPApi(): self.cache_read = True 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): """ Return True if all interfaces in the `ifname_list` exist as physical interface names in VPP. Return False otherwise.""" @@ -271,73 +261,107 @@ class VPPApiDumper(VPPApi): def __init__(self, address='/run/vpp/api.sock', clientname='vppcfg'): VPPApi.__init__(self, address, clientname) - def dump(self): - self.dump_phys() - self.dump_interfaces() - self.dump_subints() - self.dump_bridgedomains() + def write(self, outfile): + if outfile and outfile == '-': + fh = sys.stdout + outfile = "(stdout)" + else: + fh = open(outfile, 'w') - def dump_phys(self): - 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)) + config = self.cache_to_config() - def dump_subints(self): - 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))) + print(yaml.dump(config), file=fh) - subints = self.get_dot1x_interfaces() - for ifname in subints: - iface = self.cache['interface_names'][ifname] - self.logger.info("%s tags=1 idx=%d encap=%s" % (iface.interface_name, iface.sw_if_index, self.get_encapsulation(iface))) + if fh is not sys.stdout: + fh.close() + self.logger.info("Wrote YAML config to %s" % (outfile)) - def dump_bridgedomains(self): - for bd_id, bridge in self.cache['bridgedomains'].items(): - self.logger.info("BridgeDomain%d" % (bridge.bd_id)) - if bridge.bvi_sw_if_index > 0 and bridge.bvi_sw_if_index < 2**32-1 : - self.logger.info(" BVI: " + self.cache['interfaces'][bridge.bvi_sw_if_index].interface_name) - + def cache_to_config(self): + config = {"loopbacks": {}, "bondethernets": {}, "interfaces": {}, "bridgedomains": {}, "vxlan_tunnels": {} } + for idx, iface in self.cache['bondethernets'].items(): + bond = {"description": ""} + 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 + + for numtags in [ 0, 1, 2 ]: + for idx, iface in self.cache['interfaces'].items(): + if iface.sub_number_of_tags != numtags: + continue + + if iface.interface_dev_type=='Loopback': + if iface.sub_id > 0: + 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']: + loop['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: + 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']: + l2xc = self.cache['l2xcs'][iface.sw_if_index] + 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 + + encap = {} + if iface.sub_if_flags&8: + 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 + + 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 bridge.sw_if_details: + 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: - self.logger.info(" Members: " + ' '.join(members)) - - def dump_interfaces(self): - 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, - iface.sw_if_index, iface.interface_dev_type, iface.l2_address, - 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.sub_id > 0: - self.logger.info(" Encapsulation: %s" % (self.get_encapsulation(iface))) - - if iface.sw_if_index in self.cache['lcps']: - lcp = self.cache['lcps'][iface.sw_if_index] - tap_name = self.cache['interfaces'][lcp.host_sw_if_index].interface_name - 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: - self.logger.info(" L3: %s" % ' '.join(self.cache['interface_addresses'][iface.sw_if_index])) - - if iface.sw_if_index in self.cache['l2xcs']: - l2xc = self.cache['l2xcs'][iface.sw_if_index] - self.logger.info(" L2XC: %s" % self.cache['interfaces'][l2xc.tx_sw_if_index].interface_name) - - for bd_id, bridge in self.cache['bridgedomains'].items(): - if bridge.bvi_sw_if_index == iface.sw_if_index: - self.logger.info(" BVI: BridgeDomain%d" % (bd_id)) - - pass + bridge['interfaces'] = members + bridge['mtu'] = mtu + config['bridgedomains'][bridge_name] = bridge + + return config diff --git a/vppcfg b/vppcfg index 4d94a4b..1c45fef 100755 --- a/vppcfg +++ b/vppcfg @@ -40,6 +40,7 @@ def main(): 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.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.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(): logging.error("Could not retrieve config from VPP") sys.exit(-7) - d.dump() + d.write(args.outfile) sys.exit(0) try: