summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/pki.py11
-rwxr-xr-xsrc/conf_mode/protocols_isis.py15
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py13
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py15
-rwxr-xr-xsrc/conf_mode/service_https.py237
-rw-r--r--src/etc/systemd/system/nginx.service.d/10-override.conf3
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py3
-rwxr-xr-xsrc/migration-scripts/dhcp-server/8-to-969
-rwxr-xr-xsrc/migration-scripts/https/5-to-676
-rwxr-xr-xsrc/op_mode/image_manager.py25
-rw-r--r--src/op_mode/zone.py215
-rwxr-xr-xsrc/services/vyos-http-api-server10
-rwxr-xr-xsrc/system/on-dhcp-event.sh65
13 files changed, 568 insertions, 189 deletions
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 239e44c3b..4be40e99e 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -130,28 +130,27 @@ def get_config(config=None):
if len(argv) > 1 and argv[1] == 'certbot_renew':
pki['certbot_renew'] = {}
- tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True)
+ tmp = node_changed(conf, base + ['ca'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'ca' : tmp})
- tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True)
+ tmp = node_changed(conf, base + ['certificate'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'certificate' : tmp})
- tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True)
+ tmp = node_changed(conf, base + ['dh'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'dh' : tmp})
- tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True)
+ tmp = node_changed(conf, base + ['key-pair'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'key_pair' : tmp})
- tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'),
- recursive=True)
+ tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'openvpn' : tmp})
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index ce67ccff7..8d594bb68 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -220,7 +220,20 @@ def verify(isis):
if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']):
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
+ if dict_search('segment_routing.global_block', isis):
+ g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+ g_label_difference = int(g_high_label_value) - int(g_low_label_value)
+ if dict_search('segment_routing.prefix', isis):
+ for prefix, prefix_config in isis['segment_routing']['prefix'].items():
+ if 'index' in prefix_config:
+ index_size = isis['segment_routing']['prefix'][prefix]['index']['value']
+ if int(index_size) > int(g_label_difference):
+ raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\
+ f'index base size larger than the SRGB label base.')
+
# Check for LFA tiebreaker index duplication
if dict_search('fast_reroute.lfa.local.tiebreaker', isis):
comparison_dictionary = {}
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 2f07142a3..34cf49286 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -213,6 +213,19 @@ 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
+ 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)
+ g_label_difference = int(g_high_label_value) - int(g_low_label_value)
+ if dict_search('segment_routing.prefix', ospf):
+ for prefix, prefix_config in ospf['segment_routing']['prefix'].items():
+ if 'index' in prefix_config:
+ index_size = ospf['segment_routing']['prefix'][prefix]['index']['value']
+ if int(index_size) > int(g_label_difference):
+ raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\
+ f'index base size larger than the SRGB label base.')
+
# Check route summarisation
if 'summary_address' in ospf:
for prefix, prefix_options in ospf['summary_address'].items():
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 7ebc560ba..ceaba019e 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -31,6 +31,7 @@ from vyos.utils.file import chmod_775
from vyos.utils.file import makedir
from vyos.utils.file import write_file
from vyos.utils.process import call
+from vyos.utils.network import interface_exists
from vyos.utils.network import is_subnet_connected
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
@@ -222,6 +223,7 @@ def verify(dhcp):
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
+ used_ips = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
@@ -233,6 +235,11 @@ def verify(dhcp):
raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if mapping_config['ip_address'] in used_ips:
+ raise ConfigError(f'Configured IP address for static mapping "{mapping}" exists on another static mapping')
+
+ used_ips.append(mapping_config['ip_address'])
+
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
@@ -294,12 +301,18 @@ def verify(dhcp):
else:
raise ConfigError(f'listen-address "{address}" not configured on any interface')
-
if not listen_ok:
raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
'broadcast interface configured, nor was there an explicit listen-address\n'
'configured for serving DHCP relay packets!')
+ if 'listen_address' in dhcp and 'listen_interface' in dhcp:
+ raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+
+ for interface in (dict_search('listen_interface', dhcp) or []):
+ if not interface_exists(interface):
+ raise ConfigError(f'listen-interface "{interface}" does not exist')
+
return None
def generate(dhcp):
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
index 2e7ebda5a..46efc3c93 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/service_https.py
@@ -15,51 +15,41 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import socket
import sys
import json
-from copy import deepcopy
from time import sleep
-import vyos.defaults
-
from vyos.base import Warning
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdiff import get_config_diff
from vyos.configverify import verify_vrf
-from vyos import ConfigError
+from vyos.defaults import api_config_state
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
+from vyos.pki import wrap_dh_parameters
+from vyos.pki import load_dh_parameters
from vyos.template import render
+from vyos.utils.dict import dict_search
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_active
from vyos.utils.network import check_port_availability
from vyos.utils.network import is_listen_port_bind_service
from vyos.utils.file import write_file
-
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_file = '/etc/nginx/sites-available/default'
+config_file = '/etc/nginx/sites-enabled/default'
systemd_override = r'/run/systemd/system/nginx.service.d/override.conf'
-cert_dir = '/etc/ssl/certs'
-key_dir = '/etc/ssl/private'
-
-api_config_state = '/run/http-api-state'
-systemd_service = '/run/systemd/system/vyos-http-api.service'
-
-# https config needs to coordinate several subsystems: api,
-# self-signed certificate, as well as the virtual hosts defined within the
-# https config definition itself. Consequently, one needs a general dict,
-# encompassing the https and other configs, and a list of such virtual hosts
-# (server blocks in nginx terminology) to pass to the jinja2 template.
-default_server_block = {
- 'id' : '',
- 'address' : '*',
- 'port' : '443',
- 'name' : ['_'],
- 'api' : False,
- 'vyos_cert' : {},
-}
+cert_dir = '/run/nginx/certs'
+
+user = 'www-data'
+group = 'www-data'
+
+systemd_service_api = '/run/systemd/system/vyos-http-api.service'
def get_config(config=None):
if config:
@@ -71,83 +61,70 @@ def get_config(config=None):
if not conf.exists(base):
return None
- diff = get_config_diff(conf)
-
- https = conf.get_config_dict(base, get_first_key=True, with_pki=True)
+ https = conf.get_config_dict(base, get_first_key=True,
+ key_mangling=('-', '_'),
+ with_pki=True)
- https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
+ # store path to API config file for later use in templates
+ https['api_config_state'] = api_config_state
+ # get fully qualified system hsotname
+ https['hostname'] = socket.getfqdn()
- if 'api' not in https:
- return https
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = conf.get_config_defaults(**https.kwargs, recursive=True)
+ if 'api' not in https or 'graphql' not in https['api']:
+ del default_values['api']
- http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True,
- with_recursive_defaults=True)
-
- if http_api.from_defaults(['graphql']):
- del http_api['graphql']
-
- # Do we run inside a VRF context?
- vrf_path = ['service', 'https', 'vrf']
- if conf.exists(vrf_path):
- http_api['vrf'] = conf.return_value(vrf_path)
-
- https['api'] = http_api
+ # merge CLI and default dictionary
+ https = config_dict_merge(default_values, https)
return https
def verify(https):
- from vyos.utils.dict import dict_search
-
if https is None:
return None
- if 'certificates' in https:
- certificates = https['certificates']
+ if 'certificates' in https and 'certificate' in https['certificates']:
+ cert_name = https['certificates']['certificate']
+ if 'pki' not in https:
+ raise ConfigError('PKI is not configured!')
- if 'certificate' in certificates:
- if not https['pki']:
- raise ConfigError('PKI is not configured')
+ if cert_name not in https['pki']['certificate']:
+ raise ConfigError('Invalid certificate in configuration!')
- cert_name = certificates['certificate']
+ pki_cert = https['pki']['certificate'][cert_name]
- if cert_name not in https['pki']['certificate']:
- raise ConfigError('Invalid certificate on https configuration')
+ if 'certificate' not in pki_cert:
+ raise ConfigError('Missing certificate in configuration!')
- pki_cert = https['pki']['certificate'][cert_name]
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError('Missing certificate private key in configuration!')
- if 'certificate' not in pki_cert:
- raise ConfigError('Missing certificate on https configuration')
+ if 'dh_params' in https['certificates']:
+ dh_name = https['certificates']['dh_params']
+ if dh_name not in https['pki']['dh']:
+ raise ConfigError('Invalid DH parameter in configuration!')
- if 'private' not in pki_cert or 'key' not in pki_cert['private']:
- raise ConfigError("Missing certificate private key on https configuration")
- else:
- Warning('No certificate specified, using buildin self-signed certificates!')
+ pki_dh = https['pki']['dh'][dh_name]
+ dh_params = load_dh_parameters(pki_dh['parameters'])
+ dh_numbers = dh_params.parameter_numbers()
+ dh_bits = dh_numbers.p.bit_length()
+ if dh_bits < 2048:
+ raise ConfigError(f'Minimum DH key-size is 2048 bits')
- server_block_list = []
+ else:
+ Warning('No certificate specified, using build-in self-signed certificates. '\
+ 'Do not use them in a production environment!')
- # organize by vhosts
- vhost_dict = https.get('virtual-host', {})
+ # Check if server port is already in use by a different appliaction
+ listen_address = ['0.0.0.0']
+ port = int(https['port'])
+ if 'listen_address' in https:
+ listen_address = https['listen_address']
- if not vhost_dict:
- # no specified virtual hosts (server blocks); use default
- server_block_list.append(default_server_block)
- else:
- for vhost in list(vhost_dict):
- server_block = deepcopy(default_server_block)
- data = vhost_dict.get(vhost, {})
- server_block['address'] = data.get('listen-address', '*')
- server_block['port'] = data.get('port', '443')
- server_block_list.append(server_block)
-
- for entry in server_block_list:
- _address = entry.get('address')
- _address = '0.0.0.0' if _address == '*' else _address
- _port = entry.get('port')
- proto = 'tcp'
- if check_port_availability(_address, int(_port), proto) is not True and \
- not is_listen_port_bind_service(int(_port), 'nginx'):
- raise ConfigError(f'"{proto}" port "{_port}" is used by another service')
+ for address in listen_address:
+ if not check_port_availability(address, port, 'tcp') and not is_listen_port_bind_service(port, 'nginx'):
+ raise ConfigError(f'TCP port "{port}" is used by another service!')
verify_vrf(https)
@@ -172,89 +149,61 @@ def verify(https):
# If only key-based methods are enabled,
# fail the commit if no valid key configurations are found
if (not valid_keys_exist) and (not jwt_auth):
- raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled')
+ raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled!')
if (not valid_keys_exist) and jwt_auth:
- Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.')
+ Warning(f'API keys are not configured: classic (non-GraphQL) API will be unavailable!')
return None
def generate(https):
if https is None:
+ for file in [systemd_service_api, config_file, systemd_override]:
+ if os.path.exists(file):
+ os.unlink(file)
return None
- if 'api' not in https:
- if os.path.exists(systemd_service):
- os.unlink(systemd_service)
- else:
- render(systemd_service, 'https/vyos-http-api.service.j2', https['api'])
+ if 'api' in https:
+ render(systemd_service_api, 'https/vyos-http-api.service.j2', https)
with open(api_config_state, 'w') as f:
json.dump(https['api'], f, indent=2)
-
- server_block_list = []
-
- # organize by vhosts
-
- vhost_dict = https.get('virtual-host', {})
-
- if not vhost_dict:
- # no specified virtual hosts (server blocks); use default
- server_block_list.append(default_server_block)
else:
- for vhost in list(vhost_dict):
- server_block = deepcopy(default_server_block)
- server_block['id'] = vhost
- data = vhost_dict.get(vhost, {})
- server_block['address'] = data.get('listen-address', '*')
- server_block['port'] = data.get('port', '443')
- name = data.get('server-name', ['_'])
- server_block['name'] = name
- allow_client = data.get('allow-client', {})
- server_block['allow_client'] = allow_client.get('address', [])
- server_block_list.append(server_block)
+ if os.path.exists(systemd_service_api):
+ os.unlink(systemd_service_api)
# get certificate data
-
- cert_dict = https.get('certificates', {})
-
- if 'certificate' in cert_dict:
- cert_name = cert_dict['certificate']
+ if 'certificates' in https and 'certificate' in https['certificates']:
+ cert_name = https['certificates']['certificate']
pki_cert = https['pki']['certificate'][cert_name]
- cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
- key_path = os.path.join(key_dir, f'{cert_name}.pem')
+ cert_path = os.path.join(cert_dir, f'{cert_name}_cert.pem')
+ key_path = os.path.join(cert_dir, f'{cert_name}_key.pem')
server_cert = str(wrap_certificate(pki_cert['certificate']))
- if 'ca-certificate' in cert_dict:
- ca_cert = cert_dict['ca-certificate']
- server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
- write_file(cert_path, server_cert)
- write_file(key_path, wrap_private_key(pki_cert['private']['key']))
+ # Append CA certificate if specified to form a full chain
+ if 'ca_certificate' in https['certificates']:
+ ca_cert = https['certificates']['ca_certificate']
+ server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
- vyos_cert_data = {
- 'crt': cert_path,
- 'key': key_path
- }
+ write_file(cert_path, server_cert, user=user, group=group, mode=0o644)
+ write_file(key_path, wrap_private_key(pki_cert['private']['key']),
+ user=user, group=group, mode=0o600)
- for block in server_block_list:
- block['vyos_cert'] = vyos_cert_data
+ tmp_path = {'cert_path': cert_path, 'key_path': key_path}
- if 'api' in list(https):
- vhost_list = https.get('api-restrict', {}).get('virtual-host', [])
- if not vhost_list:
- for block in server_block_list:
- block['api'] = True
- else:
- for block in server_block_list:
- if block['id'] in vhost_list:
- block['api'] = True
+ if 'dh_params' in https['certificates']:
+ dh_name = https['certificates']['dh_params']
+ pki_dh = https['pki']['dh'][dh_name]
+ if 'parameters' in pki_dh:
+ dh_path = os.path.join(cert_dir, f'{dh_name}_dh.pem')
+ write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']),
+ user=user, group=group, mode=0o600)
+ tmp_path.update({'dh_file' : dh_path})
- data = {
- 'server_block_list': server_block_list,
- }
+ https['certificates'].update(tmp_path)
- render(config_file, 'https/nginx.default.j2', data)
+ render(config_file, 'https/nginx.default.j2', https)
render(systemd_override, 'https/override.conf.j2', https)
return None
@@ -273,7 +222,7 @@ def apply(https):
call(f'systemctl reload-or-restart {http_api_service_name}')
# Let uvicorn settle before (possibly) restarting nginx
sleep(1)
- else:
+ elif is_systemd_service_active(http_api_service_name):
call(f'systemctl stop {http_api_service_name}')
call(f'systemctl reload-or-restart {https_service_name}')
diff --git a/src/etc/systemd/system/nginx.service.d/10-override.conf b/src/etc/systemd/system/nginx.service.d/10-override.conf
new file mode 100644
index 000000000..1be5cec81
--- /dev/null
+++ b/src/etc/systemd/system/nginx.service.d/10-override.conf
@@ -0,0 +1,3 @@
+[Unit]
+After=
+After=vyos-router.service
diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py
index 01b06526d..42de696ce 100755
--- a/src/helpers/vyos-boot-config-loader.py
+++ b/src/helpers/vyos-boot-config-loader.py
@@ -102,7 +102,8 @@ def failsafe(config_file_name):
'authentication',
'encrypted-password'])
- cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos")
+ cmd(f"useradd --create-home --no-user-group --shell /bin/vbash --password '{passwd}' "\
+ "--groups frr,frrvty,vyattacfg,sudo,adm,dip,disk vyos")
if __name__ == '__main__':
if len(sys.argv) < 2:
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
new file mode 100755
index 000000000..908420c18
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -0,0 +1,69 @@
+#!/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
+
+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']
+
+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 + [network, 'subnet', subnet, option]):
+ config.set(base + [network, 'subnet', subnet, 'option'])
+ config.copy(base + [network, 'subnet', subnet, option], base + [network, 'subnet', subnet, 'option', option])
+ config.delete(base + [network, 'subnet', subnet, option])
+
+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/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6
index b4159f02f..6d6efd32c 100755
--- a/src/migration-scripts/https/5-to-6
+++ b/src/migration-scripts/https/5-to-6
@@ -16,12 +16,14 @@
# T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot
# to new "pki certificate" CLI tree
+# T5902: Remove virtual-host
import os
import sys
from vyos.configtree import ConfigTree
from vyos.defaults import directories
+from vyos.utils.process import cmd
vyos_certbot_dir = directories['certbot']
@@ -36,30 +38,68 @@ with open(file_name, 'r') as f:
config = ConfigTree(config_file)
-base = ['service', 'https', 'certificates']
+base = ['service', 'https']
if not config.exists(base):
# Nothing to do
sys.exit(0)
-# both domain-name and email must be set on CLI - ensured by previous verify()
-domain_names = config.return_values(base + ['certbot', 'domain-name'])
-email = config.return_value(base + ['certbot', 'email'])
-config.delete(base)
-
-# Set default certname based on domain-name
-cert_name = 'https-' + domain_names[0].split('.')[0]
-# Overwrite certname from previous certbot calls if available
-if os.path.exists(f'{vyos_certbot_dir}/live'):
- for cert in [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()]:
- cert_name = cert
- break
-
-for domain in domain_names:
- config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False)
+if config.exists(base + ['certificates']):
+ # both domain-name and email must be set on CLI - ensured by previous verify()
+ domain_names = config.return_values(base + ['certificates', 'certbot', 'domain-name'])
+ email = config.return_value(base + ['certificates', 'certbot', 'email'])
+ config.delete(base + ['certificates'])
+
+ # Set default certname based on domain-name
+ cert_name = 'https-' + domain_names[0].split('.')[0]
+ # Overwrite certname from previous certbot calls if available
+ # We can not use python code like os.scandir due to filesystem permissions.
+ # This must be run as root
+ certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing /
+ if os.path.exists(certbot_live):
+ tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d')
+ tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net']
+ tmp.remove(certbot_live)
+ cert_name = tmp[0].replace(certbot_live, '')
+
config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email)
+ config.set_tag(['pki', 'certificate'])
+ for domain in domain_names:
+ config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False)
+
+ # Update Webserver certificate
+ config.set(base + ['certificates', 'certificate'], value=cert_name)
+
+if config.exists(base + ['virtual-host']):
+ allow_client = []
+ listen_port = []
+ listen_address = []
+ for virtual_host in config.list_nodes(base + ['virtual-host']):
+ allow_path = base + ['virtual-host', virtual_host, 'allow-client', 'address']
+ if config.exists(allow_path):
+ tmp = config.return_values(allow_path)
+ allow_client.extend(tmp)
+
+ port_path = base + ['virtual-host', virtual_host, 'listen-port']
+ if config.exists(port_path):
+ tmp = config.return_value(port_path)
+ listen_port.append(tmp)
+
+ listen_address_path = base + ['virtual-host', virtual_host, 'listen-address']
+ if config.exists(listen_address_path):
+ tmp = config.return_value(listen_address_path)
+ listen_address.append(tmp)
+
+ config.delete(base + ['virtual-host'])
+ for client in allow_client:
+ config.set(base + ['allow-client', 'address'], value=client, replace=False)
+
+ # clear listen-address if "all" were specified
+ if '*' in listen_address:
+ listen_address = []
+ for address in listen_address:
+ config.set(base + ['listen-address'], value=address, replace=False)
+
-# Update Webserver certificate
-config.set(base + ['certificate'], value=cert_name)
try:
with open(file_name, 'w') as f:
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e75485f9f..e64a85b95 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'
MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'
MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first'
+def annotated_list(images_list: list[str]) -> list[str]:
+ """Annotate list of images with additional info
+
+ Args:
+ images_list (list[str]): a list of image names
+
+ Returns:
+ list[str]: a list of image names with additional info
+ """
+ index_running: int = None
+ index_default: int = None
+ try:
+ index_running = images_list.index(image.get_running_image())
+ index_default = images_list.index(image.get_default_image())
+ except ValueError:
+ pass
+ if index_running is not None:
+ images_list[index_running] += ' (running)'
+ if index_default is not None:
+ images_list[index_default] += ' (default boot)'
+ return images_list
@compat.grub_cfg_update
def delete_image(image_name: Optional[str] = None,
@@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None,
Args:
image_name (str): a name of image to delete
"""
- available_images: list[str] = grub.version_list()
+ available_images: list[str] = annotated_list(grub.version_list())
if image_name is None:
if no_prompt:
exit('An image name is required for delete action')
@@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None,
Args:
image_name (str): an image name
"""
- available_images: list[str] = grub.version_list()
+ available_images: list[str] = annotated_list(grub.version_list())
if image_name is None:
if not prompt:
exit('An image name is required for set action')
diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py
new file mode 100644
index 000000000..d24b1065b
--- /dev/null
+++ b/src/op_mode/zone.py
@@ -0,0 +1,215 @@
+#!/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 typing
+import sys
+import vyos.opmode
+
+import tabulate
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search
+
+
+def get_config_zone(conf, name=None):
+ config_path = ['firewall', 'zone']
+ if name:
+ config_path += [name]
+
+ zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ return zone_policy
+
+
+def _convert_one_zone_data(zone: str, zone_config: dict) -> dict:
+ """
+ Convert config dictionary of one zone to API dictionary
+ :param zone: Zone name
+ :type zone: str
+ :param zone_config: config dictionary
+ :type zone_config: dict
+ :return: AP dictionary
+ :rtype: dict
+ """
+ list_of_rules = []
+ intrazone_dict = {}
+ if dict_search('from', zone_config):
+ for from_zone, from_zone_config in zone_config['from'].items():
+ from_zone_dict = {'name': from_zone}
+ if dict_search('firewall.name', from_zone_config):
+ from_zone_dict['firewall'] = dict_search('firewall.name',
+ from_zone_config)
+ if dict_search('firewall.ipv6_name', from_zone_config):
+ from_zone_dict['firewall_v6'] = dict_search(
+ 'firewall.ipv6_name', from_zone_config)
+ list_of_rules.append(from_zone_dict)
+
+ zone_dict = {
+ 'name': zone,
+ 'interface': dict_search('interface', zone_config),
+ 'type': 'LOCAL' if dict_search('local_zone',
+ zone_config) is not None else None,
+ }
+ if list_of_rules:
+ zone_dict['from'] = list_of_rules
+ if dict_search('intra_zone_filtering.firewall.name', zone_config):
+ intrazone_dict['firewall'] = dict_search(
+ 'intra_zone_filtering.firewall.name', zone_config)
+ if dict_search('intra_zone_filtering.firewall.ipv6_name', zone_config):
+ intrazone_dict['firewall_v6'] = dict_search(
+ 'intra_zone_filtering.firewall.ipv6_name', zone_config)
+ if intrazone_dict:
+ zone_dict['intrazone'] = intrazone_dict
+ return zone_dict
+
+
+def _convert_zones_data(zone_policies: dict) -> list:
+ """
+ Convert all config dictionary to API list of zone dictionaries
+ :param zone_policies: config dictionary
+ :type zone_policies: dict
+ :return: API list
+ :rtype: list
+ """
+ zone_list = []
+ for zone, zone_config in zone_policies.items():
+ zone_list.append(_convert_one_zone_data(zone, zone_config))
+ return zone_list
+
+
+def _convert_config(zones_config: dict, zone: str = None) -> list:
+ """
+ convert config to API list
+ :param zones_config: zones config
+ :type zones_config:
+ :param zone: zone name
+ :type zone: str
+ :return: API list
+ :rtype: list
+ """
+ if zone:
+ if zones_config:
+ output = [_convert_one_zone_data(zone, zones_config)]
+ else:
+ raise vyos.opmode.DataUnavailable(f'Zone {zone} not found')
+ else:
+ if zones_config:
+ output = _convert_zones_data(zones_config)
+ else:
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'Zone entries are not configured')
+ return output
+
+
+def output_zone_list(zone_conf: dict) -> list:
+ """
+ Format one zone row
+ :param zone_conf: zone config
+ :type zone_conf: dict
+ :return: formatted list of zones
+ :rtype: list
+ """
+ zone_info = [zone_conf['name']]
+ if zone_conf['type'] == 'LOCAL':
+ zone_info.append('LOCAL')
+ else:
+ zone_info.append("\n".join(zone_conf['interface']))
+
+ from_zone = []
+ firewall = []
+ firewall_v6 = []
+ if 'intrazone' in zone_conf:
+ from_zone.append(zone_conf['name'])
+
+ v4_name = dict_search_args(zone_conf['intrazone'], 'firewall')
+ v6_name = dict_search_args(zone_conf['intrazone'], 'firewall_v6')
+ if v4_name:
+ firewall.append(v4_name)
+ else:
+ firewall.append('')
+ if v6_name:
+ firewall_v6.append(v6_name)
+ else:
+ firewall_v6.append('')
+
+ if 'from' in zone_conf:
+ for from_conf in zone_conf['from']:
+ from_zone.append(from_conf['name'])
+
+ v4_name = dict_search_args(from_conf, 'firewall')
+ v6_name = dict_search_args(from_conf, 'firewall_v6')
+ if v4_name:
+ firewall.append(v4_name)
+ else:
+ firewall.append('')
+ if v6_name:
+ firewall_v6.append(v6_name)
+ else:
+ firewall_v6.append('')
+
+ zone_info.append("\n".join(from_zone))
+ zone_info.append("\n".join(firewall))
+ zone_info.append("\n".join(firewall_v6))
+ return zone_info
+
+
+def get_formatted_output(zone_policy: list) -> str:
+ """
+ Formatted output of all zones
+ :param zone_policy: list of zones
+ :type zone_policy: list
+ :return: formatted table with zones
+ :rtype: str
+ """
+ headers = ["Zone",
+ "Interfaces",
+ "From Zone",
+ "Firewall IPv4",
+ "Firewall IPv6"
+ ]
+ formatted_list = []
+ for zone_conf in zone_policy:
+ formatted_list.append(output_zone_list(zone_conf))
+ tabulate.PRESERVE_WHITESPACE = True
+ output = tabulate.tabulate(formatted_list, headers, numalign="left")
+ return output
+
+
+def show(raw: bool, zone: typing.Optional[str]):
+ """
+ Show zone-policy command
+ :param raw: if API
+ :type raw: bool
+ :param zone: zone name
+ :type zone: str
+ """
+ conf: ConfigTreeQuery = ConfigTreeQuery()
+ zones_config: dict = get_config_zone(conf, zone)
+ zone_policy_api: list = _convert_config(zones_config, zone)
+ if raw:
+ return zone_policy_api
+ else:
+ return get_formatted_output(zone_policy_api)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1) \ No newline at end of file
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index b64e58132..40d442e30 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,6 +1,6 @@
#!/usr/share/vyos-http-api-tools/bin/python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright (C) 2019-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
@@ -13,8 +13,6 @@
#
# 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 sys
@@ -25,6 +23,7 @@ import logging
import signal
import traceback
import threading
+
from time import sleep
from typing import List, Union, Callable, Dict
@@ -46,11 +45,12 @@ from ariadne.asgi import GraphQL
from vyos.config import Config
from vyos.configtree import ConfigTree
from vyos.configdiff import get_config_diff
-from vyos.configsession import ConfigSession, ConfigSessionError
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
+from vyos.defaults import api_config_state
import api.graphql.state
-api_config_state = '/run/http-api-state'
CFG_GROUP = 'vyattacfg'
debug = True
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 03574bdc3..e1a9f1884 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -15,28 +15,71 @@ if [ $# -lt 1 ]; then
fi
action=$1
-client_name=$LEASE4_HOSTNAME
-client_ip=$LEASE4_ADDRESS
-client_mac=$LEASE4_HWADDR
hostsd_client="/usr/bin/vyos-hostsd-client"
-case "$action" in
- lease4_renew|lease4_recover) # add mapping for new/recovered lease address
- if [ -z "$client_name" ]; then
- logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
- client_name=$(echo "host-$client_mac" | tr : -)
- fi
+get_subnet_domain_name () {
+ python3 <<EOF
+from vyos.kea import kea_get_active_config
+from vyos.utils.dict import dict_search_args
+
+config = kea_get_active_config('4')
+shared_networks = dict_search_args(config, 'arguments', f'Dhcp4', 'shared-networks')
+
+found = False
- $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+if shared_networks:
+ for network in shared_networks:
+ for subnet in network[f'subnet4']:
+ if subnet['id'] == $1:
+ for option in subnet['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+ found = True
+
+ if not found:
+ for option in network['option-data']:
+ if option['name'] == 'domain-name':
+ print(option['data'])
+EOF
+}
+
+case "$action" in
+ lease4_renew|lease4_recover)
exit 0
;;
lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address
+ client_ip=$LEASE4_ADDRESS
$hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
exit 0
;;
- leases4_committed) # nothing to do
+ leases4_committed) # process committed leases (added/renewed/recovered)
+ for ((i = 0; i < $LEASES4_SIZE; i++)); do
+ client_ip_var="LEASES4_AT${i}_ADDRESS"
+ client_mac_var="LEASES4_AT${i}_HWADDR"
+ client_name_var="LEASES4_AT${i}_HOSTNAME"
+ client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID"
+
+ client_ip=${!client_ip_var}
+ client_mac=${!client_mac_var}
+ client_name=${!client_name_var}
+ client_subnet_id=${!client_subnet_id_var}
+
+ if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "host-$client_mac" | tr : -)
+ fi
+
+ client_domain=$(get_subnet_domain_name $client_subnet_id)
+
+ if [ -n "$client_domain" ]; then
+ client_name="$client_name.$client_domain"
+ fi
+
+ $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ done
+
exit 0
;;