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