2020-10-05 17:08:47 +02:00
|
|
|
package PVE::Network::SDN::Ipams::NetboxPlugin;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
use PVE::INotify;
|
|
|
|
use PVE::Cluster;
|
|
|
|
use PVE::Tools;
|
|
|
|
|
|
|
|
use base('PVE::Network::SDN::Ipams::Plugin');
|
|
|
|
|
|
|
|
sub type {
|
|
|
|
return 'netbox';
|
|
|
|
}
|
|
|
|
|
|
|
|
sub properties {
|
|
|
|
return {
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
sub options {
|
|
|
|
return {
|
|
|
|
url => { optional => 0},
|
|
|
|
token => { optional => 0 },
|
2025-02-10 15:19:29 +01:00
|
|
|
fingerprint => { optional => 1 },
|
2020-10-05 17:08:47 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
sub netbox_api_request {
|
|
|
|
my ($config, $method, $path, $params) = @_;
|
|
|
|
|
|
|
|
return PVE::Network::SDN::api_request(
|
|
|
|
$method,
|
|
|
|
"$config->{url}${path}",
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/json; charset=UTF-8',
|
|
|
|
'Authorization' => "token $config->{token}"
|
|
|
|
],
|
|
|
|
$params,
|
|
|
|
$config->{fingerprint},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
# Plugin implementation
|
|
|
|
|
|
|
|
sub add_subnet {
|
2021-01-05 10:35:29 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2020-10-05 17:09:08 +02:00
|
|
|
my $cidr = $subnet->{cidr};
|
2020-10-05 17:08:47 +02:00
|
|
|
my $gateway = $subnet->{gateway};
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
if (get_prefix_id($plugin_config, $cidr, $noerr)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "prefix $cidr already exists in netbox";
|
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
eval {
|
|
|
|
netbox_api_request($plugin_config, "POST", "/ipam/prefixes/", {
|
|
|
|
prefix => $cidr
|
|
|
|
});
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "error adding subnet to ipam: $@";
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub del_subnet {
|
2021-01-05 10:35:29 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $noerr) = @_;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2020-10-05 17:09:08 +02:00
|
|
|
my $cidr = $subnet->{cidr};
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $internalid = get_prefix_id($plugin_config, $cidr, $noerr);
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2025-03-10 09:50:57 +01:00
|
|
|
# definedness check, because ID could be 0
|
|
|
|
if (!defined($internalid)) {
|
|
|
|
warn "could not find id for ip prefix $cidr";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_prefix_empty($plugin_config, $cidr, $noerr)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "not deleting prefix $cidr because it still contains entries";
|
|
|
|
}
|
|
|
|
|
|
|
|
# last IP is assumed to be the gateway, delete it
|
|
|
|
if (!$class->del_ip($plugin_config, $subnetid, $subnet, $subnet->{gateway}, $noerr)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not delete gateway ip from subnet $subnetid";
|
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
netbox_api_request($plugin_config, "DELETE", "/ipam/prefixes/$internalid/");
|
2020-10-05 17:08:47 +02:00
|
|
|
};
|
2025-03-10 09:50:56 +01:00
|
|
|
die "error deleting subnet from ipam: $@" if $@ && !$noerr;
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sub add_ip {
|
2023-11-17 12:39:43 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2020-10-05 17:09:08 +02:00
|
|
|
my $mask = $subnet->{mask};
|
2023-11-17 12:39:43 +01:00
|
|
|
|
|
|
|
my $description = undef;
|
|
|
|
if ($is_gateway) {
|
|
|
|
$description = 'gateway'
|
|
|
|
} elsif ($mac) {
|
|
|
|
$description = "mac:$mac";
|
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
netbox_api_request($plugin_config, "POST", "/ipam/ip-addresses/", {
|
|
|
|
address => "$ip/$mask",
|
|
|
|
dns_name => $hostname,
|
|
|
|
description => $description,
|
|
|
|
});
|
2020-10-05 17:08:47 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if ($@) {
|
2025-02-10 15:19:29 +01:00
|
|
|
if ($is_gateway) {
|
2025-03-10 09:50:56 +01:00
|
|
|
die "error add subnet ip to ipam: ip $ip already exist: $@"
|
|
|
|
if !is_ip_gateway($plugin_config, $ip, $noerr);
|
2025-02-10 15:19:29 +01:00
|
|
|
} elsif (!$noerr) {
|
|
|
|
die "error add subnet ip to ipam: ip already exist: $@";
|
2021-06-04 13:25:00 +02:00
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-05 10:35:24 +01:00
|
|
|
sub update_ip {
|
2023-11-17 12:39:43 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_;
|
2021-01-05 10:35:24 +01:00
|
|
|
|
|
|
|
my $mask = $subnet->{mask};
|
2023-11-17 12:39:43 +01:00
|
|
|
|
|
|
|
my $description = undef;
|
|
|
|
if ($is_gateway) {
|
|
|
|
$description = 'gateway'
|
|
|
|
} elsif ($mac) {
|
|
|
|
$description = "mac:$mac";
|
|
|
|
}
|
2021-01-05 10:35:24 +01:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $ip_id = get_ip_id($plugin_config, $ip, $noerr);
|
2021-01-05 10:35:24 +01:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
# definedness check, because ID could be 0
|
|
|
|
if (!defined($ip_id)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not find id for ip address $ip";
|
|
|
|
}
|
2021-01-05 10:35:24 +01:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
netbox_api_request($plugin_config, "PATCH", "/ipam/ip-addresses/$ip_id/", {
|
|
|
|
address => "$ip/$mask",
|
|
|
|
dns_name => $hostname,
|
|
|
|
description => $description,
|
|
|
|
});
|
2021-01-05 10:35:24 +01:00
|
|
|
};
|
|
|
|
if ($@) {
|
2021-01-05 10:35:29 +01:00
|
|
|
die "error update ip $ip : $@" if !$noerr;
|
2021-01-05 10:35:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
sub add_next_freeip {
|
2023-11-17 12:39:43 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid, $noerr) = @_;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2020-10-05 17:09:08 +02:00
|
|
|
my $cidr = $subnet->{cidr};
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $internalid = get_prefix_id($plugin_config, $cidr, $noerr);
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
# definedness check, because ID could be 0
|
|
|
|
if (!defined($internalid)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not find id for prefix $cidr";
|
|
|
|
}
|
2023-11-17 12:39:43 +01:00
|
|
|
|
|
|
|
my $description = "mac:$mac" if $mac;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
my $result = netbox_api_request($plugin_config, "POST", "/ipam/prefixes/$internalid/available-ips/", {
|
|
|
|
dns_name => $hostname,
|
|
|
|
description => $description,
|
|
|
|
});
|
|
|
|
|
2024-01-04 17:11:36 +01:00
|
|
|
my ($ip, undef) = split(/\//, $result->{address});
|
|
|
|
return $ip;
|
2020-10-05 17:08:47 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if ($@) {
|
2021-01-05 10:35:29 +01:00
|
|
|
die "can't find free ip in subnet $cidr: $@" if !$noerr;
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-17 12:39:43 +01:00
|
|
|
sub add_range_next_freeip {
|
|
|
|
my ($class, $plugin_config, $subnet, $range, $data, $noerr) = @_;
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $internalid = get_iprange_id($plugin_config, $range, $noerr);
|
2023-11-17 12:39:43 +01:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
# definedness check, because ID could be 0
|
|
|
|
if (!defined($internalid)) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not find id for ip range $range->{'start-address'}:$range->{'end-address'}";
|
|
|
|
}
|
2023-11-17 12:39:43 +01:00
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $description = "mac:$data->{mac}" if $data->{mac};
|
2023-11-17 12:39:43 +01:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
my $result = netbox_api_request($plugin_config, "POST", "/ipam/ip-ranges/$internalid/available-ips/", {
|
|
|
|
dns_name => $data->{hostname},
|
|
|
|
description => $description,
|
|
|
|
});
|
|
|
|
|
2024-01-04 17:11:36 +01:00
|
|
|
my ($ip, undef) = split(/\//, $result->{address});
|
2023-11-17 12:39:43 +01:00
|
|
|
print "found ip free $ip in range $range->{'start-address'}-$range->{'end-address'}\n" if $ip;
|
2024-01-04 17:11:36 +01:00
|
|
|
return $ip;
|
2023-11-17 12:39:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
if ($@) {
|
|
|
|
die "can't find free ip in range $range->{'start-address'}-$range->{'end-address'}: $@" if !$noerr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
sub del_ip {
|
2021-01-05 10:35:29 +01:00
|
|
|
my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_;
|
2020-10-05 17:08:47 +02:00
|
|
|
|
|
|
|
return if !$ip;
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $ip_id = get_ip_id($plugin_config, $ip, $noerr);
|
|
|
|
if (!defined($ip_id)) {
|
|
|
|
warn "could not find id for ip $ip";
|
|
|
|
return;
|
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
|
|
|
|
eval {
|
2025-03-10 09:50:56 +01:00
|
|
|
netbox_api_request($plugin_config, "DELETE", "/ipam/ip-addresses/$ip_id/");
|
2020-10-05 17:08:47 +02:00
|
|
|
};
|
|
|
|
if ($@) {
|
2021-01-05 10:35:29 +01:00
|
|
|
die "error delete ip $ip : $@" if !$noerr;
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
2025-03-10 09:50:57 +01:00
|
|
|
|
|
|
|
return 1;
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
|
2023-11-17 12:39:43 +01:00
|
|
|
sub get_ips_from_mac {
|
2025-03-10 09:50:56 +01:00
|
|
|
my ($class, $plugin_config, $mac, $zoneid, $noerr) = @_;
|
2023-11-17 12:39:43 +01:00
|
|
|
|
|
|
|
my $ip4 = undef;
|
|
|
|
my $ip6 = undef;
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
my $data = eval {
|
|
|
|
netbox_api_request($plugin_config, "GET", "/ipam/ip-addresses/?description__ic=$mac");
|
|
|
|
};
|
2025-02-10 15:19:29 +01:00
|
|
|
|
2023-11-17 12:39:43 +01:00
|
|
|
for my $ip (@{$data->{results}}) {
|
|
|
|
if ($ip->{family}->{value} == 4 && !$ip4) {
|
|
|
|
($ip4, undef) = split(/\//, $ip->{address});
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ip->{family}->{value} == 6 && !$ip6) {
|
|
|
|
($ip6, undef) = split(/\//, $ip->{address});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ($ip4, $ip6);
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
sub verify_api {
|
|
|
|
my ($class, $plugin_config) = @_;
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
eval { netbox_api_request($plugin_config, "GET", "/ipam/aggregates/"); };
|
2020-10-05 17:08:47 +02:00
|
|
|
if ($@) {
|
|
|
|
die "Can't connect to netbox api: $@";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub on_update_hook {
|
|
|
|
my ($class, $plugin_config) = @_;
|
2025-03-10 09:50:56 +01:00
|
|
|
verify_api($class, $plugin_config);
|
2020-10-05 17:08:47 +02:00
|
|
|
}
|
|
|
|
|
2025-03-10 09:50:56 +01:00
|
|
|
# helpers
|
2020-10-05 17:08:47 +02:00
|
|
|
sub get_prefix_id {
|
2025-03-10 09:50:56 +01:00
|
|
|
my ($config, $cidr, $noerr) = @_;
|
|
|
|
|
|
|
|
# we need to supply any IP inside the prefix, without supplying the mask, so
|
|
|
|
# just take the one from the cidr
|
|
|
|
my ($ip, undef) = split(/\//, $cidr);
|
|
|
|
|
|
|
|
my $result = eval { netbox_api_request($config, "GET", "/ipam/prefixes/?q=$ip") };
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not obtain ID for prefix $cidr: $@";
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
my $data = @{$result->{results}}[0];
|
|
|
|
my $internalid = $data->{id};
|
|
|
|
return $internalid;
|
|
|
|
}
|
|
|
|
|
2023-11-17 12:39:43 +01:00
|
|
|
sub get_iprange_id {
|
2025-03-10 09:50:56 +01:00
|
|
|
my ($config, $range, $noerr) = @_;
|
|
|
|
|
|
|
|
my $result = eval {
|
|
|
|
netbox_api_request(
|
|
|
|
$config,
|
|
|
|
"GET",
|
|
|
|
"/ipam/ip-ranges/?start_address=$range->{'start-address'}&end_address=$range->{'end-address'}",
|
|
|
|
);
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not obtain ID for IP range $range->{'start-address'}:$range->{'end-address'}: $@";
|
|
|
|
}
|
|
|
|
|
2023-11-17 12:39:43 +01:00
|
|
|
my $data = @{$result->{results}}[0];
|
|
|
|
my $internalid = $data->{id};
|
|
|
|
return $internalid;
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
sub get_ip_id {
|
2025-03-10 09:50:56 +01:00
|
|
|
my ($config, $ip, $noerr) = @_;
|
|
|
|
|
|
|
|
my $result = eval { netbox_api_request($config, "GET", "/ipam/ip-addresses/?q=$ip") };
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not obtain ID for IP $ip: $@";
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
my $data = @{$result->{results}}[0];
|
|
|
|
my $ip_id = $data->{id};
|
|
|
|
return $ip_id;
|
|
|
|
}
|
|
|
|
|
2021-06-04 13:25:00 +02:00
|
|
|
sub is_ip_gateway {
|
2025-03-10 09:50:56 +01:00
|
|
|
my ($config, $ip, $noerr) = @_;
|
|
|
|
|
|
|
|
my $result = eval { netbox_api_request($config, "GET", "/ipam/ip-addresses/?q=$ip") };
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not obtain ipam entry for address $ip: $@";
|
|
|
|
}
|
|
|
|
|
2021-06-04 13:25:00 +02:00
|
|
|
my $data = @{$result->{data}}[0];
|
|
|
|
my $description = $data->{description};
|
|
|
|
my $is_gateway = 1 if $description eq 'gateway';
|
|
|
|
return $is_gateway;
|
|
|
|
}
|
2020-10-05 17:08:47 +02:00
|
|
|
|
2025-03-10 09:50:57 +01:00
|
|
|
sub is_prefix_empty {
|
|
|
|
my ($config, $cidr, $noerr) = @_;
|
|
|
|
|
|
|
|
my $result = eval { netbox_api_request($config, "GET", "/ipam/ip-addresses/?parent=$cidr") };
|
|
|
|
if ($@) {
|
|
|
|
return if $noerr;
|
|
|
|
die "could not query children for prefix $cidr: $@";
|
|
|
|
}
|
|
|
|
|
|
|
|
# checking against 1, because we do not count the gateway
|
|
|
|
return scalar(@{$result->{results}}) <= 1;
|
|
|
|
}
|
|
|
|
|
2020-10-05 17:08:47 +02:00
|
|
|
1;
|
|
|
|
|
|
|
|
|