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