summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/clear_dhcp_lease.py41
-rwxr-xr-xsrc/op_mode/dhcp.py116
-rwxr-xr-xsrc/op_mode/image_installer.py58
-rwxr-xr-xsrc/op_mode/nat.py6
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