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