Add nftables apply-needed header action

* Note: Track saved nftables configuration changes with Apache-style config/apply timestamp flags, expose the standard restart.cgi header action for themes, and use it as the single apply endpoint. The button expands to “Apply Changes” when saved rules need applying, while the existing Apply Configuration action now routes through restart.cgi and clears the pending state after a successful apply.
This commit is contained in:
Ilia Ross
2026-05-03 13:59:58 +02:00
parent 6c1de362e3
commit 12eff54277
15 changed files with 96 additions and 22 deletions

View File

@@ -7,7 +7,8 @@ use strict;
use warnings;
our (%text);
ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'active_title'}, "", "intro", 1, 1,
undef, restart_button());
my ($tables, $err) = get_active_nftables_save();
if ($err) {

View File

@@ -24,7 +24,8 @@ my @saved_tables = get_nftables_save();
my $status_key = active_table_status($table, \@saved_tables);
my $is_saved = table_is_webmin_managed($table, \@saved_tables);
ui_print_header(undef, $text{'active_table_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'active_table_title'}, "", "intro", 1, 1,
undef, restart_button());
print ui_table_start($text{'active_table_summary'}, "width=100%", 2);
print ui_table_row($text{'active_table'}, html_escape(nft_table_spec($table)));

View File

@@ -30,7 +30,8 @@ if ($in{'confirm'}) {
return;
}
ui_print_header(undef, $text{'clear_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'clear_title'}, "", "intro", 1, 1,
undef, restart_button());
print "<center>\n";
print ui_form_start("clear_table.cgi");
print ui_hidden("family", $table->{'family'});

View File

@@ -50,7 +50,8 @@ if ($in{'create'}) {
return;
}
ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'create_title'}, "", "intro", 1, 1,
undef, restart_button());
print ui_form_start("create_table.cgi");
print ui_hidden("create", 1);

View File

@@ -29,18 +29,21 @@ else {
$table || error($text{'delete_notable'});
if ($in{'confirm'}) {
my $needs_apply = needs_config_restart();
splice(@tables, $table_idx, 1);
my $err = delete_table_configuration($table, @tables);
error(text('delete_failed', $err)) if ($err);
$err = delete_active_table($table);
error(text('delete_failed', $err)) if ($err);
restart_last_restart_time() if (!$needs_apply);
webmin_log("delete", "table", $table->{'name'},
{ 'family' => $table->{'family'} });
redirect("index.cgi");
return;
}
ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'delete_title'}, "", "intro", 1, 1,
undef, restart_button());
print "<center>\n";
print ui_form_start("delete_table.cgi");
print ui_hidden("table", $table_idx);

View File

@@ -17,12 +17,14 @@ my $chain_name = "";
my $is_new = $in{'new'} ? 1 : 0;
if ($is_new) {
ui_print_header(undef, $text{'chain_title_new'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'chain_title_new'}, "", "intro", 1, 1,
undef, restart_button());
} else {
$chain_name = $in{'chain'};
$chain = $table->{'chains'}->{$chain_name};
$chain || error($text{'chain_nochain'});
ui_print_header(undef, $text{'chain_title_edit'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'chain_title_edit'}, "", "intro", 1, 1,
undef, restart_button());
}
my @type_opts = (
@@ -84,4 +86,3 @@ if (window.addEventListener) {
EOF
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

View File

@@ -47,10 +47,12 @@ sub split_multi_value
}
if ($in{'new'}) {
ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1,
undef, restart_button());
$rule = { 'chain' => $in{'chain'} };
} else {
ui_print_header(undef, $text{'edit_title_edit'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'edit_title_edit'}, "", "intro", 1, 1,
undef, restart_button());
$rule = $table->{'rules'}->[$in{'idx'}];
}
if ($table && $rule->{'chain'}) {

View File

@@ -17,13 +17,15 @@ my $set_name = "";
my $is_new = $in{'new'} ? 1 : 0;
if ($is_new) {
ui_print_header(undef, $text{'set_title_new'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'set_title_new'}, "", "intro", 1, 1,
undef, restart_button());
}
else {
$set_name = $in{'set'};
$set = $table->{'sets'}->{$set_name};
$set || error($text{'set_noset'});
ui_print_header(undef, $text{'set_title_edit'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'set_title_edit'}, "", "intro", 1, 1,
undef, restart_button());
}
my $elements_text = set_elements_text($set);

View File

@@ -60,7 +60,8 @@ if ($in{'import'}) {
return;
}
ui_print_header(undef, $text{'import_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'import_title'}, "", "intro", 1, 1,
undef, restart_button());
print ui_form_start("import_table.cgi");
print ui_hidden("family", $source->{'family'});

View File

@@ -9,7 +9,8 @@ our (%in, %text, %config);
ReadParse();
my $partial = $in{'partial'};
if (!$partial) {
ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1,
undef, restart_button());
}
# Check for nft command
@@ -279,7 +280,7 @@ print $rules_html;
if (@tables) {
print ui_hr();
print ui_buttons_start();
print ui_buttons_row("apply.cgi", $text{'index_apply'}, $text{'index_applydesc'});
print ui_buttons_row("restart.cgi", $text{'index_apply'}, $text{'index_applydesc'});
print ui_buttons_row("active.cgi", $text{'index_active'}, $text{'index_activedesc'});
print ui_buttons_row("setup.cgi", $text{'index_setup'}, $text{'index_setupdesc'});
print ui_buttons_end();

View File

@@ -50,6 +50,7 @@ index_cdeletesel=Delete Selected Chains
index_cmovesel=Move Selected
index_radd=Add Rule
index_apply=Apply Configuration
index_apply_changes=Apply Changes
index_applydesc=Click this button to replace the saved Webmin-managed tables in the active nftables ruleset.
index_active=View Active Ruleset
index_activedesc=View active nftables tables and import copies into Webmin's saved configuration.

View File

@@ -5,8 +5,63 @@ BEGIN { push(@INC, ".."); }; ## no critic
use WebminCore;
use strict;
use warnings;
our (%config, $module_config_directory);
our (%config, $module_config_directory, $module_var_directory);
our ($last_config_change_flag, $last_restart_time_flag);
init_config();
$last_config_change_flag = $module_var_directory."/config-flag";
$last_restart_time_flag = $module_var_directory."/restart-flag";
# restart_button()
# Returns HTML for the header apply button
sub restart_button
{
my @tables = get_nftables_save();
return "" if (!@tables);
my $args = "redir=".urlize(this_url());
my $needs = needs_config_restart();
my $apply = text('index_apply_changes');
my $label = $needs ? "<b>$apply</b>" : $apply;
my $url = "restart.cgi?$args";
$url .= "&newconfig=1" if ($needs);
return ui_link($url, $label);
}
# this_url()
# Returns the URL in the nftables module for the current script
sub this_url
{
my $url = $ENV{'SCRIPT_NAME'} || "";
my $query = $ENV{'QUERY_STRING'} || "";
$url .= "?$query" if ($query ne "");
return $url;
}
# update_last_config_change()
# Updates the flag file indicating when the saved config was changed
sub update_last_config_change
{
open_tempfile(my $fh, ">$last_config_change_flag", 0, 1);
close_tempfile($fh);
}
# restart_last_restart_time()
# Updates the flag file indicating when the saved config was applied
sub restart_last_restart_time
{
open_tempfile(my $fh, ">$last_restart_time_flag", 0, 1);
close_tempfile($fh);
}
# needs_config_restart()
# Returns 1 if saved config changes still need to be applied
sub needs_config_restart
{
my @cst = stat($last_config_change_flag);
my @rst = stat($last_restart_time_flag);
return 0 if (!@cst);
return 1 if (!@rst);
return $cst[9] > $rst[9] ? 1 : 0;
}
# get_nft_command()
# Returns the configured nft command path, or finds it in PATH
@@ -1198,6 +1253,7 @@ open_tempfile(my $fh, ">$file");
print_tempfile($fh, $out);
close_tempfile($fh);
sync_managed_metadata(@tables);
update_last_config_change();
return;
}
@@ -1294,6 +1350,7 @@ unlink_file($tmp);
if ($?) {
return "<pre>$out</pre>";
}
restart_last_restart_time();
return;
}

View File

@@ -15,7 +15,8 @@ $table || error($text{'chain_notable'});
my $chain = $table->{'chains'}->{$in{'chain'}};
$chain || error($text{'chain_nochain'});
ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'rename_chain_title'}, "", "intro", 1, 1,
undef, restart_button());
print ui_form_start("save_chain.cgi");
print ui_hidden("table", $in{'table'});
print ui_hidden("rename", 1);
@@ -30,4 +31,3 @@ print ui_table_end();
print ui_form_end([ [ undef, $text{'rename_chain_ok'} ] ]);
ui_print_footer("index.cgi?table=$in{'table'}", $text{'index_return'});

View File

@@ -1,6 +1,6 @@
#!/usr/bin/perl
# apply.cgi
# Apply the current configuration
# restart.cgi
# Apply saved nftables configuration from the header action
require './nftables-lib.pl'; ## no critic
use strict;
@@ -12,4 +12,5 @@ error_setup($text{'apply_err'});
my $err = apply_restore();
error($err) if ($err);
redirect("index.cgi");
webmin_log("apply");
redirect($in{'redir'} || "index.cgi");

View File

@@ -47,7 +47,8 @@ if ($in{'action'} eq 'create') {
return;
}
ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1);
ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1,
undef, restart_button());
print ui_form_start("setup.cgi");
print ui_hidden("action", "create");