socket-util: Move get_mtime() here from stream-ssl.
[openvswitch] / debian / ovs-switch-setup
1 #! /usr/bin/perl
2
3 use POSIX;
4 use Debconf::Client::ConfModule ':all';
5 use HTTP::Request;
6 use LWP::UserAgent;
7 use Digest::SHA1 'sha1_hex';
8 use strict;
9 use warnings;
10
11 # XXX should support configuring SWITCH_NETMASK and SWITCH_GATEWAY
12 # when the mode is in-band.
13
14 my $debconf_owner = 'openvswitch-switch';
15
16 my $default = '/etc/default/openvswitch-switch';
17 my $template = '/usr/share/openvswitch/switch/default.template';
18 my $etc = '/etc/openvswitch';
19 my $rundir = '/var/run/openvswitch';
20 my $privkey_file = "$etc/of0-privkey.pem";
21 my $req_file = "$etc/of0-req.pem";
22 my $cert_file = "$etc/of0-cert.pem";
23 my $cacert_file = "$etc/cacert.pem";
24 my $ovs_discover_pidfile = "$rundir/ovs-discover.pid";
25
26 my $ua = LWP::UserAgent->new;
27 $ua->timeout(10);
28 $ua->env_proxy;
29
30 system("/etc/init.d/openvswitch-switch stop 1>&2");
31 kill_ovs_discover();
32
33 version('2.0');
34 capb('backup');
35 title('Open vSwitch Switch Setup');
36
37 my (%netdevs) = find_netdevs();
38 db_subst('netdevs', 'choices',
39          join(', ', map($netdevs{$_}, sort(keys(%netdevs)))));
40 db_set('netdevs', join(', ', grep(!/IP/, values(%netdevs))));
41
42 my %oldconfig;
43 if (-e $default) {
44     %oldconfig = load_config($default);
45
46     my (%map) =
47       (NETDEVS => sub {
48            db_set('netdevs', join(', ', map($netdevs{$_},
49                                             grep(exists $netdevs{$_}, split))))
50        },
51        MODE => sub {
52            db_set('mode',
53                   $_ eq 'in-band' || $_ eq 'out-of-band' ? $_ : 'discovery')
54        },
55        SWITCH_IP => sub { db_set('switch-ip', $_) },
56        CONTROLLER => sub { db_set('controller-vconn', $_) },
57        PRIVKEY => sub { $privkey_file = $_ },
58        CERT => sub { $cert_file = $_ },
59        CACERT => sub { $cacert_file = $_ },
60       );
61
62     for my $key (keys(%map)) {
63         local $_ = $oldconfig{$key};
64         &{$map{$key}}() if defined && !/^\s*$/;
65     }
66 } elsif (-e $template) {
67     %oldconfig = load_config($template);
68 }
69
70 my $cacert_preverified = -e $cacert_file;
71 my ($req, $req_fingerprint);
72
73 my %options;
74
75 my (@states) =
76   (sub {
77        # User backed up from first dialog box.
78        exit(10);
79    },
80    sub {
81        # Prompt for ports to include in switch.
82        db_input('netdevs');
83        return;
84    },
85    sub {
86        # Validate the chosen ports.
87        my (@netdevs) = split(', ', db_get('netdevs'));
88        if (!@netdevs) {
89            # No ports chosen.  Disable switch.
90            db_input('no-netdevs');
91            return 'prev' if db_go();
92            return 'done';
93        } elsif (my (@conf_netdevs) = grep(/IP/, @netdevs)) {
94            # Point out that some ports have configured IP addresses.
95            db_subst('configured-netdevs', 'configured-netdevs',
96                     join(', ', @conf_netdevs));
97            db_input('configured-netdevs');
98            return;
99        } else {
100            # Otherwise proceed.
101            return 'skip';
102        }
103    },
104    sub {
105        # Discovery or in-band or out-of-band controller?
106        db_input('mode');
107        return;
108    },
109    sub {
110        return 'skip' if db_get('mode') ne 'discovery';
111        for (;;) {
112            # Notify user that we are going to do discovery.
113            db_input('discover');
114            return 'prev' if db_go();
115            print STDERR "Please wait up to 30 seconds for discovery...\n";
116
117            # Make sure that there's no running discovery process.
118            kill_ovs_discover();
119
120            # Do discovery.
121            %options = ();
122            open(DISCOVER, '-|', 'ovs-discover --timeout=30 --pidfile '
123                 . join(' ', netdev_names()));
124            while (<DISCOVER>) {
125                chomp;
126                if (my ($name, $value) = /^([^=]+)=(.*)$/) {
127                    if ($value =~ /^"(.*)"$/) {
128                        $value = $1;
129                        $value =~ s/\\([0-7][0-7][0-7])/chr($1)/ge;
130                    } else {
131                        $value =~ s/^(0x[[:xdigit:]]+)$/hex($1)/e;
132                        $value = '' if $value eq 'empty';
133                        next if $value eq 'null'; # Shouldn't happen.
134                    }
135                    $options{$name} = $value;
136                }
137                last if /^$/;
138            }
139
140            # Check results.
141            my $vconn = $options{'ovs-controller-vconn'};
142            my $pki_uri = $options{'ovs-pki-uri'};
143            return 'next'
144              if (defined($vconn)
145                  && is_valid_vconn($vconn)
146                  && (!is_ssl_vconn($vconn) || defined($pki_uri)));
147
148            # Try again?
149            kill_ovs_discover();
150            db_input('discovery-failure');
151            db_go();
152        }
153    },
154    sub {
155        return 'skip' if db_get('mode') ne 'discovery';
156
157        my $vconn = $options{'ovs-controller-vconn'};
158        my $pki_uri = $options{'ovs-pki-uri'};
159        db_subst('discovery-success', 'controller-vconn', $vconn);
160        db_subst('discovery-success',
161                 'pki-uri', is_ssl_vconn($vconn) ? $pki_uri : "no PKI in use");
162        db_input('discovery-success');
163        return 'prev' if db_go();
164        db_set('controller-vconn', $vconn);
165        db_set('pki-uri', $pki_uri);
166        return 'next';
167    },
168    sub {
169        return 'skip' if db_get('mode') ne 'in-band';
170        for (;;) {
171            db_input('switch-ip');
172            return 'prev' if db_go();
173
174            my $ip = db_get('switch-ip');
175            return 'next' if $ip =~ /^dhcp|\d+\.\d+.\d+.\d+$/i;
176
177            db_input('switch-ip-error');
178            db_go();
179        }
180    },
181    sub {
182        return 'skip' if db_get('mode') eq 'discovery';
183        for (;;) {
184            my $old_vconn = db_get('controller-vconn');
185            db_input('controller-vconn');
186            return 'prev' if db_go();
187
188            my $vconn = db_get('controller-vconn');
189            if (is_valid_vconn($vconn)) {
190                if ($old_vconn ne $vconn || db_get('pki-uri') eq '') {
191                    db_set('pki-uri', pki_host_to_uri($2));
192                }
193                return 'next';
194            }
195
196            db_input('controller-vconn-error');
197            db_go();
198        }
199    },
200    sub {
201        return 'skip' if !ssl_enabled();
202
203        if (! -e $privkey_file) {
204            my $old_umask = umask(077);
205            run_cmd("ovs-pki req $etc/of0 >&2 2>/dev/null");
206            chmod(0644, $req_file) or die "$req_file: chmod: $!\n";
207            umask($old_umask);
208        }
209
210        if (! -e $cert_file) {
211            open(REQ, '<', $req_file) or die "$req_file: open: $!\n";
212            $req = join('', <REQ>);
213            close(REQ);
214            $req_fingerprint = sha1_hex($req);
215        }
216        return 'skip';
217    },
218    sub {
219        return 'skip' if !ssl_enabled();
220        return 'skip' if -e $cacert_file && -e $cert_file;
221
222        db_input('pki-uri');
223        return 'prev' if db_go();
224        return;
225    },
226    sub {
227        return 'skip' if !ssl_enabled();
228        return 'skip' if -e $cacert_file;
229
230        my $pki_uri = db_get('pki-uri');
231        if ($pki_uri !~ /:/) {
232            $pki_uri = pki_host_to_uri($pki_uri);
233        } else {
234            # Trim trailing slashes.
235            $pki_uri =~ s%/+$%%;
236        }
237        db_set('pki-uri', $pki_uri);
238
239        my $url = "$pki_uri/controllerca/cacert.pem";
240        my $response = $ua->get($url, ':content_file' => $cacert_file);
241        if ($response->is_success) {
242            return 'next';
243        }
244
245        db_subst('fetch-cacert-failed', 'url', $url);
246        db_subst('fetch-cacert-failed', 'error', $response->status_line);
247        db_subst('fetch-cacert-failed', 'pki-uri', $pki_uri);
248        db_input('fetch-cacert-failed');
249        db_go();
250        return 'prev';
251    },
252    sub {
253        return 'skip' if !ssl_enabled();
254        return 'skip' if -e $cert_file;
255
256        for (;;) {
257            db_set('send-cert-req', 'true');
258            db_input('send-cert-req');
259            return 'prev' if db_go();
260            return 'next' if db_get('send-cert-req') eq 'false';
261
262            my $pki_uri = db_get('pki-uri');
263            my ($pki_base_uri) = $pki_uri =~ m%^([^/]+://[^/]+)/%;
264            my $url = "$pki_base_uri/cgi-bin/ovs-pki-cgi";
265            my $response = $ua->post($url, {'type' => 'switch',
266                                            'req' => $req});
267            return 'next' if $response->is_success;
268
269            db_subst('send-cert-req-failed', 'url', $url);
270            db_subst('send-cert-req-failed', 'error',
271                     $response->status_line);
272            db_subst('send-cert-req-failed', 'pki-uri', $pki_uri);
273            db_input('send-cert-req-failed');
274            db_go();
275        }
276    },
277    sub {
278        return 'skip' if !ssl_enabled();
279        return 'skip' if $cacert_preverified;
280
281        my ($cacert_fingerprint) = x509_fingerprint($cacert_file);
282        db_subst('verify-controller-ca', 'fingerprint', $cacert_fingerprint);
283        db_input('verify-controller-ca');
284        return 'prev' if db_go();
285        return 'next' if db_get('verify-controller-ca') eq 'true';
286        unlink($cacert_file);
287        return 'prev';
288    },
289    sub {
290        return 'skip' if !ssl_enabled();
291        return 'skip' if -e $cert_file;
292
293        for (;;) {
294            db_set('fetch-switch-cert', 'true');
295            db_input('fetch-switch-cert');
296            return 'prev' if db_go();
297            exit(1) if db_get('fetch-switch-cert') eq 'false';
298
299            my $pki_uri = db_get('pki-uri');
300            my $url = "$pki_uri/switchca/certs/$req_fingerprint-cert.pem";
301            my $response = $ua->get($url, ':content_file' => $cert_file);
302            if ($response->is_success) {
303                return 'next';
304            }
305
306            db_subst('fetch-switch-cert-failed', 'url', $url);
307            db_subst('fetch-switch-cert-failed', 'error',
308                     $response->status_line);
309            db_subst('fetch-switch-cert-failed', 'pki-uri', $pki_uri);
310            db_input('fetch-switch-cert-failed');
311            db_go();
312        }
313    },
314    sub {
315        db_input('complete');
316        db_go();
317        return;
318    },
319    sub {
320        return 'done';
321    },
322 );
323
324 my $state = 1;
325 my $direction = 1;
326 for (;;) {
327     my $ret = &{$states[$state]}();
328     $ret = db_go() ? 'prev' : 'next' if !defined $ret;
329     if ($ret eq 'next') {
330         $direction = 1;
331     } elsif ($ret eq 'prev') {
332         $direction = -1;
333     } elsif ($ret eq 'skip') {
334         # Nothing to do.
335     } elsif ($ret eq 'done') {
336         last;
337     } else {
338         die "unknown ret $ret";
339     }
340     $state += $direction;
341 }
342
343 my %config = %oldconfig;
344 $config{NETDEVS} = join(' ', netdev_names());
345 $config{MODE} = db_get('mode');
346 if (db_get('mode') eq 'in-band') {
347     $config{SWITCH_IP} = db_get('switch-ip');
348 }
349 if (db_get('mode') ne 'discovery') {
350     $config{CONTROLLER} = db_get('controller-vconn');
351 }
352 $config{PRIVKEY} = $privkey_file;
353 $config{CERT} = $cert_file;
354 $config{CACERT} = $cacert_file;
355 save_config($default, %config);
356
357 dup2(2, 1);                     # Get stdout back.
358 kill_ovs_discover();
359 system("/etc/init.d/openvswitch-switch start");
360
361 sub ssl_enabled {
362     return is_ssl_vconn(db_get('controller-vconn'));
363 }
364
365 sub db_subst {
366     my ($question, $key, $value) = @_;
367     $question = "$debconf_owner/$question";
368     my ($ret, $seen) = subst($question, $key, $value);
369     if ($ret && $ret != 30) {
370         die "Error substituting $value for $key in debconf question "
371           . "$question: $seen";
372     }
373 }
374
375 sub db_set {
376     my ($question, $value) = @_;
377    $question = "$debconf_owner/$question";
378     my ($ret, $seen) = set($question, $value);
379     if ($ret && $ret != 30) {
380         die "Error setting debconf question $question to $value: $seen";
381     }
382 }
383
384 sub db_get {
385     my ($question) = @_;
386     $question = "$debconf_owner/$question";
387     my ($ret, $seen) = get($question);
388     if ($ret) {
389         die "Error getting debconf question $question answer: $seen";
390     }
391     return $seen;
392 }
393
394 sub db_fset {
395     my ($question, $flag, $value) = @_;
396     $question = "$debconf_owner/$question";
397     my ($ret, $seen) = fset($question, $flag, $value);
398     if ($ret && $ret != 30) {
399         die "Error setting debconf question $question flag $flag to $value: "
400           . "$seen";
401     }
402 }
403
404 sub db_fget {
405     my ($question, $flag) = @_;
406     $question = "$debconf_owner/$question";
407     my ($ret, $seen) = fget($question, $flag);
408     if ($ret) {
409         die "Error getting debconf question $question flag $flag: $seen";
410     }
411     return $seen;
412 }
413
414 sub db_input {
415     my ($question) = @_;
416     db_fset($question, "seen", "false");
417
418     $question = "$debconf_owner/$question";
419     my ($ret, $seen) = input('high', $question);
420     if ($ret && $ret != 30) {
421         die "Error requesting debconf question $question: $seen";
422     }
423     return $ret;
424 }
425
426 sub db_go {
427     my ($ret, $seen) = go();
428     if (!defined($ret)) {
429         exit(1);                # Cancel button was pushed.
430     }
431     if ($ret && $ret != 30) {
432         die "Error asking debconf questions: $seen";
433     }
434     return $ret;
435 }
436
437 sub run_cmd {
438     my ($cmd) = @_;
439     return if system($cmd) == 0;
440
441     if ($? == -1) {
442         die "$cmd: failed to execute: $!\n";
443     } elsif ($? & 127) {
444         die sprintf("$cmd: child died with signal %d, %s coredump\n",
445                     ($? & 127),  ($? & 128) ? 'with' : 'without');
446     } else {
447         die sprintf("$cmd: child exited with value %d\n", $? >> 8);
448     }
449 }
450
451 sub x509_fingerprint {
452     my ($file) = @_;
453     my $cmd = "openssl x509 -noout -in $file -fingerprint";
454     open(OPENSSL, '-|', $cmd) or die "$cmd: failed to execute: $!\n";
455     my $line = <OPENSSL>;
456     close(OPENSSL);
457     my ($fingerprint) = $line =~ /SHA1 Fingerprint=(.*)/;
458     return $line if !defined $fingerprint;
459     $fingerprint =~ s/://g;
460     return $fingerprint;
461 }
462
463 sub find_netdevs {
464     my ($netdev, %netdevs);
465     open(IFCONFIG, "/sbin/ifconfig -a|") or die "ifconfig failed: $!";
466     while (<IFCONFIG>) {
467         if (my ($nd) = /^([^\s]+)/) {
468             $netdev = $nd;
469             $netdevs{$netdev} = "$netdev";
470             if (my ($hwaddr) = /HWaddr (\S+)/) {
471                 $netdevs{$netdev} .= " (MAC: $hwaddr)";
472             }
473         } elsif (my ($ip4) = /^\s*inet addr:(\S+)/) {
474             $netdevs{$netdev} .= " (IP: $ip4)";
475         } elsif (my ($ip6) = /^\s*inet6 addr:(\S+)/) {
476             $netdevs{$netdev} .= " (IPv6: $ip6)";
477         }
478     }
479     foreach my $nd (keys(%netdevs)) {
480         delete $netdevs{$nd} if $nd eq 'lo' || $nd =~ /^wmaster/;
481     }
482     close(IFCONFIG);
483     return %netdevs;
484 }
485
486 sub load_config {
487     my ($file) = @_;
488
489     # Get the list of the variables that the shell sets automatically.
490     my (%auto_vars) = read_vars("set -a && env");
491
492     # Get the variables from $default.
493     my (%config) = read_vars("set -a && . '$default' && env");
494
495     # Subtract.
496     delete @config{keys %auto_vars};
497
498     return %config;
499 }
500
501 sub read_vars {
502     my ($cmd) = @_;
503     local @ENV;
504     if (!open(VARS, '-|', $cmd)) {
505         print STDERR "$cmd: failed to execute: $!\n";
506         return ();
507     }
508     my (%config);
509     while (<VARS>) {
510         my ($var, $value) = /^([^=]+)=(.*)$/ or next;
511         $config{$var} = $value;
512     }
513     close(VARS);
514     return %config;
515 }
516
517 sub shell_escape {
518     local $_ = $_[0];
519     if ($_ eq '') {
520         return '""';
521     } elsif (m&^[-a-zA-Z0-9:./%^_+,]*$&) {
522         return $_;
523     } else {
524         s/'/'\\''/;
525         return "'$_'";
526     }
527 }
528
529 sub shell_assign {
530     my ($var, $value) = @_;
531     return $var . '=' . shell_escape($value);
532 }
533
534 sub save_config {
535     my ($file, %config) = @_;
536     my (@lines);
537     if (open(FILE, '<', $file)) {
538         @lines = <FILE>;
539         chomp @lines;
540         close(FILE);
541     }
542
543     # Replace all existing variable assignments.
544     for (my ($i) = 0; $i <= $#lines; $i++) {
545         local $_ = $lines[$i];
546         my ($var, $value) = /^\s*([^=#]+)=(.*)$/ or next;
547         if (exists($config{$var})) {
548             $lines[$i] = shell_assign($var, $config{$var});
549             delete $config{$var};
550         } else {
551             $lines[$i] = "#$lines[$i]";
552         }
553     }
554
555     # Find a place to put any remaining variable assignments.
556   VAR:
557     for my $var (keys(%config)) {
558         my $assign = shell_assign($var, $config{$var});
559
560         # Replace the last commented-out variable assignment to $var, if any.
561         for (my ($i) = $#lines; $i >= 0; $i--) {
562             local $_ = $lines[$i];
563             if (/^\s*#\s*$var=/) {
564                 $lines[$i] = $assign;
565                 next VAR;
566             }
567         }
568
569         # Find a place to add the var: after the final commented line
570         # just after a line that contains "$var:".
571         for (my ($i) = 0; $i <= $#lines; $i++) {
572             if ($lines[$i] =~ /^\s*#\s*$var:/) {
573                 for (my ($j) = $i + 1; $j <= $#lines; $j++) {
574                     if ($lines[$j] !~ /^\s*#/) {
575                         splice(@lines, $j, 0, $assign);
576                         next VAR;
577                     }
578                 }
579             }
580         }
581
582         # Just append it.
583         push(@lines, $assign);
584     }
585
586     open(NEWFILE, '>', "$file.tmp") or die "$file.tmp: create: $!\n";
587     print NEWFILE join('', map("$_\n", @lines));
588     close(NEWFILE);
589     rename("$file.tmp", $file) or die "$file.tmp: rename to $file: $!\n";
590 }
591
592 sub pki_host_to_uri {
593     my ($pki_host) = @_;
594     return "http://$pki_host/openvswitch/pki";
595 }
596
597 sub kill_ovs_discover {
598     # Delegate this to a subprocess because there is no portable way
599     # to invoke fcntl(F_GETLK) from Perl.
600     system("ovs-kill --force $ovs_discover_pidfile");
601 }
602
603 sub netdev_names {
604     return map(/^(\S+)/, split(', ', db_get('netdevs')));
605 }
606
607 sub is_valid_vconn {
608     my ($vconn) = @_;
609     return scalar($vconn =~ /^(tcp|ssl):([^:]+)(:.*)?/);
610 }
611
612 sub is_ssl_vconn {
613     my ($vconn) = @_;
614     return scalar($vconn =~ /^ssl:/);
615 }