X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=PVE%2FNetwork%2FSDN.pm;h=d3399ceef858d759a28cecd47086bb0beb5de6ba;hb=95e2f20f5c70f0cd0aa4fe80dad35dace9dc165d;hp=1d84a32e9f1a8d9090715170d99ff86ebe0a08d8;hpb=62e54fb7528fe1df533191633057c237e0eafc0a;p=pve-network.git diff --git a/PVE/Network/SDN.pm b/PVE/Network/SDN.pm index 1d84a32..d3399ce 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(); +my $running_cfg = "sdn/.running-config"; -sub sdn_config { - my ($cfg, $sdnid, $noerr) = @_; +my $parse_running_cfg = sub { + my ($filename, $raw) = @_; - die "no sdn ID specified\n" if !$sdnid; + my $cfg = {}; - my $scfg = $cfg->{ids}->{$sdnid}; - die "sdn '$sdnid' does not exists\n" if (!$noerr && !$scfg); + return $cfg if !defined($raw) || $raw eq ''; - return $scfg; -} + eval { + $cfg = from_json($raw); + }; + return {} if $@; -sub config { - my $config = cfs_read_file("sdn.cfg.new"); - $config = cfs_read_file("sdn.cfg") if !keys %{$config->{ids}}; - return $config; -} + return $cfg; +}; -sub write_config { - my ($cfg) = @_; +my $write_running_cfg = sub { + my ($filename, $cfg) = @_; - cfs_write_file("sdn.cfg.new", $cfg); -} + my $json = to_json($cfg); -sub lock_sdn_config { - my ($code, $errmsg) = @_; + return $json; +}; - cfs_lock_file("sdn.cfg.new", undef, $code); - if (my $err = $@) { - $errmsg ? die "$errmsg: $err" : die $err; - } -} - -sub sdn_ids { - my ($cfg) = @_; - - return keys %{$cfg->{ids}}; -} +PVE::Cluster::cfs_register_file($running_cfg, $parse_running_cfg, $write_running_cfg); -sub complete_sdn { - my ($cmdname, $pname, $cvalue) = @_; - 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,244 +71,213 @@ 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 running_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); + if ($version) { + $version++; + } else { + $version = 1; } - 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"; - } - } + 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 $vnets = { ids => $vnets_cfg->{ids} }; + my $zones = { ids => $zones_cfg->{ids} }; + my $controllers = { ids => $controllers_cfg->{ids} }; + my $subnets = { ids => $subnets_cfg->{ids} }; + + $cfg = { version => $version, vnets => $vnets, zones => $zones, controllers => $controllers, subnets => $subnets }; - return $raw_network_config; + cfs_write_file($running_cfg, $cfg); } -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; - } - } +sub lock_sdn_config { + my ($code, $errmsg) = @_; - my $frr_cfg = undef; - my $transport_cfg = undef; + cfs_lock_file($running_cfg, undef, $code); - 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}; - } + if (my $err = $@) { + $errmsg ? die "$errmsg: $err" : die $err; } +} - return undef if !$frr_cfg; +sub get_local_vnets { - #generate configuration - my $config = {}; + my $rpcenv = PVE::RPCEnvironment::get(); - 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); - } - } + my $authuser = $rpcenv->get_user(); - 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); - } - } - } + my $nodename = PVE::INotify::nodename(); - my $raw_frr_config = "log syslog informational\n"; - $raw_frr_config .= "!\n"; + my $cfg = PVE::Network::SDN::running_config(); + my $vnets_cfg = $cfg->{vnets}; + my $zones_cfg = $cfg->{zones}; - #vrf first - my $vrfconfig = $config->{vrf}; - foreach my $vrf (sort keys %$vrfconfig) { - $raw_frr_config .= "$vrf\n"; - foreach my $option (@{$vrfconfig->{$vrf}}) { - $raw_frr_config .= " $option\n"; - } - $raw_frr_config .= "!\n"; - } + my @vnetids = PVE::Network::SDN::Vnets::sdn_vnets_ids($vnets_cfg); - #routers - my $routerconfig = $config->{router}; - foreach my $router (sort keys %$routerconfig) { - $raw_frr_config .= "$router\n"; - foreach my $option (@{$routerconfig->{$router}}) { - $raw_frr_config .= " $option\n"; - } - $raw_frr_config .= "!\n"; + my $vnets = {}; + + foreach my $vnetid (@vnetids) { + + my $vnet = PVE::Network::SDN::Vnets::sdn_vnets_config($vnets_cfg, $vnetid); + my $zoneid = $vnet->{zone}; + my $comments = $vnet->{alias}; + + my $privs = [ 'SDN.Audit', 'SDN.Allocate' ]; + + next if !$zoneid; + next if !$rpcenv->check_any($authuser, "/sdn/zones/$zoneid", $privs, 1); + + my $zone_config = PVE::Network::SDN::Zones::sdn_zones_config($zones_cfg, $zoneid); + + next if defined($zone_config->{nodes}) && !$zone_config->{nodes}->{$nodename}; + my $ipam = $zone_config->{ipam} ? 1 : 0; + my $vlanaware = $vnet->{vlanaware} ? 1 : 0; + $vnets->{$vnetid} = { type => 'vnet', active => '1', ipam => $ipam, vlanaware => $vlanaware, comments => $comments }; } - $raw_frr_config .= "line vty\n"; - $raw_frr_config .= "!\n"; + return $vnets; +} - return $raw_frr_config; +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_etc_network_config { - my ($rawconfig) = @_; +sub generate_controller_config { + my ($reload) = @_; - return if !$rawconfig; - my $sdn_interfaces_file = "/etc/network/interfaces.d/sdn"; + my $raw_config = PVE::Network::SDN::Controllers::generate_controller_config(); + PVE::Network::SDN::Controllers::write_controller_config($raw_config); - my $writefh = IO::File->new($sdn_interfaces_file,">"); - print $writefh $rawconfig; - $writefh->close(); + PVE::Network::SDN::Controllers::reload_controller() if $reload; } -sub write_frr_config { - my ($rawconfig) = @_; +sub encode_value { + my ($type, $key, $value) = @_; - return if !$rawconfig; - return if !-d "/etc/frr"; - - 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; + + if ($code !~ /^2(\d+)$/) { + my $msg = $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } - my $vnet_status = {}; - my $transport_status = {}; + my $raw = ''; + if (defined($response->decoded_content)) { + $raw = $response->decoded_content; + } else { + $raw = $response->content; + } - 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}); + return if $raw eq ''; - 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'}; + my $json = ''; + eval { + $json = from_json($raw); + }; + die "api response is not a json" if $@; - 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); + return $json; } 1; -