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