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 3870A68EC2 for ; Fri, 28 Aug 2020 14:37:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 284F121312 for ; Fri, 28 Aug 2020 14:37:29 +0200 (CEST) Received: from mailpro.odiso.net (mailpro.odiso.net [89.248.211.110]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 5D36820E41 for ; Fri, 28 Aug 2020 14:37:01 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by mailpro.odiso.net (Postfix) with ESMTP id C556D15F9E0D; Fri, 28 Aug 2020 14:36:56 +0200 (CEST) Received: from mailpro.odiso.net ([127.0.0.1]) by localhost (mailpro.odiso.net [127.0.0.1]) (amavisd-new, port 10032) with ESMTP id 36NNRxgoSTgg; Fri, 28 Aug 2020 14:36:56 +0200 (CEST) Received: from localhost (localhost [127.0.0.1]) by mailpro.odiso.net (Postfix) with ESMTP id AB72915F9E10; Fri, 28 Aug 2020 14:36:56 +0200 (CEST) X-Virus-Scanned: amavisd-new at mailpro.odiso.com Received: from mailpro.odiso.net ([127.0.0.1]) by localhost (mailpro.odiso.net [127.0.0.1]) (amavisd-new, port 10026) with ESMTP id 8_3PU3vqWt6X; Fri, 28 Aug 2020 14:36:56 +0200 (CEST) Received: from pve.fritz.box (unknown [213.211.148.86]) by mailpro.odiso.net (Postfix) with ESMTPSA id 6F49F15F9E0D; Fri, 28 Aug 2020 14:36:56 +0200 (CEST) From: Alexandre Derumier To: pve-devel@lists.proxmox.com Date: Fri, 28 Aug 2020 14:36:38 +0200 Message-Id: <20200828123649.11125-11-aderumier@odiso.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200828123649.11125-1-aderumier@odiso.com> References: <20200828123649.11125-1-aderumier@odiso.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-SPAM-LEVEL: Spam detection results: 0 AWL 0.000 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [plugin.pm, subnetplugin.pm, zones.pm, subnets.pm, sdn.pm, ipams.pm, vnetplugin.pm, vnets.pm, netboxplugin.pm, phpipamplugin.pm, controllers.pm] Subject: [pve-devel] [PATCH v7 pve-network 10/21] 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: Fri, 28 Aug 2020 12:37:59 -0000 --- 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; =20 use base qw(PVE::RESTHandler); =20 @@ -39,6 +40,11 @@ __PACKAGE__->register_method ({ path =3D> 'subnets', }); =20 +__PACKAGE__->register_method ({ + subclass =3D> "PVE::API2::Network::SDN::Ipams", + path =3D> 'ipams', +}); + __PACKAGE__->register_method({ name =3D> 'index', path =3D> '', @@ -69,6 +75,7 @@ __PACKAGE__->register_method({ { id =3D> 'zones' }, { id =3D> 'controllers' }, { id =3D> 'subnets' }, + { id =3D> 'ipams' }, ]; =20 return $res; diff --git a/PVE/API2/Network/SDN/Ipams.pm b/PVE/API2/Network/SDN/Ipams.p= m 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 =3D PVE::Network::SDN::Ipams::Plugin->lookup_typ= es(); + +my $api_sdn_ipams_config =3D sub { + my ($cfg, $id) =3D @_; + + my $scfg =3D dclone(PVE::Network::SDN::Ipams::sdn_ipams_config($cfg,= $id)); + $scfg->{ipam} =3D $id; + $scfg->{digest} =3D $cfg->{digest}; + + return $scfg; +}; + +__PACKAGE__->register_method ({ + name =3D> 'index', + path =3D> '', + method =3D> 'GET', + description =3D> "SDN ipams index.", + permissions =3D> { + description =3D> "Only list entries where you have 'SDN.Audit' or 'SDN.= Allocate' permissions on '/sdn/ipams/'", + user =3D> 'all', + }, + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + type =3D> { + description =3D> "Only list sdn ipams of specific type", + type =3D> 'string', + enum =3D> $sdn_ipams_type_enum, + optional =3D> 1, + }, + }, + }, + returns =3D> { + type =3D> 'array', + items =3D> { + type =3D> "object", + properties =3D> { ipam =3D> { type =3D> 'string'}, + type =3D> { type =3D> 'string'}, + }, + }, + links =3D> [ { rel =3D> 'child', href =3D> "{ipam}" } ], + }, + code =3D> sub { + my ($param) =3D @_; + + my $rpcenv =3D PVE::RPCEnvironment::get(); + my $authuser =3D $rpcenv->get_user(); + + + my $cfg =3D PVE::Network::SDN::Ipams::config(); + + my @sids =3D PVE::Network::SDN::Ipams::sdn_ipams_ids($cfg); + my $res =3D []; + foreach my $id (@sids) { + my $privs =3D [ 'SDN.Audit', 'SDN.Allocate' ]; + next if !$rpcenv->check_any($authuser, "/sdn/ipams/$id", $privs, 1)= ; + + my $scfg =3D &$api_sdn_ipams_config($cfg, $id); + next if $param->{type} && $param->{type} ne $scfg->{type}; + + my $plugin_config =3D $cfg->{ids}->{$id}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_con= fig->{type}); + push @$res, $scfg; + } + + return $res; + }}); + +__PACKAGE__->register_method ({ + name =3D> 'read', + path =3D> '{ipam}', + method =3D> 'GET', + description =3D> "Read sdn ipam configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/ipams/{ipam}', ['SDN.Allocate']], + }, + + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + ipam =3D> get_standard_option('pve-sdn-ipam-id'), + }, + }, + returns =3D> { type =3D> 'object' }, + code =3D> sub { + my ($param) =3D @_; + + my $cfg =3D PVE::Network::SDN::Ipams::config(); + + return &$api_sdn_ipams_config($cfg, $param->{ipam}); + }}); + +__PACKAGE__->register_method ({ + name =3D> 'create', + protected =3D> 1, + path =3D> '', + method =3D> 'POST', + description =3D> "Create a new sdn ipam object.", + permissions =3D> { + check =3D> ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters =3D> PVE::Network::SDN::Ipams::Plugin->createSchema(), + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $type =3D extract_param($param, 'type'); + my $id =3D extract_param($param, 'ipam'); + + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($type); + my $opts =3D $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 =3D PVE::Network::SDN::Ipams::config(); + my $controller_cfg =3D PVE::Network::SDN::Controllers::config(); + + my $scfg =3D undef; + if ($scfg =3D PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, $i= d, 1)) { + die "sdn ipam object ID '$id' already defined\n"; + } + + $ipam_cfg->{ids}->{$id} =3D $opts; + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "create sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name =3D> 'update', + protected =3D> 1, + path =3D> '{ipam}', + method =3D> 'PUT', + description =3D> "Update sdn ipam object configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters =3D> PVE::Network::SDN::Ipams::Plugin->updateSchema(), + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $id =3D extract_param($param, 'ipam'); + my $digest =3D extract_param($param, 'digest'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + + PVE::SectionConfig::assert_if_modified($ipam_cfg, $digest); + + my $scfg =3D PVE::Network::SDN::Ipams::sdn_ipams_config($ipam_cfg, = $id); + + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{typ= e}); + my $opts =3D $plugin->check_config($id, $param, 0, 1); + + foreach my $k (%$opts) { + $scfg->{$k} =3D $opts->{$k}; + } + + PVE::Network::SDN::Ipams::write_config($ipam_cfg); + + }, "update sdn ipam object failed"); + + return undef; + }}); + +__PACKAGE__->register_method ({ + name =3D> 'delete', + protected =3D> 1, + path =3D> '{ipam}', + method =3D> 'DELETE', + description =3D> "Delete sdn ipam object configuration.", + permissions =3D> { + check =3D> ['perm', '/sdn/ipams', ['SDN.Allocate']], + }, + parameters =3D> { + additionalProperties =3D> 0, + properties =3D> { + ipam =3D> get_standard_option('pve-sdn-ipam-id', { + completion =3D> \&PVE::Network::SDN::Ipams::complete_sdn= _ipams, + }), + }, + }, + returns =3D> { type =3D> 'null' }, + code =3D> sub { + my ($param) =3D @_; + + my $id =3D extract_param($param, 'ipam'); + + PVE::Network::SDN::lock_sdn_config( + sub { + + my $cfg =3D PVE::Network::SDN::Ipams::config(); + + my $scfg =3D PVE::Network::SDN::Ipams::sdn_ipams_config($cfg, $id); + + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($scfg->{type})= ; + + my $vnet_cfg =3D 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/Makefil= e index 59626fa..1117dfa 100644 --- a/PVE/API2/Network/SDN/Makefile +++ b/PVE/API2/Network/SDN/Makefile @@ -1,4 +1,4 @@ -SOURCES=3DVnets.pm Zones.pm Controllers.pm Subnets.pm +SOURCES=3DVnets.pm Zones.pm Controllers.pm Subnets.pm Ipams.pm =20 =20 PERL5DIR=3D${DESTDIR}/usr/share/perl5 diff --git a/PVE/API2/Network/SDN/Subnets.pm b/PVE/API2/Network/SDN/Subne= ts.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; =20 use Storable qw(dclone); use PVE::JSONSchema qw(get_standard_option); @@ -133,6 +136,17 @@ __PACKAGE__->register_method ({ =20 $cfg->{ids}->{$id} =3D $opts; PVE::Network::SDN::SubnetPlugin->on_update_hook($id, $cfg); + + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $ipam =3D $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam =3D> "$ipam not existing"}) if !$ipam_cfg->= {ids}->{$ipam}; + my $plugin_config =3D $ipam_cfg->{ids}->{$ipam}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_co= nfig->{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); =20 }, "create sdn subnet object failed"); @@ -161,6 +175,7 @@ __PACKAGE__->register_method ({ sub { =20 my $cfg =3D PVE::Network::SDN::Subnets::config(); + my $scfg =3D &$api_sdn_subnets_config($cfg, $id); =20 PVE::SectionConfig::assert_if_modified($cfg, $digest); =20 @@ -168,6 +183,24 @@ __PACKAGE__->register_method ({ $cfg->{ids}->{$id} =3D $opts; =20 PVE::Network::SDN::SubnetPlugin->on_update_hook($id, $cfg); + + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $ipam =3D $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam =3D> "$ipam not existing"}) if !$ipam_cfg->{ids= }->{$ipam}; + my $plugin_config =3D $ipam_cfg->{ids}->{$ipam}; + my $plugin =3D 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}); + }=20 + $plugin->add_ip($plugin_config, $id, $opts->{gateway}, 1) if $opts->{g= ateway}; + } + PVE::Network::SDN::Subnets::write_config($cfg); =20 }, "update sdn subnet object failed"); @@ -200,7 +233,6 @@ __PACKAGE__->register_method ({ =20 PVE::Network::SDN::lock_sdn_config( sub { - my $cfg =3D PVE::Network::SDN::Subnets::config(); =20 my $scfg =3D PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id)= ; @@ -208,8 +240,19 @@ __PACKAGE__->register_method ({ my $subnets_cfg =3D PVE::Network::SDN::Subnets::config(); my $vnets_cfg =3D PVE::Network::SDN::Vnets::config(); =20 - delete $cfg->{ids}->{$id}; PVE::Network::SDN::SubnetPlugin->on_delete_hook($id, $subnets_cfg, $vn= ets_cfg); + + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $ipam =3D $cfg->{ids}->{$id}->{ipam}; + if ($ipam) { + raise_param_exc({ ipam =3D> "$ipam not existing"}) if !$ipam_cfg->= {ids}->{$ipam}; + my $plugin_config =3D $ipam_cfg->{ids}->{$ipam}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_co= nfig->{type}); + $plugin->del_subnet($plugin_config, $id, $scfg); + } + + delete $cfg->{ids}->{$id}; + PVE::Network::SDN::Subnets::write_config($cfg); =20 }, "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) =3D @_; + + die "no sdn ipam ID specified\n" if !$id; + + my $scfg =3D $cfg->{ids}->{$id}; + die "sdn '$id' does not exist\n" if (!$noerr && !$scfg); + + return $scfg; +} + +sub config { + my $config =3D cfs_read_file("sdn/ipams.cfg"); + return $config; +} + +sub get_plugin_config { + my ($vnet) =3D @_; + my $ipamid =3D $vnet->{ipam}; + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + return $ipam_cfg->{ids}->{$ipamid}; +} + +sub write_config { + my ($cfg) =3D @_; + + cfs_write_file("sdn/ipams.cfg", $cfg); +} + +sub sdn_ipams_ids { + my ($cfg) =3D @_; + + return keys %{$cfg->{ids}}; +} + +sub complete_sdn_vnet { + my ($cmdname, $pname, $cvalue) =3D @_; + + my $cfg =3D PVE::Network::SDN::Ipams::config(); + + return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Vnets::sdn_ipa= ms_ids($cfg) ]; +} + +sub next_free_ip { + my ($subnetid, $subnet) =3D @_; + + my $ipam_cfg =3D PVE::Network::SDN::Ipams::config(); + my $ipamid =3D $subnet->{ipam}; + return if !$ipamid; + + my $plugin_config =3D $ipam_cfg->{ids}->{$ipamid}; + my $plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup($plugin_conf= ig->{type}); + my $ip =3D $plugin->add_next_freeip($plugin_config, $subnetid, $subn= et); + return $ip; +} +1; + diff --git a/PVE/Network/SDN/Ipams/Makefile b/PVE/Network/SDN/Ipams/Makef= ile new file mode 100644 index 0000000..884c47a --- /dev/null +++ b/PVE/Network/SDN/Ipams/Makefile @@ -0,0 +1,8 @@ +SOURCES=3DPlugin.pm PhpIpamPlugin.pm NetboxPlugin.pm + + +PERL5DIR=3D${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/Ipam= s/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 =3D> { optional =3D> 0}, + token =3D> { optional =3D> 0 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $gateway =3D $subnet->{gateway}; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Authorization' =3D> "token $token"]; + + my $internalid =3D get_prefix_id($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + my ($network, $mask) =3D split(/-/, $subnetid); + + my $params =3D { prefix =3D> $cidr }; + + eval { + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("POST", "= $url/ipam/prefixes/", $headers, $params); + $subnet->{ipamid} =3D $result->{id} if defined($result->{id}); + }; + if ($@) { + die "error add subnet to ipam: $@"; + } + } + =20 +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $gateway =3D $subnet->{gateway}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Authorization' =3D> "token $token"]; + + my $internalid =3D 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_confi= g, $gateway) if $gateway; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("DELETE", "$url/ipam/pref= ixes/$internalid/", $headers); + }; + if ($@) { + die "error deleting subnet from ipam: $@"; + } + +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $ip, $is_gateway) =3D @_; + + my ($network, $mask) =3D split(/-/, $subnetid); + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $section =3D $plugin_config->{section}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Authorization' =3D> "token $token"]; + + my $params =3D { address =3D> "$ip/$mask" }; + + eval { + PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$url/ipam/ip-add= resses/", $headers, $params); + }; + + if ($@) { + die "error add subnet ip to ipam: ip already exist: $@"; + } +} + +sub add_next_freeip { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Authorization' =3D> "token $token"]; + + my $internalid =3D get_prefix_id($url, $cidr, $headers); + + my $params =3D {}; + + my $ip =3D undef; + eval { + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$= url/ipam/prefixes/$internalid/available-ips/", $headers, $params); + $ip =3D $result->{address}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@"; + } + + return $ip; +} + +sub del_ip { + my ($class, $plugin_config, $ip) =3D @_; + + return if !$ip; + + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Authorization' =3D> "token $token"]; + + my $ip_id =3D 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-a= ddresses/$ip_id/", $headers); + }; + if ($@) { + die "error delete ip $ip"; + } +} + +#helpers + +sub get_prefix_id { + my ($url, $cidr, $headers) =3D @_; + + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("GET", = "$url/ipam/prefixes/?q=3D$cidr", $headers); + my $data =3D @{$result->{results}}[0]; + my $internalid =3D $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) =3D @_; + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("GET", = "$url/ipam/ip-addresses/?q=3D$ip", $headers); + my $data =3D @{$result->{results}}[0]; + my $ip_id =3D $data->{id}; + return $ip_id; +} + + +1; + + diff --git a/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm b/PVE/Network/SDN/Ipa= ms/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 =3D> { + type =3D> 'string', + }, + token =3D> { + type =3D> 'string', + }, + section =3D> { + type =3D> 'integer', + }, + }; +} + +sub options { + + return { + url =3D> { optional =3D> 0}, + token =3D> { optional =3D> 0 }, + section =3D> { optional =3D> 0 }, + }; +} + +# Plugin implementation + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $gateway =3D $subnet->{gateway}; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $section =3D $plugin_config->{section}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Token' =3D> $token]; + + #search subnet + my $internalid =3D get_internalid($url, $cidr, $headers); + + #create subnet + if (!$internalid) { + my ($network, $mask) =3D split(/-/, $subnetid); + + my $params =3D { subnet =3D> $network, + mask =3D> $mask, + sectionId =3D> $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) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $section =3D $plugin_config->{section}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Token' =3D> $token]; + + my $internalid =3D 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) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $section =3D $plugin_config->{section}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Token' =3D> $token]; + + my $internalid =3D get_internalid($url, $cidr, $headers); + + my $params =3D { ip =3D> $ip, + subnetId =3D> $internalid, + is_gateway =3D> $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, $hostna= me) =3D @_; + + my $cidr =3D $subnetid =3D~ s/-/\//r; + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $section =3D $plugin_config->{section}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Token' =3D> $token]; + + $internalid =3D get_internalid($url, $cidr, $headers) if !$internali= d; + + my $params =3D {}; + + my $ip =3D undef; + eval { + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("POST", "$= url/addresses/first_free/$internalid/", $headers, $params); + $ip =3D $result->{data}; + }; + + if ($@) { + die "can't find free ip in subnet $cidr: $@"; + } + + my ($network, $mask) =3D split(/-/, $subnetid); + return "$ip/$mask"; +} + +sub del_ip { + my ($class, $plugin_config, $ip) =3D @_; + + return if !$ip; + + my $url =3D $plugin_config->{url}; + my $token =3D $plugin_config->{token}; + my $headers =3D ['Content-Type' =3D> 'application/json; charset=3DUT= F-8', 'Token' =3D> $token]; + + my $ip_id =3D 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) =3D @_; + + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("GET", = "$url/subnets/cidr/$cidr", $headers); + my $data =3D @{$result->{data}}[0]; + my $internalid =3D $data->{id}; + return $internalid; +} + +sub get_ip_id { + my ($url, $ip, $headers) =3D @_; + my $result =3D PVE::Network::SDN::Ipams::Plugin::api_request("GET", = "$url/addresses/search/$ip", $headers); + my $data =3D @{$result->{data}}[0]; + my $ip_id =3D $data->{id}; + return $ip_id; +} + +1; + + diff --git a/PVE/Network/SDN/Ipams/Plugin.pm b/PVE/Network/SDN/Ipams/Plug= in.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 =3D> "The SDN ipam object identifier.", + type =3D> 'string', format =3D> 'pve-sdn-ipam-id', +}); + +PVE::JSONSchema::register_format('pve-sdn-ipam-id', \&parse_sdn_ipam_id)= ; +sub parse_sdn_ipam_id { + my ($id, $noerr) =3D @_; + + 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 =3D { + + propertyList =3D> { + type =3D> { + description =3D> "Plugin type.", + type =3D> 'string', format =3D> 'pve-configid', + type =3D> 'string', + }, + ipam =3D> get_standard_option('pve-sdn-ipam-id', + { completion =3D> \&PVE::Network::SDN::Ipams::complete_sdn_i= pam }), + }, +}; + +sub private { + return $defaultData; +} + +sub parse_section_header { + my ($class, $line) =3D @_; + + if ($line =3D~ m/^(\S+):\s*(\S+)\s*$/) { + my ($type, $id) =3D (lc($1), $2); + my $errmsg =3D undef; # set if you want to skip whole section + eval { PVE::JSONSchema::pve_verify_configid($type); }; + $errmsg =3D $@ if $@; + my $config =3D {}; # to return additional attributes + return ($type, $id, $errmsg, $config); + } + return undef; +} + + +sub add_subnet { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; +} + +sub del_subnet { + my ($class, $plugin_config, $subnetid, $subnet) =3D @_; +} + +sub add_ip { + my ($class, $plugin_config, $subnetid, $subnet, $internalid, $ip, $h= ostname, $is_gateway) =3D @_; + +} + +sub add_next_freeip { + my ($class, $plugin_config) =3D @_; +} + +sub del_ip { + my ($class, $plugin_config, $ip) =3D @_; +} + + +#helpers +sub api_request { + my ($method, $url, $headers, $data) =3D @_; + + my $encoded_data =3D to_json($data) if $data; + + my $req =3D HTTP::Request->new($method,$url, $headers, $encoded_data= ); + + my $ua =3D LWP::UserAgent->new(protocols_allowed =3D> ['http', 'http= s'], timeout =3D> 30); + my $proxy =3D undef; + + if ($proxy) { + $ua->proxy(['http', 'https'], $proxy); + } else { + $ua->env_proxy; + } + + $ua->ssl_opts(verify_hostname =3D> 0, SSL_verify_mode =3D> 0x00); + + my $response =3D $ua->request($req); + my $code =3D $response->code; + + if ($code !~ /2(\d+)$/) { + my $msg =3D $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } + + my $raw =3D ''; + if (defined($response->decoded_content)) { + $raw =3D $response->decoded_content; + } else { + $raw =3D $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=3DVnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm Subn= etPlugin.pm +SOURCES=3DVnets.pm VnetPlugin.pm Zones.pm Controllers.pm Subnets.pm Subn= etPlugin.pm Ipams.pm =20 =20 PERL5DIR=3D${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 =20 diff --git a/PVE/Network/SDN/SubnetPlugin.pm b/PVE/Network/SDN/SubnetPlug= in.pm index ea47684..6224065 100644 --- a/PVE/Network/SDN/SubnetPlugin.pm +++ b/PVE/Network/SDN/SubnetPlugin.pm @@ -82,7 +82,7 @@ sub properties { type =3D> 'string', description =3D> "Develop some dns registrations plugins (po= werdns,...)", }, - ipam_driver =3D> { + ipam =3D> { type =3D> 'string', description =3D> "use a specific ipam", }, @@ -98,7 +98,7 @@ sub options { snat =3D> { optional =3D> 1 }, dhcp =3D> { optional =3D> 1 }, dns_driver =3D> { optional =3D> 1 }, - ipam_driver =3D> { optional =3D> 1 }, + ipam =3D> { optional =3D> 1 }, }; } =20 @@ -110,6 +110,7 @@ sub on_update_hook { =20 my $gateway =3D $subnet_cfg->{ids}->{$subnetid}->{gateway}; raise_param_exc({ gateway =3D> "$gateway is not in subnet $subnet"})= if $gateway && !$subnet_matcher->($gateway); + } =20 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; =20 use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Network::SDN::Subnets; +use PVE::Network::SDN::Ipams; =20 use PVE::Network::SDN::VnetPlugin; PVE::Network::SDN::VnetPlugin->register(); @@ -52,4 +54,27 @@ sub get_vnet { return $vnet; } =20 +sub get_next_free_ip { + my ($vnetid) =3D @_; + + my $vnets_cfg =3D PVE::Network::SDN::Vnets::config(); + my $subnets_cfg =3D PVE::Network::SDN::Subnets::config(); + my $vnet =3D $vnets_cfg->{ids}->{$vnetid}; + my @subnets =3D PVE::Tools::split_list($vnet->{subnets}) if $vnet->{= subnets}; + my $ip =3D undef; + foreach my $s (@subnets) { + my $subnetid =3D $s =3D~ s/\//-/r; + my $subnet =3D $subnets_cfg->{ids}->{$subnetid}; + if ($subnet && $subnet->{ipam}) { + eval { + $ip =3D 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; --=20 2.20.1