summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/config_mgmt.py18
-rw-r--r--python/vyos/configtree.py3
-rw-r--r--python/vyos/kea.py319
-rw-r--r--python/vyos/load_config.py200
-rw-r--r--python/vyos/remote.py2
-rw-r--r--python/vyos/system/disk.py5
-rw-r--r--python/vyos/system/grub.py2
-rw-r--r--python/vyos/system/raid.py31
-rw-r--r--python/vyos/template.py100
-rw-r--r--python/vyos/utils/file.py8
-rw-r--r--python/vyos/utils/io.py9
-rw-r--r--python/vyos/utils/network.py35
12 files changed, 705 insertions, 27 deletions
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index df7240c88..e57129ce5 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -125,6 +125,7 @@ class ConfigMgmt:
get_first_key=True)
self.max_revisions = int(d.get('commit_revisions', 0))
+ self.num_revisions = 0
self.locations = d.get('commit_archive', {}).get('location', [])
self.source_address = d.get('commit_archive',
{}).get('source_address', '')
@@ -233,7 +234,7 @@ Proceed ?'''
msg = ''
if not self._check_revision_number(rev):
- msg = f'Invalid revision number {rev}: must be 0 < rev < {maxrev}'
+ msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
return msg, 1
prompt_str = 'Proceed with reboot ?'
@@ -385,15 +386,8 @@ Proceed ?'''
_, _, netloc = url.netloc.rpartition("@")
redacted_location = urlunsplit(url._replace(netloc=netloc))
print(f" {redacted_location}", end=" ", flush=True)
- try:
- upload(archive_config_file, f'{location}/{remote_file}',
- source_host=source_address, raise_error=True)
- print("OK")
- except Exception as e:
- print("FAILED!")
- print()
- print(indent(str(e), " > "))
- print()
+ upload(archive_config_file, f'{location}/{remote_file}',
+ source_host=source_address)
# op-mode functions
#
@@ -560,8 +554,8 @@ Proceed ?'''
return len(l)
def _check_revision_number(self, rev: int) -> bool:
- maxrev = self._get_number_of_revisions()
- if not 0 <= rev < maxrev:
+ self.num_revisions = self._get_number_of_revisions()
+ if not 0 <= rev < self.num_revisions:
return False
return True
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 09cfd43d3..d048901f0 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -160,6 +160,9 @@ class ConfigTree(object):
def _get_config(self):
return self.__config
+ def get_version_string(self):
+ return self.__version
+
def to_string(self, ordered_values=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = "{0}\n{1}".format(config_string, self.__version)
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
new file mode 100644
index 000000000..cb341e0f2
--- /dev/null
+++ b/python/vyos/kea.py
@@ -0,0 +1,319 @@
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import socket
+
+from datetime import datetime
+
+from vyos.template import is_ipv6
+from vyos.template import isc_static_route
+from vyos.template import netmask_from_cidr
+from vyos.utils.dict import dict_search_args
+from vyos.utils.file import read_file
+
+kea4_options = {
+ 'name_server': 'domain-name-servers',
+ 'domain_name': 'domain-name',
+ 'domain_search': 'domain-search',
+ 'ntp_server': 'ntp-servers',
+ 'pop_server': 'pop-server',
+ 'smtp_server': 'smtp-server',
+ 'time_server': 'time-servers',
+ 'wins_server': 'netbios-name-servers',
+ 'default_router': 'routers',
+ 'server_identifier': 'dhcp-server-identifier',
+ 'tftp_server_name': 'tftp-server-name',
+ 'bootfile_size': 'boot-size',
+ 'time_offset': 'time-offset',
+ 'wpad_url': 'wpad-url',
+ 'ipv6_only_preferred': 'v6-only-preferred',
+ 'captive_portal': 'v4-captive-portal'
+}
+
+kea6_options = {
+ 'info_refresh_time': 'information-refresh-time',
+ 'name_server': 'dns-servers',
+ 'domain_search': 'domain-search',
+ 'nis_domain': 'nis-domain-name',
+ 'nis_server': 'nis-servers',
+ 'nisplus_domain': 'nisp-domain-name',
+ 'nisplus_server': 'nisp-servers',
+ 'sntp_server': 'sntp-servers',
+ 'captive_portal': 'v6-captive-portal'
+}
+
+def kea_parse_options(config):
+ options = []
+
+ for node, option_name in kea4_options.items():
+ if node not in config:
+ continue
+
+ value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
+ options.append({'name': option_name, 'data': value})
+
+ if 'client_prefix_length' in config:
+ options.append({'name': 'subnet-mask', 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length'])})
+
+ if 'ip_forwarding' in config:
+ options.append({'name': 'ip-forwarding', 'data': "true"})
+
+ if 'static_route' in config:
+ default_route = ''
+
+ if 'default_router' in config:
+ default_route = isc_static_route('0.0.0.0/0', config['default_router'])
+
+ routes = [isc_static_route(route, route_options['next_hop']) for route, route_options in config['static_route'].items()]
+
+ options.append({'name': 'rfc3442-static-route', 'data': ", ".join(routes if not default_route else routes + [default_route])})
+ options.append({'name': 'windows-static-route', 'data': ", ".join(routes)})
+
+ if 'time_zone' in config:
+ with open("/usr/share/zoneinfo/" + config['time_zone'], "rb") as f:
+ tz_string = f.read().split(b"\n")[-2].decode("utf-8")
+
+ options.append({'name': 'pcode', 'data': tz_string})
+ options.append({'name': 'tcode', 'data': config['time_zone']})
+
+ return options
+
+def kea_parse_subnet(subnet, config):
+ out = {'subnet': subnet}
+ options = kea_parse_options(config)
+
+ if 'bootfile_name' in config:
+ out['boot-file-name'] = config['bootfile_name']
+
+ if 'bootfile_server' in config:
+ out['next-server'] = config['bootfile_server']
+
+ if 'lease' in config:
+ out['valid-lifetime'] = int(config['lease'])
+ out['max-valid-lifetime'] = int(config['lease'])
+
+ if 'range' in config:
+ pools = []
+ for num, range_config in config['range'].items():
+ start, stop = range_config['start'], range_config['stop']
+ pools.append({'pool': f'{start} - {stop}'})
+ out['pools'] = pools
+
+ if 'static_mapping' in config:
+ reservations = []
+ for host, host_config in config['static_mapping'].items():
+ if 'disable' in host_config:
+ continue
+
+ reservations.append({
+ 'hw-address': host_config['mac_address'],
+ 'ip-address': host_config['ip_address']
+ })
+ out['reservations'] = reservations
+
+ unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
+ if unifi_controller:
+ options.append({
+ 'name': 'unifi-controller',
+ 'data': unifi_controller,
+ 'space': 'ubnt'
+ })
+
+ if options:
+ out['option-data'] = options
+
+ return out
+
+def kea6_parse_options(config):
+ options = []
+
+ if 'common_options' in config:
+ common_opt = config['common_options']
+
+ for node, option_name in kea6_options.items():
+ if node not in common_opt:
+ continue
+
+ value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node]
+ options.append({'name': option_name, 'data': value})
+
+ for node, option_name in kea6_options.items():
+ if node not in config:
+ continue
+
+ value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
+ options.append({'name': option_name, 'data': value})
+
+ if 'sip_server' in config:
+ sip_servers = config['sip_server']
+
+ addrs = []
+ hosts = []
+
+ for server in sip_servers:
+ if is_ipv6(server):
+ addrs.append(server)
+ else:
+ hosts.append(server)
+
+ if addrs:
+ options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)})
+
+ if hosts:
+ options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)})
+
+ cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server')
+ if cisco_tftp:
+ options.append({'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp})
+
+ return options
+
+def kea6_parse_subnet(subnet, config):
+ out = {'subnet': subnet}
+ options = kea6_parse_options(config)
+
+ if 'address_range' in config:
+ addr_range = config['address_range']
+ pools = []
+
+ if 'prefix' in addr_range:
+ for prefix in addr_range['prefix']:
+ pools.append({'pool': prefix})
+
+ if 'start' in addr_range:
+ for start, range_conf in addr_range['start'].items():
+ stop = range_conf['stop']
+ pools.append({'pool': f'{start} - {stop}'})
+
+ out['pools'] = pools
+
+ if 'prefix_delegation' in config:
+ pd_pools = []
+
+ if 'prefix' in config['prefix_delegation']:
+ for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
+ pd_pools.append({
+ 'prefix': prefix,
+ 'prefix-len': int(pd_conf['prefix_length']),
+ 'delegated-len': int(pd_conf['delegated_length'])
+ })
+
+ out['pd-pools'] = pd_pools
+
+ if 'lease_time' in config:
+ if 'default' in config['lease_time']:
+ out['valid-lifetime'] = int(config['lease_time']['default'])
+ if 'maximum' in config['lease_time']:
+ out['max-valid-lifetime'] = int(config['lease_time']['maximum'])
+ if 'minimum' in config['lease_time']:
+ out['min-valid-lifetime'] = int(config['lease_time']['minimum'])
+
+ if 'static_mapping' in config:
+ reservations = []
+ for host, host_config in config['static_mapping'].items():
+ if 'disable' in host_config:
+ continue
+
+ reservation = {}
+
+ if 'identifier' in host_config:
+ reservation['duid'] = host_config['identifier']
+
+ if 'ipv6_address' in host_config:
+ reservation['ip-addresses'] = [ host_config['ipv6_address'] ]
+
+ if 'ipv6_prefix' in host_config:
+ reservation['prefixes'] = [ host_config['ipv6_prefix'] ]
+
+ reservations.append(reservation)
+
+ out['reservations'] = reservations
+
+ if options:
+ out['option-data'] = options
+
+ return out
+
+def kea_parse_leases(lease_path):
+ contents = read_file(lease_path)
+ lines = contents.split("\n")
+ output = []
+
+ if len(lines) < 2:
+ return output
+
+ headers = lines[0].split(",")
+
+ for line in lines[1:]:
+ line_out = dict(zip(headers, line.split(",")))
+
+ lifetime = int(line_out['valid_lifetime'])
+ expiry = int(line_out['expire'])
+
+ line_out['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime)
+ line_out['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None
+
+ output.append(line_out)
+
+ return output
+
+def _ctrl_socket_command(path, command, args=None):
+ if not os.path.exists(path):
+ return None
+
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
+ sock.connect(path)
+
+ payload = {'command': command}
+ if args:
+ payload['arguments'] = args
+
+ sock.send(bytes(json.dumps(payload), 'utf-8'))
+ result = b''
+ while True:
+ data = sock.recv(4096)
+ result += data
+ if len(data) < 4096:
+ break
+
+ return json.loads(result.decode('utf-8'))
+
+def kea_get_active_config(inet):
+ ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket'
+
+ config = _ctrl_socket_command(ctrl_socket, 'config-get')
+
+ if not config or 'result' not in config or config['result'] != 0:
+ return None
+
+ return config
+
+def kea_get_pool_from_subnet_id(config, inet, subnet_id):
+ shared_networks = dict_search_args(config, 'arguments', f'Dhcp{inet}', 'shared-networks')
+
+ if not shared_networks:
+ return None
+
+ for network in shared_networks:
+ if f'subnet{inet}' not in network:
+ continue
+
+ for subnet in network[f'subnet{inet}']:
+ if 'id' in subnet and int(subnet['id']) == int(subnet_id):
+ return network['name']
+
+ return None
diff --git a/python/vyos/load_config.py b/python/vyos/load_config.py
new file mode 100644
index 000000000..af563614d
--- /dev/null
+++ b/python/vyos/load_config.py
@@ -0,0 +1,200 @@
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""This module abstracts the loading of a config file into the running
+config. It provides several varieties of loading a config file, from the
+legacy version to the developing versions, as a means of offering
+alternatives for competing use cases, and a base for profiling the
+performance of each.
+"""
+
+import sys
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+from typing import Union, Literal, TypeAlias, get_type_hints, get_args
+
+from vyos.config import Config
+from vyos.configtree import ConfigTree, DiffTree
+from vyos.configsource import ConfigSourceSession, VyOSError
+from vyos.component_version import from_string as version_from_string
+from vyos.component_version import from_system as version_from_system
+from vyos.migrator import Migrator, VirtualMigrator, MigratorError
+from vyos.utils.process import popen, DEVNULL
+
+Variety: TypeAlias = Literal['explicit', 'batch', 'tree', 'legacy']
+ConfigObj: TypeAlias = Union[str, ConfigTree]
+
+thismod = sys.modules[__name__]
+
+class LoadConfigError(Exception):
+ """Raised when an error occurs loading a config file.
+ """
+
+# utility functions
+
+def get_running_config(config: Config) -> ConfigTree:
+ return config.get_config_tree(effective=True)
+
+def get_proposed_config(config_file: str = None) -> ConfigTree:
+ config_str = Path(config_file).read_text()
+ return ConfigTree(config_str)
+
+def migration_needed(config_obj: ConfigObj) -> bool:
+ """Check if a migration is needed for the config object.
+ """
+ if not isinstance(config_obj, ConfigTree):
+ atree = get_proposed_config(config_obj)
+ else:
+ atree = config_obj
+ version_str = atree.get_version_string()
+ if not version_str:
+ return True
+ aversion = version_from_string(version_str.splitlines()[1])
+ bversion = version_from_system()
+ return aversion != bversion
+
+def check_session(strict: bool, switch: Variety) -> None:
+ """Check if we are in a config session, with no uncommitted changes, if
+ strict. This is not needed for legacy load, as these checks are
+ implicit.
+ """
+
+ if switch == 'legacy':
+ return
+
+ context = ConfigSourceSession()
+
+ if not context.in_session():
+ raise LoadConfigError('not in a config session')
+
+ if strict and context.session_changed():
+ raise LoadConfigError('commit or discard changes before loading config')
+
+# methods to call for each variety
+
+# explicit
+def diff_to_commands(ctree: ConfigTree, ntree: ConfigTree) -> list:
+ """Calculate the diff between the current and proposed config."""
+ # Calculate the diff between the current and new config tree
+ commands = DiffTree(ctree, ntree).to_commands()
+ # on an empty set of 'add' or 'delete' commands, to_commands
+ # returns '\n'; prune below
+ command_list = commands.splitlines()
+ command_list = [c for c in command_list if c]
+ return command_list
+
+def set_commands(cmds: list) -> None:
+ """Set commands in the config session."""
+ if not cmds:
+ print('no commands to set')
+ return
+ error_out = []
+ for op in cmds:
+ out, rc = popen(f'/opt/vyatta/sbin/my_{op}', shell=True, stderr=DEVNULL)
+ if rc != 0:
+ error_out.append(out)
+ continue
+ if error_out:
+ out = '\n'.join(error_out)
+ raise LoadConfigError(out)
+
+# legacy
+class LoadConfig(ConfigSourceSession):
+ """A subclass for calling 'loadFile'.
+ """
+ def load_config(self, file_name):
+ return self._run(['/bin/cli-shell-api','loadFile', file_name])
+
+# end methods to call for each variety
+
+def migrate(config_obj: ConfigObj) -> ConfigObj:
+ """Migrate a config object to the current version.
+ """
+ if isinstance(config_obj, ConfigTree):
+ config_file = NamedTemporaryFile(delete=False).name
+ Path(config_file).write_text(config_obj.to_string())
+ else:
+ config_file = config_obj
+
+ virtual_migration = VirtualMigrator(config_file)
+ migration = Migrator(config_file)
+ try:
+ virtual_migration.run()
+ migration.run()
+ except MigratorError as e:
+ raise LoadConfigError(e) from e
+ else:
+ if isinstance(config_obj, ConfigTree):
+ return ConfigTree(Path(config_file).read_text())
+ return config_file
+ finally:
+ if isinstance(config_obj, ConfigTree):
+ Path(config_file).unlink()
+
+def load_explicit(config_obj: ConfigObj):
+ """Explicit load from file or configtree.
+ """
+ config = Config()
+ ctree = get_running_config(config)
+ if isinstance(config_obj, ConfigTree):
+ ntree = config_obj
+ else:
+ ntree = get_proposed_config(config_obj)
+ # Calculate the diff between the current and proposed config
+ cmds = diff_to_commands(ctree, ntree)
+ # Set the commands in the config session
+ set_commands(cmds)
+
+def load_batch(config_obj: ConfigObj):
+ # requires legacy backend patch
+ raise NotImplementedError('batch loading not implemented')
+
+def load_tree(config_obj: ConfigObj):
+ # requires vyconf backend patch
+ raise NotImplementedError('tree loading not implemented')
+
+def load_legacy(config_obj: ConfigObj):
+ """Legacy load from file or configtree.
+ """
+ if isinstance(config_obj, ConfigTree):
+ config_file = NamedTemporaryFile(delete=False).name
+ Path(config_file).write_text(config_obj.to_string())
+ else:
+ config_file = config_obj
+
+ config = LoadConfig()
+
+ try:
+ config.load_config(config_file)
+ except VyOSError as e:
+ raise LoadConfigError(e) from e
+ finally:
+ if isinstance(config_obj, ConfigTree):
+ Path(config_file).unlink()
+
+def load(config_obj: ConfigObj, strict: bool = True,
+ switch: Variety = 'legacy'):
+ type_hints = get_type_hints(load)
+ switch_choice = get_args(type_hints['switch'])
+ if switch not in switch_choice:
+ raise ValueError(f'invalid switch: {switch}')
+
+ check_session(strict, switch)
+
+ if migration_needed(config_obj):
+ config_obj = migrate(config_obj)
+
+ func = getattr(thismod, f'load_{switch}')
+ func(config_obj)
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index fec44b571..b1efcd10b 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -452,7 +452,7 @@ def upload(local_path, urlstring, progressbar=False,
source_host='', source_port=0, timeout=10.0):
try:
progressbar = progressbar and is_interactive()
- urlc(urlstring, progressbar, source_host, source_port, timeout).upload(local_path)
+ urlc(urlstring, progressbar, False, source_host, source_port, timeout).upload(local_path)
except Exception as err:
print_error(f'Unable to upload "{urlstring}": {err}')
except KeyboardInterrupt:
diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py
index f8e0fd1bf..b8a2c0f35 100644
--- a/python/vyos/system/disk.py
+++ b/python/vyos/system/disk.py
@@ -31,12 +31,17 @@ class DiskDetails:
def disk_cleanup(drive_path: str) -> None:
"""Clean up disk partition table (MBR and GPT)
+ Remove partition and device signatures.
Zeroize primary and secondary headers - first and last 17408 bytes
(512 bytes * 34 LBA) on a drive
Args:
drive_path (str): path to a drive that needs to be cleaned
"""
+ partitions: list[str] = partition_list(drive_path)
+ for partition in partitions:
+ run(f'wipefs -af {partition}')
+ run(f'wipefs -af {drive_path}')
run(f'sgdisk -Z {drive_path}')
diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py
index 0ac16af9a..2692aaea1 100644
--- a/python/vyos/system/grub.py
+++ b/python/vyos/system/grub.py
@@ -138,6 +138,8 @@ def version_list(root_dir: str = '') -> list[str]:
versions_list: list[str] = []
for file in versions_files:
versions_list.append(file.stem)
+ versions_list.sort(reverse=True)
+
return versions_list
diff --git a/python/vyos/system/raid.py b/python/vyos/system/raid.py
index 13b99fa69..5b33d34da 100644
--- a/python/vyos/system/raid.py
+++ b/python/vyos/system/raid.py
@@ -19,7 +19,7 @@ from pathlib import Path
from shutil import copy
from dataclasses import dataclass
-from vyos.utils.process import cmd
+from vyos.utils.process import cmd, run
from vyos.system import disk
@@ -44,18 +44,11 @@ def raid_create(raid_members: list[str],
"""
raid_devices_num: int = len(raid_members)
raid_members_str: str = ' '.join(raid_members)
- if Path('/sys/firmware/efi').exists():
- for part in raid_members:
- drive: str = disk.partition_parent(part)
- command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}'
- cmd(command)
- else:
- for part in raid_members:
- drive: str = disk.partition_parent(part)
- command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}'
- cmd(command)
for part in raid_members:
- command: str = f'mdadm --zero-superblock {part}'
+ drive: str = disk.partition_parent(part)
+ # set partition type GUID for raid member; cf.
+ # https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs
+ command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}'
cmd(command)
command: str = f'mdadm --create /dev/{raid_name} -R --metadata=1.0 \
--raid-devices={raid_devices_num} --level={raid_level} \
@@ -72,6 +65,20 @@ def raid_create(raid_members: list[str],
return raid
+def clear():
+ """Deactivate all RAID arrays"""
+ command: str = 'mdadm --examine --scan'
+ raid_config = cmd(command)
+ if not raid_config:
+ return
+ command: str = 'mdadm --run /dev/md?*'
+ run(command)
+ command: str = 'mdadm --assemble --scan --auto=yes --symlink=no'
+ run(command)
+ command: str = 'mdadm --stop --scan'
+ run(command)
+
+
def update_initramfs() -> None:
"""Update initramfs"""
mdadm_script = '/etc/initramfs-tools/scripts/local-top/mdadm'
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 2d4beeec2..f0a50e728 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -791,6 +791,106 @@ def range_to_regex(num_range):
regex = range_to_regex(num_range)
return f'({regex})'
+@register_filter('kea_failover_json')
+def kea_failover_json(config):
+ from json import dumps
+
+ source_addr = config['source_address']
+ remote_addr = config['remote']
+
+ data = {
+ 'this-server-name': os.uname()[1],
+ 'mode': 'hot-standby',
+ 'heartbeat-delay': 10000,
+ 'max-response-delay': 10000,
+ 'max-ack-delay': 5000,
+ 'max-unacked-clients': 0,
+ 'peers': [
+ {
+ 'name': os.uname()[1],
+ 'url': f'http://{source_addr}:647/',
+ 'role': 'standby' if config['status'] == 'secondary' else 'primary',
+ 'auto-failover': True
+ },
+ {
+ 'name': config['name'],
+ 'url': f'http://{remote_addr}:647/',
+ 'role': 'primary' if config['status'] == 'secondary' else 'standby',
+ 'auto-failover': True
+ }]
+ }
+
+ if 'ca_cert_file' in config:
+ data['trust-anchor'] = config['ca_cert_file']
+
+ if 'cert_file' in config:
+ data['cert-file'] = config['cert_file']
+
+ if 'cert_key_file' in config:
+ data['key-file'] = config['cert_key_file']
+
+ return dumps(data)
+
+@register_filter('kea_shared_network_json')
+def kea_shared_network_json(shared_networks):
+ from vyos.kea import kea_parse_options
+ from vyos.kea import kea_parse_subnet
+ from json import dumps
+ out = []
+
+ for name, config in shared_networks.items():
+ if 'disable' in config:
+ continue
+
+ network = {
+ 'name': name,
+ 'authoritative': ('authoritative' in config),
+ 'subnet4': []
+ }
+ options = kea_parse_options(config)
+
+ if 'subnet' in config:
+ for subnet, subnet_config in config['subnet'].items():
+ network['subnet4'].append(kea_parse_subnet(subnet, subnet_config))
+
+ if options:
+ network['option-data'] = options
+
+ out.append(network)
+
+ return dumps(out, indent=4)
+
+@register_filter('kea6_shared_network_json')
+def kea6_shared_network_json(shared_networks):
+ from vyos.kea import kea6_parse_options
+ from vyos.kea import kea6_parse_subnet
+ from json import dumps
+ out = []
+
+ for name, config in shared_networks.items():
+ if 'disable' in config:
+ continue
+
+ network = {
+ 'name': name,
+ 'subnet6': []
+ }
+ options = kea6_parse_options(config)
+
+ if 'interface' in config:
+ network['interface'] = config['interface']
+
+ if 'subnet' in config:
+ for subnet, subnet_config in config['subnet'].items():
+ network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config))
+
+ if options:
+ network['option-data'] = options
+
+ out.append(network)
+
+ return dumps(out, indent=4)
+
@register_test('vyos_defined')
def vyos_defined(value, test_value=None, var_type=None):
"""
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index 9f27a7fb9..2af87a0ca 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -141,6 +141,14 @@ def chmod_2775(path):
bitmask = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH
chmod(path, bitmask)
+def chmod_775(path):
+ """ Make file executable by all """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IXOTH
+
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | \
+ S_IROTH | S_IXOTH
+ chmod(path, bitmask)
+
def makedir(path, user=None, group=None):
if os.path.exists(path):
return
diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py
index 74099b502..0afaf695c 100644
--- a/python/vyos/utils/io.py
+++ b/python/vyos/utils/io.py
@@ -26,13 +26,18 @@ def print_error(str='', end='\n'):
sys.stderr.write(end)
sys.stderr.flush()
-def ask_input(question, default='', numeric_only=False, valid_responses=[]):
+def ask_input(question, default='', numeric_only=False, valid_responses=[],
+ no_echo=False):
+ from getpass import getpass
question_out = question
if default:
question_out += f' (Default: {default})'
response = ''
while True:
- response = input(question_out + ' ').strip()
+ if not no_echo:
+ response = input(question_out + ' ').strip()
+ else:
+ response = getpass(question_out + ' ').strip()
if not response and default:
return default
if numeric_only:
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 2a0808fca..997ee6309 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -201,6 +201,7 @@ def get_all_vrfs():
return data
def interface_list() -> list:
+ from vyos.ifconfig import Section
"""
Get list of interfaces in system
:rtype: list
@@ -519,3 +520,37 @@ def get_vxlan_vni_filter(interface: str) -> list:
os_configured_vnis.append(str(vniStart))
return os_configured_vnis
+
+# Calculate prefix length of an IPv6 range, where possible
+# Python-ified from source: https://gitlab.isc.org/isc-projects/dhcp/-/blob/master/keama/confparse.c#L4591
+def ipv6_prefix_length(low, high):
+ import socket
+
+ bytemasks = [0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff]
+
+ try:
+ lo = bytearray(socket.inet_pton(socket.AF_INET6, low))
+ hi = bytearray(socket.inet_pton(socket.AF_INET6, high))
+ except:
+ return None
+
+ xor = bytearray(a ^ b for a, b in zip(lo, hi))
+
+ plen = 0
+ while plen < 128 and xor[plen // 8] == 0:
+ plen += 8
+
+ if plen == 128:
+ return plen
+
+ for i in range((plen // 8) + 1, 16):
+ if xor[i] != 0:
+ return None
+
+ for i in range(8):
+ msk = ~xor[plen // 8] & 0xff
+
+ if msk == bytemasks[i]:
+ return plen + i + 1
+
+ return None