diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/config_mgmt.py | 18 | ||||
-rw-r--r-- | python/vyos/configtree.py | 3 | ||||
-rw-r--r-- | python/vyos/kea.py | 319 | ||||
-rw-r--r-- | python/vyos/load_config.py | 200 | ||||
-rw-r--r-- | python/vyos/remote.py | 2 | ||||
-rw-r--r-- | python/vyos/system/disk.py | 5 | ||||
-rw-r--r-- | python/vyos/system/grub.py | 2 | ||||
-rw-r--r-- | python/vyos/system/raid.py | 31 | ||||
-rw-r--r-- | python/vyos/template.py | 100 | ||||
-rw-r--r-- | python/vyos/utils/file.py | 8 | ||||
-rw-r--r-- | python/vyos/utils/io.py | 9 | ||||
-rw-r--r-- | python/vyos/utils/network.py | 35 |
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 |