lib.switch
Managed switches of a lab: grading helpers around Grade.test_switch().
A network declared with 'mode': 'managed' in _network_specs is a vde_switch with VLANs
and a management console. get_switch_ports(grade, 'lan') reads its port and VLAN tables and
gives the port of each machine; get_switch_vlans(grade, 'lan') gives the members of each
VLAN. parse_switch_ports / parse_switch_vlans are the pure parsers of the
port/allprint and vlan/allprint outputs (fixtures in tests/mock_data/switch/:
VDE switch 2.3.3 behind the Kathara network plugin, which names each port's endpoint
kathara <machine>:eth<N>).
1"""Managed switches of a lab: grading helpers around ``Grade.test_switch()``. 2 3A network declared with ``'mode': 'managed'`` in ``_network_specs`` is a ``vde_switch`` with VLANs 4and a management console. ``get_switch_ports(grade, 'lan')`` reads its port and VLAN tables and 5gives the port of each machine; ``get_switch_vlans(grade, 'lan')`` gives the members of each 6VLAN. ``parse_switch_ports`` / ``parse_switch_vlans`` are the pure parsers of the 7``port/allprint`` and ``vlan/allprint`` outputs (fixtures in ``tests/mock_data/switch/``: 8VDE switch 2.3.3 behind the Kathara network plugin, which names each port's endpoint 9``kathara <machine>:eth<N>``). 10""" 11import re 12from typing import Any, Dict, List 13 14SWITCH_PORTS_CMD = "port/allprint" 15SWITCH_VLANS_CMD = "vlan/allprint" 16 17_PORT_RE = re.compile(r"^Port (\d+) untagged_vlan=(\d+) (IN)?ACTIVE") 18_ENDPOINT_RE = re.compile(r"^\s*-- endpoint ID \d+ module [^:]*: (.*?)(?: user=\S+ pid=\d+.*)?$") 19_VLAN_RE = re.compile(r"^VLAN (\d+)") 20_VLAN_PORT_RE = re.compile(r"^\s*-- Port (\d+) tagged=(\d)") 21_MACHINE_ENDPOINT_RE = re.compile(r"^kathara (\S+):(eth\d+)$") 22 23 24def parse_switch_vlans(vlans_output: str) -> Dict[int, Dict[str, List[int]]]: 25 """Parse a ``vlan/allprint`` output: ``{vlan: {'untagged': [port, ...], 'tagged': [port, ...]}}``. 26 27 VLAN 0 is the default VLAN of the switch (ports without configuration).""" 28 vlans: Dict[int, Dict[str, List[int]]] = {} 29 current = None 30 for line in (vlans_output or '').splitlines(): 31 m = _VLAN_RE.match(line) 32 if m: 33 current = vlans.setdefault(int(m.group(1)), {'untagged': [], 'tagged': []}) 34 continue 35 m = _VLAN_PORT_RE.match(line) 36 if m and current is not None: 37 current['tagged' if m.group(2) == '1' else 'untagged'].append(int(m.group(1))) 38 return vlans 39 40 41def parse_switch_ports(ports_output: str, vlans_output: str = '') -> Dict[int, Dict[str, Any]]: 42 """Parse a ``port/allprint`` output (and the ``vlan/allprint`` one for the tagged VLANs). 43 44 Returns ``{port number: {...}}`` with ``vlan`` (VLAN of the untagged frames, 0 = default), 45 ``tagged_vlans`` (sorted), ``active`` (something is plugged), ``endpoints`` (descriptions of 46 what is plugged) and, when it is the interface of a machine, ``machine`` and ``interface`` 47 (``'pc1'``, ``'eth0'``; ``None`` otherwise). 48 """ 49 ports: Dict[int, Dict[str, Any]] = {} 50 port = None 51 for line in (ports_output or '').splitlines(): 52 m = _PORT_RE.match(line) 53 if m: 54 port = {'vlan': int(m.group(2)), 'tagged_vlans': [], 'active': not m.group(3), 55 'endpoints': [], 'machine': None, 'interface': None} 56 ports[int(m.group(1))] = port 57 continue 58 m = _ENDPOINT_RE.match(line) 59 if m and port is not None: 60 description = m.group(1).strip() 61 port['endpoints'].append(description) 62 machine = _MACHINE_ENDPOINT_RE.match(description) 63 if machine: 64 port['machine'], port['interface'] = machine.group(1), machine.group(2) 65 for vlan, members in parse_switch_vlans(vlans_output).items(): 66 for number in members['tagged']: 67 if number in ports: 68 ports[number]['tagged_vlans'].append(vlan) 69 for port in ports.values(): 70 port['tagged_vlans'].sort() 71 return ports 72 73 74def ports_by_machine(ports: Dict[int, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: 75 """``{machine: {'port', 'interface', 'vlan', 'tagged_vlans'}}`` from :func:`parse_switch_ports`.""" 76 return {port['machine']: {'port': number, 'interface': port['interface'], 'vlan': port['vlan'], 77 'tagged_vlans': list(port['tagged_vlans'])} 78 for number, port in sorted(ports.items()) if port['machine']} 79 80 81def get_switch_ports(grade, network: str, step: int = 1, allow_error: bool = False) -> Dict[str, Dict[str, Any]]: 82 """Ports of the managed switch *network*, by machine name. 83 84 ``{'pc1': {'port': 2, 'interface': 'eth0', 'vlan': 10, 'tagged_vlans': []}, ...}``: ``vlan`` 85 is the VLAN of the untagged frames (0 = default VLAN of the switch), ``tagged_vlans`` the 86 VLANs of a trunk port. ``{}`` until the tests have run or when the console failed. 87 """ 88 ports_out, ports_code = grade.test_switch(network, SWITCH_PORTS_CMD, step=step, allow_error=allow_error) 89 vlans_out, vlans_code = grade.test_switch(network, SWITCH_VLANS_CMD, step=step, allow_error=allow_error) 90 if ports_code != 0: 91 return {} 92 return ports_by_machine(parse_switch_ports(ports_out, vlans_out if vlans_code == 0 else '')) 93 94 95def get_switch_vlans(grade, network: str, step: int = 1, allow_error: bool = False) -> Dict[int, Dict[str, List[int]]]: 96 """VLANs of the managed switch *network*: ``{vlan: {'untagged': [ports], 'tagged': [ports]}}`` 97 (port numbers, see :func:`get_switch_ports` for the machines). ``{}`` until the tests have 98 run or when the console failed.""" 99 out, code = grade.test_switch(network, SWITCH_VLANS_CMD, step=step, allow_error=allow_error) 100 return parse_switch_vlans(out) if code == 0 else {}
25def parse_switch_vlans(vlans_output: str) -> Dict[int, Dict[str, List[int]]]: 26 """Parse a ``vlan/allprint`` output: ``{vlan: {'untagged': [port, ...], 'tagged': [port, ...]}}``. 27 28 VLAN 0 is the default VLAN of the switch (ports without configuration).""" 29 vlans: Dict[int, Dict[str, List[int]]] = {} 30 current = None 31 for line in (vlans_output or '').splitlines(): 32 m = _VLAN_RE.match(line) 33 if m: 34 current = vlans.setdefault(int(m.group(1)), {'untagged': [], 'tagged': []}) 35 continue 36 m = _VLAN_PORT_RE.match(line) 37 if m and current is not None: 38 current['tagged' if m.group(2) == '1' else 'untagged'].append(int(m.group(1))) 39 return vlans
Parse a vlan/allprint output: {vlan: {'untagged': [port, ...], 'tagged': [port, ...]}}.
VLAN 0 is the default VLAN of the switch (ports without configuration).
42def parse_switch_ports(ports_output: str, vlans_output: str = '') -> Dict[int, Dict[str, Any]]: 43 """Parse a ``port/allprint`` output (and the ``vlan/allprint`` one for the tagged VLANs). 44 45 Returns ``{port number: {...}}`` with ``vlan`` (VLAN of the untagged frames, 0 = default), 46 ``tagged_vlans`` (sorted), ``active`` (something is plugged), ``endpoints`` (descriptions of 47 what is plugged) and, when it is the interface of a machine, ``machine`` and ``interface`` 48 (``'pc1'``, ``'eth0'``; ``None`` otherwise). 49 """ 50 ports: Dict[int, Dict[str, Any]] = {} 51 port = None 52 for line in (ports_output or '').splitlines(): 53 m = _PORT_RE.match(line) 54 if m: 55 port = {'vlan': int(m.group(2)), 'tagged_vlans': [], 'active': not m.group(3), 56 'endpoints': [], 'machine': None, 'interface': None} 57 ports[int(m.group(1))] = port 58 continue 59 m = _ENDPOINT_RE.match(line) 60 if m and port is not None: 61 description = m.group(1).strip() 62 port['endpoints'].append(description) 63 machine = _MACHINE_ENDPOINT_RE.match(description) 64 if machine: 65 port['machine'], port['interface'] = machine.group(1), machine.group(2) 66 for vlan, members in parse_switch_vlans(vlans_output).items(): 67 for number in members['tagged']: 68 if number in ports: 69 ports[number]['tagged_vlans'].append(vlan) 70 for port in ports.values(): 71 port['tagged_vlans'].sort() 72 return ports
Parse a port/allprint output (and the vlan/allprint one for the tagged VLANs).
Returns {port number: {...}} with vlan (VLAN of the untagged frames, 0 = default),
tagged_vlans (sorted), active (something is plugged), endpoints (descriptions of
what is plugged) and, when it is the interface of a machine, machine and interface
('pc1', 'eth0'; None otherwise).
75def ports_by_machine(ports: Dict[int, Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: 76 """``{machine: {'port', 'interface', 'vlan', 'tagged_vlans'}}`` from :func:`parse_switch_ports`.""" 77 return {port['machine']: {'port': number, 'interface': port['interface'], 'vlan': port['vlan'], 78 'tagged_vlans': list(port['tagged_vlans'])} 79 for number, port in sorted(ports.items()) if port['machine']}
{machine: {'port', 'interface', 'vlan', 'tagged_vlans'}} from parse_switch_ports().
82def get_switch_ports(grade, network: str, step: int = 1, allow_error: bool = False) -> Dict[str, Dict[str, Any]]: 83 """Ports of the managed switch *network*, by machine name. 84 85 ``{'pc1': {'port': 2, 'interface': 'eth0', 'vlan': 10, 'tagged_vlans': []}, ...}``: ``vlan`` 86 is the VLAN of the untagged frames (0 = default VLAN of the switch), ``tagged_vlans`` the 87 VLANs of a trunk port. ``{}`` until the tests have run or when the console failed. 88 """ 89 ports_out, ports_code = grade.test_switch(network, SWITCH_PORTS_CMD, step=step, allow_error=allow_error) 90 vlans_out, vlans_code = grade.test_switch(network, SWITCH_VLANS_CMD, step=step, allow_error=allow_error) 91 if ports_code != 0: 92 return {} 93 return ports_by_machine(parse_switch_ports(ports_out, vlans_out if vlans_code == 0 else ''))
Ports of the managed switch network, by machine name.
{'pc1': {'port': 2, 'interface': 'eth0', 'vlan': 10, 'tagged_vlans': []}, ...}: vlan
is the VLAN of the untagged frames (0 = default VLAN of the switch), tagged_vlans the
VLANs of a trunk port. {} until the tests have run or when the console failed.
96def get_switch_vlans(grade, network: str, step: int = 1, allow_error: bool = False) -> Dict[int, Dict[str, List[int]]]: 97 """VLANs of the managed switch *network*: ``{vlan: {'untagged': [ports], 'tagged': [ports]}}`` 98 (port numbers, see :func:`get_switch_ports` for the machines). ``{}`` until the tests have 99 run or when the console failed.""" 100 out, code = grade.test_switch(network, SWITCH_VLANS_CMD, step=step, allow_error=allow_error) 101 return parse_switch_vlans(out) if code == 0 else {}
VLANs of the managed switch network: {vlan: {'untagged': [ports], 'tagged': [ports]}}
(port numbers, see get_switch_ports() for the machines). {} until the tests have
run or when the console failed.