* [pve-devel] [WIP pve-network 1/3] define dhcpplugin in zone
2023-11-09 0:25 [pve-devel] [WIP pve-network 0/3] dhcp changes Alexandre Derumier
@ 2023-11-09 0:25 ` Alexandre Derumier
2023-11-09 0:25 ` [pve-devel] [WIP pve-network 2/3] dhcp : add|del_ip_mapping: only add|del dhcp reservervation Alexandre Derumier
2023-11-09 0:25 ` [pve-devel] [WIP pve-network 3/3] vnet|subnet: add_next_free_ip : implement dhcprange ipam search Alexandre Derumier
2 siblings, 0 replies; 4+ messages in thread
From: Alexandre Derumier @ 2023-11-09 0:25 UTC (permalink / raw)
To: pve-devel
simple: zone1
ipam pve
dhcp dnsmasq
simple: zone2
ipam pve
dhcp dnsmasq
This generate 1 dhcp by zone/vrf.
Don't use dhcp.cfg anymore
It's reuse node filtering from zone.
same subnets in 2 differents zones can't use
same dhcp server
Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
---
src/PVE/API2/Network/SDN/Zones.pm | 1 +
src/PVE/Network/SDN.pm | 4 +-
src/PVE/Network/SDN/Dhcp.pm | 91 +++++++----------------
src/PVE/Network/SDN/Dhcp/Dnsmasq.pm | 32 ++++----
src/PVE/Network/SDN/Dhcp/Plugin.pm | 28 ++-----
src/PVE/Network/SDN/SubnetPlugin.pm | 4 -
src/PVE/Network/SDN/Zones/SimplePlugin.pm | 7 +-
7 files changed, 54 insertions(+), 113 deletions(-)
diff --git a/src/PVE/API2/Network/SDN/Zones.pm b/src/PVE/API2/Network/SDN/Zones.pm
index 4c8b7e1..1c3356e 100644
--- a/src/PVE/API2/Network/SDN/Zones.pm
+++ b/src/PVE/API2/Network/SDN/Zones.pm
@@ -99,6 +99,7 @@ __PACKAGE__->register_method ({
reversedns => { type => 'string', optional => 1},
dnszone => { type => 'string', optional => 1},
ipam => { type => 'string', optional => 1},
+ dhcp => { type => 'string', optional => 1},
pending => { optional => 1},
state => { type => 'string', optional => 1},
nodes => { type => 'string', optional => 1},
diff --git a/src/PVE/Network/SDN.pm b/src/PVE/Network/SDN.pm
index 5c059bc..c306527 100644
--- a/src/PVE/Network/SDN.pm
+++ b/src/PVE/Network/SDN.pm
@@ -150,15 +150,13 @@ sub commit_config {
my $zones_cfg = PVE::Network::SDN::Zones::config();
my $controllers_cfg = PVE::Network::SDN::Controllers::config();
my $subnets_cfg = PVE::Network::SDN::Subnets::config();
- my $dhcp_cfg = PVE::Network::SDN::Dhcp::config();
my $vnets = { ids => $vnets_cfg->{ids} };
my $zones = { ids => $zones_cfg->{ids} };
my $controllers = { ids => $controllers_cfg->{ids} };
my $subnets = { ids => $subnets_cfg->{ids} };
- my $dhcp = { ids => $dhcp_cfg->{ids} };
- $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets, dhcps => $dhcp };
+ $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets };
cfs_write_file($running_cfg, $cfg);
}
diff --git a/src/PVE/Network/SDN/Dhcp.pm b/src/PVE/Network/SDN/Dhcp.pm
index b92c73a..e4c4078 100644
--- a/src/PVE/Network/SDN/Dhcp.pm
+++ b/src/PVE/Network/SDN/Dhcp.pm
@@ -20,41 +20,6 @@ PVE::Network::SDN::Dhcp::Plugin->init();
PVE::Network::SDN::Dhcp::Dnsmasq->register();
PVE::Network::SDN::Dhcp::Dnsmasq->init();
-sub config {
- my ($running) = @_;
-
- if ($running) {
- my $cfg = PVE::Network::SDN::running_config();
- return $cfg->{dhcps};
- }
-
- return cfs_read_file('sdn/dhcp.cfg');
-}
-
-sub sdn_dhcps_config {
- my ($cfg, $id, $noerr) = @_;
-
- die "No DHCP ID specified!\n" if !$id;
-
- my $dhcp_config = $cfg->{ids}->{$id};
- die "SDN DHCP '$id' does not exist!\n" if (!$noerr && !$dhcp_config);
-
- if ($dhcp_config) {
- $dhcp_config->{id} = $id;
- }
-
- return $dhcp_config;
-}
-
-sub get_dhcp {
- my ($dhcp_id, $running) = @_;
-
- return if !$dhcp_id;
-
- my $cfg = PVE::Network::SDN::Dhcp::config($running);
- return PVE::Network::SDN::Dhcp::sdn_dhcps_config($cfg, $dhcp_id, 1);
-}
-
sub add_mapping {
my ($vmid, $vnet, $mac) = @_;
@@ -127,58 +92,52 @@ sub remove_mapping {
sub regenerate_config {
my ($reload) = @_;
- my $dhcps = PVE::Network::SDN::Dhcp::config();
- my $subnets = PVE::Network::SDN::Subnets::config();
+ my $cfg = PVE::Network::SDN::running_config();
- my $plugins = PVE::Network::SDN::Dhcp::Plugin->lookup_types();
+ my $zone_cfg = $cfg->{zones};
+ my $subnet_cfg = $cfg->{subnets};
+ return if !$zone_cfg && !$subnet_cfg;
my $nodename = PVE::INotify::nodename();
+ my $plugins = PVE::Network::SDN::Dhcp::Plugin->lookup_types();
+
foreach my $plugin_name (@$plugins) {
my $plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($plugin_name);
-
eval { $plugin->before_regenerate() };
die "Could not run before_regenerate for DHCP plugin $plugin_name $@\n" if $@;
}
- foreach my $dhcp_id (keys %{$dhcps->{ids}}) {
- my $dhcp_config = PVE::Network::SDN::Dhcp::sdn_dhcps_config($dhcps, $dhcp_id);
- my $plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($dhcp_config->{type});
+ foreach my $zoneid (sort keys %{$zone_cfg->{ids}}) {
+ my $zone = $zone_cfg->{ids}->{$zoneid};
+ next if defined($zone->{nodes}) && !$zone->{nodes}->{$nodename};
- eval { $plugin->before_configure($dhcp_config) };
- die "Could not run before_configure for DHCP server $dhcp_id $@\n" if $@;
- }
+ my $dhcp_plugin_name = $zone->{dhcp};
+ my $dhcp_plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($dhcp_plugin_name);
- foreach my $subnet_id (keys %{$subnets->{ids}}) {
- my $subnet_config = PVE::Network::SDN::Subnets::sdn_subnets_config($subnets, $subnet_id);
- next if !$subnet_config->{'dhcp-range'};
+ eval { $dhcp_plugin->before_configure($zoneid) };
+ die "Could not run before_configure for DHCP server $zoneid $@\n" if $@;
- my @configured_servers = ();
- foreach my $dhcp_range (@{$subnet_config->{'dhcp-range'}}) {
- my $dhcp_config = PVE::Network::SDN::Dhcp::sdn_dhcps_config($dhcps, $dhcp_range->{server});
- my $plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($dhcp_config->{type});
+ foreach my $subnet_id (keys %{$subnet_cfg->{ids}}) {
+ my $subnet_config = PVE::Network::SDN::Subnets::sdn_subnets_config($subnet_cfg, $subnet_id);
+ my ($zone, $subnet_network, $subnet_mask) = split(/-/, $subnet_id);
+ next if $zone ne $zoneid;
+ next if !$subnet_config->{'dhcp-range'};
- next if $dhcp_config->{node} && !grep(/^$nodename$/, @{$dhcp_config->{node}});
+ eval { $dhcp_plugin->configure_subnet($zoneid, $subnet_config) };
+ warn "Could not configure subnet $subnet_id: $@\n" if $@;
- if (!grep(/^$subnet_id$/, @configured_servers)) {
- eval { $plugin->configure_subnet($dhcp_config, $subnet_config) };
- warn "Could not configure Subnet $subnet_id: $@\n" if $@;
- push @configured_servers, $subnet_id;
+ foreach my $dhcp_range (@{$subnet_config->{'dhcp-range'}}) {
+ eval { $dhcp_plugin->configure_range($zoneid, $subnet_config, $dhcp_range) };
+ warn "Could not configure DHCP range for $subnet_id: $@\n" if $@;
}
-
- eval { $plugin->configure_range($dhcp_config, $subnet_config, $dhcp_range) };
- warn "Could not configure DHCP range for $subnet_id: $@\n" if $@;
}
- }
- foreach my $dhcp_id (keys %{$dhcps->{ids}}) {
- my $dhcp_config = PVE::Network::SDN::Dhcp::sdn_dhcps_config($dhcps, $dhcp_id);
- my $plugin = PVE::Network::SDN::Dhcp::Plugin->lookup($dhcp_config->{type});
+ eval { $dhcp_plugin->after_configure($zoneid) };
+ warn "Could not run after_configure for DHCP server $zoneid $@\n" if $@;
- eval { $plugin->after_configure($dhcp_config) };
- warn "Could not run after_configure for DHCP server $dhcp_id $@\n" if $@;
}
foreach my $plugin_name (@$plugins) {
diff --git a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
index af109b8..64895ef 100644
--- a/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
+++ b/src/PVE/Network/SDN/Dhcp/Dnsmasq.pm
@@ -19,9 +19,9 @@ sub type {
}
sub del_ip_mapping {
- my ($class, $dhcp_config, $mac) = @_;
+ my ($class, $dhcpid, $mac) = @_;
- my $ethers_file = "$DNSMASQ_CONFIG_ROOT/$dhcp_config->{id}/ethers";
+ my $ethers_file = "$DNSMASQ_CONFIG_ROOT/$dhcpid/ethers";
my $ethers_tmp_file = "$ethers_file.tmp";
my $removeFn = sub {
@@ -48,13 +48,13 @@ sub del_ip_mapping {
return;
}
- my $service_name = "dnsmasq\@$dhcp_config->{id}";
+ my $service_name = "dnsmasq\@$dhcpid";
PVE::Tools::run_command(['systemctl', 'reload', $service_name]);
}
sub add_ip_mapping {
- my ($class, $dhcp_config, $mac, $ip) = @_;
- my $ethers_file = "$DNSMASQ_CONFIG_ROOT/$dhcp_config->{id}/ethers";
+ my ($class, $dhcpid, $mac, $ip) = @_;
+ my $ethers_file = "$DNSMASQ_CONFIG_ROOT/$dhcpid/ethers";
my $appendFn = sub {
open(my $fh, '>>', $ethers_file) or die "Could not open file '$ethers_file' $!\n";
@@ -69,12 +69,12 @@ sub add_ip_mapping {
return;
}
- my $service_name = "dnsmasq\@$dhcp_config->{id}";
+ my $service_name = "dnsmasq\@$dhcpid";
PVE::Tools::run_command(['systemctl', 'reload', $service_name]);
}
sub configure_subnet {
- my ($class, $dhcp_config, $subnet_config) = @_;
+ my ($class, $dhcpid, $subnet_config) = @_;
die "No gateway defined for subnet $subnet_config->{id}"
if !$subnet_config->{gateway};
@@ -98,15 +98,15 @@ sub configure_subnet {
if $subnet_config->{'dhcp-dns-server'};
PVE::Tools::file_set_contents(
- "$DNSMASQ_CONFIG_ROOT/$dhcp_config->{id}/10-$subnet_config->{id}.conf",
+ "$DNSMASQ_CONFIG_ROOT/$dhcpid/10-$subnet_config->{id}.conf",
join("\n", @dnsmasq_config) . "\n"
);
}
sub configure_range {
- my ($class, $dhcp_config, $subnet_config, $range_config) = @_;
+ my ($class, $dhcpid, $subnet_config, $range_config) = @_;
- my $range_file = "$DNSMASQ_CONFIG_ROOT/$dhcp_config->{id}/10-$subnet_config->{id}.ranges.conf",
+ my $range_file = "$DNSMASQ_CONFIG_ROOT/$dhcpid/10-$subnet_config->{id}.ranges.conf",
my $tag = $subnet_config->{id};
open(my $fh, '>>', $range_file) or die "Could not open file '$range_file' $!\n";
@@ -115,9 +115,9 @@ sub configure_range {
}
sub before_configure {
- my ($class, $dhcp_config) = @_;
+ my ($class, $dhcpid) = @_;
- my $config_directory = "$DNSMASQ_CONFIG_ROOT/$dhcp_config->{id}";
+ my $config_directory = "$DNSMASQ_CONFIG_ROOT/$dhcpid";
mkdir($config_directory, 755) if !-d $config_directory;
@@ -127,7 +127,7 @@ DNSMASQ_OPTS="--conf-file=/dev/null"
CFG
PVE::Tools::file_set_contents(
- "$DNSMASQ_DEFAULT_ROOT/dnsmasq.$dhcp_config->{id}",
+ "$DNSMASQ_DEFAULT_ROOT/dnsmasq.$dhcpid",
$default_config
);
@@ -136,7 +136,7 @@ except-interface=lo
bind-dynamic
no-resolv
no-hosts
-dhcp-leasefile=$DNSMASQ_LEASE_ROOT/dnsmasq.$dhcp_config->{id}.leases
+dhcp-leasefile=$DNSMASQ_LEASE_ROOT/dnsmasq.$dhcpid.leases
dhcp-hostsfile=$config_directory/ethers
dhcp-ignore=tag:!known
@@ -163,9 +163,9 @@ CFG
}
sub after_configure {
- my ($class, $dhcp_config) = @_;
+ my ($class, $dhcpid) = @_;
- my $service_name = "dnsmasq\@$dhcp_config->{id}";
+ my $service_name = "dnsmasq\@$dhcpid";
PVE::Tools::run_command(['systemctl', 'enable', $service_name]);
PVE::Tools::run_command(['systemctl', 'restart', $service_name]);
diff --git a/src/PVE/Network/SDN/Dhcp/Plugin.pm b/src/PVE/Network/SDN/Dhcp/Plugin.pm
index 75979e8..7b9e9b7 100644
--- a/src/PVE/Network/SDN/Dhcp/Plugin.pm
+++ b/src/PVE/Network/SDN/Dhcp/Plugin.pm
@@ -8,23 +8,13 @@ use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::SectionConfig);
-PVE::Cluster::cfs_register_file('sdn/dhcp.cfg',
- sub { __PACKAGE__->parse_config(@_); },
- sub { __PACKAGE__->write_config(@_); },
-);
-
my $defaultData = {
propertyList => {
- type => {
- description => "Plugin type.",
- format => 'pve-configid',
- type => 'string',
- },
- node => {
- type => 'array',
- description => 'A list of nodes where this DHCP server should be deployed',
- items => get_standard_option('pve-node'),
- },
+ type => {
+ description => "Plugin type.",
+ format => 'pve-configid',
+ type => 'string',
+ },
},
};
@@ -32,14 +22,6 @@ sub private {
return $defaultData;
}
-sub options {
- return {
- node => {
- optional => 1,
- },
- };
-}
-
sub add_ip_mapping {
my ($class, $dhcp_config, $mac, $ip) = @_;
die 'implement in sub class';
diff --git a/src/PVE/Network/SDN/SubnetPlugin.pm b/src/PVE/Network/SDN/SubnetPlugin.pm
index 47b8406..8447ece 100644
--- a/src/PVE/Network/SDN/SubnetPlugin.pm
+++ b/src/PVE/Network/SDN/SubnetPlugin.pm
@@ -62,10 +62,6 @@ sub private {
}
my $dhcp_range_fmt = {
- server => {
- type => 'pve-configid',
- description => 'ID of the DHCP server responsible for managing this range',
- },
'start-address' => {
type => 'ip',
description => 'Start address for the DHCP IP range',
diff --git a/src/PVE/Network/SDN/Zones/SimplePlugin.pm b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
index 4922903..f30278c 100644
--- a/src/PVE/Network/SDN/Zones/SimplePlugin.pm
+++ b/src/PVE/Network/SDN/Zones/SimplePlugin.pm
@@ -26,7 +26,11 @@ sub properties {
dnszone => {
type => 'string', format => 'dns-name',
description => "dns domain zone ex: mydomain.com",
- }
+ },
+ dhcp => {
+ type => 'pve-configid',
+ description => 'ID of the DHCP server responsible for managing this range',
+ },
};
}
@@ -38,6 +42,7 @@ sub options {
reversedns => { optional => 1 },
dnszone => { optional => 1 },
ipam => { optional => 1 },
+ dhcp => { optional => 1 },
};
}
--
2.39.2
^ permalink raw reply [flat|nested] 4+ messages in thread* [pve-devel] [WIP pve-network 3/3] vnet|subnet: add_next_free_ip : implement dhcprange ipam search
2023-11-09 0:25 [pve-devel] [WIP pve-network 0/3] dhcp changes Alexandre Derumier
2023-11-09 0:25 ` [pve-devel] [WIP pve-network 1/3] define dhcpplugin in zone Alexandre Derumier
2023-11-09 0:25 ` [pve-devel] [WIP pve-network 2/3] dhcp : add|del_ip_mapping: only add|del dhcp reservervation Alexandre Derumier
@ 2023-11-09 0:25 ` Alexandre Derumier
2 siblings, 0 replies; 4+ messages in thread
From: Alexandre Derumier @ 2023-11-09 0:25 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
---
src/PVE/Network/SDN/Ipams/PVEPlugin.pm | 12 ++++++------
src/PVE/Network/SDN/Ipams/Plugin.pm | 7 +++++++
src/PVE/Network/SDN/Subnets.pm | 22 +++++++++++++++++++---
src/PVE/Network/SDN/Vnets.pm | 4 ++--
4 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/src/PVE/Network/SDN/Ipams/PVEPlugin.pm b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm
index fcc8282..9fff52a 100644
--- a/src/PVE/Network/SDN/Ipams/PVEPlugin.pm
+++ b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm
@@ -110,7 +110,7 @@ sub update_ip {
}
sub add_next_freeip {
- my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description) = @_;
+ my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_;
my $cidr = $subnet->{cidr};
my $network = $subnet->{network};
@@ -156,8 +156,8 @@ sub add_next_freeip {
return "$freeip/$mask";
}
-sub add_dhcp_ip {
- my ($class, $subnet, $dhcp_range, $data) = @_;
+sub add_range_next_freeip {
+ my ($class, $subnet, $range, $data) = @_;
my $cidr = $subnet->{cidr};
my $zone = $subnet->{zone};
@@ -171,8 +171,8 @@ sub add_dhcp_ip {
my $dbsubnet = $dbzone->{subnets}->{$cidr};
die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet;
- my $ip = new Net::IP ("$dhcp_range->{'start-address'} - $dhcp_range->{'end-address'}")
- or die "Invalid IP address(es) in DHCP Range!\n";
+ my $ip = new Net::IP ("$range->{'start-address'} - $range->{'end-address'}")
+ or die "Invalid IP address(es) in Range!\n";
do {
my $ip_address = $ip->ip();
@@ -184,7 +184,7 @@ sub add_dhcp_ip {
}
} while (++$ip);
- die "No free IP left in DHCP Range $dhcp_range->{'start-address'}:$dhcp_range->{'end-address'}}\n";
+ die "No free IP left in Range $range->{'start-address'}:$range->{'end-address'}}\n";
});
}
diff --git a/src/PVE/Network/SDN/Ipams/Plugin.pm b/src/PVE/Network/SDN/Ipams/Plugin.pm
index c96eeda..8d69be4 100644
--- a/src/PVE/Network/SDN/Ipams/Plugin.pm
+++ b/src/PVE/Network/SDN/Ipams/Plugin.pm
@@ -98,6 +98,13 @@ sub add_next_freeip {
die "please implement inside plugin";
}
+
+sub add_range_next_freeip {
+ my ($class, $subnet, $range, $data) = @_;
+
+ die "please implement inside plugin";
+}
+
sub del_ip {
my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_;
diff --git a/src/PVE/Network/SDN/Subnets.pm b/src/PVE/Network/SDN/Subnets.pm
index dd9e697..42b84ab 100644
--- a/src/PVE/Network/SDN/Subnets.pm
+++ b/src/PVE/Network/SDN/Subnets.pm
@@ -11,6 +11,7 @@ use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
use PVE::JSONSchema qw(parse_property_string);
use PVE::Network::SDN::Dns;
use PVE::Network::SDN::Ipams;
+use PVE::Network::SDN::Zones;
use PVE::Network::SDN::SubnetPlugin;
PVE::Network::SDN::SubnetPlugin->register();
@@ -203,7 +204,7 @@ sub del_subnet {
}
sub next_free_ip {
- my ($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns) = @_;
+ my ($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns, $dhcprange) = @_;
my $cidr = undef;
my $ip = undef;
@@ -225,9 +226,24 @@ sub next_free_ip {
my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
eval {
- $cidr = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet, $hostname, $mac, $description);
- ($ip, undef) = split(/\//, $cidr);
+ if ($dhcprange) {
+ my ($zoneid, $subnet_network, $subnet_mask) = split(/-/, $subnetid);
+ my $zone = PVE::Network::SDN::Zones::get_zone($zoneid);
+
+ my $data = {
+ mac => $mac,
+ };
+
+ foreach my $range (@{$subnet->{'dhcp-range'}}) {
+ $ip = $plugin->add_range_next_freeip($subnet, $range, $data);
+ next if !$ip;
+ }
+ } else {
+ $cidr = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet, $hostname, $mac, $description);
+ ($ip, undef) = split(/\//, $cidr);
+ }
};
+
die $@ if $@;
}
diff --git a/src/PVE/Network/SDN/Vnets.pm b/src/PVE/Network/SDN/Vnets.pm
index 39bdda0..4ba3523 100644
--- a/src/PVE/Network/SDN/Vnets.pm
+++ b/src/PVE/Network/SDN/Vnets.pm
@@ -97,7 +97,7 @@ sub get_subnet_from_vnet_cidr {
}
sub get_next_free_cidr {
- my ($vnetid, $hostname, $mac, $description, $ipversion, $skipdns) = @_;
+ my ($vnetid, $hostname, $mac, $description, $ipversion, $skipdns, $dhcprange) = @_;
my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid);
my $zoneid = $vnet->{zone};
@@ -118,7 +118,7 @@ sub get_next_free_cidr {
$subnetcount++;
eval {
- $ip = PVE::Network::SDN::Subnets::next_free_ip($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns);
+ $ip = PVE::Network::SDN::Subnets::next_free_ip($zone, $subnetid, $subnet, $hostname, $mac, $description, $skipdns, $dhcprange);
};
warn $@ if $@;
last if $ip;
--
2.39.2
^ permalink raw reply [flat|nested] 4+ messages in thread