def _parse(raw_cli_output, cmd, nos): # Boilerplate code to get the parser functional # tb = Testbed() device = Device("new_device", os=nos) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) # User input checking of the command provided. Does the command have a Genie parser? try: get_parser(cmd, device) except Exception as e: raise AnsibleFilterError( "genie_parse: {0} - Available parsers: {1}".format( to_native(e), "https://pubhub.devnetcloud.com/media/pyats-packages/docs/genie/genie_libs/#/parsers" )) try: parsed_output = device.parse(cmd, output=raw_cli_output) return parsed_output except Exception as e: raise AnsibleFilterError( "genie_parse: {0} - Failed to parse command output.".format( to_native(e)))
def genie_parser(self, cli_output, command, os): if not PY3: raise AnsibleFilterError("Genie requires Python 3") if not HAS_GENIE: raise AnsibleFilterError( "Genie not found. Run 'pip install genie'") if not HAS_PYATS: raise AnsibleFilterError( "pyATS not found. Run 'pip install pyats'") device = Device("new_device", os=os) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) try: get_parser(command, device) except Exception as e: raise AnsibleFilterError( "Unable to find parser for command '{0}' ({1})".format( command, e)) try: parsed_output = device.parse(command, output=cli_output) except Exception as e: raise AnsibleFilterError( "Unable to parse output for command '{0}' ({1})".format( command, e)) if parsed_output: return parsed_output else: return None
def get_structured_data_genie(raw_output: str, platform: str, command: str) -> Union[str, Dict[str, Any]]: if not sys.version_info >= (3, 4): raise ValueError("Genie requires Python >= 3.4") if not GENIE_INSTALLED: msg = ( "\nGenie and PyATS are not installed. Please PIP install both Genie and PyATS:\n" "pip install genie\npip install pyats\n") raise ValueError(msg) if "cisco" not in platform: return raw_output genie_device_mapper = { "cisco_ios": "ios", "cisco_xe": "iosxe", "cisco_xr": "iosxr", "cisco_nxos": "nxos", "cisco_asa": "asa", } os = None # platform might be _ssh, _telnet, _serial strip that off if platform.count("_") > 1: base_list = platform.split("_")[:-1] base_platform = "_".join(base_list) else: base_platform = platform os = genie_device_mapper.get(base_platform) if os is None: return raw_output # Genie specific construct for doing parsing (based on Genie in Ansible) device = Device("new_device", os=os) device.custom.setdefault("abstraction", {}) device.custom["abstraction"]["order"] = ["os"] device.cli = AttrDict({"execute": None}) try: # Test whether there is a parser for given command (return Exception if fails) get_parser(command, device) parsed_output: Dict[str, Any] = device.parse(command, output=raw_output) return parsed_output except Exception: return raw_output
def test_disable_no_instance(self): tb = Genie.testbed = Testbed() dev = Device(testbed=tb, name='PE1', os='nxos') rip = Rip(instance_id=1) rip.add_force_vrf(None) dev.add_feature(rip) # Default configuration, let's make sure it works output = rip.build_unconfig(unconfig_feature=True, apply=False) self.assertMultiLineDictEqual(output, {'PE1': 'no feature rip'}) # Set a mock dev.cli = Mock() dev.configure = Mock() output = rip.build_unconfig(unconfig_feature=True, apply=True) expected_output = None self.assertEqual(output, expected_output)
def parse(self, *_args, **_kwargs): """Std entry point for a cli_parse parse execution :return: Errors or parsed text as structured data :rtype: dict :example: The parse function of a parser should return a dict: {"errors": [a list of errors]} or {"parsed": obj} """ errors = self._check_reqs() errors.extend(self._check_vars()) if errors: return {"errors": errors} command = self._task_args.get("parser").get("command") network_os = ( self._task_args.get("parser").get("os") or self._transform_ansible_network_os() ) cli_output = self._task_args.get("text") device = Device("new_device", os=network_os) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) try: parsed = device.parse(command, output=cli_output) except Exception as exc: msg = "The pyats library return an error for '{cmd}' for '{os}'. Error: {err}." return { "errors": [ ( msg.format( cmd=command, os=network_os, err=to_native(exc) ) ) ] } return {"parsed": parsed}
def _parse_generic_tabular(cli_output, os, headers, key_index): # Boilerplate code to get the parser functional tb = Testbed() device = Device("new_device", os=os) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) # Do the parsing # result = parsergen.oper_fill_tabular(device_output=cli_output, device_os=nos, header_ # fields=headers, index=[key]) result = parsergen.oper_fill_tabular(device_output=cli_output, device_os=os, header_fields=headers, index=key_index) # Structured data, but it has a blank entry because of the first line of the output # being blank under the headers. parsed_output = result.entries return parsed_output
def test_uncfg(self): tb = Genie.testbed = Testbed() dev = Device(testbed=tb, name='PE1', os='nxos') rip = Rip(instance_id=1) rip.add_force_vrf(None) # Default configuration, let's make sure it works output = rip.build_unconfig(apply=False) # There was nothing to unconfigure self.assertMultiLineDictEqual(output, {}) dev.add_feature(rip) output = rip.build_unconfig(apply=False) self.assertMultiLineDictEqual(output, {'PE1': 'feature rip\nno router rip 1'}) # Set a mock dev.cli = Mock() dev.configure = Mock() output = rip.build_unconfig(apply=True) expected_output = None self.assertEqual(output, expected_output)
def pyats_parser(cli_output, command, os): if not PY3: raise AnsibleFilterError("Genie requires Python 3") if GENIE_IMPORT_ERROR: raise_from( AnsibleError('genie must be installed to use this plugin'), GENIE_IMPORT_ERROR) if PYATS_IMPORT_ERROR: raise_from( AnsibleError('pyats must be installed to use this plugin'), PYATS_IMPORT_ERROR) # Translate from ansible_network_os values to pyATS if os in ansible_os_map.keys(): os = ansible_os_map[os] device = Device("uut", os=os) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) try: get_parser(command, device) except Exception as e: raise AnsibleFilterError("Unable to find parser for command '{0}' ({1})".format(command, e)) try: parsed_output = device.parse(command, output=cli_output) except Exception as e: raise AnsibleFilterError("Unable to parse output for command '{0}' ({1})".format(command, e)) if parsed_output: return parsed_output else: return None
def main(): argument_spec = dict(command=dict(type='str', required=True), prompt=dict(type='list', required=False), answer=dict(type='list', required=False), compare=dict(type='dict', required=False), sendonly=dict(type='bool', default=False, required=False), # newline=dict(type='bool', default=True, required=False), # check_all=dict(type='bool', default=False, required=False), ) required_together = [['prompt', 'answer']] module = AnsibleModule(argument_spec=argument_spec, required_together=required_together, supports_check_mode=True) if not PY3: module.fail_json(msg="pyATS/Genie requires Python 3") if not HAS_GENIE: module.fail_json(msg="Genie not found. Run 'pip install genie'") if not HAS_PYATS: module.fail_json(msg="pyATS not found. Run 'pip install pyats'") if module.check_mode and not module.params['command'].startswith('show'): module.fail_json( msg='Only show commands are supported when using check_mode, not ' 'executing %s' % module.params['command'] ) warnings = list() result = {'changed': False, 'warnings': warnings} connection = Connection(module._socket_path) capabilities = json.loads(connection.get_capabilities()) if capabilities['device_info']['network_os'] == 'ios': genie_os = 'iosxe' else: genie_os = capabilities['device_info']['network_os'] compare = module.params.pop('compare') response = '' try: response = connection.get(**module.params) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) device = Device("uut", os=genie_os) device.custom.setdefault("abstraction", {})["order"] = ["os"] device.cli = AttrDict({"execute": None}) try: get_parser(module.params['command'], device) except Exception as e: module.fail_json(msg="Unable to find parser for command '{0}' ({1})".format(module.params['command'], e)) try: parsed_output = device.parse(module.params['command'], output=response) except Exception as e: module.fail_json(msg="Unable to parse output for command '{0}' ({1})".format(module.params['command'], e)) # import sys; # sys.stdin = open('/dev/tty') # import pdb; # pdb.set_trace() if compare: diff = Diff(parsed_output, compare, exclude=get_parser_exclude(module.params['command'], device)) diff.findDiff() else: diff = None if not module.params['sendonly']: try: result['json'] = module.from_json(response) except ValueError: pass result.update({ 'stdout': response, 'structured': parsed_output, 'diff': "{0}".format(diff), 'exclude': get_parser_exclude(module.params['command'], device), }) module.exit_json(**result)
def test_cfg(self): tb = Genie.testbed = Testbed() dev = Device(testbed=tb, name='PE1', os='nxos') rip = Rip(instance_id=1) rip.add_force_vrf(None) dev.add_feature(rip) rip.device_attr['PE1'] output = rip.build_config(apply=False) self.assertMultiLineDictEqual(output, {'PE1': 'feature rip\n' 'router rip 1\n' ' address-family ipv4 unicast\n' ' exit\n' ' exit' }) vrf1 = Vrf('vrf1') intf1 = Interface(device=dev, name='Ethernet0/0', vrf=vrf1) intf1.add_feature(rip) rip.address_families |= {AddressFamily.ipv6_unicast} rip.shutdown = False rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].maximum_paths = 2 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].default_metric = 1 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].distance = 120 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_direct_rmap\ = 'rmap1' rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_static_rmap\ = 'rmap2' rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_lisp_rmap\ = 'rmap3' rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].maximum_paths = 7 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].default_metric = 3 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].distance = 120 rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_direct_rmap\ = 'rmap4' rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_static_rmap\ = 'rmap5' rip.device_attr['PE1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_lisp_rmap\ = 'rmap6' rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ maximum_paths = 10 rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ default_metric = 7 rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ distance = 127 rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ redistribute_direct_rmap = 'rmap14' rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ redistribute_static_rmap = 'rmap15' rip.device_attr['PE1'].vrf_attr['vrf1'].address_family_attr['ipv6 unicast'].\ redistribute_lisp_rmap = 'rmap16' # rip.build_config(apply=False) output = rip.build_config(apply=False) expected_output = {'PE1': '''\ router rip 1 no shutdown address-family ipv4 unicast default-metric 1 distance 120 maximum-paths 2 redistribute lisp route-map rmap3 redistribute direct route-map rmap1 redistribute static route-map rmap2 exit address-family ipv6 unicast default-metric 3 distance 120 maximum-paths 7 redistribute lisp route-map rmap6 redistribute direct route-map rmap4 redistribute static route-map rmap5 exit vrf vrf1 address-family ipv4 unicast exit address-family ipv6 unicast default-metric 7 distance 127 maximum-paths 10 redistribute lisp route-map rmap16 redistribute direct route-map rmap14 redistribute static route-map rmap15 exit exit exit'''} self.maxDiff = None self.assertMultiLineDictEqual(output, expected_output) # Set a mock dev.cli = Mock() dev.configure = Mock() dev.add_feature(rip) # Mock config output = rip.build_config(apply=True)
def test_multi_device_configuration(self): tb = Genie.testbed = Testbed() dev1 = Device(testbed=tb, name='dev1', os='nxos') dev2 = Device(testbed=tb, name='dev2', os='nxos') rip = Rip(instance_id=1) rip.add_force_vrf(None) dev1.cli = Mock() dev1.configure = Mock() dev2.cli = Mock() dev2.configure = Mock() dev1.add_feature(rip) dev2.add_feature(rip) # Default configuration, let's make sure it works output = rip.build_config(apply=False) self.assertMultiLineDictEqual(output, { 'dev1': 'feature rip\n' 'router rip 1\n' ' address-family ipv4 unicast\n' ' exit\n' ' exit', 'dev2': 'feature rip\n' 'router rip 1\n' ' address-family ipv4 unicast\n' ' exit\n' ' exit'}) rip.address_families |= {AddressFamily.ipv6_unicast} rip.shutdown = True rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].maximum_paths = 2 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].default_metric = 1 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].distance = 120 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_direct_rmap\ = 'rmap1' rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_static_rmap\ = 'rmap2' rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_lisp_rmap\ = 'rmap3' rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].maximum_paths = 7 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].default_metric = 3 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].distance = 120 rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_direct_rmap\ = 'rmap4' rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_static_rmap\ = 'rmap5' rip.device_attr['dev1'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_lisp_rmap\ = 'rmap6' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].maximum_paths = 4 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].default_metric = 3 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].distance = 122 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_direct_rmap\ = 'rmap_direct' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_static_rmap\ = 'rmap_static' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv4 unicast'].redistribute_lisp_rmap\ = 'rmap_lisp' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].maximum_paths = 7 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].default_metric = 3 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].distance = 120 rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_direct_rmap\ = 'rmap_direct_ipv6' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_static_rmap\ = 'rmap_static_ipv6' rip.device_attr['dev2'].vrf_attr[None].address_family_attr['ipv6 unicast'].redistribute_lisp_rmap\ = 'rmap_lisp_ipv6' output = rip.build_config(apply=False) expected_output = {'dev1': '''\ router rip 1 shutdown address-family ipv4 unicast default-metric 1 distance 120 maximum-paths 2 redistribute lisp route-map rmap3 redistribute direct route-map rmap1 redistribute static route-map rmap2 exit address-family ipv6 unicast default-metric 3 distance 120 maximum-paths 7 redistribute lisp route-map rmap6 redistribute direct route-map rmap4 redistribute static route-map rmap5 exit exit''', 'dev2': '''\ router rip 1 shutdown address-family ipv4 unicast default-metric 3 distance 122 maximum-paths 4 redistribute lisp route-map rmap_lisp redistribute direct route-map rmap_direct redistribute static route-map rmap_static exit address-family ipv6 unicast default-metric 3 distance 120 maximum-paths 7 redistribute lisp route-map rmap_lisp_ipv6 redistribute direct route-map rmap_direct_ipv6 redistribute static route-map rmap_static_ipv6 exit exit'''} self.maxDiff = None self.assertMultiLineDictEqual(output, expected_output) output = rip.build_config(apply=True)
def test_basic_uncfg(self): testbed = Genie.testbed = Testbed() dev1 = Device(testbed=testbed, name='PE1', os='nxos') dev2 = Device(testbed=testbed, name='PE2', os='nxos') intf1 = Interface(name='Ethernet0/0/1', device=dev1, layer=Layer.L3) intf2 = Interface(name='Ethernet0/0/2', device=dev2, layer=Layer.L2) link = Link(name='1_2_1', testbed=testbed) link.connect_interface(interface=intf1) link.connect_interface(interface=intf2) vlan = Vlan() link.add_feature(vlan) vlan.vlan_id = 100 access_map_id = 'ed' vlan_configuration_id = '3' vlan.device_attr[dev1] vlan.device_attr[dev2] vlan.device_attr[dev1].access_map_attr[access_map_id] vlan.device_attr[dev2].access_map_attr[access_map_id] vlan.device_attr[dev1].interface_attr[intf1] vlan.device_attr[dev2].interface_attr[intf2] vlan.device_attr[dev2].interface_attr[intf2].switchport_mode = \ L2_type.TRUNK vlan.device_attr[dev2].interface_attr[intf2].sw_trunk_allowed_vlan = \ '200-201' vlan.device_attr[dev1].vlan_configuration_attr[vlan_configuration_id] vlan.device_attr[dev2].vlan_configuration_attr[vlan_configuration_id] # Defining attributes section vlan.shutdown = False with self.assertRaises(ValueError): vlan.media = 'invalid' vlan.media = 'enet' self.assertIs(type(vlan.media), Vlan.Media) vlan.egress_port_channel_load_balance_random = True vlan.device_attr[dev1].access_map_action = 'drop' vlan.datalink_flow_monitor = True # Unconfig testing # Set a mock dev1.cli = Mock() dev1.configure = Mock() dev2.cli = Mock() dev2.configure = Mock() dev1.add_feature(vlan) dev2.add_feature(vlan) # Mock config output = vlan.build_config(apply=True) uncfg1 = vlan.build_unconfig(apply=False) self.assertCountEqual(uncfg1.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(uncfg1['PE1']), '\n'.join([ 'no vlan 100', 'no vlan access-map ed', 'no vlan configuration 3', ])) self.assertMultiLineEqual( str(uncfg1['PE2']), '\n'.join([ 'no vlan 100', 'no vlan access-map ed', 'no vlan configuration 3', 'interface Ethernet0/0/2', ' no switchport mode trunk', ' no switchport trunk allowed vlan 200-201', ' exit', ])) partial_uncfg1 = vlan.build_unconfig(apply=False, attributes={'device_attr': \ {'*': "media"}}) self.assertCountEqual(partial_uncfg1.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(partial_uncfg1['PE1']), '\n'.join([ 'vlan 100', ' no media enet', ' exit', ])) partial_unconfigure = vlan.build_unconfig(apply=False, attributes={'device_attr': \ {'*': {'access_map_attr': \ {'*': "access_map_action"}}}}) self.assertCountEqual(partial_unconfigure.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(partial_unconfigure['PE1']), '\n'.join([ 'vlan access-map ed', ' no action drop', ' exit', ])) all_vlan_interface_uncfg = vlan.build_unconfig(apply=False, attributes={ \ 'device_attr': {'*': { \ 'interface_attr': '*'}}}) self.assertCountEqual(all_vlan_interface_uncfg.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(all_vlan_interface_uncfg['PE2']), '\n'.join([ 'interface Ethernet0/0/2', ' no switchport mode trunk', ' no switchport trunk allowed vlan 200-201', ' exit', ])) partial_vlan_interface_uncfg = vlan.build_unconfig(apply=False, attributes={ \ 'device_attr': {'*': \ {'interface_attr': \ { '*': "sw_trunk_allowed_vlan"}}}}) self.assertCountEqual(partial_vlan_interface_uncfg.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(partial_vlan_interface_uncfg['PE2']), '\n'.join([ 'interface Ethernet0/0/2', ' no switchport trunk allowed vlan 200-201', ' exit', ]))
def test_basic_uncfg(self): testbed = Genie.testbed = Testbed() dev1 = Device(testbed=testbed, name='PE1', os='iosxr') dev2 = Device(testbed=testbed, name='PE2', os='iosxr') intf1 = Interface(name='GigabitEthernet0/0/1', device=dev1) intf2 = Interface(name='GigabitEthernet0/0/2', device=dev2) intf3 = Interface(name='GigabitEthernet0/0/3', device=dev1) link = Link(name='1_2_1', testbed=testbed) link.connect_interface(interface=intf1) link.connect_interface(interface=intf2) vlan = Vlan() link.add_feature(vlan) # Defining attributes section vlan.device_attr[dev1] vlan.device_attr[dev2] vlan.device_attr[dev1].interface_attr[intf1] vlan.device_attr[dev1].interface_attr[intf1].eth_encap_type1 = 'dot1q' vlan.device_attr[dev1].interface_attr[intf1].eth_encap_val1 = 2 vlan.device_attr[dev1].interface_attr[ intf1].eth_encap_type2 = 'second-dot1q' vlan.device_attr[dev1].interface_attr[intf1].eth_encap_val2 = 5 # Unconfig testing # Set a mock dev1.cli = Mock() dev1.configure = Mock() dev2.cli = Mock() dev2.configure = Mock() dev1.add_feature(vlan) dev2.add_feature(vlan) # Mock config output = vlan.build_config(apply=True) uncfg = vlan.build_unconfig(apply=False) self.assertMultiLineEqual( str(uncfg['PE1']), '\n'.join([ 'interface GigabitEthernet0/0/1', ' no encapsulation dot1q 2 second-dot1q 5', ' exit', ])) all_vlan_interface_uncfg = vlan.build_unconfig(apply=False, attributes={'device_attr':\ {'*':{'interface_attr':'*'}}}) self.assertCountEqual(all_vlan_interface_uncfg.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(all_vlan_interface_uncfg['PE1']), '\n'.join([ 'interface GigabitEthernet0/0/1', ' no encapsulation dot1q 2 second-dot1q 5', ' exit', ])) partial_vlan_interface_uncfg = vlan.build_unconfig(apply=False, attributes={'device_attr':\ {'*':{'interface_attr':\ {'*':"eth_encap_type1"}}}}) self.assertCountEqual(partial_vlan_interface_uncfg.keys(), ['PE1', 'PE2']) self.assertMultiLineEqual( str(partial_vlan_interface_uncfg['PE1']), '\n'.join([ 'interface GigabitEthernet0/0/1', ' no encapsulation dot1q 2 second-dot1q 5', ' exit', ]))