]> git.proxmox.com Git - pve-network.git/blob - PVE/Network/SDN.pm
generate_frr_config: cleanup hash and ordering
[pve-network.git] / PVE / Network / SDN.pm
1 package PVE::Network::SDN;
2
3 use strict;
4 use warnings;
5
6 use Data::Dumper;
7 use JSON;
8
9 use PVE::Tools qw(extract_param dir_glob_regex run_command);
10 use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
11 use PVE::Network::SDN::Plugin;
12 use PVE::Network::SDN::VnetPlugin;
13 use PVE::Network::SDN::VlanPlugin;
14 use PVE::Network::SDN::VxlanPlugin;
15 use PVE::Network::SDN::FrrPlugin;
16
17 PVE::Network::SDN::VnetPlugin->register();
18 PVE::Network::SDN::VlanPlugin->register();
19 PVE::Network::SDN::VxlanPlugin->register();
20 PVE::Network::SDN::FrrPlugin->register();
21 PVE::Network::SDN::Plugin->init();
22
23
24 sub sdn_config {
25 my ($cfg, $sdnid, $noerr) = @_;
26
27 die "no sdn ID specified\n" if !$sdnid;
28
29 my $scfg = $cfg->{ids}->{$sdnid};
30 die "sdn '$sdnid' does not exists\n" if (!$noerr && !$scfg);
31
32 return $scfg;
33 }
34
35 sub config {
36 my $config = cfs_read_file("sdn.cfg.new");
37 $config = cfs_read_file("sdn.cfg") if !keys %{$config->{ids}};
38 return $config;
39 }
40
41 sub write_config {
42 my ($cfg) = @_;
43
44 cfs_write_file("sdn.cfg.new", $cfg);
45 }
46
47 sub lock_sdn_config {
48 my ($code, $errmsg) = @_;
49
50 cfs_lock_file("sdn.cfg.new", undef, $code);
51 if (my $err = $@) {
52 $errmsg ? die "$errmsg: $err" : die $err;
53 }
54 }
55
56 sub sdn_ids {
57 my ($cfg) = @_;
58
59 return keys %{$cfg->{ids}};
60 }
61
62 sub complete_sdn {
63 my ($cmdname, $pname, $cvalue) = @_;
64
65 my $cfg = PVE::Network::SDN::config();
66
67 return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::sdn_ids($cfg) ];
68 }
69
70 sub ifquery_check {
71
72 my $cmd = ['ifquery', '-a', '-c', '-o','json'];
73
74 my $result = '';
75 my $reader = sub { $result .= shift };
76
77 eval {
78 run_command($cmd, outfunc => $reader);
79 };
80
81 my $resultjson = decode_json($result);
82 my $interfaces = {};
83
84 foreach my $interface (@$resultjson) {
85 my $name = $interface->{name};
86 $interfaces->{$name} = {
87 status => $interface->{status},
88 config => $interface->{config},
89 config_status => $interface->{config_status},
90 };
91 }
92
93 return $interfaces;
94 }
95
96
97 sub generate_etc_network_config {
98
99 my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
100 return if !$sdn_cfg;
101
102 #read main config for physical interfaces
103 my $current_config_file = "/etc/network/interfaces";
104 my $fh = IO::File->new($current_config_file);
105 my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
106 $fh->close();
107
108 #check uplinks
109 my $uplinks = {};
110 foreach my $id (keys %{$interfaces_config->{ifaces}}) {
111 my $interface = $interfaces_config->{ifaces}->{$id};
112 if (my $uplink = $interface->{'uplink-id'}) {
113 die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
114 $interface->{name} = $id;
115 $uplinks->{$interface->{'uplink-id'}} = $interface;
116 }
117 }
118
119 my $vnet_cfg = undef;
120 my $transport_cfg = undef;
121
122 foreach my $id (keys %{$sdn_cfg->{ids}}) {
123 if ($sdn_cfg->{ids}->{$id}->{type} eq 'vnet') {
124 $vnet_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
125 } else {
126 $transport_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
127 }
128 }
129
130 #generate configuration
131 my $config = {};
132 foreach my $id (keys %{$vnet_cfg->{ids}}) {
133 my $vnet = $vnet_cfg->{ids}->{$id};
134 my $zone = $vnet->{transportzone};
135
136 if(!$zone) {
137 warn "can't generate vnet $vnet : zone $zone don't exist";
138 next;
139 }
140
141 my $plugin_config = $transport_cfg->{ids}->{$zone};
142
143 if (!defined($plugin_config)) {
144 warn "can't generate vnet $vnet : zone $zone don't exist";
145 next;
146 }
147
148 my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
149 $plugin->generate_sdn_config($plugin_config, $zone, $id, $vnet, $uplinks, $config);
150 }
151
152 my $raw_network_config = "";
153 foreach my $iface (keys %$config) {
154 $raw_network_config .= "\n";
155 $raw_network_config .= "auto $iface\n";
156 $raw_network_config .= "iface $iface\n";
157 foreach my $option (@{$config->{$iface}}) {
158 $raw_network_config .= "\t$option\n";
159 }
160 }
161
162 return $raw_network_config;
163 }
164
165 sub generate_frr_config {
166
167 my $sdn_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
168 return if !$sdn_cfg;
169
170 #read main config for physical interfaces
171 my $current_config_file = "/etc/network/interfaces";
172 my $fh = IO::File->new($current_config_file);
173 my $interfaces_config = PVE::INotify::read_etc_network_interfaces(1,$fh);
174 $fh->close();
175
176 #check uplinks
177 my $uplinks = {};
178 foreach my $id (keys %{$interfaces_config->{ifaces}}) {
179 my $interface = $interfaces_config->{ifaces}->{$id};
180 if (my $uplink = $interface->{'uplink-id'}) {
181 die "uplink-id $uplink is already defined on $uplinks->{$uplink}" if $uplinks->{$uplink};
182 $interface->{name} = $id;
183 $uplinks->{$interface->{'uplink-id'}} = $interface;
184 }
185 }
186
187 my $frr_cfg = undef;
188 my $transport_cfg = undef;
189
190 foreach my $id (keys %{$sdn_cfg->{ids}}) {
191 if ($sdn_cfg->{ids}->{$id}->{type} eq 'frr') {
192 $frr_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
193 } elsif ($sdn_cfg->{ids}->{$id}->{type} ne 'vnet') {
194 $transport_cfg->{ids}->{$id} = $sdn_cfg->{ids}->{$id};
195 }
196 }
197
198 return undef if !$frr_cfg;
199
200 #generate configuration
201 my $config = {};
202
203 foreach my $id (keys %{$frr_cfg->{ids}}) {
204 my $plugin_config = $frr_cfg->{ids}->{$id};
205 my $asn = $plugin_config->{asn};
206 if ($asn) {
207 my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
208 $plugin->generate_frr_config($plugin_config, $asn, $id, $uplinks, $config);
209 }
210 }
211
212 foreach my $id (keys %{$transport_cfg->{ids}}) {
213 my $plugin_config = $transport_cfg->{ids}->{$id};
214 my $router = $plugin_config->{router};
215 if ($router) {
216 my $asn = $frr_cfg->{ids}->{$router}->{asn};
217 if ($asn) {
218 my $plugin = PVE::Network::SDN::Plugin->lookup($plugin_config->{type});
219 $plugin->generate_frr_config($plugin_config, $asn, $id, $uplinks, $config);
220 }
221 }
222 }
223
224 my $final_config = [];
225 push @{$final_config}, "log syslog informational";
226
227 generate_frr_recurse($final_config, $config, undef, 0);
228
229 push @{$final_config}, "!";
230 push @{$final_config}, "line vty";
231 push @{$final_config}, "!";
232
233 my $raw_frr_config = join("\n", @{$final_config});
234 return $raw_frr_config;
235 }
236
237 sub sort_frr_config {
238 my $order = {};
239 $order->{''} = 0;
240 $order->{'vrf'} = 1;
241 $order->{'ipv4 unicast'} = 1;
242 $order->{'l2vpn evpn'} = 2;
243
244 my $a_val = 100;
245 my $b_val = 100;
246
247 $a_val = $order->{$a} if defined($order->{$a});
248 $b_val = $order->{$b} if defined($order->{$b});
249
250 if($a =~ /bgp (\d+)$/) {
251 $a_val = 2;
252 }
253
254 if($b =~ /bgp (\d+)$/) {
255 $b_val = 2;
256 }
257
258 return $a_val <=> $b_val;
259 }
260
261 sub generate_frr_recurse{
262 my ($final_config, $content, $parentkey, $level) = @_;
263
264 my $keylist = {};
265 $keylist->{vrf} = 1;
266 $keylist->{'address-family'} = 1;
267 $keylist->{router} = 1;
268
269 my $exitkeylist = {};
270 $exitkeylist->{vrf} = 1;
271 $exitkeylist->{'address-family'} = 1;
272
273 #fix me, make this generic
274 my $paddinglevel = undef;
275 if($level == 1 || $level == 2) {
276 $paddinglevel = $level - 1;
277 } elsif ($level == 3 || $level == 4) {
278 $paddinglevel = $level - 2;
279 }
280
281 my $padding = "";
282 $padding = ' ' x ($paddinglevel) if $paddinglevel;
283
284 if (ref $content eq ref {}) {
285 foreach my $key (sort sort_frr_config keys %$content) {
286 if ($parentkey && defined($keylist->{$parentkey})) {
287 push @{$final_config}, $padding."!";
288 push @{$final_config}, $padding."$parentkey $key";
289 } else {
290 push @{$final_config}, $padding."$key" if $key ne '' && !defined($keylist->{$key});
291 }
292
293 my $option = $content->{$key};
294 generate_frr_recurse($final_config, $option, $key, $level+1);
295
296 push @{$final_config}, $padding."exit-$parentkey" if $parentkey && defined($exitkeylist->{$parentkey});
297 }
298 }
299
300 if (ref $content eq 'ARRAY') {
301 foreach my $value (@$content) {
302 push @{$final_config}, $padding."$value";
303 }
304 }
305 }
306 sub write_etc_network_config {
307 my ($rawconfig) = @_;
308
309 return if !$rawconfig;
310 my $sdn_interfaces_file = "/etc/network/interfaces.d/sdn";
311
312 my $writefh = IO::File->new($sdn_interfaces_file,">");
313 print $writefh $rawconfig;
314 $writefh->close();
315 }
316
317 sub write_frr_config {
318 my ($rawconfig) = @_;
319
320 return if !$rawconfig;
321 return if !-d "/etc/frr";
322
323 my $frr_config_file = "/etc/frr/frr.conf";
324
325 my $writefh = IO::File->new($frr_config_file,">");
326 print $writefh $rawconfig;
327 $writefh->close();
328 }
329
330
331 sub status {
332
333 my $cluster_sdn_file = "/etc/pve/sdn.cfg";
334 my $local_sdn_file = "/etc/network/interfaces.d/sdn";
335 my $err_config = undef;
336
337 return if !-e $cluster_sdn_file;
338
339 if (!-e $local_sdn_file) {
340 warn "local sdn network configuration is not yet generated, please reload";
341 $err_config = 'pending';
342 } else {
343 # fixme : use some kind of versioning info?
344 my $cluster_sdn_timestamp = (stat($cluster_sdn_file))[9];
345 my $local_sdn_timestamp = (stat($local_sdn_file))[9];
346
347 if ($local_sdn_timestamp < $cluster_sdn_timestamp) {
348 warn "local sdn network configuration is too old, please reload";
349 $err_config = 'unknown';
350 }
351 }
352
353 my $status = ifquery_check();
354
355 my $network_cfg = PVE::Cluster::cfs_read_file('sdn.cfg');
356 my $vnet_cfg = undef;
357 my $transport_cfg = undef;
358
359 my $vnet_status = {};
360 my $transport_status = {};
361
362 foreach my $id (keys %{$network_cfg->{ids}}) {
363 if ($network_cfg->{ids}->{$id}->{type} eq 'vnet') {
364 my $transportzone = $network_cfg->{ids}->{$id}->{transportzone};
365 $vnet_status->{$id}->{transportzone} = $transportzone;
366 $transport_status->{$transportzone}->{status} = 'available' if !defined($transport_status->{$transportzone}->{status});
367
368 if($err_config) {
369 $vnet_status->{$id}->{status} = $err_config;
370 $transport_status->{$transportzone}->{status} = $err_config;
371 } elsif ($status->{$id}->{status} && $status->{$id}->{status} eq 'pass') {
372 $vnet_status->{$id}->{status} = 'available';
373 my $bridgeport = $status->{$id}->{config}->{'bridge-ports'};
374
375 if ($status->{$bridgeport}->{status} && $status->{$bridgeport}->{status} ne 'pass') {
376 $vnet_status->{$id}->{status} = 'error';
377 $transport_status->{$transportzone}->{status} = 'error';
378 }
379 } else {
380 $vnet_status->{$id}->{status} = 'error';
381 $transport_status->{$transportzone}->{status} = 'error';
382 }
383 }
384 }
385 return($transport_status, $vnet_status);
386 }
387
388 1;
389