summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/firewall.py9
-rwxr-xr-xsrc/conf_mode/nat.py18
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py6
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py8
-rwxr-xr-xsrc/conf_mode/system_option.py2
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py21
-rwxr-xr-xsrc/helpers/vyos-config-encrypt.py276
-rwxr-xr-xsrc/init/vyos-router71
-rwxr-xr-xsrc/migration-scripts/dhcp-server/6-to-769
-rwxr-xr-xsrc/migration-scripts/dhcp-server/7-to-866
-rwxr-xr-xsrc/migration-scripts/dhcp-server/8-to-942
-rwxr-xr-xsrc/migration-scripts/dhcp-server/9-to-1075
-rwxr-xr-xsrc/op_mode/image_installer.py79
-rwxr-xr-xsrc/op_mode/image_manager.py21
-rwxr-xr-xsrc/services/vyos-http-api-server3
15 files changed, 645 insertions, 121 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index acb7dfa41..3c27655b0 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -282,6 +282,15 @@ def verify_rule(firewall, rule_conf, ipv6):
if direction in rule_conf:
if 'name' in rule_conf[direction] and 'group' in rule_conf[direction]:
raise ConfigError(f'Cannot specify both interface group and interface name for {direction}')
+ if 'group' in rule_conf[direction]:
+ group_name = rule_conf[direction]['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(firewall, 'group', 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on firewall rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 26822b755..b3f38c04a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -153,6 +153,15 @@ def verify(nat):
elif 'name' in config['outbound_interface']:
if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["outbound_interface"]["name"]}" for source NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['outbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on source nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config):
if 'exclude' not in config and 'backend' not in config['load_balance']:
@@ -177,6 +186,15 @@ def verify(nat):
elif 'name' in config['inbound_interface']:
if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["inbound_interface"]["name"]}" for destination NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['inbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on destination nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:
if 'exclude' not in config and 'backend' not in config['load_balance']:
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 34cf49286..695842795 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -91,6 +91,8 @@ def get_config(config=None):
for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
+ if not bool(default_values['redistribute']):
+ del default_values['redistribute']
for interface in ospf.get('interface', []):
# We need to reload the defaults on every pass b/c of
@@ -213,7 +215,7 @@ def verify(ospf):
raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\
f'and no-php-flag configured at the same time.')
- # Check for index ranges being larger than the segment routing global block
+ # Check for index ranges being larger than the segment routing global block
if dict_search('segment_routing.global_block', ospf):
g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf)
g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf)
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 5b1adce30..afd767dbf 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -85,6 +85,12 @@ def get_config(config=None):
if 'graceful_restart' not in ospfv3:
del default_values['graceful_restart']
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']:
+ if dict_search(f'redistribute.{protocol}', ospfv3) is None:
+ del default_values['redistribute'][protocol]
+ if not bool(default_values['redistribute']):
+ del default_values['redistribute']
+
default_values.pop('interface', {})
# merge in remaining default values
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index 3b5b67437..7ed451e16 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -92,6 +92,8 @@ def generate(options):
if 'kernel' in options:
if 'disable_mitigations' in options['kernel']:
cmdline_options.append('mitigations=off')
+ if 'disable_power_saving' in options['kernel']:
+ cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1')
grub_util.update_kernel_cmdline_options(' '.join(cmdline_options))
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index d074ed159..388f2a709 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -25,6 +25,8 @@ from time import time
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
@@ -97,6 +99,9 @@ def get_config(config=None):
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
+ if ipsec['nhrp_exists']:
+ set_dependents('nhrp', conf)
+
tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True)
@@ -575,13 +580,6 @@ def generate(ipsec):
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
-def resync_nhrp(ipsec):
- if ipsec and not ipsec['nhrp_exists']:
- return
-
- tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py')
- if tmp > 0:
- print('ERROR: failed to reapply NHRP settings!')
def apply(ipsec):
systemd_service = 'strongswan.service'
@@ -590,7 +588,14 @@ def apply(ipsec):
else:
call(f'systemctl reload-or-restart {systemd_service}')
- resync_nhrp(ipsec)
+ if ipsec.get('nhrp_exists', False):
+ try:
+ call_dependents()
+ except ConfigError:
+ # Ignore config errors on dependent due to being called too early. Example:
+ # ConfigError("ConfigError('Interface ethN requires an IP address!')")
+ pass
+
if __name__ == '__main__':
try:
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
new file mode 100755
index 000000000..8f7359767
--- /dev/null
+++ b/src/helpers/vyos-config-encrypt.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import shutil
+import sys
+
+from argparse import ArgumentParser
+from cryptography.fernet import Fernet
+from tempfile import NamedTemporaryFile
+from tempfile import TemporaryDirectory
+
+from vyos.tpm import clear_tpm_key
+from vyos.tpm import init_tpm
+from vyos.tpm import read_tpm_key
+from vyos.tpm import write_tpm_key
+from vyos.util import ask_input
+from vyos.util import ask_yes_no
+from vyos.util import cmd
+
+persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
+mount_paths = ['/config', '/opt/vyatta/etc/config']
+dm_device = '/dev/mapper/vyos_config'
+
+def is_opened():
+ return os.path.exists(dm_device)
+
+def get_current_image():
+ with open('/proc/cmdline', 'r') as f:
+ args = f.read().split(" ")
+ for arg in args:
+ if 'vyos-union' in arg:
+ k, v = arg.split("=")
+ path_split = v.split("/")
+ return path_split[-1]
+ return None
+
+def load_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ if is_opened():
+ print('Encrypted config volume is already mounted')
+ return
+
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp -R vyattacfg {path}')
+
+ os.unlink(key_file)
+
+ return True
+
+def encrypt_config(key, recovery_key):
+ if is_opened():
+ raise Exception('An encrypted config volume is already mapped')
+
+ # Clear and write key to TPM
+ try:
+ clear_tpm_key()
+ except:
+ pass
+ write_tpm_key(key)
+
+ persist_path = cmd(persistpath_cmd).strip()
+ size = ask_input('Enter size of encrypted config partition (MB): ', numeric_only=True, default=512)
+
+ luks_folder = os.path.join(persist_path, 'luks')
+
+ if not os.path.isdir(luks_folder):
+ os.mkdir(luks_folder)
+
+ image_name = get_current_image()
+ image_path = os.path.join(luks_folder, image_name)
+
+ # Create file for encrypted config
+ cmd(f'fallocate -l {size}M {image_path}')
+
+ # Write TPM key for slot #1
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ # Format and add main key to volume
+ cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
+
+ if recovery_key:
+ # Write recovery key for slot 2
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(recovery_key)
+ recovery_key_file = f.name
+
+ cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
+
+ # Open encrypted volume and format with ext4
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+ cmd('mkfs.ext4 /dev/mapper/vyos_config')
+
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move /config to encrypted volume
+ shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
+
+ cmd(f'umount {d}')
+
+ os.unlink(key_file)
+
+ if recovery_key:
+ os.unlink(recovery_key_file)
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp vyattacfg {path}')
+
+ return True
+
+def decrypt_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ key_file = None
+
+ if not is_opened():
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ # unmount encrypted volume mount points
+ for path in mount_paths:
+ if os.path.ismount(path):
+ cmd(f'umount {path}')
+
+ # If /config is populated, move to /config.old
+ if len(os.listdir('/config')) > 0:
+ print('Moving existing /config folder to /config.old')
+ shutil.move('/config', '/config.old')
+
+ # Temporarily mount encrypted volume and migrate files to /config on rootfs
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move encrypted volume to /config
+ shutil.copytree(d, '/config', copy_function=shutil.move, dirs_exist_ok=True)
+ cmd(f'chgrp -R vyattacfg /config')
+
+ cmd(f'umount {d}')
+
+ # Close encrypted volume
+ cmd('cryptsetup -q close vyos_config')
+
+ # Remove encrypted volume image file and key
+ if key_file:
+ os.unlink(key_file)
+ os.unlink(image_path)
+
+ try:
+ clear_tpm_key()
+ except:
+ pass
+
+ return True
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify action.")
+ sys.exit(1)
+
+ parser = ArgumentParser(description='Config encryption')
+ parser.add_argument('--disable', help='Disable encryption', action="store_true")
+ parser.add_argument('--enable', help='Enable encryption', action="store_true")
+ parser.add_argument('--load', help='Load encrypted config volume', action="store_true")
+ args = parser.parse_args()
+
+ tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
+
+ key = None
+ recovery_key = None
+ need_recovery = False
+
+ question_key_str = 'recovery key' if tpm_exists else 'key'
+
+ if tpm_exists:
+ if args.enable:
+ key = Fernet.generate_key()
+ elif args.disable or args.load:
+ try:
+ key = read_tpm_key()
+ need_recovery = False
+ except:
+ print('Failed to read key from TPM, recovery key required')
+ need_recovery = True
+ else:
+ need_recovery = True
+
+ if args.enable and not tpm_exists:
+ print('WARNING: VyOS will boot into a default config when encrypted without a TPM')
+ print('You will need to manually login with default credentials and use "encryption load"')
+ print('to mount the encrypted volume and use "load /config/config.boot"')
+
+ if not ask_yes_no('Are you sure you want to proceed?'):
+ sys.exit(0)
+
+ if need_recovery or (args.enable and not ask_yes_no(f'Automatically generate a {question_key_str}?', default=True)):
+ while True:
+ recovery_key = ask_input(f'Enter {question_key_str}:', default=None).encode()
+
+ if len(recovery_key) >= 32:
+ break
+
+ print('Invalid key - must be at least 32 characters, try again.')
+ else:
+ recovery_key = Fernet.generate_key()
+
+ try:
+ if args.disable:
+ decrypt_config(key or recovery_key)
+
+ print('Encrypted config volume has been disabled')
+ print('Contents have been migrated to /config on rootfs')
+ elif args.load:
+ load_config(key or recovery_key)
+
+ print('Encrypted config volume has been mounted')
+ print('Use "load /config/config.boot" to load configuration')
+ elif args.enable and tpm_exists:
+ encrypt_config(key, recovery_key)
+
+ print('Encrypted config volume has been enabled with TPM')
+ print('Backup the recovery key in a safe place!')
+ print('Recovery key: ' + recovery_key.decode())
+ elif args.enable:
+ encrypt_config(recovery_key)
+
+ print('Encrypted config volume has been enabled without TPM')
+ print('Backup the key in a safe place!')
+ print('Key: ' + recovery_key.decode())
+ except Exception as e:
+ word = 'decrypt' if args.disable or args.load else 'encrypt'
+ print(f'Failed to {word} config: {e}')
diff --git a/src/init/vyos-router b/src/init/vyos-router
index eac3e7e47..adf892371 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -64,6 +64,69 @@ disabled () {
grep -q -w no-vyos-$1 /proc/cmdline
}
+# Load encrypted config volume
+mount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ vyos_tpm_key=$(python3 -c 'from vyos.tpm import read_tpm_key; print(read_tpm_key().decode())' 2>/dev/null)
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to fetch encryption key from TPM. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ echo $vyos_tpm_key | tr -d '\r\n' | cryptsetup open $persist_path/luks/$image_name vyos_config --key-file=-
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to decrypt config volume. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ mount /dev/mapper/vyos_config /config
+ mount /dev/mapper/vyos_config $vyatta_sysconfdir/config
+
+ echo "Mounted encrypted config volume"
+ fi
+ fi
+}
+
+unmount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ umount /config
+ umount $vyatta_sysconfdir/config
+
+ cryptsetup close vyos_config
+ fi
+ fi
+}
+
# if necessary, provide initial config
init_bootfile () {
if [ ! -r $BOOTFILE ] ; then
@@ -218,8 +281,8 @@ cleanup_post_commit_hooks () {
# note that this approach only supports hooks that are "configured",
# i.e., it does not support hooks that need to always be present.
cpostdir=$(cli-shell-api getPostCommitHookDir)
- # exclude commits hooks from vyatta-cfg
- excluded="10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
+ # exclude commit hooks that need to always be present
+ excluded="00vyos-sync 10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
if [ -d "$cpostdir" ]; then
for f in $cpostdir/*; do
if [[ ! $excluded =~ $(basename $f) ]]; then
@@ -402,6 +465,8 @@ start ()
&& chgrp ${GROUP} ${vyatta_configdir}
log_action_end_msg $?
+ mount_encrypted_config
+
# T5239: early read of system hostname as this value is read-only once during
# FRR initialisation
tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "system host-name")
@@ -470,6 +535,8 @@ stop()
log_action_end_msg $?
systemctl stop frr.service
+
+ unmount_encrypted_config
}
case "$action" in
diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7
index ccf385a30..e6c298a60 100755
--- a/src/migration-scripts/dhcp-server/6-to-7
+++ b/src/migration-scripts/dhcp-server/6-to-7
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,19 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# T3316: Migrate to Kea
-# - global-parameters will not function
-# - shared-network-parameters will not function
-# - subnet-parameters will not function
-# - static-mapping-parameters will not function
-# - host-decl-name is on by default, option removed
-# - ping-check no longer supported
-# - failover is default enabled on all subnets that exist on failover servers
+# T6079: Disable duplicate static mappings
import sys
from vyos.configtree import ConfigTree
-if (len(sys.argv) < 2):
+if len(sys.argv) < 2:
print("Must specify file name!")
sys.exit(1)
@@ -38,46 +31,42 @@ with open(file_name, 'r') as f:
base = ['service', 'dhcp-server']
config = ConfigTree(config_file)
-if not config.exists(base):
+if not config.exists(base + ['shared-network-name']):
# Nothing to do
- sys.exit(0)
+ exit(0)
-if config.exists(base + ['host-decl-name']):
- config.delete(base + ['host-decl-name'])
+# Run this for every instance if 'shared-network-name'
+for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
-if config.exists(base + ['global-parameters']):
- config.delete(base + ['global-parameters'])
+ if not config.exists(base_network + ['subnet']):
+ continue
-if config.exists(base + ['shared-network-name']):
- for network in config.list_nodes(base + ['shared-network-name']):
- base_network = base + ['shared-network-name', network]
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
- if config.exists(base_network + ['ping-check']):
- config.delete(base_network + ['ping-check'])
+ if config.exists(base_subnet + ['static-mapping']):
+ used_mac = []
+ used_ip = []
- if config.exists(base_network + ['shared-network-parameters']):
- config.delete(base_network +['shared-network-parameters'])
+ for mapping in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', mapping]
- if not config.exists(base_network + ['subnet']):
- continue
+ if config.exists(base_mapping + ['mac-address']):
+ mac = config.return_value(base_mapping + ['mac-address'])
- # Run this for every specified 'subnet'
- for subnet in config.list_nodes(base_network + ['subnet']):
- base_subnet = base_network + ['subnet', subnet]
+ if mac in used_mac:
+ config.set(base_mapping + ['disable'])
+ else:
+ used_mac.append(mac)
- if config.exists(base_subnet + ['enable-failover']):
- config.delete(base_subnet + ['enable-failover'])
+ if config.exists(base_mapping + ['ip-address']):
+ ip = config.return_value(base_mapping + ['ip-address'])
- if config.exists(base_subnet + ['ping-check']):
- config.delete(base_subnet + ['ping-check'])
-
- if config.exists(base_subnet + ['subnet-parameters']):
- config.delete(base_subnet + ['subnet-parameters'])
-
- if config.exists(base_subnet + ['static-mapping']):
- for mapping in config.list_nodes(base_subnet + ['static-mapping']):
- if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']):
- config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters'])
+ if ip in used_ip:
+ config.set(base_subnet + ['static-mapping', mapping, 'disable'])
+ else:
+ used_ip.append(ip)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8
index 151aa6d7b..ccf385a30 100755
--- a/src/migration-scripts/dhcp-server/7-to-8
+++ b/src/migration-scripts/dhcp-server/7-to-8
@@ -14,16 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# T3316:
-# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
-# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..."
-# to "service dhcp-server shared-network-name ... static-mapping <hostname> mac ..."
+# T3316: Migrate to Kea
+# - global-parameters will not function
+# - shared-network-parameters will not function
+# - subnet-parameters will not function
+# - static-mapping-parameters will not function
+# - host-decl-name is on by default, option removed
+# - ping-check no longer supported
+# - failover is default enabled on all subnets that exist on failover servers
import sys
-import re
from vyos.configtree import ConfigTree
-if len(sys.argv) < 2:
+if (len(sys.argv) < 2):
print("Must specify file name!")
sys.exit(1)
@@ -32,30 +35,49 @@ file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['service', 'dhcp-server', 'shared-network-name']
+base = ['service', 'dhcp-server']
config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
sys.exit(0)
-for network in config.list_nodes(base):
- # Run this for every specified 'subnet'
- if config.exists(base + [network, 'subnet']):
- for subnet in config.list_nodes(base + [network, 'subnet']):
- base_subnet = base + [network, 'subnet', subnet]
- if config.exists(base_subnet + ['static-mapping']):
- for hostname in config.list_nodes(base_subnet + ['static-mapping']):
- base_mapping = base_subnet + ['static-mapping', hostname]
+if config.exists(base + ['host-decl-name']):
+ config.delete(base + ['host-decl-name'])
+
+if config.exists(base + ['global-parameters']):
+ config.delete(base + ['global-parameters'])
+
+if config.exists(base + ['shared-network-name']):
+ for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
+
+ if config.exists(base_network + ['ping-check']):
+ config.delete(base_network + ['ping-check'])
+
+ if config.exists(base_network + ['shared-network-parameters']):
+ config.delete(base_network +['shared-network-parameters'])
- # Rename the 'mac-address' node to 'mac'
- if config.exists(base_mapping + ['mac-address']):
- config.rename(base_mapping + ['mac-address'], 'mac')
+ if not config.exists(base_network + ['subnet']):
+ continue
- # Adjust hostname to have valid FQDN characters only
- new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
- if new_hostname != hostname:
- config.rename(base_mapping, new_hostname)
+ # Run this for every specified 'subnet'
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
+
+ if config.exists(base_subnet + ['enable-failover']):
+ config.delete(base_subnet + ['enable-failover'])
+
+ if config.exists(base_subnet + ['ping-check']):
+ config.delete(base_subnet + ['ping-check'])
+
+ if config.exists(base_subnet + ['subnet-parameters']):
+ config.delete(base_subnet + ['subnet-parameters'])
+
+ if config.exists(base_subnet + ['static-mapping']):
+ for mapping in config.list_nodes(base_subnet + ['static-mapping']):
+ if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']):
+ config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters'])
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
index 810e403a6..151aa6d7b 100755
--- a/src/migration-scripts/dhcp-server/8-to-9
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright (C) 2023 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -15,8 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# T3316:
-# - Migrate dhcp options under new option node
-# - Add subnet IDs to existing subnets
+# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
+# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..."
+# to "service dhcp-server shared-network-name ... static-mapping <hostname> mac ..."
import sys
import re
@@ -38,34 +39,23 @@ if not config.exists(base):
# Nothing to do
sys.exit(0)
-option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
- 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
- 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
- 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
- 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
- 'vendor-option', 'wins-server', 'wpad-url']
-
-subnet_id = 1
-
for network in config.list_nodes(base):
- for option in option_nodes:
- if config.exists(base + [network, option]):
- config.set(base + [network, 'option'])
- config.copy(base + [network, option], base + [network, 'option', option])
- config.delete(base + [network, option])
-
+ # Run this for every specified 'subnet'
if config.exists(base + [network, 'subnet']):
for subnet in config.list_nodes(base + [network, 'subnet']):
base_subnet = base + [network, 'subnet', subnet]
-
- for option in option_nodes:
- if config.exists(base_subnet + [option]):
- config.set(base_subnet + ['option'])
- config.copy(base_subnet + [option], base_subnet + ['option', option])
- config.delete(base_subnet + [option])
+ if config.exists(base_subnet + ['static-mapping']):
+ for hostname in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', hostname]
+
+ # Rename the 'mac-address' node to 'mac'
+ if config.exists(base_mapping + ['mac-address']):
+ config.rename(base_mapping + ['mac-address'], 'mac')
- config.set(base_subnet + ['subnet-id'], value=subnet_id)
- subnet_id += 1
+ # Adjust hostname to have valid FQDN characters only
+ new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
+ if new_hostname != hostname:
+ config.rename(base_mapping, new_hostname)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/dhcp-server/9-to-10 b/src/migration-scripts/dhcp-server/9-to-10
new file mode 100755
index 000000000..810e403a6
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/9-to-10
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T3316:
+# - Migrate dhcp options under new option node
+# - Add subnet IDs to existing subnets
+
+import sys
+import re
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcp-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
+ 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
+ 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
+ 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
+ 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
+ 'vendor-option', 'wins-server', 'wpad-url']
+
+subnet_id = 1
+
+for network in config.list_nodes(base):
+ for option in option_nodes:
+ if config.exists(base + [network, option]):
+ config.set(base + [network, 'option'])
+ config.copy(base + [network, option], base + [network, 'option', option])
+ config.delete(base + [network, option])
+
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+
+ for option in option_nodes:
+ if config.exists(base_subnet + [option]):
+ config.set(base_subnet + ['option'])
+ config.copy(base_subnet + [option], base_subnet + ['option', option])
+ config.delete(base_subnet + [option])
+
+ config.set(base_subnet + ['subnet-id'], value=subnet_id)
+ subnet_id += 1
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index d677c2cf8..85ebd19ba 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -65,6 +65,8 @@ MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root part
MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-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?'
+MSG_INPUT_CHOOSE_COPY_ENC_DATA: str = 'From which image would you like to copy the encrypted config?'
MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?'
MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
@@ -212,14 +214,17 @@ def search_previous_installation(disks: list[str]) -> None:
disks (list[str]): a list of available disks
"""
mnt_config = '/mnt/config'
+ mnt_encrypted_config = '/mnt/encrypted_config'
mnt_ssh = '/mnt/ssh'
mnt_tmp = '/mnt/tmp'
rmtree(Path(mnt_config), ignore_errors=True)
rmtree(Path(mnt_ssh), ignore_errors=True)
Path(mnt_tmp).mkdir(exist_ok=True)
+ Path(mnt_encrypted_config).unlink(missing_ok=True)
print('Searching for data from previous installations')
image_data = []
+ encrypted_configs = []
for disk_name in disks:
for partition in disk.partition_list(disk_name):
if disk.partition_mount(partition, mnt_tmp):
@@ -227,32 +232,61 @@ def search_previous_installation(disks: list[str]) -> None:
for path in Path(mnt_tmp + '/boot').iterdir():
if path.joinpath('rw/config/.vyatta_config').exists():
image_data.append((path.name, partition))
+ if Path(mnt_tmp + '/luks').exists():
+ for path in Path(mnt_tmp + '/luks').iterdir():
+ encrypted_configs.append((path.name, partition))
disk.partition_umount(partition)
- if len(image_data) == 1:
- image_name, image_drive = image_data[0]
- print('Found data from previous installation:')
- print(f'\t{image_name} on {image_drive}')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- elif len(image_data) > 1:
- print('Found data from previous installations')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- image_name, image_drive = select_entry(image_data,
- 'Available versions:',
- MSG_INPUT_CHOOSE_COPY_DATA,
- search_format_selection)
+ image_name = None
+ image_drive = None
+ encrypted = False
+
+ if len(image_data) > 0:
+ if len(image_data) == 1:
+ print('Found data from previous installation:')
+ print(f'\t{" on ".join(image_data[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = image_data[0]
+
+ elif len(image_data) > 1:
+ print('Found data from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = select_entry(image_data,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_DATA,
+ search_format_selection)
+ elif len(encrypted_configs) > 0:
+ if len(encrypted_configs) == 1:
+ print('Found encrypted config from previous installation:')
+ print(f'\t{" on ".join(encrypted_configs[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = encrypted_configs[0]
+ encrypted = True
+
+ elif len(encrypted_configs) > 1:
+ print('Found encrypted configs from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = select_entry(encrypted_configs,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_ENC_DATA,
+ search_format_selection)
+ encrypted = True
+
else:
print('No previous installation found')
return
+ if not image_name:
+ return
+
disk.partition_mount(image_drive, mnt_tmp)
- copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ if not encrypted:
+ copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ else:
+ copy(f'{mnt_tmp}/luks/{image_name}', mnt_encrypted_config)
+
Path(mnt_ssh).mkdir()
host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*')
for host_key in host_keys:
@@ -279,6 +313,12 @@ def copy_previous_installation_data(target_dir: str) -> None:
dirs_exist_ok=True)
+def copy_previous_encrypted_config(target_dir: str, image_name: str) -> None:
+ if Path('/mnt/encrypted_config').exists():
+ Path(target_dir).mkdir(exist_ok=True)
+ copy('/mnt/encrypted_config', Path(target_dir).joinpath(image_name))
+
+
def ask_single_disk(disks_available: dict[str, int]) -> str:
"""Ask user to select a disk for installation
@@ -712,6 +752,9 @@ def install_image() -> None:
# owner restored on copy of config data by chmod_2775, above
copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw')
+ # copy saved encrypted config volume
+ copy_previous_encrypted_config(f'{DIR_DST_ROOT}/luks', image_name)
+
if is_raid_install(install_target):
write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
raid.update_default(write_dir)
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e64a85b95..1510a667c 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -95,6 +95,15 @@ def delete_image(image_name: Optional[str] = None,
except Exception as err:
exit(f'Unable to remove the image "{image_name}": {err}')
+ # remove LUKS volume if it exists
+ luks_path: Path = Path(f'{persistence_storage}/luks/{image_name}')
+ if luks_path.is_file():
+ try:
+ luks_path.unlink()
+ print(f'The encrypted config for "{image_name}" was successfully deleted')
+ except Exception as err:
+ exit(f'Unable to remove the encrypted config for "{image_name}": {err}')
+
@compat.grub_cfg_update
def set_image(image_name: Optional[str] = None,
@@ -174,6 +183,16 @@ def rename_image(name_old: str, name_new: str) -> None:
except Exception as err:
exit(f'Unable to rename image "{name_old}" to "{name_new}": {err}')
+ # rename LUKS volume if it exists
+ old_luks_path: Path = Path(f'{persistence_storage}/luks/{name_old}')
+ if old_luks_path.is_file():
+ try:
+ new_luks_path: Path = Path(f'{persistence_storage}/luks/{name_new}')
+ old_luks_path.rename(new_luks_path)
+ print(f'The encrypted config for "{name_old}" was successfully renamed to "{name_new}"')
+ except Exception as err:
+ exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}')
+
def list_images() -> None:
"""Print list of available images for CLI hints"""
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 40d442e30..a7b14a1a3 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -459,7 +459,6 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
request: Request, background_tasks: BackgroundTasks):
session = app.state.vyos_session
env = session.get_session_env()
- config = Config(session_env=env)
endpoint = request.url.path
@@ -474,6 +473,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
# so the lock is really global
lock.acquire()
+ config = Config(session_env=env)
+
status = 200
msg = None
error_msg = None