Files
webmin/kea-dhcp/edit_subnet.cgi
Ilia Ross 184887d365
Some checks failed
Tests / prove (push) Has been cancelled
Build / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Fix to use direct ACL checks in Kea DHCP module
*Note: Removes Kea-specific ACL wrapper helpers and switches ACL editor/runtime checks to standard Webmin handling with direct supplied ACL values and get_module_acl checks.
2026-05-30 02:53:48 +02:00

260 lines
9.8 KiB
Perl
Executable File

#!/usr/local/bin/perl
# Edit or create a Kea subnet.
use strict;
use warnings;
require './kea-dhcp-lib.pl'; ## no critic
&ReadParse();
our (%in, %text);
&error_setup($text{'eacl_aviol'});
my $ver = $in{'version'} == 6 ? 6 : 4;
my %access = &get_module_acl();
&error("$text{'eacl_np'} $text{'eacl_pedit'.$ver}")
if (!$access{'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');
}