mirror of
https://github.com/webmin/webmin.git
synced 2026-06-05 04:40:24 +01:00
Merge pull request #2719 from webmin/dev/fix-netplan
Fix to preserve Netplan YAML when saving DNS
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
|
||||
require './net-lib.pl';
|
||||
$access{'apply'} || &error($text{'apply_ecannot'});
|
||||
&apply_network();
|
||||
&error_setup($text{'apply_err'});
|
||||
$err = &apply_network();
|
||||
$err && &error("<pre>".&html_escape($err)."</pre>");
|
||||
sleep(1);
|
||||
&webmin_log("apply");
|
||||
&redirect("");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
index_title=Network Configuration
|
||||
index_return=network configuration
|
||||
index_apply=Apply Configuration
|
||||
apply_err=Failed to apply network configuration
|
||||
index_applydesc=Click to apply the boot-time interface and routing settings as if the system were rebooted. ⚠ Misconfiguration may make your system unreachable over the network and cut off Webmin access. Editing hostname and DNS settings doesn’t require this step.
|
||||
index_delete1=De-Activate Selected Interfaces
|
||||
index_delete2=Delete Selected Interfaces
|
||||
|
||||
@@ -968,7 +968,8 @@ if ($use_suse_dns) {
|
||||
|
||||
# Update resolv.conf from network interfaces config
|
||||
if ($need_apply) {
|
||||
&apply_network();
|
||||
my $err = &apply_network();
|
||||
&error("<pre>".&html_escape($err)."</pre>") if ($err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1007,4 +1008,3 @@ else {
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
|
||||
@@ -165,6 +165,30 @@ foreach my $f (glob("$netplan_dir/*.yaml")) {
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# yaml_directive_indent(&directive, default-spaces)
|
||||
# Returns the indentation to use for an existing or new YAML directive.
|
||||
sub yaml_directive_indent
|
||||
{
|
||||
my ($yaml, $def) = @_;
|
||||
my $indent = $yaml && defined($yaml->{'indent'}) ? $yaml->{'indent'} : $def;
|
||||
return " " x $indent;
|
||||
}
|
||||
|
||||
# yaml_child_indent(&directive, default-spaces)
|
||||
# Returns the indentation step used by an existing YAML directive's children.
|
||||
sub yaml_child_indent
|
||||
{
|
||||
my ($yaml, $def) = @_;
|
||||
if ($yaml && $yaml->{'members'}) {
|
||||
foreach my $m (@{$yaml->{'members'}}) {
|
||||
if (defined($m->{'indent'}) && $m->{'indent'} > $yaml->{'indent'}) {
|
||||
return $m->{'indent'} - $yaml->{'indent'};
|
||||
}
|
||||
}
|
||||
}
|
||||
return $def;
|
||||
}
|
||||
|
||||
# save_interface(&details, [&all-interfaces])
|
||||
# Create or update a boot-time interface
|
||||
sub save_interface
|
||||
@@ -190,22 +214,25 @@ if ($iface->{'virtual'} ne '') {
|
||||
}
|
||||
else {
|
||||
# Build interface config lines
|
||||
my $id = " " x 8;
|
||||
my $id = &yaml_directive_indent($iface->{'yaml'}, 8);
|
||||
my $step = &yaml_child_indent($iface->{'yaml'}, 4);
|
||||
my $cid = $id.(" " x $step);
|
||||
my $gid = $cid.(" " x $step);
|
||||
my @lines;
|
||||
push(@lines, $id.$iface->{'fullname'}.":");
|
||||
my @addrs;
|
||||
if (!$iface->{'up'}) {
|
||||
push(@lines, $id." "."optional: true");
|
||||
push(@lines, $cid."optional: true");
|
||||
}
|
||||
if ($iface->{'dhcp'}) {
|
||||
push(@lines, $id." "."dhcp4: true");
|
||||
push(@lines, $cid."dhcp4: true");
|
||||
}
|
||||
elsif ($iface->{'address'}) {
|
||||
push(@addrs, $iface->{'address'}."/".
|
||||
&mask_to_prefix($iface->{'netmask'}));
|
||||
}
|
||||
if ($iface->{'auto6'}) {
|
||||
push(@lines, $id." "."dhcp6: true");
|
||||
push(@lines, $cid."dhcp6: true");
|
||||
}
|
||||
for(my $i=0; $i<@{$iface->{'address6'}}; $i++) {
|
||||
push(@addrs, $iface->{'address6'}->[$i]."/".
|
||||
@@ -218,37 +245,37 @@ else {
|
||||
}
|
||||
}
|
||||
if (@addrs) {
|
||||
push(@lines, $id." "."addresses: [".
|
||||
push(@lines, $cid."addresses: [".
|
||||
&join_addr_list(@addrs)."]");
|
||||
}
|
||||
if ($iface->{'gateway'}) {
|
||||
push(@lines, $id." "."gateway4: ".$iface->{'gateway'});
|
||||
push(@lines, $cid."gateway4: ".$iface->{'gateway'});
|
||||
}
|
||||
if ($iface->{'gateway6'}) {
|
||||
push(@lines, $id." "."gateway6: ".$iface->{'gateway6'});
|
||||
push(@lines, $cid."gateway6: ".$iface->{'gateway6'});
|
||||
}
|
||||
if ($iface->{'nameserver'}) {
|
||||
push(@lines, $id." "."nameservers:");
|
||||
push(@lines, $id." "."addresses: [".
|
||||
push(@lines, $cid."nameservers:");
|
||||
push(@lines, $gid."addresses: [".
|
||||
&join_addr_list(@{$iface->{'nameserver'}})."]");
|
||||
if ($iface->{'search'}) {
|
||||
push(@lines, $id." "."search: [".
|
||||
push(@lines, $gid."search: [".
|
||||
&join_addr_list(@{$iface->{'search'}})."]");
|
||||
}
|
||||
}
|
||||
if ($iface->{'ether'}) {
|
||||
push(@lines, $id." "."macaddress: ".$iface->{'ether'});
|
||||
push(@lines, $cid."macaddress: ".$iface->{'ether'});
|
||||
}
|
||||
if ($iface->{'routes'}) {
|
||||
push(@lines, &yaml_lines($iface->{'routes'}, $id." "));
|
||||
push(@lines, &yaml_lines($iface->{'routes'}, $cid, $step));
|
||||
}
|
||||
if ($iface->{'bridgeto'}) {
|
||||
push(@lines, $id." "."interfaces: [".$iface->{'bridgeto'}."]");
|
||||
push(@lines, $id." "."parameters:");
|
||||
push(@lines, $id." "."stp: ".
|
||||
push(@lines, $cid."interfaces: [".$iface->{'bridgeto'}."]");
|
||||
push(@lines, $cid."parameters:");
|
||||
push(@lines, $gid."stp: ".
|
||||
($iface->{'bridgestp'} eq 'on' ? 'true' : 'false'));
|
||||
if ($iface->{'bridgefd'}) {
|
||||
push(@lines, $id." "."forward-delay: ".
|
||||
push(@lines, $gid."forward-delay: ".
|
||||
$iface->{'bridgefd'});
|
||||
}
|
||||
}
|
||||
@@ -259,7 +286,7 @@ else {
|
||||
if ($iface->{'yaml'}) {
|
||||
foreach my $y (@{$iface->{'yaml'}->{'members'}}) {
|
||||
next if (&indexof($y->{'name'}, @poss) >= 0);
|
||||
push(@lines, &yaml_lines($y, $id." "));
|
||||
push(@lines, &yaml_lines($y, $cid, $step));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +637,29 @@ return ( "/etc/hostname", "/etc/HOSTNAME", "/etc/mailname" );
|
||||
# Apply the interface and routing settings
|
||||
sub apply_network
|
||||
{
|
||||
&system_logged("(cd / ; netplan apply) >/dev/null 2>&1");
|
||||
my $err = &validate_netplan_config();
|
||||
return $err if ($err);
|
||||
my $out;
|
||||
my $cmd = "(cd / && ".&netplan_command()." apply)";
|
||||
my $rv = &execute_command_logged($cmd, undef, \$out, \$out);
|
||||
return $rv ? $out || "netplan apply failed" : undef;
|
||||
}
|
||||
|
||||
# validate_netplan_config()
|
||||
# Check that the current YAML can be rendered before applying it.
|
||||
sub validate_netplan_config
|
||||
{
|
||||
my $out;
|
||||
my $cmd = "(cd / && ".&netplan_command()." generate)";
|
||||
my $rv = &execute_command_logged($cmd, undef, \$out, \$out);
|
||||
return $rv ? $out || "netplan generate failed" : undef;
|
||||
}
|
||||
|
||||
# netplan_command()
|
||||
# Returns the netplan command path.
|
||||
sub netplan_command
|
||||
{
|
||||
return &has_command("netplan") || "netplan";
|
||||
}
|
||||
|
||||
# get_default_gateway()
|
||||
@@ -728,13 +777,49 @@ sub os_save_dns_config
|
||||
{
|
||||
my ($conf) = @_;
|
||||
my @boot = &boot_interfaces();
|
||||
my @fix = grep { $_->{'nameserver'} } @boot;
|
||||
@fix = @boot if (!@fix);
|
||||
foreach my $iface (@fix) {
|
||||
$iface->{'nameserver'} = $conf->{'nameserver'};
|
||||
$iface->{'search'} = $conf->{'domain'};
|
||||
&save_interface($iface);
|
||||
my $newns = @{$conf->{'nameserver'} || []} ? $conf->{'nameserver'} : undef;
|
||||
my $newsearch = @{$conf->{'domain'} || []} ? $conf->{'domain'} : undef;
|
||||
my @fix = grep { (!defined($_->{'virtual'}) || $_->{'virtual'} eq '') &&
|
||||
$_->{'nameserver'} } @boot;
|
||||
if (!@fix) {
|
||||
@fix = grep { (!defined($_->{'virtual'}) || $_->{'virtual'} eq '') &&
|
||||
$_->{'name'} !~ /^lo/ } @boot;
|
||||
@fix = @fix ? ( $fix[0] ) : ( );
|
||||
}
|
||||
my $need_apply = 0;
|
||||
foreach my $iface (@fix) {
|
||||
next if (&same_list($iface->{'nameserver'}, $newns) &&
|
||||
&same_list($iface->{'search'}, $newsearch));
|
||||
if ($newns) {
|
||||
$iface->{'nameserver'} = $newns;
|
||||
}
|
||||
else {
|
||||
delete($iface->{'nameserver'});
|
||||
}
|
||||
if ($newsearch) {
|
||||
$iface->{'search'} = $newsearch;
|
||||
}
|
||||
else {
|
||||
delete($iface->{'search'});
|
||||
}
|
||||
&save_interface($iface, \@boot);
|
||||
$need_apply = 1;
|
||||
}
|
||||
return ($need_apply, 1);
|
||||
}
|
||||
|
||||
# same_list(&list1, &list2)
|
||||
# Returns 1 if two optional array refs contain the same values.
|
||||
sub same_list
|
||||
{
|
||||
my ($a, $b) = @_;
|
||||
my @a = $a ? @$a : ( );
|
||||
my @b = $b ? @$b : ( );
|
||||
return 0 if (@a != @b);
|
||||
for(my $i=0; $i<@a; $i++) {
|
||||
return 0 if ($a[$i] ne $b[$i]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
# read_yaml_file(file)
|
||||
@@ -754,7 +839,8 @@ foreach my $origl (@$lref) {
|
||||
if ($l =~ /^(\s*)(\S+):\s*(.*)/) {
|
||||
# Name and possibly value
|
||||
my $i = length($1);
|
||||
if ($i >= $lastdir->{'indent'} + 2 &&
|
||||
if ($lastdir &&
|
||||
$i >= $lastdir->{'indent'} + 2 &&
|
||||
ref($lastdir->{'value'}) eq 'ARRAY' &&
|
||||
@{$lastdir->{'value'}} &&
|
||||
ref($lastdir->{'value'}->[0]) eq 'HASH') {
|
||||
@@ -844,11 +930,12 @@ foreach my $c (@$conf) {
|
||||
}
|
||||
}
|
||||
|
||||
# yaml_lines(&directive, indent-string)
|
||||
# yaml_lines(&directive, indent-string, [indent-step])
|
||||
# Converts a YAML directive into text lines
|
||||
sub yaml_lines
|
||||
{
|
||||
my ($yaml, $id) = @_;
|
||||
my ($yaml, $id, $step) = @_;
|
||||
$step ||= 4;
|
||||
my @rv;
|
||||
my $v = $id.$yaml->{'name'}.":";
|
||||
if (ref($yaml->{'value'}) eq 'ARRAY') {
|
||||
@@ -878,7 +965,7 @@ else {
|
||||
}
|
||||
if ($yaml->{'members'}) {
|
||||
foreach my $m (@{$yaml->{'members'}}) {
|
||||
push(@rv, &yaml_lines($m, $id." "));
|
||||
push(@rv, &yaml_lines($m, $id.(" " x $step), $step));
|
||||
}
|
||||
}
|
||||
return @rv;
|
||||
|
||||
170
net/t/run-tests.t
Normal file
170
net/t/run-tests.t
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Temp qw(tempdir);
|
||||
|
||||
my $root = abs_path(dirname(__FILE__)."/../..") or die "rootdir: $!";
|
||||
my $tmp = tempdir(CLEANUP => 1);
|
||||
my %file_cache;
|
||||
my @commands;
|
||||
my %command_status;
|
||||
my %command_output;
|
||||
|
||||
sub write_text
|
||||
{
|
||||
my ($file, $text) = @_;
|
||||
open(my $fh, ">", $file) || die "write $file: $!";
|
||||
print $fh $text;
|
||||
close($fh) || die "close $file: $!";
|
||||
delete($file_cache{$file});
|
||||
}
|
||||
|
||||
sub read_text
|
||||
{
|
||||
my ($file) = @_;
|
||||
open(my $fh, "<", $file) || die "read $file: $!";
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh) || die "close $file: $!";
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub read_file_lines
|
||||
{
|
||||
my ($file) = @_;
|
||||
if (!exists($file_cache{$file})) {
|
||||
open(my $fh, "<", $file) || die "read_file_lines $file: $!";
|
||||
my @lines = <$fh>;
|
||||
close($fh) || die "close $file: $!";
|
||||
chomp(@lines);
|
||||
$file_cache{$file} = \@lines;
|
||||
}
|
||||
return $file_cache{$file};
|
||||
}
|
||||
|
||||
sub flush_file_lines
|
||||
{
|
||||
my ($file) = @_;
|
||||
open(my $fh, ">", $file) || die "flush $file: $!";
|
||||
foreach my $line (@{$file_cache{$file}}) {
|
||||
print $fh $line, "\n";
|
||||
}
|
||||
close($fh) || die "close $file: $!";
|
||||
}
|
||||
|
||||
sub lock_file { return 1; }
|
||||
sub unlock_file { return 1; }
|
||||
sub error { die join("", @_), "\n"; }
|
||||
sub has_command { return $_[0] eq "netplan" ? "/usr/sbin/netplan" : undef; }
|
||||
sub execute_command_logged
|
||||
{
|
||||
my ($cmd, undef, $stdout, $stderr) = @_;
|
||||
push(@commands, $cmd);
|
||||
my $out = $command_output{$cmd} || "";
|
||||
$$stdout = $out if (ref($stdout));
|
||||
$$stderr = $out if (ref($stderr) && $stderr ne $stdout);
|
||||
return $command_status{$cmd} || 0;
|
||||
}
|
||||
sub check_ipaddress { return $_[0] =~ /^\d+\.\d+\.\d+\.\d+$/; }
|
||||
sub check_ip6address { return $_[0] =~ /:/; }
|
||||
sub check_ipaddress_any { return &check_ipaddress($_[0]) || &check_ip6address($_[0]); }
|
||||
sub mask_to_prefix { return $_[0] eq "255.255.255.0" ? 24 : $_[0]; }
|
||||
sub prefix_to_mask { return $_[0] == 24 ? "255.255.255.0" : $_[0]; }
|
||||
sub indexof
|
||||
{
|
||||
my ($needle, @haystack) = @_;
|
||||
for(my $i=0; $i<@haystack; $i++) {
|
||||
return $i if ($haystack[$i] eq $needle);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
unshift(@INC, "$root/net", $root);
|
||||
do "$root/net/netplan-lib.pl" || die "netplan-lib.pl: $@ $!";
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
$main::netplan_dir = $tmp;
|
||||
}
|
||||
|
||||
my $netplan = "$tmp/50-cloud-init.yaml";
|
||||
write_text($netplan, <<'YAML');
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
dhcp4: true
|
||||
nameservers:
|
||||
addresses:
|
||||
- 1.1.1.1
|
||||
search:
|
||||
- example.com
|
||||
eth1:
|
||||
dhcp4: true
|
||||
YAML
|
||||
|
||||
my @boot = main::boot_interfaces();
|
||||
is(scalar(grep { !defined($_->{'virtual'}) || $_->{'virtual'} eq '' } @boot),
|
||||
2, "parsed two boot interfaces");
|
||||
my ($eth0) = grep { $_->{'fullname'} eq "eth0" } @boot;
|
||||
$eth0->{'nameserver'} = [ "127.0.0.1", "2001:4860:4860::8888" ];
|
||||
$eth0->{'search'} = [ "example.com" ];
|
||||
main::save_interface($eth0, \@boot);
|
||||
|
||||
my $saved = read_text($netplan);
|
||||
like($saved, qr/^ eth0:\n dhcp4: true\n nameservers:\n addresses: \[127\.0\.0\.1, '2001:4860:4860::8888'\]\n search: \[example\.com\]/m,
|
||||
"save_interface preserves existing two-space Netplan hierarchy");
|
||||
like($saved, qr/^ eth1:\n dhcp4: true/m,
|
||||
"untouched sibling interface keeps matching indentation");
|
||||
unlike($saved, qr/^ eth0:/m,
|
||||
"rewritten interface is not moved to an eight-space sibling indent");
|
||||
|
||||
write_text($netplan, <<'YAML');
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
dhcp4: true
|
||||
nameservers:
|
||||
addresses:
|
||||
- 1.1.1.1
|
||||
eth1:
|
||||
dhcp4: true
|
||||
YAML
|
||||
@boot = main::boot_interfaces();
|
||||
my ($need_apply, $generated_resolv) =
|
||||
main::os_save_dns_config({ 'nameserver' => [ "127.0.0.1" ],
|
||||
'domain' => [ "example.com" ] });
|
||||
ok($need_apply, "DNS changes request a netplan apply");
|
||||
ok($generated_resolv, "Netplan DNS save reports resolv.conf as generated");
|
||||
$saved = read_text($netplan);
|
||||
like($saved, qr/^ eth0:\n dhcp4: true\n nameservers:\n addresses: \[127\.0\.0\.1\]\n search: \[example\.com\]/m,
|
||||
"os_save_dns_config updates existing nameservers block");
|
||||
unlike($saved, qr/eth1:\n(?:.*\n)*?\s+nameservers:/,
|
||||
"os_save_dns_config does not add nameservers to every interface");
|
||||
|
||||
@commands = ( );
|
||||
%command_status = (
|
||||
"(cd / && /usr/sbin/netplan generate)" => 1,
|
||||
);
|
||||
%command_output = (
|
||||
"(cd / && /usr/sbin/netplan generate)" => "bad yaml\n",
|
||||
);
|
||||
is(main::apply_network(), "bad yaml\n",
|
||||
"apply_network returns validation errors");
|
||||
is_deeply(\@commands, [ "(cd / && /usr/sbin/netplan generate)" ],
|
||||
"apply_network skips apply when generate fails");
|
||||
|
||||
@commands = ( );
|
||||
%command_status = ( );
|
||||
%command_output = ( );
|
||||
is(main::apply_network(), undef, "apply_network applies after validation");
|
||||
is_deeply(\@commands,
|
||||
[ "(cd / && /usr/sbin/netplan generate)",
|
||||
"(cd / && /usr/sbin/netplan apply)" ],
|
||||
"apply_network validates before applying");
|
||||
|
||||
done_testing();
|
||||
Reference in New Issue
Block a user