X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FNetwork%2FSDN.pm;h=befaaeebefd0d69694870cbc1109c6eb40926c29;hb=4083537ba65806b07d6facc2970f920daf0814b9;hp=d72b94aa8cf46a2ef6d07d8d59e0a6df5c119265;hpb=17854295b8eedde9e6dbfbeed678e179bd56a076;p=pve-network.git diff --git a/PVE/Network/SDN.pm b/PVE/Network/SDN.pm index d72b94a..befaaee 100644 --- a/PVE/Network/SDN.pm +++ b/PVE/Network/SDN.pm @@ -6,66 +6,44 @@ use warnings; use Data::Dumper; use JSON; +use PVE::Network::SDN::Vnets; +use PVE::Network::SDN::Zones; +use PVE::Network::SDN::Controllers; +use PVE::Network::SDN::Subnets; + 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::SDN::Plugin; -use PVE::Network::SDN::VnetPlugin; -use PVE::Network::SDN::VlanPlugin; -use PVE::Network::SDN::VxlanPlugin; -use PVE::Network::SDN::FrrPlugin; - -PVE::Network::SDN::VnetPlugin->register(); -PVE::Network::SDN::VlanPlugin->register(); -PVE::Network::SDN::VxlanPlugin->register(); -PVE::Network::SDN::FrrPlugin->register(); -PVE::Network::SDN::Plugin->init(); - -sub sdn_config { - my ($cfg, $sdnid, $noerr) = @_; - die "no sdn ID specified\n" if !$sdnid; +my $running_cfg = "sdn/.running-config"; - my $scfg = $cfg->{ids}->{$sdnid}; - die "sdn '$sdnid' does not exists\n" if (!$noerr && !$scfg); +my $parse_running_cfg = sub { + my ($filename, $raw) = @_; - return $scfg; -} + my $cfg = {}; -sub config { - my $config = cfs_read_file("sdn.cfg.new"); - $config = cfs_read_file("sdn.cfg") if !keys %{$config->{ids}}; - return $config; -} + return $cfg if !defined($raw) || $raw eq ''; -sub write_config { - my ($cfg) = @_; - - cfs_write_file("sdn.cfg.new", $cfg); -} + eval { + $cfg = from_json($raw); + }; + return {} if $@; -sub lock_sdn_config { - my ($code, $errmsg) = @_; + return $cfg; +}; - cfs_lock_file("sdn.cfg.new", undef, $code); - if (my $err = $@) { - $errmsg ? die "$errmsg: $err" : die $err; - } -} +my $write_running_cfg = sub { + my ($filename, $cfg) = @_; -sub sdn_ids { - my ($cfg) = @_; + my $json = to_json($cfg); - return keys %{$cfg->{ids}}; -} + return $json; +}; -sub complete_sdn { - my ($cmdname, $pname, $cvalue) = @_; +PVE::Cluster::cfs_register_file($running_cfg, $parse_running_cfg, $write_running_cfg); - my $cfg = PVE::Network::SDN::config(); - return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_ids($cfg) ]; -} +# improve me : move status code inside plugins ? sub ifquery_check { @@ -93,297 +71,211 @@ sub ifquery_check { return $interfaces; } +sub status { -sub generate_etc_network_config { - - my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg'); - return if !$sdn_cfg; + my ($zone_status, $vnet_status) = PVE::Network::SDN::Zones::status(); + return($zone_status, $vnet_status); +} - #read main config for physical interfaces - my $current_config_file = "/etc/network/interfaces"; - my $fh = IO::File->new($current_config_file); - my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh); - $fh->close(); +sub config { + return cfs_read_file($running_cfg); +} - #check uplinks - my $uplinks = {}; - foreach my $id (keys %{$interfaces_config->{ifaces}}) { - my $interface = $interfaces_config->{ifaces}->{$id}; - if (my $uplink = $interface->{'uplink-id'}) { - die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink}; - $interface->{name} = $id; - $uplinks->{$interface->{'uplink-id'}} = $interface; +sub pending_config { + my ($running_cfg, $cfg, $type) = @_; + + my $pending = {}; + + my $running_objects = $running_cfg->{$type}->{ids}; + my $config_objects = $cfg->{ids}; + + foreach my $id (sort keys %{$running_objects}) { + my $running_object = $running_objects->{$id}; + my $config_object = $config_objects->{$id}; + foreach my $key (sort keys %{$running_object}) { + $pending->{$id}->{$key} = $running_object->{$key}; + if(!keys %{$config_object}) { + $pending->{$id}->{state} = "deleted"; + } elsif (!defined($config_object->{$key})) { + $pending->{$id}->{"pending"}->{$key} = 'deleted'; + $pending->{$id}->{state} = "changed"; + } elsif (PVE::Network::SDN::encode_value(undef, $key, $running_object->{$key}) + ne PVE::Network::SDN::encode_value(undef, $key, $config_object->{$key})) { + $pending->{$id}->{state} = "changed"; + } } + $pending->{$id}->{"pending"} = {} if $pending->{$id}->{state} && !defined($pending->{$id}->{"pending"}); } - my $vnet_cfg = undef; - my $transport_cfg = undef; + foreach my $id (sort keys %{$config_objects}) { + my $running_object = $running_objects->{$id}; + my $config_object = $config_objects->{$id}; - foreach my $id (keys %{$sdn_cfg->{ids}}) { - if ($sdn_cfg->{ids}->{$id}->{type} eq 'vnet') { - $vnet_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id}; - } else { - $transport_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id}; + foreach my $key (sort keys %{$config_object}) { + my $config_value = PVE::Network::SDN::encode_value(undef, $key, $config_object->{$key}) if $config_object->{$key}; + my $running_value = PVE::Network::SDN::encode_value(undef, $key, $running_object->{$key}) if $running_object->{$key}; + if($key eq 'type' || $key eq 'vnet') { + $pending->{$id}->{$key} = $config_value; + } else { + $pending->{$id}->{"pending"}->{$key} = $config_value if !defined($running_value) || ($config_value ne $running_value); + } + if(!keys %{$running_object}) { + $pending->{$id}->{state} = "new"; + } elsif (!defined($running_value) && defined($config_value)) { + $pending->{$id}->{state} = "changed"; + } } - } + $pending->{$id}->{"pending"} = {} if $pending->{$id}->{state} && !defined($pending->{$id}->{"pending"}); + } - #generate configuration - my $config = {}; - foreach my $id (keys %{$vnet_cfg->{ids}}) { - my $vnet = $vnet_cfg->{ids}->{$id}; - my $zone = $vnet->{transportzone}; + return {ids => $pending}; - if(!$zone) { - warn "can't generate vnet $vnet : zone $zone don't exist"; - next; - } +} - my $plugin_config = $transport_cfg->{ids}->{$zone}; +sub commit_config { - if (!defined($plugin_config)) { - warn "can't generate vnet $vnet : zone $zone don't exist"; - next; - } + my $cfg = cfs_read_file($running_cfg); + my $version = $cfg->{version}; - my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type}); - $plugin->generate_sdn_config($plugin_config, $zone, $id, $vnet, $uplinks, $config); - } - - my $raw_network_config = ""; - foreach my $iface (keys %$config) { - $raw_network_config .= "\n"; - $raw_network_config .= "auto $iface\n"; - $raw_network_config .= "iface $iface\n"; - foreach my $option (@{$config->{$iface}}) { - $raw_network_config .= "\t$option\n"; - } + if ($version) { + $version++; + } else { + $version = 1; } - return $raw_network_config; -} - -sub generate_frr_config { - - my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg'); - return if !$sdn_cfg; - - #read main config for physical interfaces - my $current_config_file = "/etc/network/interfaces"; - my $fh = IO::File->new($current_config_file); - my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh); - $fh->close(); - - #check uplinks - my $uplinks = {}; - foreach my $id (keys %{$interfaces_config->{ifaces}}) { - my $interface = $interfaces_config->{ifaces}->{$id}; - if (my $uplink = $interface->{'uplink-id'}) { - die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink}; - $interface->{name} = $id; - $uplinks->{$interface->{'uplink-id'}} = $interface; - } - } + my $vnets_cfg = PVE::Network::SDN::Vnets::config(); + my $zones_cfg = PVE::Network::SDN::Zones::config(); + my $controllers_cfg = PVE::Network::SDN::Controllers::config(); + my $subnets_cfg = PVE::Network::SDN::Subnets::config(); - my $frr_cfg = undef; - my $transport_cfg = undef; + my $vnets = { ids => $vnets_cfg->{ids} }; + my $zones = { ids => $zones_cfg->{ids} }; + my $controllers = { ids => $controllers_cfg->{ids} }; + my $subnets = { ids => $subnets_cfg->{ids} }; - foreach my $id (keys %{$sdn_cfg->{ids}}) { - if ($sdn_cfg->{ids}->{$id}->{type} eq 'frr') { - $frr_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id}; - } elsif ($sdn_cfg->{ids}->{$id}->{type} ne 'vnet') { - $transport_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id}; - } - } + $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets }; - return undef if !$frr_cfg; + cfs_write_file($running_cfg, $cfg); +} - #generate configuration - my $config = {}; +sub lock_sdn_config { + my ($code, $errmsg) = @_; - foreach my $id (keys %{$frr_cfg->{ids}}) { - my $plugin_config = $frr_cfg->{ids}->{$id}; - my $asn = $plugin_config->{asn}; - if ($asn) { - my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type}); - $plugin->generate_frr_config($plugin_config, $asn, $id, $uplinks, $config); - } - } + cfs_lock_file($running_cfg, undef, $code); - foreach my $id (keys %{$transport_cfg->{ids}}) { - my $plugin_config = $transport_cfg->{ids}->{$id}; - my $router = $plugin_config->{router}; - if ($router) { - my $asn = $frr_cfg->{ids}->{$router}->{asn}; - if ($asn) { - my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type}); - $plugin->generate_frr_config($plugin_config, $asn, $id, $uplinks, $config); - } - } + if (my $err = $@) { + $errmsg ? die "$errmsg: $err" : die $err; } +} - my $final_config = []; - push @{$final_config}, "log syslog informational"; - - generate_frr_recurse($final_config, $config, undef, 0); +sub get_local_vnets { - push @{$final_config}, "!"; - push @{$final_config}, "line vty"; - push @{$final_config}, "!"; + my $rpcenv = PVE::RPCEnvironment::get(); - my $raw_frr_config = join("\n", @{$final_config}); - return $raw_frr_config; -} + my $authuser = $rpcenv->get_user(); -sub sort_frr_config { - my $order = {}; - $order->{''} = 0; - $order->{'vrf'} = 1; - $order->{'ipv4 unicast'} = 1; - $order->{'l2vpn evpn'} = 2; - - my $a_val = 100; - my $b_val = 100; - - $a_val = $order->{$a} if defined($order->{$a}); - $b_val = $order->{$b} if defined($order->{$b}); - - if($a =~ /bgp (\d+)$/) { - $a_val = 2; - } + my $nodename = PVE::INotify::nodename(); - if($b =~ /bgp (\d+)$/) { - $b_val = 2; - } + my $cfg = PVE::Network::SDN::config(); + my $vnets_cfg = $cfg->{vnets}; + my $zones_cfg = $cfg->{zones}; - return $a_val <=> $b_val; -} + my @vnetids = PVE::Network::SDN::Vnets::sdn_vnets_ids($vnets_cfg); -sub generate_frr_recurse{ - my ($final_config, $content, $parentkey, $level) = @_; + my $vnets = {}; - my $keylist = {}; - $keylist->{vrf} = 1; - $keylist->{'address-family'} = 1; - $keylist->{router} = 1; + foreach my $vnetid (@vnetids) { - my $exitkeylist = {}; - $exitkeylist->{vrf} = 1; - $exitkeylist->{'address-family'} = 1; + my $vnet = PVE::Network::SDN::Vnets::sdn_vnets_config($vnets_cfg, $vnetid); + my $zoneid = $vnet->{zone}; + my $comments = $vnet->{alias}; - #fix me, make this generic - my $paddinglevel = undef; - if($level == 1 || $level == 2) { - $paddinglevel = $level - 1; - } elsif ($level == 3 || $level == 4) { - $paddinglevel = $level - 2; - } + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; - my $padding = ""; - $padding = ' ' x ($paddinglevel) if $paddinglevel; + next if !$zoneid; + next if !$rpcenv->check_any($authuser, "/sdn/zones/$zoneid", $privs, 1); - if (ref $content eq ref {}) { - foreach my $key (sort sort_frr_config keys %$content) { - if ($parentkey && defined($keylist->{$parentkey})) { - push @{$final_config}, $padding."!"; - push @{$final_config}, $padding."$parentkey $key"; - } else { - push @{$final_config}, $padding."$key" if $key ne '' && !defined($keylist->{$key}); - } + my $zone_config = PVE::Network::SDN::Zones::sdn_zones_config($zones_cfg, $zoneid); - my $option = $content->{$key}; - generate_frr_recurse($final_config, $option, $key, $level+1); - - push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey}); - } + next if defined($zone_config->{nodes}) && !$zone_config->{nodes}->{$nodename}; + $vnets->{$vnetid} = { type => 'vnet', active => '1', comments => $comments }; } - if (ref $content eq 'ARRAY') { - foreach my $value (@$content) { - push @{$final_config}, $padding."$value"; - } - } + return $vnets; } -sub write_etc_network_config { - my ($rawconfig) = @_; - return if !$rawconfig; - my $sdn_interfaces_file = "/etc/network/interfaces.d/sdn"; - - my $writefh = IO::File->new($sdn_interfaces_file,">"); - print $writefh $rawconfig; - $writefh->close(); +sub generate_zone_config { + my $raw_config = PVE::Network::SDN::Zones::generate_etc_network_config(); + PVE::Network::SDN::Zones::write_etc_network_config($raw_config); } -sub write_frr_config { - my ($rawconfig) = @_; +sub generate_controller_config { + my ($reload) = @_; + + my $raw_config = PVE::Network::SDN::Controllers::generate_controller_config(); + PVE::Network::SDN::Controllers::write_controller_config($raw_config); + + PVE::Network::SDN::Controllers::reload_controller() if $reload; +} - return if !$rawconfig; - return if !-d "/etc/frr"; +sub encode_value { + my ($type, $key, $value) = @_; - my $frr_config_file = "/etc/frr/frr.conf"; + if ($key eq 'nodes' || $key eq 'exitnodes') { + if(ref($value) eq 'HASH') { + return join(',', sort keys(%$value)); + } else { + return $value; + } + } - my $writefh = IO::File->new($frr_config_file,">"); - print $writefh $rawconfig; - $writefh->close(); + return $value; } -sub status { +#helpers +sub api_request { + my ($method, $url, $headers, $data) = @_; - my $cluster_sdn_file = "/etc/pve/sdn.cfg"; - my $local_sdn_file = "/etc/network/interfaces.d/sdn"; - my $err_config = undef; + my $encoded_data = to_json($data) if $data; - return if !-e $cluster_sdn_file; + my $req = HTTP::Request->new($method,$url, $headers, $encoded_data); - if (!-e $local_sdn_file) { - warn "local sdn network configuration is not yet generated, please reload"; - $err_config = 'pending'; - } else { - # fixme : use some kind of versioning info? - my $cluster_sdn_timestamp = (stat($cluster_sdn_file))[9]; - my $local_sdn_timestamp = (stat($local_sdn_file))[9]; + my $ua = LWP::UserAgent->new(protocols_allowed => ['http', 'https'], timeout => 30); + my $proxy = undef; - if ($local_sdn_timestamp < $cluster_sdn_timestamp) { - warn "local sdn network configuration is too old, please reload"; - $err_config = 'unknown'; - } + if ($proxy) { + $ua->proxy(['http', 'https'], $proxy); + } else { + $ua->env_proxy; } - my $status = ifquery_check(); + $ua->ssl_opts(verify_hostname => 0, SSL_verify_mode => 0x00); - my $network_cfg = PVE::Cluster::cfs_read_file('sdn.cfg'); - my $vnet_cfg = undef; - my $transport_cfg = undef; + my $response = $ua->request($req); + my $code = $response->code; - my $vnet_status = {}; - my $transport_status = {}; + if ($code !~ /^2(\d+)$/) { + my $msg = $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } - foreach my $id (keys %{$network_cfg->{ids}}) { - if ($network_cfg->{ids}->{$id}->{type} eq 'vnet') { - my $transportzone = $network_cfg->{ids}->{$id}->{transportzone}; - $vnet_status->{$id}->{transportzone} = $transportzone; - $transport_status->{$transportzone}->{status} = 'available' if !defined($transport_status->{$transportzone}->{status}); + my $raw = ''; + if (defined($response->decoded_content)) { + $raw = $response->decoded_content; + } else { + $raw = $response->content; + } - if($err_config) { - $vnet_status->{$id}->{status} = $err_config; - $transport_status->{$transportzone}->{status} = $err_config; - } elsif ($status->{$id}->{status} && $status->{$id}->{status} eq 'pass') { - $vnet_status->{$id}->{status} = 'available'; - my $bridgeport = $status->{$id}->{config}->{'bridge-ports'}; + return if $raw eq ''; - if ($status->{$bridgeport}->{status} && $status->{$bridgeport}->{status} ne 'pass') { - $vnet_status->{$id}->{status} = 'error'; - $transport_status->{$transportzone}->{status} = 'error'; - } - } else { - $vnet_status->{$id}->{status} = 'error'; - $transport_status->{$transportzone}->{status} = 'error'; - } - } - } - return($transport_status, $vnet_status); + my $json = ''; + eval { + $json = from_json($raw); + }; + die "api response is not a json" if $@; + + return $json; } 1; -