diff --git a/kea-dhcp/acl_security.pl b/kea-dhcp/acl_security.pl new file mode 100644 index 000000000..d663e4b85 --- /dev/null +++ b/kea-dhcp/acl_security.pl @@ -0,0 +1,42 @@ +use strict; +use warnings; +no warnings 'redefine'; +no warnings 'uninitialized'; + +require 'kea-dhcp-lib.pl'; ## no critic + +our (%in, %text); + +# acl_security_form(&options) +# Output HTML for editing security options for the Kea DHCP module +sub acl_security_form +{ +my ($o) = @_; + +# Keep read-only capabilities separate from actions that change files or daemon +# state, matching the way the module's tabs and buttons are gated. +print ui_table_span(&ui_tag('b', &html_escape($text{'acl_section_view'}))); +foreach my $a (qw(dhcp4 dhcp6 ddns services runtime)) { + print ui_table_row($text{'acl_'.$a}, + ui_yesno_radio($a, kea_check_acl($a, $o)), 3); + } +print ui_table_hr(); +print ui_table_span(&ui_tag('b', &html_escape($text{'acl_section_change'}))); +foreach my $a (qw(edit4 edit6 editddns manual apply install)) { + print ui_table_row($text{'acl_'.$a}, + ui_yesno_radio($a, kea_check_acl($a, $o)), 3); + } +} + +# acl_security_save(&options) +# Parse the form for security options for the Kea DHCP module +sub acl_security_save +{ +my ($o) = @_; + +foreach my $a (kea_acl_keys()) { + $o->{$a} = $in{$a} || 0; + } +} + +1; diff --git a/kea-dhcp/backup_config.pl b/kea-dhcp/backup_config.pl new file mode 100644 index 000000000..472c4a54f --- /dev/null +++ b/kea-dhcp/backup_config.pl @@ -0,0 +1,46 @@ + +use strict; +use warnings; +require 'kea-dhcp-lib.pl'; + +# backup_config_files() +# Returns files that can be backed up. +sub backup_config_files +{ +return &get_all_config_files(); +} + +# pre_backup() +# Runs before Webmin backs up Kea configuration files. +sub pre_backup +{ +# No pre-backup daemon action is needed; Kea configs are ordinary files. +return; +} + +# post_backup() +# Runs after Webmin completes a Kea configuration backup. +sub post_backup +{ +# Backups are read-only, so leave running services untouched. +return; +} + +# pre_restore() +# Runs before Webmin restores Kea configuration files. +sub pre_restore +{ +# Restore writes happen before service reload, so there is nothing to prepare. +return; +} + +# post_restore() +# Runs after Webmin restores Kea configuration files. +sub post_restore +{ +# If Kea was active before restore, reload it so restored files take effect. +return &kea_run_action('restart') if (&kea_running_pids()); +return; +} + +1; diff --git a/kea-dhcp/config b/kea-dhcp/config new file mode 100644 index 000000000..3db87310c --- /dev/null +++ b/kea-dhcp/config @@ -0,0 +1,22 @@ +dhcp4_conf=/etc/kea/kea-dhcp4.conf +dhcp6_conf=/etc/kea/kea-dhcp6.conf +ddns_conf=/etc/kea/kea-dhcp-ddns.conf +ctrl_agent_conf=/etc/kea/kea-ctrl-agent.conf +dhcp4_path=/usr/sbin/kea-dhcp4 +dhcp6_path=/usr/sbin/kea-dhcp6 +ddns_path=/usr/sbin/kea-dhcp-ddns +ctrl_agent_path=/usr/sbin/kea-ctrl-agent +keactrl_path=/usr/sbin/keactrl +dhcp4_lease_file=/var/lib/kea/kea-leases4.csv +dhcp6_lease_file=/var/lib/kea/kea-leases6.csv +dhcp4_pid_file=/run/kea/kea-dhcp4.kea-dhcp4.pid +dhcp6_pid_file=/run/kea/kea-dhcp6.kea-dhcp6.pid +ddns_pid_file=/run/kea/kea-dhcp-ddns.kea-dhcp-ddns.pid +ctrl_agent_pid_file=/run/kea/kea-ctrl-agent.kea-ctrl-agent.pid +dhcp4_unit=kea-dhcp4-server.service +dhcp6_unit=kea-dhcp6-server.service +ddns_unit=kea-dhcp-ddns-server.service +ctrl_agent_unit=kea-ctrl-agent.service +start_cmd=systemctl start kea-dhcp4-server.service kea-dhcp6-server.service kea-dhcp-ddns-server.service kea-ctrl-agent.service +stop_cmd=systemctl stop kea-dhcp4-server.service kea-dhcp6-server.service kea-dhcp-ddns-server.service kea-ctrl-agent.service +restart_cmd=systemctl restart kea-dhcp4-server.service kea-dhcp6-server.service kea-dhcp-ddns-server.service kea-ctrl-agent.service diff --git a/kea-dhcp/config-freebsd b/kea-dhcp/config-freebsd new file mode 100644 index 000000000..0d28c672a --- /dev/null +++ b/kea-dhcp/config-freebsd @@ -0,0 +1,22 @@ +dhcp4_conf=/usr/local/etc/kea/kea-dhcp4.conf +dhcp6_conf=/usr/local/etc/kea/kea-dhcp6.conf +ddns_conf=/usr/local/etc/kea/kea-dhcp-ddns.conf +ctrl_agent_conf=/usr/local/etc/kea/kea-ctrl-agent.conf +dhcp4_path=/usr/local/sbin/kea-dhcp4 +dhcp6_path=/usr/local/sbin/kea-dhcp6 +ddns_path=/usr/local/sbin/kea-dhcp-ddns +ctrl_agent_path=/usr/local/sbin/kea-ctrl-agent +keactrl_path=/usr/local/sbin/keactrl +dhcp4_lease_file=/var/db/kea/kea-leases4.csv +dhcp6_lease_file=/var/db/kea/kea-leases6.csv +dhcp4_pid_file=/var/run/kea/kea-dhcp4.kea-dhcp4.pid +dhcp6_pid_file=/var/run/kea/kea-dhcp6.kea-dhcp6.pid +ddns_pid_file=/var/run/kea/kea-dhcp-ddns.kea-dhcp-ddns.pid +ctrl_agent_pid_file=/var/run/kea/kea-ctrl-agent.kea-ctrl-agent.pid +dhcp4_unit= +dhcp6_unit= +ddns_unit= +ctrl_agent_unit= +start_cmd=/usr/local/sbin/keactrl start +stop_cmd=/usr/local/sbin/keactrl stop +restart_cmd=/usr/local/sbin/keactrl reload diff --git a/kea-dhcp/config-redhat-linux b/kea-dhcp/config-redhat-linux new file mode 100644 index 000000000..fde84f112 --- /dev/null +++ b/kea-dhcp/config-redhat-linux @@ -0,0 +1,22 @@ +dhcp4_conf=/etc/kea/kea-dhcp4.conf +dhcp6_conf=/etc/kea/kea-dhcp6.conf +ddns_conf=/etc/kea/kea-dhcp-ddns.conf +ctrl_agent_conf=/etc/kea/kea-ctrl-agent.conf +dhcp4_path=/usr/sbin/kea-dhcp4 +dhcp6_path=/usr/sbin/kea-dhcp6 +ddns_path=/usr/sbin/kea-dhcp-ddns +ctrl_agent_path=/usr/sbin/kea-ctrl-agent +keactrl_path=/usr/sbin/keactrl +dhcp4_lease_file=/var/lib/kea/kea-leases4.csv +dhcp6_lease_file=/var/lib/kea/kea-leases6.csv +dhcp4_pid_file=/run/kea/kea-dhcp4.kea-dhcp4.pid +dhcp6_pid_file=/run/kea/kea-dhcp6.kea-dhcp6.pid +ddns_pid_file=/run/kea/kea-dhcp-ddns.kea-dhcp-ddns.pid +ctrl_agent_pid_file=/run/kea/kea-ctrl-agent.kea-ctrl-agent.pid +dhcp4_unit=kea-dhcp4.service +dhcp6_unit=kea-dhcp6.service +ddns_unit=kea-dhcp-ddns.service +ctrl_agent_unit=kea-ctrl-agent.service +start_cmd=systemctl start kea-dhcp4.service kea-dhcp6.service kea-dhcp-ddns.service kea-ctrl-agent.service +stop_cmd=systemctl stop kea-dhcp4.service kea-dhcp6.service kea-dhcp-ddns.service kea-ctrl-agent.service +restart_cmd=systemctl restart kea-dhcp4.service kea-dhcp6.service kea-dhcp-ddns.service kea-ctrl-agent.service diff --git a/kea-dhcp/config.info b/kea-dhcp/config.info new file mode 100644 index 000000000..d74e28106 --- /dev/null +++ b/kea-dhcp/config.info @@ -0,0 +1,28 @@ +line1=Configuration files,11 +dhcp4_conf=Kea DHCPv4 config file,0 +dhcp6_conf=Kea DHCPv6 config file,0 +ddns_conf=Kea DHCP-DDNS config file,0 +ctrl_agent_conf=Kea Control Agent config file,0 +line2=Executables,11 +dhcp4_path=Kea DHCPv4 executable,0 +dhcp6_path=Kea DHCPv6 executable,0 +ddns_path=Kea DHCP-DDNS executable,0 +ctrl_agent_path=Kea Control Agent executable,0 +keactrl_path=Kea control script,0 +line3=Lease files,11 +dhcp4_lease_file=Kea DHCPv4 memfile leases file,0 +dhcp6_lease_file=Kea DHCPv6 memfile leases file,0 +line4=PID files,11 +dhcp4_pid_file=Kea DHCPv4 PID file,3,None +dhcp6_pid_file=Kea DHCPv6 PID file,3,None +ddns_pid_file=Kea DHCP-DDNS PID file,3,None +ctrl_agent_pid_file=Kea Control Agent PID file,3,None +line5=Systemd units,11 +dhcp4_unit=Kea DHCPv4 systemd unit,0 +dhcp6_unit=Kea DHCPv6 systemd unit,0 +ddns_unit=Kea DHCP-DDNS systemd unit,0 +ctrl_agent_unit=Kea Control Agent systemd unit,0 +line6=Commands,11 +start_cmd=Command to start Kea services,3,Use systemd service units +stop_cmd=Command to stop Kea services,3,Use systemd service units +restart_cmd=Command to apply Kea configuration,3,Restart systemd service units diff --git a/kea-dhcp/defaultacl b/kea-dhcp/defaultacl new file mode 100644 index 000000000..04e4e6352 --- /dev/null +++ b/kea-dhcp/defaultacl @@ -0,0 +1,11 @@ +dhcp4=1 +dhcp6=1 +ddns=1 +services=1 +runtime=1 +edit4=1 +edit6=1 +editddns=1 +manual=1 +apply=1 +install=1 diff --git a/kea-dhcp/delete_objects.cgi b/kea-dhcp/delete_objects.cgi new file mode 100755 index 000000000..758434fd6 --- /dev/null +++ b/kea-dhcp/delete_objects.cgi @@ -0,0 +1,53 @@ +#!/usr/local/bin/perl +# Delete selected Kea DHCP subnets and shared networks. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); + +my $ver = $in{'version'} == 6 ? 6 : 4; +&kea_assert_acl('edit'.$ver); +my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver); +&error($err) if ($err); + +&error_setup($text{'delete_failsave'}); +my @shared = split(/\0/, defined($in{'d_shared'}) ? $in{'d_shared'} : ""); +my @subnets = split(/\0/, defined($in{'d_subnet'}) ? $in{'d_subnet'} : ""); +@shared || @subnets || &error($text{'delete_enone'}); + +# Group subnet deletions by parent so indexes can be removed descending within +# each array without disturbing later deletions. +my %subnets_by_parent; +foreach my $v (@subnets) { + $v =~ /^(\d*):(\d+)$/ || &error($text{'subnet_enone'}); + my ($sidx, $idx) = ($1, $2); + &error($text{'subnet_enone'}) + if (!&kea_valid_subnet_parent($root, $sidx)); + my $list = &kea_subnet_list($root, $ver, $sidx); + &error($text{'subnet_enone'}) if (!$list->[$idx]); + push(@{$subnets_by_parent{$sidx}}, $idx); + } +foreach my $sidx (keys %subnets_by_parent) { + my $list = &kea_subnet_list($root, $ver, $sidx); + foreach my $idx (sort { $b <=> $a } @{$subnets_by_parent{$sidx}}) { + splice(@$list, $idx, 1); + } + } + +# Shared networks are top-level siblings, so delete them after nested subnets. +my $shareds = &kea_shared_networks($root); +foreach my $idx (sort { $b <=> $a } @shared) { + $idx =~ /^\d+$/ || &error($text{'shared_enone'}); + &error($text{'shared_enone'}) if (!$shareds->[$idx]); + my $subs = &kea_subnet_list($root, $ver, $idx); + &error($text{'shared_enonempty'}) if (@$subs); + splice(@$shareds, $idx, 1); + } + +my $saveerr = &kea_save_component_config($c, $data); +&error($saveerr) if ($saveerr); +&webmin_log("delete", "objects", scalar(@shared) + scalar(@subnets), \%in); +&redirect("index.cgi?mode=dhcp$ver"); diff --git a/kea-dhcp/edit_ddns.cgi b/kea-dhcp/edit_ddns.cgi new file mode 100755 index 000000000..5f63dccbe --- /dev/null +++ b/kea-dhcp/edit_ddns.cgi @@ -0,0 +1,100 @@ +#!/usr/local/bin/perl +# Edit settings for the Kea DHCP-DDNS daemon. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); +&kea_assert_acl('editddns'); + +my $c = &kea_component('ddns'); +my ($root, $err) = &kea_read_component_config($c); +&error($err) if ($err); + +# D2 is a separate daemon shared by DHCPv4 and DHCPv6, so keep its settings +# outside the protocol-specific global DHCP pages. +&ui_print_header(undef, $text{'ddns_title'}, "", undef, 1, 1); +print &kea_comment_loss_warning($c); +print &ui_form_start("save_ddns.cgi", "post"); + +my @tabs = ( + [ 'listener', $text{'tab_listener'} ], + [ 'zones', $text{'tab_zones'} ], + [ 'tsig', $text{'tab_tsig'} ], + [ 'logging', $text{'tab_logging'} ], + ); +print &ui_tabs_start(\@tabs, "mode", $in{'mode'} || "listener", 1); + +# The listener receives name-change requests from DHCPv4/DHCPv6 and exposes a +# local control socket for daemon management. +print &ui_tabs_start_tab("mode", "listener"); +print &ui_div($text{'ddns_listener_desc'}); +print &ui_alert_box($text{'ddns_listener_warn'}, "warn", undef, undef, "") + if (&kea_ddns_listener_non_loopback($root)); +print &ui_alert_box($text{'ddns_listener_warn_loopback'}, "warn", undef, undef, "") + if (!&kea_ddns_listener_non_loopback($root) && + &kea_ddns_listener_non_default_loopback($root)); +print &ui_table_start($text{'ddns_listener'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('ddns-ip-address', + $text{'ddns_ip_address'}), + &ui_textbox("ip_address", $root->{'ip-address'} || "", 24)); +print &ui_table_row(&kea_field_hlink('ddns-port', $text{'ddns_port'}), + &ui_textbox("port", defined($root->{'port'}) ? $root->{'port'} : "", + 8)); +print &ui_table_row(&kea_field_hlink('ddns-timeout', $text{'ddns_timeout'}), + &ui_textbox("dns_server_timeout", + defined($root->{'dns-server-timeout'}) ? + $root->{'dns-server-timeout'} : "", 8)); +print &ui_table_row(&kea_field_hlink('ncr-protocol', + $text{'ddns_ncr_protocol'}), + &ui_select("ncr_protocol", $root->{'ncr-protocol'} || "UDP", + &kea_select_options($root->{'ncr-protocol'}, $text{'socket_default'}, + 'UDP'))); +print &ui_table_row(&kea_field_hlink('ncr-format', $text{'ddns_ncr_format'}), + &ui_select("ncr_format", $root->{'ncr-format'} || "JSON", + &kea_select_options($root->{'ncr-format'}, $text{'socket_default'}, + 'JSON'))); +print &ui_table_end(); + +my $socket = ref($root->{'control-socket'}) eq 'HASH' ? + $root->{'control-socket'} : { }; +print &ui_table_start($text{'control_socket'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('control-socket-type', + $text{'control_socket_type'}), + &ui_select("control_socket_type", $socket->{'socket-type'} || "", + [ [ "", $text{'socket_default'} ], + [ "unix", "Unix" ] ])); +print &ui_table_row(&kea_field_hlink('control-socket-name', + $text{'control_socket_name'}), + &ui_textbox("control_socket_name", $socket->{'socket-name'} || "", 50)); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "listener"); + +# Forward and reverse DDNS domains tell D2 where to send DNS updates. +print &ui_tabs_start_tab("mode", "zones"); +print &ui_div($text{'ddns_zones_desc'}); +print &ui_subheading($text{'ddns_forward'}); +&kea_ddns_domain_rows(&kea_ddns_domains($root, 'forward-ddns'), "fwd_"); +print &ui_subheading($text{'ddns_reverse'}); +&kea_ddns_domain_rows(&kea_ddns_domains($root, 'reverse-ddns'), "rev_"); +print &ui_tabs_end_tab("mode", "zones"); + +# TSIG keys are referenced by update domains using key-name. +print &ui_tabs_start_tab("mode", "tsig"); +print &ui_div($text{'ddns_tsig_desc'}); +print &ui_subheading($text{'ddns_tsig_keys'}); +&kea_tsig_key_rows($root->{'tsig-keys'}, "key_"); +print &ui_tabs_end_tab("mode", "tsig"); + +# D2 uses the same Kea logger format as the DHCP daemons. +print &ui_tabs_start_tab("mode", "logging"); +print &ui_div($text{'logging_desc'}); +print &ui_subheading($text{'logging_loggers'}); +&kea_logger_rows($root->{'loggers'}, "log_"); +print &ui_tabs_end_tab("mode", "logging"); +print &ui_tabs_end(); + +print &ui_form_end([ [ "save", $text{'save'} ] ]); +&ui_print_footer("index.cgi?mode=ddns", $text{'index_return'}); diff --git a/kea-dhcp/edit_options.cgi b/kea-dhcp/edit_options.cgi new file mode 100755 index 000000000..1e735446d --- /dev/null +++ b/kea-dhcp/edit_options.cgi @@ -0,0 +1,245 @@ +#!/usr/local/bin/perl +# Edit global Kea DHCP options for DHCPv4 or DHCPv6. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); + +my $ver = $in{'version'} == 6 ? 6 : 4; +&kea_assert_acl('edit'.$ver); +my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver); +&error($err) if ($err); + +# Render one global settings form for either Dhcp4 or Dhcp6. Each tab writes +# back to the same JSON root object, so the save handler can update all fields +# in one pass without losing hidden tab values. +&ui_print_header(undef, &text('options_title', $ver), "", undef, 1, 1); +print &kea_comment_loss_warning($c); +print &ui_alert_box($text{'dhcp6_ra_warn'}, "warn", undef, undef, "") + if ($ver == 6); +print &ui_form_start("save_options.cgi", "post"); +print &ui_hidden("version", $ver); + +my @tabs = ( + [ 'interfaces', $text{'tab_interfaces'} ], + [ 'storage', $text{'tab_storage'} ], + [ 'logging', $text{'tab_logging'} ], + [ 'ddns_sender', $text{'tab_ddns_sender'} ], + [ 'timers', $text{'tab_timers'} ], + [ 'options', $text{'tab_options'} ], + [ 'advanced', $text{'tab_advanced'} ], + ); +print &ui_tabs_start(\@tabs, "mode", $in{'mode'} || "interfaces", 1); + +# Interfaces decide whether Kea listens at all. The DHCPv4 socket mode is kept +# beside the interface list because it only affects packet capture on DHCPv4. +print &ui_tabs_start_tab("mode", "interfaces"); +print &ui_div($text{'interfaces_desc'}); +my $ifconf = ref($root->{'interfaces-config'}) eq 'HASH' ? + $root->{'interfaces-config'} : { }; +my $ifaces = ref($ifconf->{'interfaces'}) eq 'ARRAY' ? + join(" ", @{$ifconf->{'interfaces'}}) : ""; +print &ui_table_start($text{'interfaces_title'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('interfaces', $text{'interfaces_list'}), + &ui_textbox("interfaces", $ifaces, 60)); +if ($ver == 4) { + print &ui_table_row(&kea_field_hlink('dhcp-socket-type', + $text{'interfaces_socket'}), + &ui_select("dhcp-socket-type", $ifconf->{'dhcp-socket-type'} || "", + [ [ "", $text{'socket_default'} ], + [ "raw", $text{'socket_raw'} ], + [ "udp", $text{'socket_udp'} ] ])); + } +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "interfaces"); + +# Storage and control sockets are global daemon settings, not subnet settings. +print &ui_tabs_start_tab("mode", "storage"); +print &ui_div($text{'storage_desc'}); +my $lease = ref($root->{'lease-database'}) eq 'HASH' ? + $root->{'lease-database'} : { }; +print &ui_table_start($text{'lease_database'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('lease-database-type', $text{'lease_type'}), + &ui_textbox("lease_type", $lease->{'type'} || "", 20)); +print &ui_table_row(&kea_field_hlink('lfc-interval', + $text{'lease_lfc_interval'}), + &ui_textbox("lease_lfc_interval", $lease->{'lfc-interval'} || "", 12)); +print &ui_table_row(&kea_field_hlink('lease-database-name', + $text{'lease_name'}), + &ui_textbox("lease_name", $lease->{'name'} || "", 30)); +print &ui_table_row(&kea_field_hlink('lease-database-host', + $text{'lease_host'}), + &ui_textbox("lease_host", $lease->{'host'} || "", 30)); +print &ui_table_row(&kea_field_hlink('lease-database-port', + $text{'lease_port'}), + &ui_textbox("lease_port", $lease->{'port'} || "", 8)); +print &ui_table_row(&kea_field_hlink('lease-database-user', + $text{'lease_user'}), + &ui_textbox("lease_user", $lease->{'user'} || "", 24)); +my $password_note = $lease->{'password'} ? + " ".&ui_tag('small', $text{'secret_keep_blank'}, { + 'style' => 'color:var(--text-color-light, #777)' }) : ""; +print &ui_table_row(&kea_field_hlink('lease-database-password', + $text{'lease_password'}), + &ui_password("lease_password", "", 24).$password_note); +print &ui_table_end(); + +my $socket = ref($root->{'control-socket'}) eq 'HASH' ? + $root->{'control-socket'} : { }; +print &ui_table_start($text{'control_socket'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('control-socket-type', + $text{'control_socket_type'}), + &ui_select("control_socket_type", $socket->{'socket-type'} || "", + [ [ "", $text{'socket_default'} ], + [ "unix", "Unix" ] ])); +print &ui_table_row(&kea_field_hlink('control-socket-name', + $text{'control_socket_name'}), + &ui_textbox("control_socket_name", $socket->{'socket-name'} || "", 50)); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "storage"); + +# Logger settings live at the daemon root, beside lease database and timers. +print &ui_tabs_start_tab("mode", "logging"); +print &ui_div($text{'logging_desc'}); +print &ui_subheading($text{'logging_loggers'}); +&kea_logger_rows($root->{'loggers'}, "log_"); +print &ui_tabs_end_tab("mode", "logging"); + +# DHCP-DDNS sender settings control whether this daemon submits name-change +# requests to the standalone D2 daemon. They are distinct from D2's own +# listener/zones/keys settings. +print &ui_tabs_start_tab("mode", "ddns_sender"); +print &ui_div(&text('ddns_sender_settings_desc', $ver)); +my $ddns = ref($root->{'dhcp-ddns'}) eq 'HASH' ? + $root->{'dhcp-ddns'} : { }; +my $bool_opts = [ + [ "", $text{'inherit_default'} ], + [ "true", $text{'yes'} ], + [ "false", $text{'no'} ], + ]; +print &ui_table_start($text{'ddns_sender_connectivity'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('ddns-enable-updates', + $text{'ddns_enable_updates'}), + &ui_select("ddns_enable_updates", + &kea_bool_value($ddns->{'enable-updates'}), $bool_opts)); +print &ui_table_row(&kea_field_hlink('ddns-server-ip', + $text{'ddns_server_ip'}), + &ui_textbox("ddns_server_ip", $ddns->{'server-ip'} || "", 24)); +print &ui_table_row(&kea_field_hlink('ddns-server-port', + $text{'ddns_server_port'}), + &ui_textbox("ddns_server_port", + defined($ddns->{'server-port'}) ? $ddns->{'server-port'} : "", + 8)); +print &ui_table_row(&kea_field_hlink('ddns-sender-ip', + $text{'ddns_sender_ip'}), + &ui_textbox("ddns_sender_ip", $ddns->{'sender-ip'} || "", 24)); +print &ui_table_row(&kea_field_hlink('ddns-sender-port', + $text{'ddns_sender_port'}), + &ui_textbox("ddns_sender_port", + defined($ddns->{'sender-port'}) ? $ddns->{'sender-port'} : "", + 8)); +print &ui_table_row(&kea_field_hlink('ddns-max-queue-size', + $text{'ddns_max_queue_size'}), + &ui_textbox("ddns_max_queue_size", + defined($ddns->{'max-queue-size'}) ? + $ddns->{'max-queue-size'} : "", 10)); +print &ui_table_row(&kea_field_hlink('ncr-protocol', + $text{'ddns_ncr_protocol'}), + &ui_select("ddns_ncr_protocol", $ddns->{'ncr-protocol'} || "", + &kea_select_options($ddns->{'ncr-protocol'}, + $text{'socket_default'}, 'UDP'))); +print &ui_table_row(&kea_field_hlink('ncr-format', + $text{'ddns_ncr_format'}), + &ui_select("ddns_ncr_format", $ddns->{'ncr-format'} || "", + &kea_select_options($ddns->{'ncr-format'}, + $text{'socket_default'}, 'JSON'))); +print &ui_table_end(); + +print &ui_table_start($text{'ddns_sender_behavior'}, "width=100%", 4); +my %ddns_bool_labels = ( + 'ddns-send-updates' => $text{'ddns_send_updates'}, + 'ddns-override-no-update' => $text{'ddns_override_no_update'}, + 'ddns-override-client-update' => $text{'ddns_override_client_update'}, + 'ddns-update-on-renew' => $text{'ddns_update_on_renew'}, + ); +foreach my $k ('ddns-send-updates', 'ddns-override-no-update', + 'ddns-override-client-update', 'ddns-update-on-renew') { + print &ui_table_row(&kea_field_hlink($k, $ddns_bool_labels{$k}), + &ui_select($k, &kea_bool_value($root->{$k}), $bool_opts)); + } +print &ui_table_row(&kea_field_hlink('ddns-replace-client-name', + $text{'ddns_replace_client_name'}), + &ui_select("ddns-replace-client-name", + $root->{'ddns-replace-client-name'} || "", + &kea_select_options($root->{'ddns-replace-client-name'}, + $text{'socket_default'}, + 'never', 'when-present', + 'when-not-present', 'always'))); +my %ddns_text_labels = ( + 'ddns-generated-prefix' => $text{'ddns_generated_prefix'}, + 'ddns-qualifying-suffix' => $text{'ddns_qualifying_suffix'}, + 'ddns-conflict-resolution-mode' => $text{'ddns_conflict_resolution_mode'}, + 'hostname-char-set' => $text{'hostname_char_set'}, + 'hostname-char-replacement' => $text{'hostname_char_replacement'}, + ); +foreach my $k ('ddns-generated-prefix', 'ddns-qualifying-suffix', + 'ddns-conflict-resolution-mode', 'hostname-char-set', + 'hostname-char-replacement') { + print &ui_table_row(&kea_field_hlink($k, $ddns_text_labels{$k}), + &ui_textbox($k, defined($root->{$k}) ? $root->{$k} : "", 32)); + } +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "ddns_sender"); + +# Timer defaults apply only when shared networks or subnets do not override. +print &ui_tabs_start_tab("mode", "timers"); +print &ui_div($text{'timers_desc'}); +print &ui_table_start($text{'options_timers'}, "width=100%", 4); +foreach my $k ('renew-timer', 'rebind-timer', 'valid-lifetime', + 'min-valid-lifetime', 'max-valid-lifetime') { + print &ui_table_row(&kea_field_hlink($k), + &ui_textbox($k, defined($root->{$k}) ? $root->{$k} : "", 12)); + } +print &ui_table_row(&kea_field_hlink('preferred-lifetime'), + &ui_textbox("preferred-lifetime", + defined($root->{'preferred-lifetime'}) ? $root->{'preferred-lifetime'} : "", 12)) + if ($ver == 6); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "timers"); + +# Common options get named fields; everything else remains editable in the +# additional option-data table below them. +print &ui_tabs_start_tab("mode", "options"); +print &ui_div($text{'options_desc'}); +&kea_common_option_rows($root->{'option-data'}, $ver, "common_"); +&kea_option_data_section($root->{'option-data'}, "opt_", $ver, 1); +print &ui_tabs_end_tab("mode", "options"); + +# Advanced fields are valid Kea globals but are easy to misuse, so keep them +# away from the everyday options page. +print &ui_tabs_start_tab("mode", "advanced"); +print &ui_div(&text('global_advanced_desc', $ver)); +print &ui_table_start($text{'global_advanced'}, "width=100%", 4); +if ($ver == 4) { + print &ui_table_row(&kea_field_hlink('authoritative'), + &ui_select("authoritative", &kea_bool_value($root->{'authoritative'}), + [ [ "", $text{'inherit_default'} ], + [ "true", $text{'yes'} ], + [ "false", $text{'no'} ] ])); + } +&kea_advanced_option_rows($root->{'option-data'}, $ver, "adv_"); +if ($ver == 4) { + foreach my $k ('next-server', 'server-hostname', 'boot-file-name') { + print &ui_table_row(&kea_field_hlink($k), + &ui_textbox($k, defined($root->{$k}) ? $root->{$k} : "", 40)); + } + } +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "advanced"); +print &ui_tabs_end(); + +print &ui_form_end([ [ "save", $text{'save'} ] ]); +&ui_print_footer("index.cgi?mode=dhcp$ver", $text{'index_return'}); diff --git a/kea-dhcp/edit_shared.cgi b/kea-dhcp/edit_shared.cgi new file mode 100755 index 000000000..cb505c9ae --- /dev/null +++ b/kea-dhcp/edit_shared.cgi @@ -0,0 +1,128 @@ +#!/usr/local/bin/perl +# Edit or create a Kea shared network. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); + +my $ver = $in{'version'} == 6 ? 6 : 4; +&kea_assert_acl('edit'.$ver); +my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver); +&error($err) if ($err); +my $shareds = &kea_shared_networks($root); +&error($text{'shared_enone'}) + if (!$in{'new'} && (!defined($in{'idx'}) || $in{'idx'} !~ /^\d+$/)); +my $shared = $in{'new'} ? { } : $shareds->[$in{'idx'}]; +&error($text{'shared_enone'}) if (!$shared); + +# Shared networks are containers for same-link subnets. New shared networks do +# not show the Subnets tab until they have a stable index to attach subnets to. +my $title = $in{'new'} ? $text{'shared_create'} : $text{'shared_edit'}; +&ui_print_header(undef, $title, "", undef, 1, 1); +print &kea_comment_loss_warning($c); +print &ui_form_start("save_shared.cgi", "post"); +print &ui_hidden("version", $ver); +print &ui_hidden("new", 1) if ($in{'new'}); +print &ui_hidden("idx", $in{'idx'}) if (!$in{'new'}); + +my @tabs = ( + [ 'general', $text{'tab_general'} ], + [ 'options', $text{'tab_options'} ], + [ 'advanced', $text{'tab_advanced'} ], + ); +splice(@tabs, 1, 0, [ 'subnets', $text{'tab_subnets'} ]) + if (!$in{'new'}); +my $mode = $in{'mode'} || "general"; +$mode = "general" if ($in{'new'} && $mode eq "subnets"); +print &ui_tabs_start(\@tabs, "mode", $mode, 1); + +# General data identifies the shared network and optionally scopes it to an +# interface or relay address used by Kea during subnet selection. +print &ui_tabs_start_tab("mode", "general"); +print &ui_div($text{'shared_general_desc'}); +print &ui_table_start($text{'shared_general'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('shared-network-name', + $text{'shared_name'}), + &ui_textbox("name", $shared->{'name'} || "", 40)); +print &ui_table_row(&kea_field_hlink('description', $text{'shared_desc'}), + &ui_textbox("desc", &kea_get_comment($shared) || "", 60)); +print &ui_table_row(&kea_field_hlink('interface'), + &ui_textbox("interface", $shared->{'interface'} || "", 30)); +print &ui_table_row(&kea_field_hlink('relay_ip_addresses'), + &ui_textbox("relay_ip_addresses", + join(" ", &kea_relay_addresses($shared)), 50)); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "general"); + +if (!$in{'new'}) { + # Existing shared networks can show their member subnets and provide a + # shortcut for creating a subnet directly under this parent. + print &ui_tabs_start_tab("mode", "subnets"); + print &ui_div($text{'shared_subnets_desc'}); + my $subs = &kea_subnet_list($root, $ver, $in{'idx'}); + print &ui_columns_start([ + $text{'col_id'}, $text{'col_subnet'}, $text{'col_pools'}, + $text{'col_reservations'}, $text{'col_options'} ], 100); +for(my $i=0; $i<@$subs; $i++) { + my $s = $subs->[$i]; + print &ui_columns_row([ + $s->{'id'} || "", + &ui_link("edit_subnet.cgi?version=$ver&sidx=$in{'idx'}&idx=$i", + &html_escape($s->{'subnet'} || "")), + &kea_count_array($s, 'pools'), + &kea_count_array($s, 'reservations'), + &kea_count_array($s, 'option-data'), + ]); + } +print &ui_columns_row([ &ui_tag('i', &html_escape($text{'index_empty'})) ], + [ "colspan=5" ]) + if (!@$subs); +print &ui_columns_end(); +print &ui_link_button("edit_subnet.cgi?version=$ver&sidx=$in{'idx'}&new=1", + $text{'index_add_subnet'}); +print &ui_tabs_end_tab("mode", "subnets"); +} + +# Shared-network options are inherited by subnets unless a more specific scope +# overrides them. +print &ui_tabs_start_tab("mode", "options"); +print &ui_div($text{'shared_options_desc'}); +&kea_common_option_rows($shared->{'option-data'}, $ver, "common_"); +&kea_option_data_section($shared->{'option-data'}, "opt_", $ver); +print &ui_tabs_end_tab("mode", "options"); + +# Advanced shared-network settings mirror Kea fields that affect all member +# subnets, including timers and protocol-specific behavior flags. +print &ui_tabs_start_tab("mode", "advanced"); +print &ui_div($text{'shared_advanced_desc'}); +print &ui_table_start($text{'shared_advanced'}, "width=100%", 4); +if ($ver == 4) { + print &ui_table_row(&kea_field_hlink('authoritative'), + &ui_select("authoritative", &kea_bool_value($shared->{'authoritative'}), + [ [ "", $text{'inherit_default'} ], + [ "true", $text{'yes'} ], + [ "false", $text{'no'} ] ])); + } +foreach my $k ('renew-timer', 'rebind-timer', 'valid-lifetime', + 'min-valid-lifetime', 'max-valid-lifetime') { + print &ui_table_row(&kea_field_hlink($k), + &ui_textbox($k, defined($shared->{$k}) ? $shared->{$k} : "", 12)); + } +print &ui_table_row(&kea_field_hlink('preferred-lifetime'), + &ui_textbox("preferred-lifetime", + defined($shared->{'preferred-lifetime'}) ? $shared->{'preferred-lifetime'} : "", 12)) + if ($ver == 6); +&kea_advanced_option_rows($shared->{'option-data'}, $ver, "adv_"); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "advanced"); + +print &ui_tabs_end(); + +my @buttons = $in{'new'} ? ([ "save", $text{'create'} ]) : + ([ "save", $text{'save'} ], + [ "delete", $text{'delete'} ]); +print &ui_form_end(\@buttons); +&ui_print_footer("", $text{'index_return'}); diff --git a/kea-dhcp/edit_subnet.cgi b/kea-dhcp/edit_subnet.cgi new file mode 100755 index 000000000..24f012af1 --- /dev/null +++ b/kea-dhcp/edit_subnet.cgi @@ -0,0 +1,257 @@ +#!/usr/local/bin/perl +# Edit or create a Kea subnet. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); + +my $ver = $in{'version'} == 6 ? 6 : 4; +&kea_assert_acl('edit'.$ver); +my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver); +&error($err) if ($err); +my $sidx = defined($in{'sidx'}) ? $in{'sidx'} : ""; +&error($text{'subnet_enone'}) if (!&kea_valid_subnet_parent($root, $sidx)); +&error($text{'subnet_enone'}) + if (!$in{'new'} && (!defined($in{'idx'}) || $in{'idx'} !~ /^\d+$/)); +my $sub = $in{'new'} ? { 'id' => &kea_next_subnet_id($root, $ver) } + : &kea_get_subnet($root, $ver, $sidx, $in{'idx'}); +&error($text{'subnet_enone'}) if (!$sub); + +# Main request flow: render the tabbed subnet editor, then delegate repeated +# row-heavy controls to helpers below. +my $title = $in{'new'} ? $text{'subnet_create'} : $text{'subnet_edit'}; +&ui_print_header(undef, $title, "", undef, 1, 1); +print &kea_comment_loss_warning($c); +print &ui_form_start("save_subnet.cgi", "post"); +print &ui_hidden("version", $ver); +print &ui_hidden("new", 1) if ($in{'new'}); +print &ui_hidden("idx", $in{'idx'}) if (!$in{'new'}); +print &ui_hidden("sidx", $sidx) if ($sidx ne ''); + +my @tabs = ( + [ 'general', $text{'tab_general'} ], + [ 'pools', $text{'tab_pools'} ], + [ 'reservations', $text{'tab_reservations'} ], + [ 'options', $text{'tab_options'} ], + [ 'advanced', $text{'tab_advanced'} ], + ); +print &ui_tabs_start(\@tabs, "mode", $in{'mode'} || "general", 1); + +# General owns the required subnet identity plus the parent shared-network +# pointer, which determines where the subnet is stored in Kea JSON. +print &ui_tabs_start_tab("mode", "general"); +print &ui_div($text{'subnet_general_desc'}); +print &ui_table_start($text{'subnet_general'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('subnet-id', $text{'subnet_id'}), + &ui_textbox("id", $sub->{'id'} || "", 8)); +print &ui_table_row(&kea_field_hlink('subnet-prefix', + $text{'subnet_prefix'}), + &ui_textbox("subnet", $sub->{'subnet'} || "", 40)); +print &ui_table_row(&kea_field_hlink('calculated-subnet-mask', + $text{'subnet_mask_auto'}), + &ui_tag('tt', &html_escape(&kea_ipv4_mask_from_subnet($sub->{'subnet'} || "") + || $text{'index_empty'}))) + if ($ver == 4); +print &ui_table_row(&kea_field_hlink('description', $text{'subnet_desc'}), + &ui_textbox("desc", &kea_get_comment($sub) || "", 60)); + +# A subnet may be top-level or nested under a shared network. Kea stores those +# in different arrays, so the selected parent is carried through saves. +my @shared_opts = ([ "", "<$text{'shared_none'}>" ]); +my $shareds = &kea_shared_networks($root); +for(my $i=0; $i<@$shareds; $i++) { + push(@shared_opts, [ $i, &kea_scope_name($shareds->[$i], &text('index_shared_num', $i+1)) ]); + } +print &ui_table_row(&kea_field_hlink('shared-network', + $text{'subnet_shared'}), + &ui_select("parent", $sidx ne '' ? $sidx : "", \@shared_opts)); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "general"); + +# Pools are row editors: DHCPv4/DHCPv6 address pools are common, while prefix +# delegation is rendered only for DHCPv6. +print &ui_tabs_start_tab("mode", "pools"); +print &ui_div($text{'subnet_pools_desc'}); +&pool_rows($sub->{'pools'}); +if ($ver == 6) { + print &ui_subheading($text{'pd_pools'}); + &pd_pool_rows($sub->{'pd-pools'}); + } +print &ui_tabs_end_tab("mode", "pools"); + +# Reservations stay compact here, but the parser preserves advanced fields that +# the UI does not expose. +print &ui_tabs_start_tab("mode", "reservations"); +print &ui_div($text{'subnet_reservations_desc'}); +&reservation_rows($sub->{'reservations'}, $ver); +print &ui_tabs_end_tab("mode", "reservations"); + +# Option editing is split between named common fields and generic option-data +# rows so uncommon options can still round-trip. +print &ui_tabs_start_tab("mode", "options"); +print &ui_div($text{'subnet_options_desc'}); +&kea_common_option_rows($sub->{'option-data'}, $ver, "common_"); +&kea_option_data_section($sub->{'option-data'}, "opt_", $ver); +print &ui_tabs_end_tab("mode", "options"); + +# Advanced values affect subnet selection, relay matching, lease timing, and +# DHCPv4 boot fields. They are top-level subnet keys, not normal options. +print &ui_tabs_start_tab("mode", "advanced"); +print &ui_div($text{'subnet_advanced_desc'}); +print &ui_table_start($text{'subnet_advanced'}, "width=100%", 4); +print &ui_table_row(&kea_field_hlink('interface'), + &ui_textbox("interface", &text_value($sub->{'interface'}), 30)); +print &ui_table_row(&kea_field_hlink('relay_ip_addresses'), + &ui_textbox("relay_ip_addresses", + join(" ", &kea_relay_addresses($sub)), 50)); +if ($ver == 4) { + print &ui_table_row(&kea_field_hlink('authoritative'), + &ui_select("authoritative", &kea_bool_value($sub->{'authoritative'}), + [ [ "", $text{'inherit_default'} ], + [ "true", $text{'yes'} ], + [ "false", $text{'no'} ] ])); + } +foreach my $k ('renew-timer', 'rebind-timer', 'valid-lifetime', + 'min-valid-lifetime', 'max-valid-lifetime') { + print &ui_table_row(&kea_field_hlink($k), + &ui_textbox($k, &text_value($sub->{$k}), 12)); + } +print &ui_table_row(&kea_field_hlink('preferred-lifetime'), + &ui_textbox("preferred-lifetime", &text_value($sub->{'preferred-lifetime'}), 12)) + if ($ver == 6); +foreach my $k ('next-server', 'server-hostname', 'boot-file-name') { + print &ui_table_row(&kea_field_hlink($k), + &ui_textbox($k, &text_value($sub->{$k}), 40)) + if ($ver == 4); + } +&kea_advanced_option_rows($sub->{'option-data'}, $ver, "adv_"); +print &ui_table_end(); +print &ui_tabs_end_tab("mode", "advanced"); + +print &ui_tabs_end(); + +my @buttons = $in{'new'} ? ([ "save", $text{'create'} ]) : + ([ "save", $text{'save'} ], + [ "delete", $text{'delete'} ]); +print &ui_form_end(\@buttons); +&ui_print_footer("", $text{'index_return'}); + +# text_value(value) +# Returns a defined scalar for textboxes without hiding valid zero values. +sub text_value +{ +my ($v) = @_; +return defined($v) ? $v : ""; +} + +# pool_rows(&pools) +# Renders address pools with one extra empty row for adding a pool. +sub pool_rows +{ +my ($pools) = @_; +$pools = [ ] if (ref($pools) ne 'ARRAY'); +print &ui_table_start($text{'tab_pools'}, "width=100%", 2); + +# Always include one empty row so adding a pool does not need a separate +# client-side table mutation. +for(my $i=0; $i<=$#$pools+1; $i++) { + my $p = $pools->[$i] || { }; + print &ui_table_row(&kea_field_hlink('address-pool', + $text{'pool_range'}), + &ui_textbox("pool_pool_$i", $p->{'pool'} || "", 60)); + } +print &ui_table_end(); +} + +# pd_pool_rows(&pd-pools) +# Renders DHCPv6 prefix delegation pools with room for one new entry. +sub pd_pool_rows +{ +my ($pools) = @_; +$pools = [ ] if (ref($pools) ne 'ARRAY'); + +# Prefix delegation rows are wider than standard form rows, so keep them in the +# same table wrapper used by generic option-data editors. +print &ui_tag_start('div', { 'class' => 'option-data-table' }); +print &ui_columns_start([ + &kea_field_hlink('pd-prefix', $text{'pd_prefix'}), + &kea_field_hlink('pd-prefix-len', $text{'pd_prefix_len'}), + &kea_field_hlink('pd-delegated-len', $text{'pd_delegated_len'}), + &kea_field_hlink('pd-excluded-prefix', $text{'pd_excluded_prefix'}), + &kea_field_hlink('pd-excluded-prefix-len', + $text{'pd_excluded_prefix_len'}) ], 100); +for(my $i=0; $i<=$#$pools+1; $i++) { + my $p = $pools->[$i] || { }; + print &ui_columns_row([ + &ui_textbox("pd_prefix_$i", $p->{'prefix'} || "", 26), + &ui_textbox("pd_prefix_len_$i", $p->{'prefix-len'} || "", 5), + &ui_textbox("pd_delegated_len_$i", $p->{'delegated-len'} || "", 5), + &ui_textbox("pd_excluded_prefix_$i", $p->{'excluded-prefix'} || "", 26), + &ui_textbox("pd_excluded_prefix_len_$i", $p->{'excluded-prefix-len'} || "", 5), + ]); + } +print &ui_columns_end(); +print &ui_tag_end('div'); +} + +# reservation_rows(&reservations, version) +# Renders host reservations without trying to flatten every advanced Kea field. +sub reservation_rows +{ +my ($reservations, $ver) = @_; +$reservations = [ ] if (ref($reservations) ne 'ARRAY'); + +# Kea accepts different reservation identifiers per protocol; the dropdown is +# limited to identifiers that the selected daemon can actually use. +my @types = $ver == 6 ? + ([ 'duid', 'DUID' ], [ 'hw-address', $text{'res_hw'} ], + [ 'flex-id', 'Flex ID' ]) : + ([ 'hw-address', $text{'res_hw'} ], [ 'client-id', $text{'res_client'} ], + [ 'duid', 'DUID' ], [ 'circuit-id', $text{'res_circuit'} ], + [ 'flex-id', 'Flex ID' ]); +my @heads = ( &kea_field_hlink('reservation-identifier-type', + $text{'res_type'}), + &kea_field_hlink('reservation-identifier', + $text{'res_identifier'}), + $ver == 6 ? + &kea_field_hlink('reservation-addresses', + $text{'res_addresses'}) : + &kea_field_hlink('reservation-address', + $text{'res_address'}), + &kea_field_hlink('reservation-hostname', + $text{'res_hostname'}) ); +push(@heads, &kea_field_hlink('reservation-prefixes', + $text{'res_prefixes'})) if ($ver == 6); +print &ui_tag_start('div', { 'class' => 'option-data-table' }); +print &ui_columns_start(\@heads, 100); + +# Pick the first identifier field already present, otherwise default to the +# common identifier for the protocol. +for(my $i=0; $i<=$#$reservations+1; $i++) { + my $r = $reservations->[$i] || { }; + my $rtype = ""; + foreach my $k (map { $_->[0] } @types) { + if (defined($r->{$k})) { + $rtype = $k; + last; + } + } + $rtype ||= $types[0]->[0]; + my $addr = $ver == 6 ? join(" ", @{$r->{'ip-addresses'} || [ ]}) : + $r->{'ip-address'}; + my @cols = ( + &ui_select("res_type_$i", $rtype, \@types), + &ui_textbox("res_identifier_$i", $r->{$rtype} || "", 28), + &ui_textbox("res_address_$i", $addr || "", 32), + &ui_textbox("res_hostname_$i", $r->{'hostname'} || "", 22), + ); + push(@cols, &ui_textbox("res_prefixes_$i", + join(" ", @{$r->{'prefixes'} || [ ]}), 30)) if ($ver == 6); + print &ui_columns_row(\@cols); + } +print &ui_columns_end(); +print &ui_tag_end('div'); +} diff --git a/kea-dhcp/edit_text.cgi b/kea-dhcp/edit_text.cgi new file mode 100755 index 000000000..094c33aac --- /dev/null +++ b/kea-dhcp/edit_text.cgi @@ -0,0 +1,42 @@ +#!/usr/local/bin/perl +# Edit Kea files as raw text. + +use strict; +use warnings; +require './kea-dhcp-lib.pl'; +&ReadParse(); +our (%in, %text); +&error_setup($text{'eacl_aviol'}); +&kea_assert_acl('manual'); + +my @files = &kea_manual_edit_files(); +&error($text{'edit_enofile'}) if (!@files); +my $info = &kea_manual_edit_file($in{'file'}) || $files[0]; +my $file = $info->{'file'}; +&error($text{'save_efile'}) if (!$file); + +# The manual editor is intentionally constrained to known Kea config files and +# Control Agent password files discovered by the library. +my $data = ""; +if (-r $file) { + &lock_file($file); + $data = &read_file_contents($file); + &unlock_file($file); + } + +&ui_print_header(undef, $text{'index_edit_manual'}, "", undef, 1, 1); + +# Keep file selection and file contents as separate forms, matching nftables. +print &ui_form_start("edit_text.cgi"); +print &ui_tag('b', &html_escape($text{'edit_select'})),"\n"; +print &ui_select("file", $file, [ map { $_->{'file'} } @files ]),"\n"; +print &ui_submit($text{'edit_ok'}); +print &ui_form_end(); + +print &ui_form_start("save_text.cgi", "form-data"); +print &ui_hidden("file", $file); +print &ui_table_start(undef, undef, 2); +print &ui_table_row(undef, &ui_textarea("data", $data, 30, 120), 2); +print &ui_table_end(); +print &ui_form_end([ [ "save", $text{'save'} ] ]); +&ui_print_footer("index.cgi", $text{'index_return'}); diff --git a/kea-dhcp/help/field_address_pool.html b/kea-dhcp/help/field_address_pool.html new file mode 100644 index 000000000..318d4c8bf --- /dev/null +++ b/kea-dhcp/help/field_address_pool.html @@ -0,0 +1,9 @@ +
Address pool
+ +Dynamic lease range inside the subnet. DHCPv4 pools are usually ranges such as 192.0.2.10 - 192.0.2.200; DHCPv6 pools are often prefixes such as 2001:db8:1::/80. +

+ +If a subnet has no pools, ordinary clients will not receive dynamic leases unless matching reservations exist. +

+ +