Files
webmin/kea-dhcp/index.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

707 lines
21 KiB
Perl
Executable File

#!/usr/local/bin/perl
# Display Kea DHCP service and configuration status.
use strict;
use warnings;
require './kea-dhcp-lib.pl'; ## no critic
&ReadParse();
our (%in, %text);
our $module_name;
my %access = &get_module_acl();
my $delete_formno;
&error_setup($text{'eacl_aviol'});
&error("$text{'eacl_np'} $text{'eacl_penter'}")
unless grep { $access{$_} } qw(dhcp4 dhcp6 ddns services runtime
edit4 edit6 editddns manual apply
install);
# If the Kea daemons cannot be found, keep the index page focused on the
# repair path. Leftover config files are not enough to run the module safely.
if (!&kea_any_installed()) {
&ui_print_header(undef, $text{'index_title'}, "", undef, 1, 1);
print &ui_p(&text('index_missing_exe',
"@{[&get_webprefix()]}/config.cgi?$module_name"));
if ($access{'install'}) {
&foreign_require("software", "software-lib.pl");
my $lnk = &software::missing_install_link("kea", "Kea DHCP",
"../$module_name/", $text{'index_title'});
print &ui_p($lnk) if ($lnk);
}
&ui_print_footer("/", $text{'index_return'});
exit;
}
# The page header owns service controls, like BIND/nftables modules do.
&ui_print_header(undef, $text{'index_title'}, "", undef, 1, 1, 0,
&service_action_links());
# Build the visible tab set from ACLs instead of showing links that later fail.
$delete_formno = 0;
my @tabs;
push(@tabs, [ 'dhcp4', $text{'tab_dhcp4'} ])
if (&kea_can_view_dhcp(\%access, 4));
push(@tabs, [ 'dhcp6', $text{'tab_dhcp6'} ])
if (&kea_can_view_dhcp(\%access, 6));
push(@tabs, [ 'ddns', $text{'tab_ddns'} ])
if (&kea_can_view_ddns(\%access));
push(@tabs, [ 'services', $text{'tab_services'} ])
if (&kea_can_view_services(\%access));
if (@tabs) {
my %valid_tabs = map { $_->[0] => 1 } @tabs;
my $mode = $in{'mode'} && $valid_tabs{$in{'mode'}} ?
$in{'mode'} : $tabs[0]->[0];
print &ui_tabs_start(\@tabs, "mode", $mode, 1);
if (&kea_can_view_dhcp(\%access, 4)) {
print &ui_tabs_start_tab("mode", "dhcp4");
&print_dhcp_tab(4);
print &ui_tabs_end_tab("mode", "dhcp4");
}
if (&kea_can_view_dhcp(\%access, 6)) {
print &ui_tabs_start_tab("mode", "dhcp6");
&print_dhcp_tab(6);
print &ui_tabs_end_tab("mode", "dhcp6");
}
if (&kea_can_view_ddns(\%access)) {
print &ui_tabs_start_tab("mode", "ddns");
&print_ddns_tab();
print &ui_tabs_end_tab("mode", "ddns");
}
if (&kea_can_view_services(\%access)) {
print &ui_tabs_start_tab("mode", "services");
print &ui_div($text{'index_services_desc'});
&print_services_table();
print &ui_tabs_end_tab("mode", "services");
}
print &ui_tabs_end();
}
&print_action_buttons();
&ui_print_footer("/", $text{'index_return'});
# html_join_br(html, ...)
# Joins existing HTML fragments with API-generated line breaks.
sub html_join_br
{
return join(&ui_br(), grep { defined($_) && $_ ne '' } @_);
}
# html_join_escaped_lines(text, ...)
# Escapes text lines and joins them with API-generated line breaks.
sub html_join_escaped_lines
{
return &html_join_br(map { &html_escape($_) } @_);
}
# status_badge(&status)
# Renders the status cell with a compact state pill and expandable diagnostics.
sub status_badge
{
my ($status) = @_;
my $state = $status->{'state'} || 'unknown';
my $label = $state eq 'running' && $status->{'pid'} ?
&text('status_running', $status->{'pid'}) :
$state eq 'running' ? $text{'status_running_nopid'} :
$state eq 'failed' ? $text{'status_failed'} :
$state eq 'skipped' ? $text{'status_skipped'} :
$state eq 'starting' ? $text{'status_starting'} :
$state eq 'stopping' ? $text{'status_stopping'} :
$state eq 'stopped' ? $text{'status_stopped'} :
$text{'status_unknown'};
my $color = $state eq 'running' ? "var(--success-color, #138a5b)" :
$state eq 'failed' ? "var(--danger-color, #d33)" :
$state eq 'skipped' ||
$state eq 'starting' || $state eq 'stopping' ?
"var(--warning-color, #b7791f)" :
"var(--text-color-muted, #666)";
my $html = &ui_tag('span', &html_escape($label), {
'style' => 'display:inline-block;padding: 0.06em 0.6em;'.
'border:1px solid '.$color.';border-radius:999px;'.
'color:'.$color.';white-space:nowrap;'.
'line-height:1;font-size:0.9em;'.
'vertical-align: text-top;'
});
# Keep the most actionable status line as the details summary and move the rest
# into muted supporting text.
my @details = @{$status->{'details'} || [ ]};
my $statusmsg;
if (@{$status->{'logs'} || [ ]} && @details) {
$statusmsg = &status_message_details($status, pop(@details));
}
else {
$statusmsg = &status_message_details($status);
}
if (@details) {
$html .= &ui_br().&ui_tag('small',
&html_join_escaped_lines(@details),
{ 'style' => 'color:var(--text-color-muted, #666);'.
'white-space:pre-wrap;' });
}
$html .= &ui_br().$statusmsg if ($statusmsg);
return $html;
}
# status_message_details(&status, [title])
# Displays service logs inline, but collapsed by default.
sub status_message_details
{
my ($status, $title) = @_;
my @logs = @{$status->{'logs'} || [ ]};
return "" if (!@logs);
my $state = $status->{'state'} || 'unknown';
my $color = $state eq 'skipped' ?
"var(--warning-color, #b7791f)" :
"var(--danger-color, #d33)";
my $content = &ui_tag('small',
&html_join_escaped_lines(@logs),
{ 'style' => 'color:'.$color.';white-space:pre-wrap;'.
'overflow-wrap:anywhere;line-height:1.35;'.
'display:block;margin-top:0.2em;'.
'max-width:100%;' });
my $summary = defined($title) && $title ne '' ?
&ui_tag('small', &html_escape($title), {
'style' => 'color:var(--text-color-muted, #666);'.
'white-space:pre-wrap;',
}) :
&html_escape($text{'status_messages'});
return &ui_details({
'html' => 1,
'title' => $summary,
'class' => 'inline kea-status-details',
'content' => $content,
});
}
# status_details_style()
# Softens the native details arrow so it does not dominate status text.
sub status_details_style
{
return &ui_tag('style', <<'EOF');
details.inline.kea-status-details > summary:after {
opacity: .4;
}
EOF
}
# error_text(text)
# Renders an inline error fragment for table cells.
sub error_text
{
my ($msg) = @_;
return &ui_tag('span', &html_escape($msg), {
'style' => 'color:var(--danger-color, #d33);',
});
}
# warning_lines(lines...)
# Renders warning text that can be embedded into table cells.
sub warning_lines
{
my @msgs = @_;
return "" if (!@msgs);
return &ui_tag('small',
&html_join_escaped_lines(@msgs),
{ 'style' => 'color:var(--warning-color, #b7791f);'.
'white-space:pre-wrap;' });
}
# scope_location_summary(&scope)
# Returns the interface or relay selector for a shared network/subnet.
sub scope_location_summary
{
my ($scope) = @_;
return $scope->{'interface'} if ($scope->{'interface'});
my @relays = &kea_relay_addresses($scope);
return join(", ", @relays) if (@relays);
return "";
}
# shared_summary(&shared-network, &subnets)
# Summarizes production-relevant shared-network state.
sub shared_summary
{
my ($shared, $subnets) = @_;
my @parts;
my $loc = &scope_location_summary($shared);
push(@parts, &html_escape($loc)) if ($loc ne '');
my @warnings;
push(@warnings, @$subnets == 0 ? $text{'warn_shared_empty'} :
$text{'warn_shared_single'})
if (@$subnets < 2);
push(@parts, &warning_lines(@warnings)) if (@warnings);
return &html_join_br(@parts);
}
# subnet_warnings(&subnet, version)
# Returns warnings for subnet shapes that Kea accepts but admins should notice.
sub subnet_warnings
{
my ($sub, $ver) = @_;
my @warnings;
if ($ver == 4) {
my $canonical = &kea_ipv4_canonical_subnet($sub->{'subnet'} || "");
push(@warnings, &text('warn_subnet_noncanonical', $canonical))
if ($canonical && $canonical ne ($sub->{'subnet'} || ""));
}
my $pools = &kea_count_array($sub, 'pools');
my $reservations = &kea_count_array($sub, 'reservations');
push(@warnings, $text{'warn_subnet_no_leases'})
if (!$pools && !$reservations);
return @warnings;
}
# subnet_link_cell(url, label, &subnet, version)
# Links subnet names only for users allowed to edit that DHCP version.
sub subnet_link_cell
{
my ($url, $label, $sub, $ver) = @_;
my $html = &kea_can_edit_dhcp(\%access, $ver) ?
&ui_link($url, &html_escape($label)) :
&html_escape($label);
my @warnings = &subnet_warnings($sub, $ver);
$html .= &ui_br().&warning_lines(@warnings) if (@warnings);
return $html;
}
# service_action_links()
# Returns header service actions according to current daemon state and ACLs.
sub service_action_links
{
my @links;
if ($access{'apply'}) {
if (&kea_running_pids()) {
push(@links, &ui_link("stop.cgi", $text{'index_stop'}));
push(@links, &ui_link("restart.cgi", $text{'index_restart'}));
}
else {
push(@links, &ui_link("start.cgi", $text{'index_start'}));
}
}
return &html_join_br(@links);
}
# config_cell(&component, file)
# Links known config files into the raw editor when ACLs allow manual edits.
sub config_cell
{
my ($c, $file) = @_;
return &ui_tag('i', &html_escape($text{'index_not_configured'})) if (!$file);
my $html = &ui_tag('tt', &html_escape($file));
return $html if (!$access{'manual'});
return $html if (!&kea_manual_edit_file($file));
return &ui_link("edit_text.cgi?file=".&urlize($file), $html);
}
# print_services_table()
# Displays daemon state and config summaries for all Kea components.
sub print_services_table
{
my @tdtags = ( undef, "width=25%", undef, undef, undef );
print &status_details_style();
print &ui_columns_start([
$text{'col_service'},
$text{'col_status'},
$text{'col_config'},
$text{'col_interfaces'},
$text{'col_summary'} ], 100, 0,
\@tdtags);
# Each row combines three sources of truth: configured module paths, live daemon
# state, and parsed config summaries.
foreach my $c (&kea_components()) {
my $file = &kea_config_file($c);
my $status = &kea_component_status($c);
my ($root, $err) = &kea_read_component_config($c);
my ($ifaces, $summary);
if ($err) {
$ifaces = "";
$summary = &error_text($err);
}
else {
$ifaces = $c->{'id'} =~ /^dhcp/ ?
&html_escape(&kea_component_interfaces($root)) : "";
if ($c->{'id'} =~ /^dhcp([46])$/ &&
&kea_dhcp_needs_interface_warning($root, $1)) {
$ifaces .= &ui_br().
&ui_tag('small',
&html_escape($text{'warn_interfaces_empty_short'}),
{ 'style' => 'color:var(--warning-color, #b7791f)' });
}
$summary = &html_escape(&kea_component_summary($c, $root));
}
print &ui_columns_row([
&html_escape($c->{'label'}),
&status_badge($status),
&config_cell($c, $file),
$ifaces,
$summary,
], \@tdtags);
}
print &ui_columns_end();
}
# print_action_buttons()
# Shows bottom editor shortcuts that are valid for the current ACL.
sub print_action_buttons
{
my @editable = &kea_editable_components();
return if (!@editable);
return if (!&kea_can_view_runtime(\%access) &&
!$access{'manual'} &&
!&kea_can_edit_dhcp(\%access, 4) &&
!&kea_can_edit_dhcp(\%access, 6) &&
!&kea_can_edit_ddns(\%access));
print &ui_hr();
print &ui_buttons_start();
# Global settings shortcuts are shown only when the backing config exists and
# the user can edit that protocol.
foreach my $ver (4, 6) {
my $c = &kea_dhcp_component($ver);
next if (!&kea_config_file($c) ||
!&kea_can_edit_dhcp(\%access, $ver));
print &ui_buttons_row("edit_options.cgi",
&text('index_global_settings', $ver),
&text('index_global_settingsdesc', $ver),
[ [ "version", $ver ] ]);
}
my $ddns = &kea_component('ddns');
if (&kea_config_file($ddns) && &kea_can_edit_ddns(\%access)) {
print &ui_buttons_row("edit_ddns.cgi",
$text{'index_ddns_settings'},
$text{'index_ddns_settingsdesc'});
}
if (&kea_can_view_runtime(\%access)) {
print &ui_buttons_row("runtime.cgi",
$text{'index_runtime_status'},
$text{'index_runtime_statusdesc'});
}
if ($access{'manual'}) {
print &ui_buttons_row("edit_text.cgi",
$text{'index_edit_manual'},
$text{'index_edit_manualdesc'});
}
print &ui_buttons_end();
}
# print_ddns_tab()
# Displays the standalone DHCP-DDNS daemon configuration at module level.
sub print_ddns_tab
{
my $c = &kea_component('ddns');
my $can_edit = &kea_can_edit_ddns(\%access);
my ($root, $err) = &kea_read_component_config($c);
if ($err) {
print &ui_alert_box(&html_escape($err), "danger", undef, undef, "");
return;
}
print &ui_div($text{'index_ddns_desc'});
print &kea_comment_loss_warning($c) if ($can_edit);
# The D2 overview deliberately pairs the standalone receiver with DHCPv4/DHCPv6
# sender state, because both sides must match before DNS updates flow.
print &ui_columns_start([
$text{'ddns_area'},
$text{'col_summary'} ], 100, 0,
[ "width=25%", "width=75%" ]);
print &ui_columns_row([
$text{'ddns_listener'},
&ddns_listener_cell($root),
]);
print &ui_columns_row([
$text{'ddns_sender4'},
&ddns_sender_cell(4, $root),
]);
print &ui_columns_row([
$text{'ddns_sender6'},
&ddns_sender_cell(6, $root),
]);
print &ui_columns_row([
$text{'control_socket'},
&ddns_control_socket_cell($root),
]);
print &ui_columns_row([
$text{'ddns_forward'},
&ddns_domains_cell($root, 'forward-ddns'),
]);
print &ui_columns_row([
$text{'ddns_reverse'},
&ddns_domains_cell($root, 'reverse-ddns'),
]);
print &ui_columns_row([
$text{'ddns_tsig_keys'},
&text('ddns_count', &kea_count_array($root, 'tsig-keys')),
]);
print &ui_columns_row([
$text{'logging_loggers'},
&text('ddns_count', &kea_count_array($root, 'loggers')),
]);
print &ui_columns_end();
}
# ddns_listener_cell(&root)
# Summarizes the D2 listener endpoint and any listener safety warning.
sub ddns_listener_cell
{
my ($root) = @_;
my $target = &kea_ddns_listener_target($root);
return &ui_tag('i', &html_escape($text{'index_not_configured'}))
if ($target eq '');
my @parts = (&ui_tag('tt', &html_escape($target)));
my $warn = &kea_ddns_listener_non_loopback($root) ?
$text{'ddns_listener_warn'} :
&kea_ddns_listener_non_default_loopback($root) ?
$text{'ddns_listener_warn_loopback'} : "";
push(@parts, &ui_tag('span', $warn, {
'style' => 'color:var(--warning-color, #b7791f)' }))
if ($warn ne '');
return &html_join_br(@parts);
}
# ddns_sender_cell(version, &d2-root)
# Shows whether DHCPv4/DHCPv6 is configured to send updates to this D2 daemon.
sub ddns_sender_cell
{
my ($ver, $d2root) = @_;
my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver);
return &ui_tag('span', &text('ddns_sender_unknown', $ver, &html_escape($err)),
{ 'style' => 'color:var(--danger-color, #d33)' })
if ($err);
my $sender = ref($root->{'dhcp-ddns'}) eq 'HASH' ?
$root->{'dhcp-ddns'} : { };
return $text{'ddns_sender_disabled'} if (!$sender->{'enable-updates'});
my $host = $sender->{'server-ip'} || "";
my $port = defined($sender->{'server-port'}) ? $sender->{'server-port'} : "";
my $target = $host.($port ne '' ? ":".$port : "");
my $d2target = &kea_ddns_listener_target($d2root);
my @parts = ($text{'ddns_sender_enabled'});
push(@parts, &text('ddns_sender_target',
&ui_tag('tt', &html_escape($target))))
if ($target ne '');
if ($target ne '' && $d2target ne '') {
push(@parts, $target eq $d2target ?
$text{'ddns_sender_match'} :
&text('ddns_sender_mismatch',
&ui_tag('tt', &html_escape($d2target))));
}
return &html_join_br(@parts);
}
# ddns_control_socket_cell(&root)
# Summarizes the DHCP-DDNS control socket from the daemon config.
sub ddns_control_socket_cell
{
my ($root) = @_;
my $socket = ref($root->{'control-socket'}) eq 'HASH' ?
$root->{'control-socket'} : { };
my $type = $socket->{'socket-type'} || "";
my $name = $socket->{'socket-name'} || "";
return &ui_tag('i', &html_escape($text{'index_not_configured'}))
if ($type eq '' && $name eq '');
return &ui_tag('tt', &html_escape(join(" ", grep { $_ ne '' } ($type, $name))));
}
# ddns_domains_cell(&root, section-name)
# Summarizes configured forward or reverse DDNS domains for the overview table.
sub ddns_domains_cell
{
my ($root, $section) = @_;
my $domains = &kea_ddns_domains($root, $section);
return &ui_tag('i', &html_escape($text{'index_empty'})) if (!@$domains);
return &html_join_br(map {
my $name = $_->{'name'} || "";
my $key = $_->{'key-name'} || "";
my ($servers) = &kea_ddns_domain_server_fields($_);
my @parts = (&html_escape($name));
push(@parts, &html_escape($key)) if ($key ne '');
push(@parts, &ui_tag('tt', &html_escape($servers))) if ($servers ne '');
join(" &middot; ", @parts);
} @$domains);
}
# print_dhcp_tab(4|6)
# Displays shared networks and subnets for one Kea DHCP daemon.
sub print_dhcp_tab
{
my ($ver) = @_;
my $can_edit = &kea_can_edit_dhcp(\%access, $ver);
my ($c, $root, $data, $err) = &kea_read_dhcp_config($ver);
if ($err) {
print &ui_alert_box(&html_escape($err), "danger", undef, undef, "");
return;
}
print &ui_div($text{'index_dhcp'.$ver.'_desc'});
print &kea_comment_loss_warning($c) if ($can_edit);
print &ui_alert_box(&html_escape($text{'dhcp6_ra_warn'}), "warn", undef, undef, "")
if ($ver == 6);
print &ui_alert_box(&text('warn_interfaces_empty',
&ui_tag('tt', 'interfaces-config')), "warn",
undef, undef, "")
if (&kea_dhcp_needs_interface_warning($root, $ver));
# Shared networks and subnets share one delete endpoint but use separate forms
# so checked-table controls stay close to the table they act on.
my $shareds = &kea_shared_networks($root);
my @subnets = &kea_all_subnets($root, $ver);
my $shared_formno = $delete_formno;
print &ui_form_start("delete_objects.cgi", "post")
if ($can_edit && @$shareds);
if ($can_edit && @$shareds) {
$delete_formno++;
print &ui_hidden("version", $ver);
}
my @shared_links = @$shareds && $can_edit ?
( &select_all_link("d_shared", $shared_formno),
&select_invert_link("d_shared", $shared_formno) ) : ( );
push(@shared_links, &ui_link("edit_shared.cgi?version=$ver&new=1",
$text{'index_add_shared'}))
if ($can_edit);
print &ui_subheading($text{'index_shared_networks'});
print &ui_links_row(\@shared_links) if (@shared_links);
if (@$shareds) {
print &ui_columns_start([
($can_edit ? ( "" ) : ( )),
$text{'col_name'}, $text{'col_subnets'}, $text{'col_options'},
$text{'col_summary'} ], 100, 0,
&shared_tdtags($ver));
for(my $i=0; $i<@$shareds; $i++) {
my $s = $shareds->[$i];
my $subnets = &kea_subnet_list($root, $ver, $i);
my $name = &kea_scope_name($s, &text('index_shared_num', $i+1));
my @cols = (
$can_edit ?
&ui_link("edit_shared.cgi?version=$ver&idx=$i",
&html_escape($name)) :
&html_escape($name),
scalar(@$subnets),
&kea_count_array($s, 'option-data'),
&shared_summary($s, $subnets),
);
if ($can_edit) {
print &ui_checked_columns_row(\@cols, &shared_tdtags($ver),
"d_shared", $i);
}
else {
print &ui_columns_row(\@cols);
}
}
print &ui_columns_end();
}
else {
print &ui_div($can_edit ?
$text{'index_empty_shared_add'} :
$text{'index_empty_shared'});
}
if ($can_edit && @$shareds) {
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
}
my $subnet_formno = $delete_formno;
print &ui_form_start("delete_objects.cgi", "post")
if ($can_edit && @subnets);
if ($can_edit && @subnets) {
$delete_formno++;
print &ui_hidden("version", $ver);
}
my @subnet_links = @subnets && $can_edit ?
( &select_all_link("d_subnet", $subnet_formno),
&select_invert_link("d_subnet", $subnet_formno) ) : ( );
push(@subnet_links, &ui_link("edit_subnet.cgi?version=$ver&new=1",
$text{'index_add_subnet'}))
if ($can_edit);
print &ui_subheading($text{'index_subnets'});
print &ui_links_row(\@subnet_links) if (@subnet_links);
if (@subnets) {
print &ui_columns_start(&subnet_headings($ver), 100, 0,
&subnet_tdtags($ver));
foreach my $row (@subnets) {
my $sub = $row->{'subnet'};
my $shared = $row->{'shared'} ?
&kea_scope_name($row->{'shared'}, "") : "";
my $url = "edit_subnet.cgi?version=$ver&idx=$row->{'idx'}".
($row->{'sidx'} ne '' ? "&sidx=$row->{'sidx'}" : "");
my @cols = (
$sub->{'id'} || "",
&subnet_link_cell($url, $sub->{'subnet'} || "", $sub, $ver),
&html_escape($shared),
&kea_count_array($sub, 'pools'),
);
push(@cols, &kea_count_array($sub, 'pd-pools')) if ($ver == 6);
push(@cols, &kea_count_array($sub, 'reservations'));
push(@cols, &kea_count_array($sub, 'option-data'));
if ($can_edit) {
print &ui_checked_columns_row(\@cols, &subnet_tdtags($ver),
"d_subnet",
$row->{'sidx'}.":".$row->{'idx'});
}
else {
print &ui_columns_row(\@cols);
}
}
print &ui_columns_end();
}
else {
print &ui_div($can_edit ?
$text{'index_empty_subnets_add'} :
$text{'index_empty_subnets'});
}
if ($can_edit && @subnets) {
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
}
}
# shared_tdtags(version)
# Returns column width hints for the shared-network table.
sub shared_tdtags
{
my ($ver) = @_;
return &kea_can_edit_dhcp(\%access, $ver) ?
[ "width=5", "width=30%", "width=15%", "width=15%", "width=40%" ] :
[ "width=30%", "width=15%", "width=15%", "width=40%" ];
}
# subnet_headings(version)
# Returns protocol-specific headings for the subnet table.
sub subnet_headings
{
my ($ver) = @_;
return [
(&kea_can_edit_dhcp(\%access, $ver) ? ( "" ) : ( )),
$text{'col_id'}, $text{'col_subnet'}, $text{'col_shared'},
$text{'col_pools'},
($ver == 6 ? ( $text{'col_pd_pools'} ) : ( )),
$text{'col_reservations'}, $text{'col_options'} ];
}
# subnet_tdtags(version)
# Returns column width hints for the subnet table.
sub subnet_tdtags
{
my ($ver) = @_;
if ($ver == 6) {
return &kea_can_edit_dhcp(\%access, $ver) ?
[ "width=5", "width=7%", "width=27%", "width=18%",
"width=10%", "width=10%", "width=11%", "width=12%" ] :
[ "width=7%", "width=28%", "width=20%", "width=10%",
"width=10%", "width=12%", "width=13%" ];
}
return &kea_can_edit_dhcp(\%access, $ver) ?
[ "width=5", "width=8%", "width=30%", "width=20%",
"width=12%", "width=12%", "width=13%" ] :
[ "width=8%", "width=32%", "width=22%", "width=12%",
"width=13%", "width=13%" ];
}