#!/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'); }