From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id B3B0461AB5 for ; Mon, 28 Sep 2020 10:44:40 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 18D712B730 for ; Mon, 28 Sep 2020 10:44:04 +0200 (CEST) Received: from kvmformation1.odiso.net (globalOdiso.M6Lille.odiso.net [89.248.211.242]) by firstgate.proxmox.com (Proxmox) with ESMTP id ED24F2B417 for ; Mon, 28 Sep 2020 10:43:37 +0200 (CEST) Received: by kvmformation1.odiso.net (Postfix, from userid 0) id C94C3D686F; Mon, 28 Sep 2020 10:43:37 +0200 (CEST) From: Alexandre Derumier To: pve-devel@lists.proxmox.com Date: Mon, 28 Sep 2020 10:43:20 +0200 Message-Id: <20200928084336.3487196-11-aderumier@odiso.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200928084336.3487196-1-aderumier@odiso.com> References: <20200928084336.3487196-1-aderumier@odiso.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 1 AWL -0.430 Adjusted score from AWL reputation of From: address HEADER_FROM_DIFFERENT_DOMAINS 0.078 From and EnvelopeFrom 2nd level mail domains are different KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods KHOP_HELO_FCRDNS 0.399 Relay HELO differs from its IP's reverse DNS NO_DNS_FOR_FROM 0.379 Envelope sender has no MX or A DNS records SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Subject: [pve-devel] [PATCH v9 pve-network 10/26] add ipams plugins X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 28 Sep 2020 08:44:40 -0000 Signed-off-by: Alexandre Derumier --- PVE/API2/Network/SDN.pm | 7 + PVE/API2/Network/SDN/Ipams.pm | 241 +++++++++++++++++++++++++ PVE/API2/Network/SDN/Makefile | 2 +- PVE/API2/Network/SDN/Subnets.pm | 47 ++++- PVE/Network/SDN/Ipams.pm | 78 ++++++++ PVE/Network/SDN/Ipams/Makefile | 8 + PVE/Network/SDN/Ipams/NetboxPlugin.pm | 169 +++++++++++++++++ PVE/Network/SDN/Ipams/PhpIpamPlugin.pm | 189 +++++++++++++++++++ PVE/Network/SDN/Ipams/Plugin.pm | 127 +++++++++++++ PVE/Network/SDN/Makefile | 3 +- PVE/Network/SDN/SubnetPlugin.pm | 5 +- PVE/Network/SDN/Vnets.pm | 25 +++ 12 files changed, 895 insertions(+), 6 deletions(-) create mode 100644 PVE/API2/Network/SDN/Ipams.pm create mode 100644 PVE/Network/SDN/Ipams.pm create mode 100644 PVE/Network/SDN/Ipams/Makefile create mode 100644 PVE/Network/SDN/Ipams/NetboxPlugin.pm create mode 100644 PVE/Network/SDN/Ipams/PhpIpamPlugin.pm create mode 100644 PVE/Network/SDN/Ipams/Plugin.pm diff --git a/PVE/API2/Network/SDN.pm b/PVE/API2/Network/SDN.pm index 175f76f..6055fe5 100644 --- a/PVE/API2/Network/SDN.pm +++ b/PVE/API2/Network/SDN.pm @@ -16,6 +16,7 @@ use PVE::API2::Network::SDN::Controllers; use PVE::API2::Network::SDN::Vnets; use PVE::API2::Network::SDN::Zones; use PVE::API2::Network::SDN::Subnets; +use PVE::API2::Network::SDN::Ipams; use base qw(PVE::RESTHandler); @@ -39,6 +40,11 @@ __PACKAGE__->register_method ({ path => 'subnets', }); +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Network::SDN::Ipams", + path => 'ipams', +}); + __PACKAGE__->register_method({ name => 'index', path => '', @@ -69,6 +75,7 @@ __PACKAGE__->register_method({ { id => 'zones' }, { id => 'controllers' }, { id => 'subnets' }, + { id => 'ipams' }, ]; return $res; diff --git a/PVE/API2/Network/SDN/Ipams.pm b/PVE/API2/Network/SDN/Ipams.pm new file mode 100644 index 0000000..f8665a1 --- /dev/null +++ b/PVE/API2/Network/SDN/Ipams.pm @@ -0,0 +1,241 @@ +package PVE::API2::Network::SDN::Ipams; + +use strict; +use warnings; + +use PVE::SafeSyslog; +use PVE::Tools qw(extract_param); +use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Network::SDN; +use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Ipams::Plugin; +use PVE::Network::SDN::Ipams::PhpIpamPlugin; +use PVE::Network::SDN::Ipams::NetboxPlugin; + +use Storable qw(dclone); +use PVE::JSONSchema qw(get_standard_option); +use PVE::RPCEnvironment; + +use PVE::RESTHandler; + +use base qw(PVE::RESTHandler); + +my $sdn_ipams_type_enum = PVE::Network::SDN::Ipams::Plugin->lookup_types(); + +my $api_sdn_ipams_config = sub { + my ($cfg, $id) = @_; + + my $scfg = dclone(PVE::Network::SDN::Ipams::sdn_ipams_config($cfg, $id)); + $scfg->{ipam} = $id; + $scfg->{digest} = $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name => 'index', + path => '', + method => 'GET', + description => "SDN ipams index.", + permissions => { + description => "Only list entries where you have 'SDN.Audit' or 'SDN.Allocate' permissions on '/sdn/ipams/'", + user => 'all', + }, + parameters => { + additionalProperties => 0, + properties => { + type => { + description => "Only list sdn ipams of specific type", + type => 'string', + enum => $sdn_ipams_type_enum, + optional => 1, + }, + }, + }, + returns => { + type => 'array', + items => { + type => "object", + properties => { ipam => { type => 'string'}, + type => { type => 'string'}, + }, + }, + links => [ { rel => 'child', href => "{ipam}" } ], + }, + code => sub { + my ($param) = @_; + + my $rpcenv = PVE::RPCEnvironment::get(); + my $authuser = $rpcenv->get_user(); + + + my $cfg = PVE::Network::SDN::Ipams::config(); + + my @sids = PVE::Network::SDN::Ipams::sdn_ipams_ids($cfg); + my $res = []; + foreach my $id (@sids) { + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/ipams/$id", $privs, 1); + + my $scfg = &$api_sdn_ipams_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config = $cfg->{ids}->{$id}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name => 'read', + path => '{ipam}', + method => 'GET', + description => "Read sdn ipam configuration.", + permissions => { + check => ['perm', '/sdn/ipams/{ipam}', ['SDN.Allocate']], + }, + + parameters => { + additionalProperties => 0, + properties => { + ipam => get_standard_option('pve-sdn-ipam-id'), + }, + }, + returns => { type => 'object' }, + code => sub { + my ($param) = @_; + + my $cfg = PVE::Network::SDN::Ipams::config(); + + return &$api_sdn_ipams_config($cfg, $param->{ipam}); + }}); + +__PACKAGE__->register_method ({ + name => 'create', + protected => 1, + path => '', + method => 'POST', + description => "Create a new sdn ipam object.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Ipams::Plugin->createSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $type = extract_param($param, 'type'); + my $id = extract_param($param, 'ipam'); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($type); + my $opts = $plugin->check_config($id, $param, 1, 1); + + # create /etc/pve/sdn directory + PVE::Cluster::check_cfs_quorum(); + mkdir("/etc/pve/sdn"); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $controller_cfg = PVE::Network::SDN::Controllers::config(); + + my $scfg = undef; + if ($scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, $id, 1)) { + die "sdn ipam object ID '$id' already defined\n"; + } + + $ipam_cfg->{ids}->{$id} = $opts; + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "create sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'update', + protected => 1, + path => '{ipam}', + method => 'PUT', + description => "Update sdn ipam object configuration.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => PVE::Network::SDN::Ipams::Plugin->updateSchema(), + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'ipam'); + my $digest = extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + + PVE::SectionConfig::assert_if_modified($ipam_cfg, $digest); + + my $scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, $id); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{type}); + my $opts = $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} = $opts->{$k}; + } + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "update sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name => 'delete', + protected => 1, + path => '{ipam}', + method => 'DELETE', + description => "Delete sdn ipam object configuration.", + permissions => { + check => ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters => { + additionalProperties => 0, + properties => { + ipam => get_standard_option('pve-sdn-ipam-id', { + completion => \&PVE::Network::SDN::Ipams::complete_sdn_ipams, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my $id = extract_param($param, 'ipam'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg = PVE::Network::SDN::Ipams::config(); + + my $scfg = PVE::Network::SDN::Ipams::sdn_ipams_config($cfg, $id); + + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{type}); + + my $vnet_cfg = PVE::Network::SDN::Vnets::config(); + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Ipams::write_config($cfg); + + }, "delete sdn zone object failed"); + + return undef; + }}); + +1; diff --git a/PVE/API2/Network/SDN/Makefile b/PVE/API2/Network/SDN/Makefile index 59626fa..1117dfa 100644 --- a/PVE/API2/Network/SDN/Makefile +++ b/PVE/API2/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm +SOURCES=Vnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm PERL5DIR=${DESTDIR}/usr/share/perl5 diff --git a/PVE/API2/Network/SDN/Subnets.pm b/PVE/API2/Network/SDN/Subnets.pm index d9cb9e9..b60db3d 100644 --- a/PVE/API2/Network/SDN/Subnets.pm +++ b/PVE/API2/Network/SDN/Subnets.pm @@ -6,10 +6,13 @@ use warnings; use PVE::SafeSyslog; use PVE::Tools qw(extract_param); use PVE::Cluster qw(cfs_read_file cfs_write_file); +use PVE::Exception qw(raise raise_param_exc); use PVE::Network::SDN; use PVE::Network::SDN::Subnets; use PVE::Network::SDN::SubnetPlugin; use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::Ipams; +use PVE::Network::SDN::Ipams::Plugin; use Storable qw(dclone); use PVE::JSONSchema qw(get_standard_option); @@ -133,6 +136,17 @@ __PACKAGE__->register_method ({ $cfg->{ids}->{$id} = $opts; PVE::Network::SDN::SubnetPlugin->on_update_hook($id, $cfg); + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $ipam = $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam => "$ipam not existing"}) if !$ipam_cfg->{ids}->{$ipam}; + my $plugin_config = $ipam_cfg->{ids}->{$ipam}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->add_subnet($plugin_config, $id, $cfg->{ids}->{$id}); + $plugin->add_ip($plugin_config, $id, $opts->{gateway}, 1) if $opts->{gateway}; + } + PVE::Network::SDN::Subnets::write_config($cfg); }, "create sdn subnet object failed"); @@ -161,6 +175,7 @@ __PACKAGE__->register_method ({ sub { my $cfg = PVE::Network::SDN::Subnets::config(); + my $scfg = &$api_sdn_subnets_config($cfg, $id); PVE::SectionConfig::assert_if_modified($cfg, $digest); @@ -168,6 +183,24 @@ __PACKAGE__->register_method ({ $cfg->{ids}->{$id} = $opts; PVE::Network::SDN::SubnetPlugin->on_update_hook($id, $cfg); + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $ipam = $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam => "$ipam not existing"}) if !$ipam_cfg->{ids}->{$ipam}; + my $plugin_config = $ipam_cfg->{ids}->{$ipam}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->add_subnet($plugin_config, $id, $cfg->{ids}->{$id}); + + if($opts->{gateway} && $scfg->{gateway} && $opts->{gateway} ne $scfg->{gateway}) { + $plugin->del_ip($plugin_config, $scfg->{gateway}); + } + if (!defined($opts->{gateway}) && $scfg->{gateway}) { + $plugin->del_ip($plugin_config, $scfg->{gateway}); + } + $plugin->add_ip($plugin_config, $id, $opts->{gateway}, 1) if $opts->{gateway}; + } + PVE::Network::SDN::Subnets::write_config($cfg); }, "update sdn subnet object failed"); @@ -200,7 +233,6 @@ __PACKAGE__->register_method ({ PVE::Network::SDN::lock_sdn_config( sub { - my $cfg = PVE::Network::SDN::Subnets::config(); my $scfg = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id); @@ -208,8 +240,19 @@ __PACKAGE__->register_method ({ my $subnets_cfg = PVE::Network::SDN::Subnets::config(); my $vnets_cfg = PVE::Network::SDN::Vnets::config(); - delete $cfg->{ids}->{$id}; PVE::Network::SDN::SubnetPlugin->on_delete_hook($id, $subnets_cfg, $vnets_cfg); + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $ipam = $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam => "$ipam not existing"}) if !$ipam_cfg->{ids}->{$ipam}; + my $plugin_config = $ipam_cfg->{ids}->{$ipam}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + $plugin->del_subnet($plugin_config, $id, $scfg); + } + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Subnets::write_config($cfg); }, "delete sdn subnet object failed"); diff --git a/PVE/Network/SDN/Ipams.pm b/PVE/Network/SDN/Ipams.pm new file mode 100644 index 0000000..3d33632 --- /dev/null +++ b/PVE/Network/SDN/Ipams.pm @@ -0,0 +1,78 @@ +package PVE::Network::SDN::Ipams; + +use strict; +use warnings; + +use Data::Dumper; +use JSON; + +use PVE::Tools qw(extract_param dir_glob_regex run_command); +use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network; + +use PVE::Network::SDN::Ipams::NetboxPlugin; +use PVE::Network::SDN::Ipams::PhpIpamPlugin; +use PVE::Network::SDN::Ipams::Plugin; + +PVE::Network::SDN::Ipams::NetboxPlugin->register(); +PVE::Network::SDN::Ipams::PhpIpamPlugin->register(); +PVE::Network::SDN::Ipams::Plugin->init(); + + +sub sdn_ipams_config { + my ($cfg, $id, $noerr) = @_; + + die "no sdn ipam ID specified\n" if !$id; + + my $scfg = $cfg->{ids}->{$id}; + die "sdn '$id' does not exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config = cfs_read_file("sdn/ipams.cfg"); + return $config; +} + +sub get_plugin_config { + my ($vnet) = @_; + my $ipamid = $vnet->{ipam}; + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + return $ipam_cfg->{ids}->{$ipamid}; +} + +sub write_config { + my ($cfg) = @_; + + cfs_write_file("sdn/ipams.cfg", $cfg); +} + +sub sdn_ipams_ids { + my ($cfg) = @_; + + return keys %{$cfg->{ids}}; +} + +sub complete_sdn_vnet { + my ($cmdname, $pname, $cvalue) = @_; + + my $cfg = PVE::Network::SDN::Ipams::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Vnets::sdn_ipams_ids($cfg) ]; +} + +sub next_free_ip { + my ($subnetid, $subnet) = @_; + + my $ipam_cfg = PVE::Network::SDN::Ipams::config(); + my $ipamid = $subnet->{ipam}; + return if !$ipamid; + + my $plugin_config = $ipam_cfg->{ids}->{$ipamid}; + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + my $ip = $plugin->add_next_freeip($plugin_config, $subnetid, $subnet); + return $ip; +} +1; + diff --git a/PVE/Network/SDN/Ipams/Makefile b/PVE/Network/SDN/Ipams/Makefile new file mode 100644 index 0000000..884c47a --- /dev/null +++ b/PVE/Network/SDN/Ipams/Makefile @@ -0,0 +1,8 @@ +SOURCES=Plugin.pm PhpIpamPlugin.pm NetboxPlugin.pm + + +PERL5DIR=${DESTDIR}/usr/share/perl5 + +.PHONY: install +install: + for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/Ipams/$$i; done diff --git a/PVE/Network/SDN/Ipams/NetboxPlugin.pm b/PVE/Network/SDN/Ipams/NetboxPlugin.pm new file mode 100644 index 0000000..ccc1184 --- /dev/null +++ b/PVE/Network/SDN/Ipams/NetboxPlugin.pm @@ -0,0 +1,169 @@ +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 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $gateway = $subnet->{gateway}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + my ($network, $mask) = split(/-/, $subnetid); + + my $params = { prefix => $cidr }; + + eval { + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/ipam/prefixes/", $headers, $params); + $subnet->{ipamid} = $result->{id} if defined($result->{id}); + }; + if ($@) { + die "error add subnet to ipam: $@"; + } + } + +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $gateway = $subnet->{gateway}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + return if !$internalid; + #fixme: check that prefix is empty exluding gateway, before delete + + PVE::Network::SDN::Ipams::NetboxPlugin::del_ip($class, $plugin_config, $gateway) if $gateway; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("DELETE", "$url/ipam/prefixes/$internalid/", $headers); + }; + if ($@) { + die "error deleting subnet from ipam: $@"; + } + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $ip, $is_gateway) = @_; + + my ($network, $mask) = split(/-/, $subnetid); + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $params = { address => "$ip/$mask" }; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/ipam/ip-addresses/", $headers, $params); + }; + + if ($@) { + die "error add subnet ip to ipam: ip already exist: $@"; + } +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_prefix_id($url, $cidr, $headers); + + my $params = {}; + + my $ip = undef; + eval { + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/ipam/prefixes/$internalid/available-ips/", $headers, $params); + $ip = $result->{address}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@"; + } + + return $ip; +} + +sub del_ip { + my ($class, $plugin_config, $ip) = @_; + + return if !$ip; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $ip_id = get_ip_id($url, $ip, $headers); + die "can't find ip $ip in ipam" if !$ip_id; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("DELETE", "$url/ipam/ip-addresses/$ip_id/", $headers); + }; + if ($@) { + die "error delete ip $ip"; + } +} + +#helpers + +sub get_prefix_id { + my ($url, $cidr, $headers) = @_; + + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/ipam/prefixes/?q=$cidr", $headers); + my $data = @{$result->{results}}[0]; + my $internalid = $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/ipam/ip-addresses/?q=$ip", $headers); + my $data = @{$result->{results}}[0]; + my $ip_id = $data->{id}; + return $ip_id; +} + + +1; + + diff --git a/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm b/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm new file mode 100644 index 0000000..7380bf3 --- /dev/null +++ b/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm @@ -0,0 +1,189 @@ +package PVE::Network::SDN::Ipams::PhpIpamPlugin; + +use strict; +use warnings; +use PVE::INotify; +use PVE::Cluster; +use PVE::Tools; + +use base('PVE::Network::SDN::Ipams::Plugin'); + +sub type { + return 'phpipam'; +} + +sub properties { + return { + url => { + type => 'string', + }, + token => { + type => 'string', + }, + section => { + type => 'integer', + }, + }; +} + +sub options { + + return { + url => { optional => 0}, + token => { optional => 0 }, + section => { optional => 0 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $gateway = $subnet->{gateway}; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + #search subnet + my $internalid = get_internalid($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + my ($network, $mask) = split(/-/, $subnetid); + + my $params = { subnet => $network, + mask => $mask, + sectionId => $section, + }; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/subnets/", $headers, $params); + }; + if ($@) { + die "error add subnet to ipam: $@"; + } + } + +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $internalid = get_internalid($url, $cidr, $headers); + return if !$internalid; + + #fixme: check that prefix is empty exluding gateway, before delete + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("DELETE", "$url/subnets/$internalid", $headers); + }; + if ($@) { + die "error deleting subnet from ipam: $@"; + } + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $ip, $is_gateway) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $internalid = get_internalid($url, $cidr, $headers); + + my $params = { ip => $ip, + subnetId => $internalid, + is_gateway => $is_gateway, + }; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/addresses/", $headers, $params); + }; + + if ($@) { + die "error add subnet ip to ipam: ip $ip already exist: $@"; + } +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet, $internalid, $hostname) = @_; + + my $cidr = $subnetid =~ s/-/\//r; + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $section = $plugin_config->{section}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + $internalid = get_internalid($url, $cidr, $headers) if !$internalid; + + my $params = {}; + + my $ip = undef; + eval { + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/addresses/first_free/$internalid/", $headers, $params); + $ip = $result->{data}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@"; + } + + my ($network, $mask) = split(/-/, $subnetid); + return "$ip/$mask"; +} + +sub del_ip { + my ($class, $plugin_config, $ip) = @_; + + return if !$ip; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $ip_id = get_ip_id($url, $ip, $headers); + return if !$ip_id; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("DELETE", "$url/addresses/$ip_id", $headers); + }; + if ($@) { + die "error delete ip $ip"; + } +} + + +#helpers + +sub get_internalid { + my ($url, $cidr, $headers) = @_; + + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/subnets/cidr/$cidr", $headers); + my $data = @{$result->{data}}[0]; + my $internalid = $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) = @_; + my $result = PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/addresses/search/$ip", $headers); + my $data = @{$result->{data}}[0]; + my $ip_id = $data->{id}; + return $ip_id; +} + +1; + + diff --git a/PVE/Network/SDN/Ipams/Plugin.pm b/PVE/Network/SDN/Ipams/Plugin.pm new file mode 100644 index 0000000..8a44090 --- /dev/null +++ b/PVE/Network/SDN/Ipams/Plugin.pm @@ -0,0 +1,127 @@ +package PVE::Network::SDN::Ipams::Plugin; + +use strict; +use warnings; + +use PVE::Tools qw(run_command); +use PVE::JSONSchema; +use PVE::Cluster; +use HTTP::Request; +use LWP::UserAgent; +use JSON; + +use Data::Dumper; +use PVE::JSONSchema qw(get_standard_option); +use base qw(PVE::SectionConfig); + +PVE::Cluster::cfs_register_file('sdn/ipams.cfg', + sub { __PACKAGE__->parse_config(@_); }, + sub { __PACKAGE__->write_config(@_); }); + +PVE::JSONSchema::register_standard_option('pve-sdn-ipam-id', { + description => "The SDN ipam object identifier.", + type => 'string', format => 'pve-sdn-ipam-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-ipam-id', \&parse_sdn_ipam_id); +sub parse_sdn_ipam_id { + my ($id, $noerr) = @_; + + if ($id !~ m/^[a-z][a-z0-9]*[a-z0-9]$/i) { + return undef if $noerr; + die "ipam ID '$id' contains illegal characters\n"; + } + return $id; +} + +my $defaultData = { + + propertyList => { + type => { + description => "Plugin type.", + type => 'string', format => 'pve-configid', + type => 'string', + }, + ipam => get_standard_option('pve-sdn-ipam-id', + { completion => \&PVE::Network::SDN::Ipams::complete_sdn_ipam }), + }, +}; + +sub private { + return $defaultData; +} + +sub parse_section_header { + my ($class, $line) = @_; + + if ($line =~ m/^(\S+):\s*(\S+)\s*$/) { + my ($type, $id) = (lc($1), $2); + my $errmsg = undef; # set if you want to skip whole section + eval { PVE::JSONSchema::pve_verify_configid($type); }; + $errmsg = $@ if $@; + my $config = {}; # to return additional attributes + return ($type, $id, $errmsg, $config); + } + return undef; +} + + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) = @_; +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $internalid, $ip, $hostname, $is_gateway) = @_; + +} + +sub add_next_freeip { + my ($class, $plugin_config) = @_; +} + +sub del_ip { + my ($class, $plugin_config, $ip) = @_; +} + + +#helpers +sub api_request { + my ($method, $url, $headers, $data) = @_; + + my $encoded_data = to_json($data) if $data; + + my $req = HTTP::Request->new($method,$url, $headers, $encoded_data); + + my $ua = LWP::UserAgent->new(protocols_allowed => ['http', 'https'], timeout => 30); + my $proxy = undef; + + if ($proxy) { + $ua->proxy(['http', 'https'], $proxy); + } else { + $ua->env_proxy; + } + + $ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00); + + my $response = $ua->request($req); + my $code = $response->code; + + if ($code !~ /2(\d+)$/) { + my $msg = $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } + + my $raw = ''; + if (defined($response->decoded_content)) { + $raw = $response->decoded_content; + } else { + $raw = $response->content; + } + return from_json($raw) if $raw ne ''; +} + +1; diff --git a/PVE/Network/SDN/Makefile b/PVE/Network/SDN/Makefile index 59f8c34..fb68856 100644 --- a/PVE/Network/SDN/Makefile +++ b/PVE/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm +SOURCES=Vnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm SubnetPlugin.pm Ipams.pm PERL5DIR=${DESTDIR}/usr/share/perl5 @@ -8,4 +8,5 @@ install: for i in ${SOURCES}; do install -D -m 0644 $$i ${PERL5DIR}/PVE/Network/SDN/$$i; done make -C Controllers install make -C Zones install + make -C Ipams install diff --git a/PVE/Network/SDN/SubnetPlugin.pm b/PVE/Network/SDN/SubnetPlugin.pm index ea47684..6224065 100644 --- a/PVE/Network/SDN/SubnetPlugin.pm +++ b/PVE/Network/SDN/SubnetPlugin.pm @@ -82,7 +82,7 @@ sub properties { type => 'string', description => "Develop some dns registrations plugins (powerdns,...)", }, - ipam_driver => { + ipam => { type => 'string', description => "use a specific ipam", }, @@ -98,7 +98,7 @@ sub options { snat => { optional => 1 }, dhcp => { optional => 1 }, dns_driver => { optional => 1 }, - ipam_driver => { optional => 1 }, + ipam => { optional => 1 }, }; } @@ -110,6 +110,7 @@ sub on_update_hook { my $gateway = $subnet_cfg->{ids}->{$subnetid}->{gateway}; raise_param_exc({ gateway => "$gateway is not in subnet $subnet"}) if $gateway && !$subnet_matcher->($gateway); + } sub on_delete_hook { diff --git a/PVE/Network/SDN/Vnets.pm b/PVE/Network/SDN/Vnets.pm index 073ab80..d474037 100644 --- a/PVE/Network/SDN/Vnets.pm +++ b/PVE/Network/SDN/Vnets.pm @@ -4,6 +4,8 @@ use strict; use warnings; use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network::SDN::Subnets; +use PVE::Network::SDN::Ipams; use PVE::Network::SDN::VnetPlugin; PVE::Network::SDN::VnetPlugin->register(); @@ -52,4 +54,27 @@ sub get_vnet { return $vnet; } +sub get_next_free_ip { + my ($vnetid) = @_; + + my $vnets_cfg = PVE::Network::SDN::Vnets::config(); + my $subnets_cfg = PVE::Network::SDN::Subnets::config(); + my $vnet = $vnets_cfg->{ids}->{$vnetid}; + my @subnets = PVE::Tools::split_list($vnet->{subnets}) if $vnet->{subnets}; + my $ip = undef; + foreach my $s (@subnets) { + my $subnetid = $s =~ s/\//-/r; + my $subnet = $subnets_cfg->{ids}->{$subnetid}; + if ($subnet && $subnet->{ipam}) { + eval { + $ip = PVE::Network::SDN::Ipams::next_free_ip($subnetid, $subnet); + }; + warn $@ if $@; + } + last if $ip; + } + die "can't find any ip" if !$ip; + return $ip; +} + 1; -- 2.20.1