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