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