diff options
-rw-r--r-- | data/config-mode-dependencies/vyos-1x.json | 3 | ||||
-rw-r--r-- | data/templates/grub/grub_compat.j2 | 4 | ||||
-rw-r--r-- | data/templates/grub/grub_options.j2 | 6 | ||||
-rw-r--r-- | data/templates/wifi/hostapd.conf.j2 | 7 | ||||
-rw-r--r-- | interface-definitions/interfaces_bonding.xml.in | 12 | ||||
-rw-r--r-- | op-mode-definitions/system-image.xml.in | 9 | ||||
-rw-r--r-- | python/vyos/ifconfig/bond.py | 38 | ||||
-rw-r--r-- | python/vyos/system/compat.py | 10 | ||||
-rw-r--r-- | python/vyos/utils/assertion.py | 4 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_bonding.py | 28 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_wireless.py | 11 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_bonding.py | 11 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_bridge.py | 31 | ||||
-rwxr-xr-x | src/op_mode/firewall.py | 12 | ||||
-rwxr-xr-x | src/op_mode/image_installer.py | 24 | ||||
-rwxr-xr-x | src/op_mode/image_manager.py | 28 |
16 files changed, 199 insertions, 39 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index afe3dd838..13de434bd 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -11,7 +11,8 @@ "ethernet": ["interfaces_ethernet"] }, "interfaces_bridge": { - "vxlan": ["interfaces_vxlan"] + "vxlan": ["interfaces_vxlan"], + "wlan": ["interfaces_wireless"] }, "load_balancing_wan": { "conntrack": ["system_conntrack"] diff --git a/data/templates/grub/grub_compat.j2 b/data/templates/grub/grub_compat.j2 index d1085eec8..8fb4f71dc 100644 --- a/data/templates/grub/grub_compat.j2 +++ b/data/templates/grub/grub_compat.j2 @@ -14,8 +14,6 @@ KVM {%- elif type == 'ttyS' -%} Serial -{%- elif type == 'ttyUSB' -%} - USB {%- else -%} Unknown {%- endif %} @@ -25,8 +23,6 @@ console=ttyS0,{{ console_speed }} console=tty0 {%- elif type == 'ttyS' -%} console=tty0 console=ttyS0,{{ console_speed }} -{%- elif type == 'ttyUSB' -%} - console=tty0 console=ttyUSB0,115200 {%- else -%} console=tty0 console=ttyS0,{{ console_speed }} {%- endif %} diff --git a/data/templates/grub/grub_options.j2 b/data/templates/grub/grub_options.j2 index c8a1472e1..a00bf4e37 100644 --- a/data/templates/grub/grub_options.j2 +++ b/data/templates/grub/grub_options.j2 @@ -33,12 +33,6 @@ submenu "Boot options" { setup_serial configfile ${prefix}/grub.cfg.d/*vyos-menu*.cfg } - menuentry "ttyUSB (USB serial)" { - set console_type="ttyUSB" - export console_type - setup_serial - configfile ${prefix}/grub.cfg.d/*vyos-menu*.cfg - } } menuentry "Enter console number" { read console_num diff --git a/data/templates/wifi/hostapd.conf.j2 b/data/templates/wifi/hostapd.conf.j2 index 83009242b..769325b49 100644 --- a/data/templates/wifi/hostapd.conf.j2 +++ b/data/templates/wifi/hostapd.conf.j2 @@ -28,6 +28,12 @@ interface={{ ifname }} {% for bridge in is_bridge_member %} bridge={{ bridge }} {% endfor %} + +# WDS (4-address frame) mode with per-station virtual interfaces +# (only supported with driver=nl80211) +# This mode allows associated stations to use 4-address frames to allow layer 2 +# bridging to be used. +wds_sta=1 {% endif %} # Driver interface type (hostap/wired/none/nl80211/bsd); @@ -739,4 +745,3 @@ wmm_ac_vo_cwmin=2 wmm_ac_vo_cwmax=3 wmm_ac_vo_txop_limit=47 wmm_ac_vo_acm=0 - diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in index d503760e6..cc0327f3d 100644 --- a/interface-definitions/interfaces_bonding.xml.in +++ b/interface-definitions/interfaces_bonding.xml.in @@ -171,6 +171,18 @@ </properties> <defaultValue>0</defaultValue> </leafNode> + <leafNode name="system-mac"> + <properties> + <help>System MAC address for 802.3ad</help> + <valueHelp> + <format>macaddr</format> + <description>MAC address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> <leafNode name="lacp-rate"> <properties> <help>Rate in which we will ask our link partner to transmit LACPDU packets</help> diff --git a/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in index 7b5260b4e..44b055be6 100644 --- a/op-mode-definitions/system-image.xml.in +++ b/op-mode-definitions/system-image.xml.in @@ -72,6 +72,15 @@ <help>Set system operational parameters</help> </properties> <children> + <tagNode name="boot-console"> + <properties> + <help>Set system console type at boot</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/image_manager.py --action list_console_types</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action set_console_type --console-type "${4}"</command> + </tagNode> <node name="image"> <properties> <help>Set system image parameters</help> diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index c6d0f1cff..b8ea90049 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -18,6 +18,7 @@ import os from vyos.ifconfig.interface import Interface from vyos.utils.dict import dict_search from vyos.utils.assertion import assert_list +from vyos.utils.assertion import assert_mac from vyos.utils.assertion import assert_positive @Interface.register @@ -54,6 +55,10 @@ class BondIf(Interface): 'validate': lambda v: assert_list(v, ['slow', 'fast']), 'location': '/sys/class/net/{ifname}/bonding/lacp_rate', }, + 'bond_system_mac': { + 'validate': lambda v: assert_mac(v, test_all_zero=False), + 'location': '/sys/class/net/{ifname}/bonding/ad_actor_system', + }, 'bond_miimon': { 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/bonding/miimon' @@ -385,6 +390,24 @@ class BondIf(Interface): """ return self.set_interface('bond_mode', mode) + def set_system_mac(self, mac): + """ + In an AD system, this specifies the mac-address for the actor in + protocol packet exchanges (LACPDUs). The value cannot be NULL or + multicast. It is preferred to have the local-admin bit set for this + mac but driver does not enforce it. If the value is not given then + system defaults to using the masters' mac address as actors' system + address. + + This parameter has effect only in 802.3ad mode and is available through + SysFs interface. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_system_mac('00:50:ab:cd:ef:01') + """ + return self.set_interface('bond_system_mac', mac) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -426,14 +449,13 @@ class BondIf(Interface): Interface(interface).set_admin_state('up') # Bonding policy/mode - default value, always present - mode = config.get('mode') - self.set_mode(mode) + self.set_mode(config['mode']) # LACPDU transmission rate - default value - if mode == '802.3ad': + if config['mode'] == '802.3ad': self.set_lacp_rate(config.get('lacp_rate')) - if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']: + if config['mode'] not in ['802.3ad', 'balance-tlb', 'balance-alb']: tmp = dict_search('arp_monitor.interval', config) value = tmp if (tmp != None) else '0' self.set_arp_interval(value) @@ -468,6 +490,14 @@ class BondIf(Interface): Interface(interface).flush_addrs() self.add_port(interface) + # Add system mac address for 802.3ad - default address is all zero + # mode is always present (defaultValue) + if config['mode'] == '802.3ad': + mac = '00:00:00:00:00:00' + if 'system_mac' in config: + mac = config['system_mac'] + self.set_system_mac(mac) + # Primary device interface - must be set after 'mode' value = config.get('primary') if value: self.set_primary(value) diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 1b487c1d2..d35bddea2 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -220,14 +220,8 @@ def get_default(data: dict, root_dir: str = '') -> Union[int, None]: sublist = list(filter(lambda x: (x.get('version') == image_name and x.get('console_type') == console_type and - x.get('console_num') == console_num and x.get('bootmode') == 'normal'), menu_entries)) - # legacy images added with legacy tools omitted 'ttyUSB'; if entry not - # available, default to initial entry of version - if not sublist: - sublist = list(filter(lambda x: x.get('version') == image_name, - menu_entries)) if sublist: return menu_entries.index(sublist[0]) @@ -253,6 +247,10 @@ def update_version_list(root_dir: str = '') -> list[dict]: menu_entries = parse_menuentries(grub_cfg_main) menu_versions = find_versions(menu_entries) + # remove deprecated console-type ttyUSB + menu_entries = list(filter(lambda x: x.get('console_type') != 'ttyUSB', + menu_entries)) + # get list of versions added/removed by image-tools current_versions = grub.version_list(root_dir) diff --git a/python/vyos/utils/assertion.py b/python/vyos/utils/assertion.py index 1aaa54dff..c7fa220c3 100644 --- a/python/vyos/utils/assertion.py +++ b/python/vyos/utils/assertion.py @@ -53,7 +53,7 @@ def assert_mtu(mtu, ifname): if (max_mtu and cur_mtu > max_mtu) or cur_mtu > 65536: raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} > {max_mtu}') -def assert_mac(m): +def assert_mac(m, test_all_zero=True): split = m.split(':') size = len(split) @@ -74,7 +74,7 @@ def assert_mac(m): raise ValueError(f'{m} is a multicast MAC address') # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(octets) == 0: + if test_all_zero and sum(octets) == 0: raise ValueError('00:00:00:00:00:00 is not a valid MAC address') if octets[:5] == (0, 0, 94, 0, 1): diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 419de774a..f436424b8 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -241,6 +241,34 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): for member in self._members: self.assertIn(member, slaves) + def test_bonding_system_mac(self): + # configure member interfaces and system-mac + default_system_mac = '00:00:00:00:00:00' # default MAC is all zeroes + system_mac = '00:50:ab:cd:ef:11' + + for interface in self._interfaces: + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_set(self._base_path + [interface, 'system-mac', system_mac]) + + self.cli_commit() + + # verify config + for interface in self._interfaces: + tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system') + self.assertIn(tmp, system_mac) + + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'system-mac']) + + self.cli_commit() + + # verify default value + for interface in self._interfaces: + tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system') + self.assertIn(tmp, default_system_mac) + def test_bonding_evpn_multihoming(self): id = '5' for interface in self._interfaces: diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 83b00ac0c..b45754cae 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -236,6 +236,17 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(interface, bridge_members) + # Now generate a VLAN on the bridge + self.cli_set(bridge_path + ['enable-vlan']) + self.cli_set(bridge_path + ['vif', '20', 'address', '10.0.0.1/24']) + + self.cli_commit() + + tmp = get_config_value(interface, 'bridge') + self.assertEqual(tmp, bridge) + tmp = get_config_value(interface, 'wds_sta') + self.assertEqual(tmp, '1') + self.cli_delete(bridge_path) def test_wireless_security_station_address(self): diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 371b219c0..5e5d5fba1 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -33,6 +33,7 @@ from vyos.ifconfig import BondIf from vyos.ifconfig.ethernet import EthernetIf from vyos.ifconfig import Section from vyos.template import render_to_string +from vyos.utils.assertion import assert_mac from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.network import interface_exists @@ -244,6 +245,16 @@ def verify(bond): raise ConfigError('primary interface only works for mode active-backup, ' \ 'transmit-load-balance or adaptive-load-balance') + if 'system_mac' in bond: + if bond['mode'] != '802.3ad': + raise ConfigError('Actor MAC address only available in 802.3ad mode!') + + system_mac = bond['system_mac'] + try: + assert_mac(system_mac, test_all_zero=False) + except: + raise ConfigError(f'Cannot use a multicast MAC address "{system_mac}" as system-mac!') + return None def generate(bond): diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 9789f7bd3..7b2c1ee0b 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -56,6 +56,17 @@ def get_config(config=None): bridge['member'].update({'interface_remove' : tmp }) else: bridge.update({'member' : {'interface_remove' : tmp }}) + for interface in tmp: + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to + # re-create VLAN to VNI mappings if required, but only if the interface + # is already live on the system - this must not be done on first commit + if interface.startswith('vxlan') and interface_exists(interface): + set_dependents('vxlan', conf, interface) + # When using Wireless member interfaces we need to inform hostapd + # to properly set-up the bridge + elif interface.startswith('wlan') and interface_exists(interface): + set_dependents('wlan', conf, interface) if dict_search('member.interface', bridge) is not None: for interface in list(bridge['member']['interface']): @@ -91,6 +102,10 @@ def get_config(config=None): # is already live on the system - this must not be done on first commit if interface.startswith('vxlan') and interface_exists(interface): set_dependents('vxlan', conf, interface) + # When using Wireless member interfaces we need to inform hostapd + # to properly set-up the bridge + elif interface.startswith('wlan') and interface_exists(interface): + set_dependents('wlan', conf, interface) # delete empty dictionary keys - no need to run code paths if nothing is there to do if 'member' in bridge: @@ -140,9 +155,6 @@ def verify(bridge): if 'enable_vlan' in bridge: if 'has_vlan' in interface_config: raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!') - - if 'wlan' in interface: - raise ConfigError(error_msg + 'VLAN aware cannot be set!') else: for option in ['allowed_vlan', 'native_vlan']: if option in interface_config: @@ -168,12 +180,19 @@ def apply(bridge): else: br.update(bridge) - for interface in dict_search('member.interface', bridge) or []: - if interface.startswith('vxlan') and interface_exists(interface): + tmp = [] + if 'member' in bridge: + if 'interface_remove' in bridge['member']: + tmp.extend(bridge['member']['interface_remove']) + if 'interface' in bridge['member']: + tmp.extend(bridge['member']['interface']) + + for interface in tmp: + if interface.startswith(tuple(['vxlan', 'wlan'])) and interface_exists(interface): try: call_dependents() except ConfigError: - raise ConfigError('Error in updating VXLAN interface after changing bridge!') + raise ConfigError('Error updating member interface configuration after changing bridge!') return None diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 442c186cc..15fbb65a2 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -531,9 +531,15 @@ def show_firewall_group(name=None): continue for idx, member in enumerate(members): - val = member.get('val', 'N/D') - timeout = str(member.get('timeout', 'N/D')) - expires = str(member.get('expires', 'N/D')) + if isinstance(member, str): + # Only member, and no timeout: + val = member + timeout = "N/D" + expires = "N/D" + else: + val = member.get('val', 'N/D') + timeout = str(member.get('timeout', 'N/D')) + expires = str(member.get('expires', 'N/D')) if args.detail: row.append(f'{val} (timeout: {timeout}, expires: {expires})') diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index ba0e3b6db..0d2d7076c 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -23,6 +23,8 @@ from shutil import copy, chown, rmtree, copytree from glob import glob from sys import exit from os import environ +from os import readlink +from os import getpid, getppid from typing import Union from urllib.parse import urlparse from passlib.hosts import linux_context @@ -65,7 +67,7 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:' MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:' MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?' MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?' -MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?' +MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial)?' MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?' MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?' MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?' @@ -614,6 +616,20 @@ def copy_ssh_host_keys() -> bool: return False +def console_hint() -> str: + pid = getppid() if 'SUDO_USER' in environ else getpid() + try: + path = readlink(f'/proc/{pid}/fd/1') + except OSError: + path = '/dev/tty' + + name = Path(path).name + if name == 'ttyS0': + return 'S' + else: + return 'K' + + def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None: """Clean up after installation @@ -709,9 +725,9 @@ def install_image() -> None: # ask for default console console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, - default='K', - valid_responses=['K', 'S', 'U']) - console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'} + default=console_hint(), + valid_responses=['K', 'S']) + console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'} config_boot_list = ['/opt/vyatta/etc/config/config.boot', '/opt/vyatta/etc/config.boot.default'] diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index 1cfb5f5a1..fb4286dbc 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -21,7 +21,7 @@ from argparse import ArgumentParser, Namespace from pathlib import Path from shutil import rmtree from sys import exit -from typing import Optional +from typing import Optional, Literal, TypeAlias, get_args from vyos.system import disk, grub, image, compat from vyos.utils.io import ask_yes_no, select_entry @@ -33,6 +33,8 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:' MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first' MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +ConsoleType: TypeAlias = Literal['tty', 'ttyS'] + def annotate_list(images_list: list[str]) -> list[str]: """Annotate list of images with additional info @@ -202,6 +204,15 @@ def rename_image(name_old: str, name_new: str) -> None: exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}') +@compat.grub_cfg_update +def set_console_type(console_type: ConsoleType) -> None: + console_choice = get_args(ConsoleType) + if console_type not in console_choice: + exit(f'console type \'{console_type}\' not available') + + grub.set_console_type(console_type) + + def list_images() -> None: """Print list of available images for CLI hints""" images_list: list[str] = grub.version_list() @@ -209,6 +220,13 @@ def list_images() -> None: print(image_name) +def list_console_types() -> None: + """Print list of console types for CLI hints""" + console_types: list[str] = list(get_args(ConsoleType)) + for console_type in console_types: + print(console_type) + + def parse_arguments() -> Namespace: """Parse arguments @@ -217,7 +235,8 @@ def parse_arguments() -> Namespace: """ parser: ArgumentParser = ArgumentParser(description='Manage system images') parser.add_argument('--action', - choices=['delete', 'set', 'rename', 'list'], + choices=['delete', 'set', 'set_console_type', + 'rename', 'list', 'list_console_types'], required=True, help='action to perform with an image') parser.add_argument('--no-prompt', action='store_true', @@ -227,6 +246,7 @@ def parse_arguments() -> Namespace: help= 'a name of an image to add, delete, install, rename, or set as default') parser.add_argument('--image-new-name', help='a new name for image') + parser.add_argument('--console-type', help='console type for boot') args: Namespace = parser.parse_args() # Validate arguments if args.action == 'rename' and (not args.image_name or @@ -243,10 +263,14 @@ if __name__ == '__main__': delete_image(args.image_name, args.no_prompt) if args.action == 'set': set_image(args.image_name) + if args.action == 'set_console_type': + set_console_type(args.console_type) if args.action == 'rename': rename_image(args.image_name, args.image_new_name) if args.action == 'list': list_images() + if args.action == 'list_console_types': + list_console_types() exit() |