summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/config-mode-dependencies/vyos-1x.json3
-rw-r--r--data/templates/grub/grub_compat.j24
-rw-r--r--data/templates/grub/grub_options.j26
-rw-r--r--data/templates/wifi/hostapd.conf.j27
-rw-r--r--interface-definitions/interfaces_bonding.xml.in12
-rw-r--r--op-mode-definitions/system-image.xml.in9
-rw-r--r--python/vyos/ifconfig/bond.py38
-rw-r--r--python/vyos/system/compat.py10
-rw-r--r--python/vyos/utils/assertion.py4
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py28
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py11
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py11
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py31
-rwxr-xr-xsrc/op_mode/firewall.py12
-rwxr-xr-xsrc/op_mode/image_installer.py24
-rwxr-xr-xsrc/op_mode/image_manager.py28
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()