diff options
Diffstat (limited to 'src/op_mode')
-rwxr-xr-x | src/op_mode/clear_dhcp_lease.py | 41 | ||||
-rwxr-xr-x | src/op_mode/dhcp.py | 116 | ||||
-rwxr-xr-x | src/op_mode/image_installer.py | 58 | ||||
-rwxr-xr-x | src/op_mode/nat.py | 6 |
4 files changed, 128 insertions, 93 deletions
diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py index f372d3af0..2c95a2b08 100755 --- a/src/op_mode/clear_dhcp_lease.py +++ b/src/op_mode/clear_dhcp_lease.py @@ -1,20 +1,34 @@ #!/usr/bin/env python3 +# +# 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 argparse import re -from isc_dhcp_leases import Lease -from isc_dhcp_leases import IscDhcpLeases - from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_parse_leases from vyos.utils.io import ask_yes_no from vyos.utils.process import call from vyos.utils.commit import commit_in_progress +# TODO: Update to use Kea control socket command "lease4-del" config = ConfigTreeQuery() base = ['service', 'dhcp-server'] -lease_file = '/config/dhcpd.leases' +lease_file = '/config/dhcp4.leases' def del_lease_ip(address): @@ -25,8 +39,7 @@ def del_lease_ip(address): """ with open(lease_file, encoding='utf-8') as f: data = f.read().rstrip() - lease_config_ip = '{(?P<config>[\s\S]+?)\n}' - pattern = rf"lease {address} {lease_config_ip}" + pattern = rf"^{address},[^\n]+\n" # Delete lease for ip block data = re.sub(pattern, '', data) @@ -38,15 +51,13 @@ def is_ip_in_leases(address): """ Return True if address found in the lease file """ - leases = IscDhcpLeases(lease_file) + leases = kea_parse_leases(lease_file) lease_ips = [] - for lease in leases.get(): - lease_ips.append(lease.ip) - if address not in lease_ips: - print(f'Address "{address}" not found in "{lease_file}"') - return False - return True - + for lease in leases: + if address == lease['address']: + return True + print(f'Address "{address}" not found in "{lease_file}"') + return False if not config.exists(base): print('DHCP-server not configured!') @@ -75,4 +86,4 @@ if __name__ == '__main__': exit(1) else: del_lease_ip(address) - call('systemctl restart isc-dhcp-server.service') + call('systemctl restart kea-dhcp4-server.service') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d6b8aa0b8..a9271ea79 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -21,7 +21,6 @@ import typing from datetime import datetime from glob import glob from ipaddress import ip_address -from isc_dhcp_leases import IscDhcpLeases from tabulate import tabulate import vyos.opmode @@ -29,6 +28,9 @@ import vyos.opmode from vyos.base import Warning from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_get_active_config +from vyos.kea import kea_get_pool_from_subnet_id +from vyos.kea import kea_parse_leases from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.process import cmd @@ -77,67 +79,62 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig Get DHCP server leases :return list """ - lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' + lease_file = '/config/dhcp6.leases' if family == 'inet6' else '/config/dhcp4.leases' data = [] - leases = IscDhcpLeases(lease_file).get() + leases = kea_parse_leases(lease_file) if pool is None: pool = _get_dhcp_pools(family=family) - aux = False else: pool = [pool] - aux = True - - ## Search leases for every pool - for pool_name in pool: - for lease in leases: - if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '': - #if lease.sets.get('shared-networkname', '') == pool_name: - data_lease = {} - data_lease['ip'] = lease.ip - data_lease['state'] = lease.binding_state - #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server' - data_lease['pool'] = lease.sets.get('shared-networkname', '') - data_lease['end'] = lease.end.timestamp() if lease.end else None - data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote' - - if family == 'inet': - data_lease['mac'] = lease.ethernet - data_lease['start'] = lease.start.timestamp() - data_lease['hostname'] = lease.hostname - - if family == 'inet6': - data_lease['last_communication'] = lease.last_communication.timestamp() - data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) - lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} - data_lease['type'] = lease_types_long[lease.type] - - data_lease['remaining'] = '-' - - if lease.end: - data_lease['remaining'] = lease.end - datetime.utcnow() - - if data_lease['remaining'].days >= 0: - # substraction gives us a timedelta object which can't be formatted with strftime - # so we use str(), split gets rid of the microseconds - data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] - - # Do not add old leases - if data_lease['remaining'] != '' and data_lease['state'] != 'free': - if not state or data_lease['state'] in state or state == 'all': - if not origin or data_lease['origin'] in origin: - if not aux or (aux and data_lease['pool'] == pool_name): - data.append(data_lease) - - # deduplicate - checked = [] - for entry in data: - addr = entry.get('ip') - if addr not in checked: - checked.append(addr) - else: - idx = _find_list_of_dict_index(data, key='ip', value=addr) - data.pop(idx) + + inet_suffix = '6' if family == 'inet6' else '4' + active_config = kea_get_active_config(inet_suffix) + + for lease in leases: + data_lease = {} + data_lease['ip'] = lease['address'] + lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'} + data_lease['state'] = lease_state_long[lease['state']] + data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-' + data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None + data_lease['origin'] = 'local' # TODO: Determine remote in HA + + if family == 'inet': + data_lease['mac'] = lease['hwaddr'] + data_lease['start'] = lease['start_timestamp'].timestamp() + data_lease['hostname'] = lease['hostname'] + + if family == 'inet6': + data_lease['last_communication'] = lease['start_timestamp'].timestamp() + data_lease['iaid_duid'] = _format_hex_string(lease['duid']) + lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'} + data_lease['type'] = lease_types_long[lease['lease_type']] + + data_lease['remaining'] = '-' + + if lease['expire']: + data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() + + if data_lease['remaining'].days >= 0: + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + + # Do not add old leases + if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': + if not state or state == 'all' or data_lease['state'] in state: + data.append(data_lease) + + # deduplicate + checked = [] + for entry in data: + addr = entry.get('ip') + if addr not in checked: + checked.append(addr) + else: + idx = _find_list_of_dict_index(data, key='ip', value=addr) + data.pop(idx) if sorted: if sorted == 'ip': @@ -282,10 +279,9 @@ def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], sorted: typing.Optional[str], state: typing.Optional[ArgState], origin: typing.Optional[ArgOrigin] ): # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - v = '6' if family == 'inet6' else '' - service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' - if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): - Warning(f'{service_name} server is configured but not started. Data may be stale.') + v = '6' if family == 'inet6' else '4' + if not is_systemd_service_running(f'kea-dhcp{v}-server.service'): + Warning('DHCP server is configured but not started. Data may be stale.') v = 'v6' if family == 'inet6' else '' if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index b3e6e518c..6a8797aec 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -22,6 +22,7 @@ from pathlib import Path from shutil import copy, chown, rmtree, copytree from glob import glob from sys import exit +from os import environ from time import sleep from typing import Union from urllib.parse import urlparse @@ -83,6 +84,8 @@ DIR_KERNEL_SRC: str = '/boot/' FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs' ISO_DOWNLOAD_PATH: str = '/tmp/vyos_installation.iso' +external_download_script = '/usr/libexec/vyos/simple-download.py' + # default boot variables DEFAULT_BOOT_VARS: dict[str, str] = { 'timeout': '5', @@ -179,6 +182,7 @@ def create_partitions(target_disk: str, target_size: int, rootfs_size: int = available_size print(MSG_INFO_INSTALL_PARTITONING) + raid.clear() disk.disk_cleanup(target_disk) disk_details: disk.DiskDetails = disk.parttable_create(target_disk, rootfs_size) @@ -459,8 +463,23 @@ def validate_signature(file_path: str, sign_type: str) -> None: else: print('Signature is valid') - -def image_fetch(image_path: str, no_prompt: bool = False) -> Path: +def download_file(local_file: str, remote_path: str, vrf: str, + username: str, password: str, + progressbar: bool = False, check_space: bool = False): + environ['REMOTE_USERNAME'] = username + environ['REMOTE_PASSWORD'] = password + if vrf is None: + download(local_file, remote_path, progressbar=progressbar, + check_space=check_space, raise_error=True) + else: + vrf_cmd = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \ + ip vrf exec {vrf} {external_download_script} \ + --local-file {local_file} --remote-path {remote_path}' + cmd(vrf_cmd) + +def image_fetch(image_path: str, vrf: str = None, + username: str = '', password: str = '', + no_prompt: bool = False) -> Path: """Fetch an ISO image Args: @@ -473,14 +492,17 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path: # check a type of path if urlparse(image_path).scheme: # download an image - download(ISO_DOWNLOAD_PATH, image_path, True, True, - raise_error=True) + download_file(ISO_DOWNLOAD_PATH, image_path, vrf, + username, password, + progressbar=True, check_space=True) + # download a signature sign_file = (False, '') for sign_type in ['minisig', 'asc']: try: - download(f'{ISO_DOWNLOAD_PATH}.{sign_type}', - f'{image_path}.{sign_type}', raise_error=True) + download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}', + f'{image_path}.{sign_type}', vrf, + username, password) sign_file = (True, sign_type) break except Exception: @@ -501,8 +523,8 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path: return local_path else: raise FileNotFoundError - except Exception: - print(f'The image cannot be fetched from: {image_path}') + except Exception as e: + print(f'The image cannot be fetched from: {image_path} {e}') exit(1) @@ -611,7 +633,8 @@ def install_image() -> None: print(MSG_WARN_IMAGE_NAME_WRONG) # ask for password - user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos') + user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos', + no_echo=True) # ask for default console console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, @@ -730,7 +753,8 @@ def install_image() -> None: @compat.grub_cfg_update -def add_image(image_path: str, no_prompt: bool = False) -> None: +def add_image(image_path: str, vrf: str = None, username: str = '', + password: str = '', no_prompt: bool = False) -> None: """Add a new image Args: @@ -740,7 +764,7 @@ def add_image(image_path: str, no_prompt: bool = False) -> None: exit(MSG_ERR_LIVE) # fetch an image - iso_path: Path = image_fetch(image_path, no_prompt) + iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt) try: # mount an ISO Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True) @@ -840,10 +864,15 @@ def parse_arguments() -> Namespace: choices=['install', 'add'], required=True, help='action to perform with an image') + parser.add_argument('--vrf', + help='vrf name for image download') parser.add_argument('--no-prompt', action='store_true', help='perform action non-interactively') - parser.add_argument( - '--image-path', + parser.add_argument('--username', default='', + help='username for image download') + parser.add_argument('--password', default='', + help='password for image download') + parser.add_argument('--image-path', help='a path (HTTP or local file) to an image that needs to be installed' ) # parser.add_argument('--image_new_name', help='a new name for image') @@ -861,7 +890,8 @@ if __name__ == '__main__': if args.action == 'install': install_image() if args.action == 'add': - add_image(args.image_path, args.no_prompt) + add_image(args.image_path, args.vrf, + args.username, args.password, args.no_prompt) exit() diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 71a40c0e1..2bc7e24fe 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -28,9 +28,6 @@ from vyos.configquery import ConfigTreeQuery from vyos.utils.process import cmd from vyos.utils.dict import dict_search -base = 'nat' -unconf_message = 'NAT is not configured' - ArgDirection = typing.Literal['source', 'destination'] ArgFamily = typing.Literal['inet', 'inet6'] @@ -293,8 +290,9 @@ def _verify(func): @wraps(func) def _wrapper(*args, **kwargs): config = ConfigTreeQuery() + base = 'nat66' if 'inet6' in sys.argv[1:] else 'nat' if not config.exists(base): - raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + raise vyos.opmode.UnconfiguredSubsystem(f'{base.upper()} is not configured') return func(*args, **kwargs) return _wrapper |