]> git.proxmox.com Git - pve-network.git/blame_incremental - src/PVE/Network/SDN/Subnets.pm
ipam: improve update / delete behavior
[pve-network.git] / src / PVE / Network / SDN / Subnets.pm
... / ...
CommitLineData
1package PVE::Network::SDN::Subnets;
2
3use strict;
4use warnings;
5
6use Net::Subnet qw(subnet_matcher);
7use Net::IP;
8use NetAddr::IP qw(:lower);
9
10use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
11use PVE::JSONSchema qw(parse_property_string);
12use PVE::Network::SDN::Dns;
13use PVE::Network::SDN::Ipams;
14
15use PVE::Network::SDN::SubnetPlugin;
16PVE::Network::SDN::SubnetPlugin->register();
17PVE::Network::SDN::SubnetPlugin->init();
18
19sub sdn_subnets_config {
20 my ($cfg, $id, $noerr) = @_;
21
22 die "no sdn subnet ID specified\n" if !$id;
23
24 my $scfg = $cfg->{ids}->{$id};
25 die "sdn subnet '$id' does not exist\n" if (!$noerr && !$scfg);
26
27 if ($scfg) {
28 $scfg->{id} = $id;
29
30 my ($zone, $network, $mask) = split(/-/, $id);
31 $scfg->{cidr} = "$network/$mask";
32 $scfg->{zone} = $zone;
33 $scfg->{network} = $network;
34 $scfg->{mask} = $mask;
35 }
36
37 return $scfg;
38}
39
40sub get_dhcp_ranges {
41 my ($subnet_config) = @_;
42
43 my @dhcp_ranges = ();
44
45 if ($subnet_config->{'dhcp-range'}) {
46 foreach my $element (@{$subnet_config->{'dhcp-range'}}) {
47 my $dhcp_range = eval { parse_property_string('pve-sdn-dhcp-range', $element) };
48
49 if ($@ || !$dhcp_range) {
50 warn "Unable to parse dhcp-range string: $element\n";
51 warn "$@\n" if $@;
52 next;
53 }
54
55 push @dhcp_ranges, $dhcp_range;
56 }
57 }
58
59 return \@dhcp_ranges;
60}
61
62sub config {
63 my ($running) = @_;
64
65 if ($running) {
66 my $cfg = PVE::Network::SDN::running_config();
67 return $cfg->{subnets};
68 }
69
70 return cfs_read_file("sdn/subnets.cfg");
71}
72
73sub write_config {
74 my ($cfg) = @_;
75
76 cfs_write_file("sdn/subnets.cfg", $cfg);
77}
78
79sub sdn_subnets_ids {
80 my ($cfg) = @_;
81
82 return sort keys %{$cfg->{ids}};
83}
84
85sub complete_sdn_subnet {
86 my ($cmdname, $pname, $cvalue) = @_;
87
88 my $cfg = PVE::Network::SDN::Subnets::config();
89
90 return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Subnets::sdn_subnets_ids($cfg) ];
91}
92
93sub get_subnet {
94 my ($subnetid, $running) = @_;
95
96 my $cfg = PVE::Network::SDN::Subnets::config($running);
97 return PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $subnetid, 1);
98}
99
100sub find_ip_subnet {
101 my ($ip, $subnets) = @_;
102
103 my $subnet = undef;
104 my $subnetid = undef;
105
106 foreach my $id (sort keys %{$subnets}) {
107 my $cidr = $subnets->{$id}->{cidr};
108 my $subnet_matcher = subnet_matcher($cidr);
109 next if !$subnet_matcher->($ip);
110 $subnet = $subnets->{$id};
111 $subnetid = $id;
112 last;
113 }
114 die "can't find any subnet for ip $ip" if !$subnet;
115
116 return ($subnetid, $subnet);
117}
118
119sub verify_dns_zone {
120 my ($zone, $dns) = @_;
121
122 return if !$zone || !$dns;
123
124 my $dns_cfg = PVE::Network::SDN::Dns::config();
125 my $plugin_config = $dns_cfg->{ids}->{$dns};
126 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
127 $plugin->verify_zone($plugin_config, $zone);
128}
129
130sub get_reversedns_zone {
131 my ($subnetid, $subnet, $dns, $ip) = @_;
132
133 return if !$subnetid || !$dns || !$ip;
134
135 my $dns_cfg = PVE::Network::SDN::Dns::config();
136 my $plugin_config = $dns_cfg->{ids}->{$dns};
137 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
138 $plugin->get_reversedns_zone($plugin_config, $subnetid, $subnet, $ip);
139}
140
141sub add_dns_record {
142 my ($zone, $dns, $hostname, $ip) = @_;
143 return if !$zone || !$dns || !$hostname || !$ip;
144
145 my $dns_cfg = PVE::Network::SDN::Dns::config();
146 my $plugin_config = $dns_cfg->{ids}->{$dns};
147 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
148 $plugin->add_a_record($plugin_config, $zone, $hostname, $ip);
149
150}
151
152sub add_dns_ptr_record {
153 my ($reversezone, $zone, $dns, $hostname, $ip) = @_;
154
155 return if !$zone || !$reversezone || !$dns || !$hostname || !$ip;
156
157 $hostname .= ".$zone";
158 my $dns_cfg = PVE::Network::SDN::Dns::config();
159 my $plugin_config = $dns_cfg->{ids}->{$dns};
160 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
161 $plugin->add_ptr_record($plugin_config, $reversezone, $hostname, $ip);
162}
163
164sub del_dns_record {
165 my ($zone, $dns, $hostname, $ip) = @_;
166
167 return if !$zone || !$dns || !$hostname || !$ip;
168
169 my $dns_cfg = PVE::Network::SDN::Dns::config();
170 my $plugin_config = $dns_cfg->{ids}->{$dns};
171 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
172 $plugin->del_a_record($plugin_config, $zone, $hostname, $ip);
173}
174
175sub del_dns_ptr_record {
176 my ($reversezone, $dns, $ip) = @_;
177
178 return if !$reversezone || !$dns || !$ip;
179
180 my $dns_cfg = PVE::Network::SDN::Dns::config();
181 my $plugin_config = $dns_cfg->{ids}->{$dns};
182 my $plugin = PVE::Network::SDN::Dns::Plugin->lookup($plugin_config->{type});
183 $plugin->del_ptr_record($plugin_config, $reversezone, $ip);
184}
185
186sub add_subnet {
187 my ($zone, $subnetid, $subnet) = @_;
188
189 my $ipam = $zone->{ipam};
190 return if !$ipam;
191 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
192 my $plugin_config = $ipam_cfg->{ids}->{$ipam};
193 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
194 $plugin->add_subnet($plugin_config, $subnetid, $subnet);
195}
196
197sub del_subnet {
198 my ($zone, $subnetid, $subnet) = @_;
199
200 my $ipam = $zone->{ipam};
201 return if !$ipam;
202 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
203 my $plugin_config = $ipam_cfg->{ids}->{$ipam};
204 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
205 $plugin->del_subnet($plugin_config, $subnetid, $subnet);
206}
207
208sub add_next_free_ip {
209 my ($zone, $subnetid, $subnet, $hostname, $mac, $vmid, $skipdns, $dhcprange) = @_;
210
211 my $cidr = undef;
212 my $ip = undef;
213
214 my $ipamid = $zone->{ipam};
215 my $dns = $zone->{dns};
216 my $dnszone = $zone->{dnszone};
217 my $reversedns = $zone->{reversedns};
218 my $dnszoneprefix = $subnet->{dnszoneprefix};
219
220 $hostname .= ".$dnszoneprefix" if $dnszoneprefix;
221
222 #verify dns zones before ipam
223 verify_dns_zone($dnszone, $dns) if !$skipdns;
224
225 if($ipamid) {
226 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
227 my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
228 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
229 eval {
230 if ($dhcprange) {
231 my $data = {
232 mac => $mac,
233 hostname => $hostname,
234 vmid => $vmid,
235 };
236
237 my $dhcp_ranges = PVE::Network::SDN::Subnets::get_dhcp_ranges($subnet);
238
239 foreach my $range (@$dhcp_ranges) {
240 $ip = $plugin->add_range_next_freeip($plugin_config, $subnet, $range, $data);
241 last if $ip;
242 }
243 } else {
244 $ip = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid);
245 }
246 };
247
248 die $@ if $@;
249
250 eval { PVE::Network::SDN::Ipams::add_cache_mac_ip($mac, $ip); };
251 warn $@ if $@;
252 }
253
254 eval {
255 my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip);
256
257 if(!$skipdns) {
258 #add dns
259 add_dns_record($dnszone, $dns, $hostname, $ip);
260 #add reverse dns
261 add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip);
262 }
263 };
264 if ($@) {
265 #rollback
266 my $err = $@;
267 eval {
268 PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $subnet, $ip, $hostname, $mac)
269 };
270 die $err;
271 }
272 return $ip;
273}
274
275sub add_ip {
276 my ($zone, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $skipdns) = @_;
277
278 return if !$subnet || !$ip;
279
280 my $ipaddr = NetAddr::IP->new($ip);
281 $ip = $ipaddr->canon();
282
283 my $ipamid = $zone->{ipam};
284 my $dns = $zone->{dns};
285 my $dnszone = $zone->{dnszone};
286 my $reversedns = $zone->{reversedns};
287 my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip);
288 my $dnszoneprefix = $subnet->{dnszoneprefix};
289
290 $hostname .= ".$dnszoneprefix" if $dnszoneprefix;
291
292 #verify dns zones before ipam
293 if(!$skipdns) {
294 verify_dns_zone($dnszone, $dns);
295 verify_dns_zone($reversednszone, $reversedns);
296 }
297
298 if ($ipamid) {
299
300 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
301 my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
302 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
303
304 eval {
305 $plugin->add_ip($plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway);
306 };
307 die $@ if $@;
308
309 eval { PVE::Network::SDN::Ipams::add_cache_mac_ip($mac, $ip) if $mac; };
310 warn $@ if $@;
311 }
312
313 eval {
314 if(!$skipdns) {
315 #add dns
316 add_dns_record($dnszone, $dns, $hostname, $ip);
317 #add reverse dns
318 add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip);
319 }
320 };
321 if ($@) {
322 #rollback
323 my $err = $@;
324 eval {
325 PVE::Network::SDN::Subnets::del_ip($zone, $subnetid, $subnet, $ip, $hostname, $mac)
326 };
327 die $err;
328 }
329}
330
331sub update_ip {
332 my ($zone, $subnetid, $subnet, $ip, $hostname, $oldhostname, $mac, $vmid, $skipdns) = @_;
333
334 return if !$subnet || !$ip;
335
336 my $ipaddr = NetAddr::IP->new($ip);
337 $ip = $ipaddr->canon();
338
339 my $ipamid = $zone->{ipam};
340 my $dns = $zone->{dns};
341 my $dnszone = $zone->{dnszone};
342 my $reversedns = $zone->{reversedns};
343 my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip);
344 my $dnszoneprefix = $subnet->{dnszoneprefix};
345
346 $hostname .= ".$dnszoneprefix" if $dnszoneprefix;
347
348 #verify dns zones before ipam
349 if(!$skipdns) {
350 verify_dns_zone($dnszone, $dns);
351 verify_dns_zone($reversednszone, $reversedns);
352 }
353
354 if ($ipamid) {
355 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
356 my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
357 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
358 eval {
359 $plugin->update_ip($plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid);
360 };
361 die $@ if $@;
362 }
363
364 return if $hostname eq $oldhostname;
365
366 eval {
367 if(!$skipdns) {
368 #add dns
369 del_dns_record($dnszone, $dns, $oldhostname, $ip);
370 add_dns_record($dnszone, $dns, $hostname, $ip);
371 #add reverse dns
372 del_dns_ptr_record($reversednszone, $reversedns, $ip);
373 add_dns_ptr_record($reversednszone, $dnszone, $reversedns, $hostname, $ip);
374 }
375 };
376}
377
378sub del_ip {
379 my ($zone, $subnetid, $subnet, $ip, $hostname, $mac, $skipdns) = @_;
380
381 return if !$subnet || !$ip;
382
383 my $ipaddr = NetAddr::IP->new($ip);
384 $ip = $ipaddr->canon();
385
386 my $ipamid = $zone->{ipam};
387 my $dns = $zone->{dns};
388 my $dnszone = $zone->{dnszone};
389 my $reversedns = $zone->{reversedns};
390 my $reversednszone = get_reversedns_zone($subnetid, $subnet, $reversedns, $ip);
391 my $dnszoneprefix = $subnet->{dnszoneprefix};
392 $hostname .= ".$dnszoneprefix" if $dnszoneprefix;
393
394 if(!$skipdns) {
395 verify_dns_zone($dnszone, $dns);
396 verify_dns_zone($reversednszone, $reversedns);
397 }
398
399 if ($ipamid) {
400 my $ipam_cfg = PVE::Network::SDN::Ipams::config();
401 my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
402 my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
403 $plugin->del_ip($plugin_config, $subnetid, $subnet, $ip);
404
405 if ($mac) {
406 eval { PVE::Network::SDN::Ipams::del_cache_mac_ip($mac, $ip) };
407 warn $@ if $@;
408 }
409 }
410
411 eval {
412 if(!$skipdns) {
413 del_dns_record($dnszone, $dns, $hostname, $ip);
414 del_dns_ptr_record($reversednszone, $reversedns, $ip);
415 }
416 };
417 if ($@) {
418 warn $@;
419 }
420}
421
4221;