]> git.proxmox.com Git - pve-network.git/commitdiff
add IPAMs plugins
authorAlexandre Derumier <aderumier@odiso.com>
Mon, 5 Oct 2020 15:08:47 +0000 (17:08 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Tue, 6 Oct 2020 16:11:17 +0000 (18:11 +0200)
Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
15 files changed:
PVE/API2/Network/SDN.pm
PVE/API2/Network/SDN/Ipams.pm [new file with mode: 0644]
PVE/API2/Network/SDN/Makefile
PVE/API2/Network/SDN/Subnets.pm
PVE/Network/SDN/Ipams.pm [new file with mode: 0644]
PVE/Network/SDN/Ipams/Makefile [new file with mode: 0644]
PVE/Network/SDN/Ipams/NetboxPlugin.pm [new file with mode: 0644]
PVE/Network/SDN/Ipams/PVEPlugin.pm [new file with mode: 0644]
PVE/Network/SDN/Ipams/PhpIpamPlugin.pm [new file with mode: 0644]
PVE/Network/SDN/Ipams/Plugin.pm [new file with mode: 0644]
PVE/Network/SDN/Makefile
PVE/Network/SDN/SubnetPlugin.pm
PVE/Network/SDN/Subnets.pm
PVE/Network/SDN/Vnets.pm
debian/control

index 38af7467e85de5fe6129fc0dd942a8d163761dfd..736204f76a943342a2c475499fc01cdfbfa4e085 100644 (file)
@@ -15,6 +15,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);
 
@@ -38,6 +39,11 @@ __PACKAGE__->register_method ({
     path => 'subnets',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Network::SDN::Ipams",
+    path => 'ipams',
+});
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
@@ -68,6 +74,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 (file)
index 0000000..6410e8e
--- /dev/null
@@ -0,0 +1,248 @@
+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::PVEPlugin;
+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/<ipam>'",
+       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;
+
+               my $plugin_config = $opts;
+               my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
+               $plugin->on_update_hook($plugin_config);
+
+               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};
+           }
+
+            $plugin->on_update_hook($scfg);
+
+           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;
index 59626fa54e48260e85f4e3684994db110fb9fe24..1117dfa587b595d564a82eb0035ab206ad5b0d8f 100644 (file)
@@ -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
index 87dc119571aba5c8c4aec14eef667ace0b3f2c65..f06a6eec9106695e12ad744fb3837370c5e5ccc6 100644 (file)
@@ -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);
                PVE::Network::SDN::increase_version();
 
@@ -162,6 +176,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);
 
@@ -169,6 +184,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, $id, $scfg->{gateway});
+               }
+               if (!defined($opts->{gateway}) && $scfg->{gateway}) {
+                   $plugin->del_ip($plugin_config, $id, $scfg->{gateway});
+               } 
+               $plugin->add_ip($plugin_config, $id, $opts->{gateway}, 1) if $opts->{gateway};
+           }
+
            PVE::Network::SDN::Subnets::write_config($cfg);
            PVE::Network::SDN::increase_version();
 
@@ -202,7 +235,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);
@@ -210,8 +242,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);
                PVE::Network::SDN::increase_version();
 
diff --git a/PVE/Network/SDN/Ipams.pm b/PVE/Network/SDN/Ipams.pm
new file mode 100644 (file)
index 0000000..a979d46
--- /dev/null
@@ -0,0 +1,68 @@
+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::PVEPlugin;
+use PVE::Network::SDN::Ipams::NetboxPlugin;
+use PVE::Network::SDN::Ipams::PhpIpamPlugin;
+use PVE::Network::SDN::Ipams::Plugin;
+
+PVE::Network::SDN::Ipams::PVEPlugin->register();
+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) ];
+}
+
+1;
+
diff --git a/PVE/Network/SDN/Ipams/Makefile b/PVE/Network/SDN/Ipams/Makefile
new file mode 100644 (file)
index 0000000..4e7d65f
--- /dev/null
@@ -0,0 +1,8 @@
+SOURCES=Plugin.pm PhpIpamPlugin.pm NetboxPlugin.pm PVEPlugin.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 (file)
index 0000000..01f82f2
--- /dev/null
@@ -0,0 +1,191 @@
+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, $subnetid, $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, $subnetid, $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 : $@";
+    }
+}
+
+sub verify_api {
+    my ($class, $plugin_config) = @_;
+
+    my $url = $plugin_config->{url};
+    my $token = $plugin_config->{token};
+    my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"];
+
+
+    eval {
+       PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/ipam/aggregates/", $headers);
+    };
+    if ($@) {
+       die "Can't connect to netbox api: $@";
+    }
+}
+
+sub on_update_hook {
+    my ($class, $plugin_config) = @_;
+
+    PVE::Network::SDN::Ipams::NetboxPlugin::verify_api($class, $plugin_config);
+}
+
+#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/PVEPlugin.pm b/PVE/Network/SDN/Ipams/PVEPlugin.pm
new file mode 100644 (file)
index 0000000..e109861
--- /dev/null
@@ -0,0 +1,166 @@
+package PVE::Network::SDN::Ipams::PVEPlugin;
+
+use strict;
+use warnings;
+use PVE::INotify;
+use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_register_file cfs_lock_file);
+use PVE::Tools;
+use JSON;
+use NetAddr::IP;
+use Digest::SHA;
+
+use base('PVE::Network::SDN::Ipams::Plugin');
+
+
+my $ipamdb_file = "priv/ipam.db";
+
+PVE::Cluster::cfs_register_file($ipamdb_file,
+                                 sub { PVE::Network::SDN::Ipams::PVEPlugin->parse_config(@_); },
+                                 sub { PVE::Network::SDN::Ipams::PVEPlugin->write_config(@_); });
+
+sub type {
+    return 'pve';
+}
+
+sub properties {
+}
+
+sub options {
+}
+
+# Plugin implementation
+
+sub add_subnet {
+    my ($class, $plugin_config, $subnetid, $subnet) = @_;
+
+    my $cidr = $subnetid =~ s/-/\//r;
+    my $gateway = $subnet->{gateway};
+
+    cfs_lock_file($ipamdb_file, undef, sub {
+       my $config = read_db();
+       #create subnet
+       if (!defined($config->{subnets}->{$cidr})) {
+           $config->{subnets}->{$cidr}->{ips} = {};
+           write_db($config);
+       }
+    });
+    die "$@" if $@;
+}
+
+sub del_subnet {
+    my ($class, $plugin_config, $subnetid, $subnet) = @_;
+
+    my $cidr = $subnetid =~ s/-/\//r;
+
+    cfs_lock_file($ipamdb_file, undef, sub {
+
+       my $db = read_db();
+       my $ips = $db->{subnets}->{$cidr}->{ips};
+       die "can't delete subnet, not empty" if keys %{$ips} > 0;
+       delete $db->{subnets}->{$cidr};
+       write_db($db);
+    });
+    die "$@" if $@;
+
+}
+
+sub add_ip {
+    my ($class, $plugin_config, $subnetid, $ip, $is_gateway) = @_;
+
+    my $cidr = $subnetid =~ s/-/\//r;
+
+    cfs_lock_file($ipamdb_file, undef, sub {
+
+       my $db = read_db();
+       my $s = $db->{subnets}->{$cidr};
+
+       die "ip already exist" if defined($s->{ips}->{$ip});
+
+       #verify that ip is valid for this subnet
+       $s->{ips}->{$ip} = 1;
+       write_db($db);
+    });
+    die "$@" if $@;
+}
+
+sub add_next_freeip {
+    my ($class, $plugin_config, $subnetid, $subnet) = @_;
+
+    my $cidr = $subnetid =~ s/-/\//r;
+    my $freeip = undef;
+
+    cfs_lock_file($ipamdb_file, undef, sub {
+
+       my $db = read_db();
+       my $s = $db->{subnets}->{$cidr};
+       my $iplist = new NetAddr::IP($cidr);
+       my $broadcast = $iplist->broadcast();
+
+       while (1) {
+           $iplist++;
+           last if $iplist eq $broadcast;
+           my $ip = $iplist->addr();
+           next if defined($s->{ips}->{$ip});
+           $freeip = $ip;
+           last;
+       }
+
+       die "can't find free ip in subnet '$cidr'\n" if !$freeip;
+
+       $s->{ips}->{$freeip} = 1;
+       write_db($db);
+    });
+    die "$@" if $@;
+
+    my ($network, $mask) = split(/-/, $subnetid);
+    return "$freeip/$mask";
+}
+
+sub del_ip {
+    my ($class, $plugin_config, $subnetid, $ip) = @_;
+
+    my $cidr = $subnetid =~ s/-/\//r;
+
+    cfs_lock_file($ipamdb_file, undef, sub {
+
+       my $db = read_db();
+       my $s = $db->{subnets}->{$cidr};
+       return if !$ip;
+
+       die "IP '$ip' does not exist in IPAM DB\n" if !defined($s->{ips}->{$ip});
+       delete $s->{ips}->{$ip};
+       write_db($db);
+    });
+    die "$@" if $@;
+}
+
+#helpers
+
+sub read_db {
+    my $db = cfs_read_file($ipamdb_file);
+    return $db;
+}
+
+sub write_db {
+    my ($cfg) = @_;
+
+    my $json = to_json($cfg);
+    cfs_write_file($ipamdb_file, $json);
+}
+
+sub write_config {
+    my ($class, $filename, $cfg) = @_;
+
+    return $cfg;
+}
+
+sub parse_config {
+    my ($class, $filename, $raw) = @_;
+
+    $raw = '{}' if !defined($raw) ||$raw eq '';
+    my $cfg = from_json($raw);
+
+    return $cfg;
+}
+
+1;
diff --git a/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm b/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm
new file mode 100644 (file)
index 0000000..4a9e6c4
--- /dev/null
@@ -0,0 +1,211 @@
+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, $subnetid, $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: $@";
+    }
+}
+
+sub verify_api {
+    my ($class, $plugin_config) = @_;
+
+    my $url = $plugin_config->{url};
+    my $token = $plugin_config->{token};
+    my $sectionid = $plugin_config->{section};
+    my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token];
+
+    eval {
+       PVE::Network::SDN::Ipams::Plugin::api_request("GET", "$url/sections/$sectionid", $headers);
+    };
+    if ($@) {
+       die "Can't connect to phpipam api: $@";
+    }
+}
+
+sub on_update_hook {
+    my ($class, $plugin_config) = @_;
+
+    PVE::Network::SDN::Ipams::PhpIpamPlugin::verify_api($class, $plugin_config);
+}
+
+
+#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 (file)
index 0000000..e31fb05
--- /dev/null
@@ -0,0 +1,131 @@
+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, $subnetid, $ip) = @_;
+}
+
+sub on_update_hook {
+    my ($class, $plugin_config)  = @_;
+}
+
+
+#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;
index 59f8c3403dae4a5ebcfcb013848e51eaae4d475b..fb6885608e6a11142ab830b7b7ff3a63e31ada91 100644 (file)
@@ -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
 
index ea47684bf7287c0bb9590de17c784457fb88de18..6224065767062d9c52da87623dd7ca41a528ee90 100644 (file)
@@ -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 {
index 454a9cf0fa7a574c9c474748a7abf409fffb1544..3ce2d44ab24653cb2c27b17f801f21b55dd220d8 100644 (file)
@@ -3,8 +3,10 @@ package PVE::Network::SDN::Subnets;
 use strict;
 use warnings;
 
+use Net::Subnet qw(subnet_matcher);
 use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
 
+use PVE::Network::SDN::Ipams;
 use PVE::Network::SDN::SubnetPlugin;
 PVE::Network::SDN::SubnetPlugin->register();
 PVE::Network::SDN::SubnetPlugin->init();
@@ -52,4 +54,62 @@ sub get_subnet {
     return $subnet;
 }
 
+sub find_ip_subnet {
+    my ($ip, $subnetslist) = @_;
+
+    my $subnets_cfg = PVE::Network::SDN::Subnets::config();
+    my @subnets = PVE::Tools::split_list($subnetslist) if $subnetslist;
+
+    my $subnet = undef;
+    my $subnetid = undef;
+
+    foreach my $s (@subnets) {
+        my $subnet_matcher = subnet_matcher($s);
+        next if !$subnet_matcher->($ip);
+        $subnetid = $s =~ s/\//-/r;
+        $subnet = $subnets_cfg->{ids}->{$subnetid};
+        last;
+    }
+    die  "can't find any subnet for ip $ip" if !$subnet;
+
+    return ($subnetid, $subnet);
+}
+
+sub next_free_ip {
+    my ($subnetid, $subnet) = @_;
+
+    my $ipamid = $subnet->{ipam};
+    return if !$ipamid;
+
+    my $ipam_cfg = PVE::Network::SDN::Ipams::config();
+    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;
+}
+
+sub add_ip {
+    my ($subnetid, $subnet, $ip) = @_;
+
+    my $ipamid = $subnet->{ipam};
+    return if !$ipamid;
+
+    my $ipam_cfg = PVE::Network::SDN::Ipams::config();
+    my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
+    my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
+    $plugin->add_ip($plugin_config, $subnetid, $ip);
+}
+
+sub del_ip {
+    my ($subnetid, $subnet, $ip) = @_;
+
+    my $ipamid = $subnet->{ipam};
+    return if !$ipamid;
+
+    my $ipam_cfg = PVE::Network::SDN::Ipams::config();
+    my $plugin_config = $ipam_cfg->{ids}->{$ipamid};
+    my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type});
+    $plugin->del_ip($plugin_config, $subnetid, $ip);
+}
+
 1;
index 073ab80301a4103fc17c50449afa142cb2458b25..6ea3a9a5cf9c86f7959250da0127283d79cfc118 100644 (file)
@@ -4,6 +4,8 @@ use strict;
 use warnings;
 
 use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
+use Net::IP;
+use PVE::Network::SDN::Subnets;
 
 use PVE::Network::SDN::VnetPlugin;
 PVE::Network::SDN::VnetPlugin->register();
@@ -52,4 +54,52 @@ sub get_vnet {
     return $vnet;
 }
 
+sub get_next_free_ip {
+    my ($vnet, $ipversion) = @_;
+
+    $ipversion = 4 if !$ipversion;
+    my $subnets_cfg = PVE::Network::SDN::Subnets::config();
+    my @subnets = PVE::Tools::split_list($vnet->{subnets}) if $vnet->{subnets};
+    my $ip = undef;
+    my $subnet = undef;
+    my $subnetcount = 0;
+    foreach my $s (@subnets) {
+       my $subnetid = $s =~ s/\//-/r;
+       my ($network, $mask) = split(/-/, $subnetid);
+       next if $ipversion != Net::IP::ip_get_version($network);
+       $subnetcount++;
+       $subnet = $subnets_cfg->{ids}->{$subnetid};
+       if ($subnet && $subnet->{ipam}) {
+           eval {
+               $ip = PVE::Network::SDN::Subnets::next_free_ip($subnetid, $subnet);
+           };
+           warn $@ if $@;
+       }
+       last if $ip;
+    }
+    die "can't find any free ip" if !$ip && $subnetcount > 0;
+
+    return $ip;
+}
+
+sub add_ip {
+    my ($vnet, $cidr, $name) = @_;
+
+    my ($ip, $mask) = split(/\//, $cidr);
+    my ($subnetid, $subnet) = PVE::Network::SDN::Subnets::find_ip_subnet($ip, $vnet->{subnets});
+    return if !$subnet->{ipam};
+
+    PVE::Network::SDN::Subnets::add_ip($subnetid, $subnet, $ip);
+}
+
+sub del_ip {
+    my ($vnet, $cidr) = @_;
+
+    my ($ip, $mask) = split(/\//, $cidr);
+    my ($subnetid, $subnet) = PVE::Network::SDN::Subnets::find_ip_subnet($ip, $vnet->{subnets});
+    return if !$subnet->{ipam};
+
+    PVE::Network::SDN::Subnets::del_ip($subnetid, $subnet, $ip);
+}
+
 1;
index 8b67d74c5508d42d58a877595979b86108e30d48..b2e36144a053f3dd577995b999782e5ac734e586 100644 (file)
@@ -17,6 +17,8 @@ Depends: libpve-common-perl (>= 5.0-45),
          perl (>= 5.6.0-16),
          pve-cluster (>= 5.0-32),
          libnet-subnet-perl,
+         libnet-ip-perl,
+         libnetaddr-ip-perl,
          ${misc:Depends},
          ${perl:Depends},
 Recommends: frr-pythontools, ifupdown2