]> rtime.felk.cvut.cz Git - novaboot.git/blob - novaboot
Remove $iprelay configuration variable
[novaboot.git] / novaboot
1 #!/usr/bin/perl -w
2
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 2 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16 use strict;
17 use warnings;
18 use Getopt::Long qw(GetOptionsFromString);
19 use Pod::Usage;
20 use File::Basename;
21 use File::Spec;
22 use IO::Handle;
23 use Time::HiRes("usleep");
24 use Socket;
25 use FileHandle;
26 use IPC::Open2;
27 use POSIX qw(:errno_h);
28 use Cwd qw(getcwd abs_path);
29
30 # always flush
31 $| = 1;
32
33 my $invocation_dir = getcwd();
34
35 chomp(my $gittop = `git rev-parse --show-toplevel 2>/dev/null`);
36
37 ## Configuration file handling
38
39 # Default configuration
40 $CFG::hypervisor = "";
41 $CFG::hypervisor_params = "serial";
42 $CFG::genisoimage = "genisoimage";
43 $CFG::qemu = 'qemu';
44 @CFG::chainloaders = (); #('bin/boot/bender promisc'); # TODO: convert to option
45 $CFG::pulsar_root = '';  # TODO: convert to option
46 $CFG::pulsar_mac = '52-54-00-12-34-56';
47 %CFG::targets = (
48     "tud" => '--server=erwin.inf.tu-dresden.de:~sojka/boot/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --grub --grub-prefix=(nd)/tftpboot/sojka/novaboot --grub-preamble="timeout 0" --concat --iprelay=141.76.48.80:2324 --scriptmod=s/\\\\bhostserial\\\\b/hostserialpci/g',
49     "novabox" => '--server=rtime.felk.cvut.cz:/srv/tftp/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --pulsar=novaboot --iprelay=147.32.86.92:2324',
50     "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog="  set root=\'(hd0,msdos1)\'"',
51     );
52 $CFG::scons = "scons -j2";
53
54 my @qemu_flags = qw(-cpu coreduo -smp 2);
55 sub read_config($) {
56     my ($cfg) = @_;
57     {
58         package CFG; # Put config data into a separate namespace
59         my $rc = do($cfg);
60
61         # Check for errors
62         if ($@) {
63             die("ERROR: Failure compiling '$cfg' - $@");
64         } elsif (! defined($rc)) {
65             die("ERROR: Failure reading '$cfg' - $!");
66         } elsif (! $rc) {
67             die("ERROR: Failure processing '$cfg'");
68         }
69     }
70     print "novaboot: Read $cfg\n";
71 }
72
73 my @cfgs;
74 {
75     # We don't use $0 here, because it points to the novaboot itself and
76     # not to the novaboot script. The problem with this approach is that
77     # when a script is run as "novaboot <options> <script>" then $ARGV[0]
78     # contains the first option. Hence the -f check.
79     my $dir = abs_path($invocation_dir . ((-f $ARGV[0]) ? '/'.dirname($ARGV[0]) : ''));
80     while (-d $dir && $dir ne "/") {
81         push @cfgs, "$dir/.novaboot" if -r "$dir/.novaboot";
82         $dir = abs_path($dir."/..");
83     }
84 }
85 my $cfg = $ENV{'NOVABOOT_CONFIG'};
86 Getopt::Long::Configure(qw/no_ignore_case pass_through/);
87 GetOptions ("config|c=s" => \$cfg);
88 read_config($_) foreach $cfg or reverse @cfgs;
89
90 ## Command line handling
91
92 my ($append, $bender, $builddir, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $iprelay, $iso_image, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $qemu, $qemu_append, $qemu_flags_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server);
93
94 $rsync_flags = '';
95 $rom_prefix = 'rom://';
96
97 Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
98 my %opt_spec;
99 %opt_spec = (
100     "append|a=s"     => \$append,
101     "bender|b"       => \$bender,
102     "build-dir=s"    => \$builddir,
103     "concat"         => \$concat,
104     "dhcp-tftp|d"    => \$dhcp_tftp,
105     "dump"           => \$dump_opt,
106     "dump-config"    => \$dump_config,
107     "grub|g:s"       => \$grub_config,
108     "grub-preamble=s"=> \$grub_preamble,
109     "grub-prefix=s"  => \$grub_prefix,
110     "grub2:s"        => \$grub2_config,
111     "grub2-prolog=s" => \$grub2_prolog,
112     "iprelay=s"      => \$iprelay,
113     "iso|i:s"        => \$iso_image,
114     "name=s"         => \$config_name_opt,
115     "no-file-gen"    => \$no_file_gen,
116     "off"            => \$off_opt,
117     "on"             => \$on_opt,
118     "pulsar|p:s"     => \$pulsar,
119     "qemu|Q=s"       => \$qemu,
120     "qemu-append:s"  => \$qemu_append,
121     "qemu-flags|q=s" => \$qemu_flags_cmd,
122     "rsync-flags=s"  => \$rsync_flags,
123     "scons:s"        => \$scons,
124     "scriptmod=s"    => \@scriptmod,
125     "serial|s:s"     => \$serial,
126     "server:s"       => \$server,
127     "strip-rom"      => sub { $rom_prefix = ''; },
128     "target|t=s"     => sub { my ($opt_name, $opt_value) = @_;
129                               exists $CFG::targets{$opt_value} or die("Unknown target '$opt_value' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
130                               GetOptionsFromString($CFG::targets{$opt_value}, %opt_spec); },
131     "h"              => \$help,
132     "help"           => \$man,
133     );
134 GetOptions %opt_spec or pod2usage(2);
135 pod2usage(1) if $help;
136 pod2usage(-exitstatus => 0, -verbose => 2) if $man;
137
138 ### Sanitize configuration
139
140 if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
141
142 if ($qemu) { $CFG::qemu = $qemu; }
143 $qemu_append ||= '';
144
145 if ($pulsar) {
146     $CFG::pulsar_mac = $pulsar;
147 }
148
149 if ($scons) { $CFG::scons = $scons; }
150
151 ### Dump sanitized configuration (if requested)
152
153 if ($dump_config) {
154     use Data::Dumper;
155     $Data::Dumper::Indent=0;
156     print "# This file is in perl syntax.\n";
157     foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1)
158         if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); }
159         if (        @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); }
160         if (        %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); }
161         print "\n";
162     }
163     print "1;\n";
164     exit;
165 }
166
167 if (defined $serial) {
168     $serial ||= "/dev/ttyUSB0";
169 }
170
171 if (defined $grub_config) {
172     $grub_config ||= "menu.lst";
173 }
174
175 if (defined $grub2_config) {
176     $grub2_config ||= "grub.cfg";
177 }
178
179 ## Parse the novaboot script(s)
180 my @scripts;
181 my $file;
182 my $line;
183 my $EOF;
184 my $last_fn = '';
185 my ($modules, $variables, $generated, $continuation);
186 while (<>) {
187     if ($ARGV ne $last_fn) { # New script
188         die "Missing EOF in $last_fn" if $file;
189         die "Unfinished line in $last_fn" if $line;
190         $last_fn = $ARGV;
191         push @scripts, { 'filename' => $ARGV,
192                          'modules' => $modules = [],
193                          'variables' => $variables = {},
194                          'generated' => $generated = []};
195
196     }
197     chomp();
198     next if /^#/ || /^\s*$/;    # Skip comments and empty lines
199
200     foreach my $mod(@scriptmod) { eval $mod; }
201
202     print "$_\n" if $dump_opt;
203
204     if (/^([A-Z_]+)=(.*)$/) {   # Internal variable
205         $$variables{$1} = $2;
206         next;
207     }
208     if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
209         push @$modules, "$1$2";
210         $file = [];
211         push @$generated, {filename => $1, content => $file};
212         $EOF = $3;
213         next;
214     }
215     if ($file && $_ eq $EOF) {  # Heredoc end
216         undef $file;
217         next;
218     }
219     if ($file) {                # Heredoc content
220         push @{$file}, "$_\n";
221         next;
222     }
223     $_ =~ s/^[[:space:]]*// if ($continuation);
224     if (/\\$/) {                # Line continuation
225         $line .= substr($_, 0, length($_)-1);
226         $continuation = 1;
227         next;
228     }
229     $continuation = 0;
230     $line .= $_;
231     $line .= " $append" if ($append && scalar(@$modules) == 0);
232
233     if ($line =~ /^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
234         push @$modules, "$1$2";
235         push @$generated, {filename => $1, command => $3};
236         $line = '';
237         next;
238     }
239     push @$modules, $line;
240     $line = '';
241 }
242 #use Data::Dumper;
243 #print Dumper(\@scripts);
244
245 exit if $dump_opt;
246
247 ## Helper functions
248
249 sub generate_configs($$$) {
250     my ($base, $generated, $filename) = @_;
251     if ($base) { $base = "$base/"; };
252     foreach my $g(@$generated) {
253       if (exists $$g{content}) {
254         my $config = $$g{content};
255         my $fn = $$g{filename};
256         open(my $f, '>', $fn) || die("$fn: $!");
257         map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
258         close($f);
259         print "novaboot: Created $fn\n";
260       } elsif (exists $$g{command} && ! $no_file_gen) {
261         $ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
262         system_verbose("( $$g{command} ) > $$g{filename}");
263       }
264     }
265 }
266
267 sub generate_grub_config($$$$;$)
268 {
269     my ($filename, $title, $base, $modules_ref, $preamble) = @_;
270     if ($base) { $base = "$base/"; };
271     open(my $fg, '>', $filename) or die "$filename: $!";
272     print $fg "$preamble\n" if $preamble;
273     my $endmark = ($serial || defined $iprelay) ? ';' : '';
274     print $fg "title $title$endmark\n" if $title;
275     #print $fg "root $base\n"; # root doesn't really work for (nd)
276     my $first = 1;
277     foreach (@$modules_ref) {
278         if ($first) {
279             $first = 0;
280             my ($kbin, $kcmd) = split(' ', $_, 2);
281             $kcmd = '' if !defined $kcmd;
282             print $fg "kernel ${base}$kbin $kcmd\n";
283         } else {
284             s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
285             print $fg "module $base$_\n";
286         }
287     }
288     close($fg);
289     print("novaboot: Created $CFG::builddir/$filename\n");
290     return $filename;
291 }
292
293 sub generate_grub2_config($$$$;$$)
294 {
295     my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
296     if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
297     open(my $fg, '>', $filename) or die "$filename: $!";
298     print $fg "$preamble\n" if $preamble;
299     my $endmark = ($serial || defined $iprelay) ? ';' : '';
300     $title ||= 'novaboot';
301     print $fg "menuentry $title$endmark {\n";
302     print $fg "$prolog\n" if $prolog;
303     my $first = 1;
304     foreach (@$modules_ref) {
305         if ($first) {
306             $first = 0;
307             my ($kbin, $kcmd) = split(' ', $_, 2);
308             $kcmd = '' if !defined $kcmd;
309             print $fg "  multiboot ${base}$kbin $kcmd\n";
310         } else {
311             my @args = split;
312             # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
313             $_ = join(' ', ($args[0], @args));
314             s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
315             print $fg "  module $base$_\n";
316         }
317     }
318     print $fg "}\n";
319     close($fg);
320     print("novaboot: Created $CFG::builddir/$filename\n");
321     return $filename;
322 }
323
324 sub generate_pulsar_config($$)
325 {
326     my ($filename, $modules_ref) = @_;
327     open(my $fg, '>', $filename) or die "$filename: $!";
328     print $fg "root $CFG::pulsar_root\n" if $CFG::pulsar_root;
329     my $first = 1;
330     my ($kbin, $kcmd);
331     foreach (@$modules_ref) {
332         if ($first) {
333             $first = 0;
334             ($kbin, $kcmd) = split(' ', $_, 2);
335             $kcmd = '' if !defined $kcmd;
336         } else {
337             my @args = split;
338             s|\brom://|$rom_prefix|g;
339             print $fg "load $_\n";
340         }
341     }
342     # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
343     print $fg "exec $kbin $kcmd\n";
344     close($fg);
345     print("novaboot: Created $CFG::builddir/$filename\n");
346     return $filename;
347 }
348
349 sub exec_verbose(@)
350 {
351     print "novaboot: Running: ".join(' ', map("'$_'", @_))."\n";
352     exec(@_);
353 }
354
355 sub system_verbose($)
356 {
357     my $cmd = shift;
358     print "novaboot: Running: $cmd\n";
359     my $ret = system($cmd);
360     if ($ret & 0x007f) { die("Command terminated by a signal"); }
361     if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
362     if ($ret) { die("Command failure $ret"); }
363 }
364
365 ## WvTest handline
366
367 if (exists $variables->{WVDESC}) {
368     print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
369 } elsif ($last_fn =~ /\.wv$/) {
370     print "Testing \"all\" in $last_fn:\n";
371 }
372
373 ## Handle reset and power on/off
374
375 my $IPRELAY;
376 if (defined $iprelay) {
377     $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
378     my $addr = $1;
379     my $port = $3 || 23;
380     my $paddr   = sockaddr_in($port, inet_aton($addr));
381     my $proto   = getprotobyname('tcp');
382     socket($IPRELAY, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
383     print "novaboot: Connecting to IP relay... ";
384     connect($IPRELAY, $paddr)    || die "connect: $!";
385     print "done\n";
386     $IPRELAY->autoflush(1);
387
388     while (1) {
389         print $IPRELAY "\xFF\xF6";
390         alarm(20);
391         local $SIG{ALRM} = sub { die "Relay AYT timeout"; };
392         my $ayt_reponse = "";
393         my $read = sysread($IPRELAY, $ayt_reponse, 100);
394         alarm(0);
395
396         chomp($ayt_reponse);
397         print "$ayt_reponse\n";
398         if ($ayt_reponse =~ /<iprelayd: not connected/) {
399             sleep(10);
400             next;
401         }
402         last;
403     }
404
405     sub relaycmd($$) {
406         my ($relay, $onoff) = @_;
407         die unless ($relay == 1 || $relay == 2);
408
409         my $cmd = ($relay == 1 ? 0x5 : 0x6) | ($onoff ? 0x20 : 0x10);
410         return "\xFF\xFA\x2C\x32".chr($cmd)."\xFF\xF0";
411     }
412
413     sub relayconf($$) {
414         my ($relay, $onoff) = @_;
415         die unless ($relay == 1 || $relay == 2);
416         my $cmd = ($relay == 1 ? 0xdf : 0xbf) | ($onoff ? 0x00 : 0xff);
417         return "\xFF\xFA\x2C\x97".chr($cmd)."\xFF\xF0";
418     }
419
420     sub relay($$;$) {
421         my ($relay, $onoff, $can_giveup) = @_;
422         my $confirmation = '';
423         print $IPRELAY relaycmd($relay, $onoff);
424
425         # We use non-blocking I/O and polling here because for some
426         # reason read() on blocking FD returns only after all
427         # requested data is available. If we get during the first
428         # read() only a part of confirmation, we may get the rest
429         # after the system boots and print someting, which may be too
430         # long.
431         $IPRELAY->blocking(0);
432
433         alarm(20); # Timeout in seconds
434         my $giveup = 0;
435         local $SIG{ALRM} = sub {
436             if ($can_giveup) { print("Relay confirmation timeout - ignoring\n"); $giveup = 1;}
437             else {die "Relay confirmation timeout";}
438         };
439         my $index;
440         while (($index=index($confirmation, relayconf($relay, $onoff))) < 0 && !$giveup) {
441             my $read = read($IPRELAY, $confirmation, 70, length($confirmation));
442             if (!defined($read)) {
443                 die("IP relay: $!") unless $! == EAGAIN;
444                 usleep(10000);
445                 next;
446             }
447             #use MIME::QuotedPrint;
448             #print "confirmation = ".encode_qp($confirmation)."\n";
449         }
450         alarm(0);
451         $IPRELAY->blocking(1);
452     }
453 }
454
455 if ($iprelay && (defined $on_opt || defined $off_opt)) {
456      relay(1, 1); # Press power button
457     if (defined $on_opt) {
458         usleep(100000);         # Short press
459     } else {
460         print "novaboot: Switching the target off...\n";
461         usleep(6000000);        # Long press to switch off
462     }
463     print $IPRELAY relay(1, 0);
464     exit;
465 }
466
467 ## Figure out the location of builddir and chdir() there
468 if ($builddir) {
469     $CFG::builddir = $builddir;
470 } else {
471     if (! defined $CFG::builddir) {
472         $CFG::builddir = ( $gittop || $ENV{'HOME'}."/nul" ) . "/build";
473         if (! -d $CFG::builddir) {
474             $CFG::builddir = $ENV{SRCDIR} = dirname(File::Spec->rel2abs( ${$scripts[0]}{filename}, $invocation_dir ));
475         }
476     }
477 }
478
479 chdir($CFG::builddir) or die "Can't change directory to $CFG::builddir: $!";
480 print "novaboot: Entering directory `$CFG::builddir'\n";
481
482 ## File generation phase
483 my (%files_iso, $menu_iso, $config_name, $filename);
484
485 foreach my $script (@scripts) {
486     $filename = $$script{filename};
487     $modules = $$script{modules};
488     $generated = $$script{generated};
489     $variables = $$script{variables};
490
491     ($config_name = $filename) =~ s#.*/##;
492     $config_name = $config_name_opt if (defined $config_name_opt);
493
494     my $kernel;
495     if (exists $variables->{KERNEL}) {
496         $kernel = $variables->{KERNEL};
497     } else {
498         if ($CFG::hypervisor) {
499             $kernel = $CFG::hypervisor . " ";
500             if (exists $variables->{HYPERVISOR_PARAMS}) {
501                 $kernel .= $variables->{HYPERVISOR_PARAMS};
502             } else {
503                 $kernel .= $CFG::hypervisor_params;
504             }
505         }
506     }
507     @$modules = ($kernel, @$modules) if $kernel;
508     @$modules = (@CFG::chainloaders, @$modules);
509     @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
510
511     my $prefix;
512     ($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
513     $prefix ||= $CFG::builddir;
514     # TODO: use $grub_prefix as first parameter if some switch is given
515     generate_configs('', $generated, $filename);
516
517 ### Generate bootloader configuration files
518     my $pulsar_config;
519     my @bootloader_configs;
520     push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
521     push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
522     push @bootloader_configs, generate_pulsar_config($pulsar_config = "config-$CFG::pulsar_mac", $modules) if (defined $pulsar);
523
524 ### Run scons
525     if (defined $scons) {
526         my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
527         # Filter-out generated files
528         my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
529         system_verbose($CFG::scons." ".join(" ", @to_build));
530     }
531
532 ### Copy files (using rsync)
533     if (defined $server) {
534         (my $real_server = $server) =~ s/\$NAME/$config_name/;
535
536         my ($hostname, $path) = split(":", $real_server, 2);
537         if (! defined $path) {
538             $path = $hostname;
539             $hostname = "";
540         }
541         my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
542         map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
543         my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
544         my $progress = $istty ? "--progress" : "";
545         system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
546         if ($server =~ m|/\$NAME$| && $concat) {
547             my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
548             system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
549         }
550     }
551
552 ### Prepare ISO image generation
553     if (defined $iso_image) {
554         generate_configs("(cd)", $generated, $filename);
555         my $menu;
556         generate_grub_config(\$menu, $config_name, "(cd)", $modules);
557         $menu_iso .= "$menu\n";
558         map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
559     }
560 }
561
562 ## Generate ISO image
563 if (defined $iso_image) {
564     open(my $fh, ">menu-iso.lst");
565     print $fh "timeout 5\n\n$menu_iso";
566     close($fh);
567     my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
568     $iso_image ||= "$config_name.iso";
569     system_verbose("$CFG::genisoimage -R -b stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table -hide-rr-moved -J -joliet-long -o $iso_image -graft-points bin/boot/grub/ $files");
570     print("ISO image created: $CFG::builddir/$iso_image\n");
571 }
572
573 ## Boot the system using various methods and send serial output to stdout
574
575 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
576     die "You cannot do this with multiple scripts simultaneously";
577 }
578
579 if ($variables->{WVTEST_TIMEOUT}) {
580     print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
581 }
582
583 sub trim($) {
584     my ($str) = @_;
585     $str =~ s/^\s+|\s+$//g;
586     return $str
587 }
588
589 ### Qemu
590
591 if (!(defined $dhcp_tftp || defined $serial || defined $iprelay || defined $server || defined $iso_image)) {
592     # Qemu
593     if (!$qemu && $variables->{QEMU}) {
594         @qemu_flags = split(" ", $variables->{QEMU});
595         $CFG::qemu = shift(@qemu_flags);
596     }
597
598     @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
599     @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
600     push(@qemu_flags, split(/ +/, trim($qemu_append)));
601
602     if (defined $iso_image) {
603         # Boot NOVA with grub (and test the iso image)
604         push(@qemu_flags, ('-cdrom', "$config_name.iso"));
605     } else {
606         # Boot NOVA without GRUB
607
608         # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
609         foreach (@$modules) {s/,/+/g;}
610         generate_configs("", $generated, $filename);
611
612         my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
613         $kcmd = '' if !defined $kcmd;
614         my $dtb;
615         @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
616         my $initrd = join ",", @$modules;
617
618         push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
619         push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
620         push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
621     }
622     push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
623     exec_verbose(($CFG::qemu,  '-name', $config_name, @qemu_flags));
624 }
625
626 ### Local DHCPD and TFTPD
627
628 my ($dhcpd_pid, $tftpd_pid);
629
630 if (defined $dhcp_tftp)
631 {
632     generate_configs("(nd)", $generated, $filename);
633     system_verbose('mkdir -p tftpboot');
634     generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
635     open(my $fh, '>', 'dhcpd.conf');
636     my $mac = `cat /sys/class/net/eth0/address`;
637     chomp $mac;
638     print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
639                       range 10.23.23.10 10.23.23.100;
640                       filename \"bin/boot/grub/pxegrub.pxe\";
641                       next-server 10.23.23.1;
642 }
643 host server {
644         hardware ethernet $mac;
645         fixed-address 10.23.23.1;
646 }";
647     close($fh);
648     system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
649             sudo ip l set dev eth0 up;
650             sudo touch dhcpd.leases");
651
652     $dhcpd_pid = fork();
653     if ($dhcpd_pid == 0) {
654         # This way, the spawned server are killed when this script is killed.
655         exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid");
656     }
657     $tftpd_pid = fork();
658     if ($tftpd_pid == 0) {
659         exec_verbose("sudo in.tftpd --foreground --secure -v -v -v $CFG::builddir");
660     }
661     $SIG{TERM} = sub { print "CHILDS KILLED\n"; kill 15, $dhcpd_pid, $tftpd_pid; };
662 }
663
664 ### Serial line or IP relay
665
666 if ($serial || defined $iprelay) {
667     my $CONN;
668     if (defined $iprelay) {
669         print "novaboot: Reseting the test box... ";
670         relay(2, 1, 1); # Reset the machine
671         usleep(100000);
672         relay(2, 0);
673         print "done\n";
674
675         $CONN = $IPRELAY;
676     } elsif ($serial) {
677         system("stty -F $serial raw -crtscts -onlcr 115200");
678         open($CONN, "+<", $serial) || die "open $serial: $!";
679         $CONN->autoflush(1);
680     }
681
682     # Pass the NOVA output to stdout.
683     while (<$CONN>) {
684         print;
685     }
686     kill 15, $dhcpd_pid, $tftpd_pid if ($dhcp_tftp);
687     exit;
688 }
689
690 ### Wait for dhcpc or tftpd
691 if (defined $dhcp_tftp) {
692     my $pid = wait();
693     if ($pid == $dhcpd_pid) { print "dhcpd exited!\n"; }
694     elsif ($pid == $tftpd_pid) { print "tftpd exited!\n"; }
695     else { print "wait returned: $pid\n"; }
696     kill(15, 0); # Kill current process group i.e. all remaining children
697 }
698
699 ## Documentation
700
701 =head1 NAME
702
703 novaboot - A tool for booting various operating systems on various hardware or in qemu
704
705 =head1 SYNOPSIS
706
707 B<novaboot> [ options ] [--] script...
708
709 B<./script> [ options ]
710
711 B<novaboot> --help
712
713 =head1 DESCRIPTION
714
715 This program makes it easier to boot NOVA or other operating system
716 (OS) in different environments. It reads a so called novaboot script
717 and uses it either to boot the OS in an emulator (e.g. in qemu) or to
718 generate the configuration for a specific bootloader and optionally to
719 copy the necessary binaries and other needed files to proper
720 locations, perhaps on a remote server. In case the system is actually
721 booted, its serial output is redirected to standard output if that is
722 possible.
723
724 A typical way of using novaboot is to make the novaboot script
725 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
726 booting a particular OS configuration becomes the same as executing a
727 local program - the novaboot script.
728
729 With C<novaboot> you can:
730
731 =over 3
732
733 =item 1.
734
735 Run an OS in Qemu. This is the default action when no other action is
736 specified by command line switches. Thus running C<novaboot ./script>
737 (or C<./script> as described above) will run Qemu and make it boot the
738 configuration specified in the I<script>.
739
740 =item 2.
741
742 Create a bootloader configuration file (currently supported
743 bootloaders are GRUB, GRUB2 and Pulsar) and copy it with all other
744 files needed for booting to another, perhaps remote, location.
745
746  ./script --server --iprelay=192.168.1.2
747
748 This command copies files to a TFTP server specified in the
749 configuration file and uses TCP/IP-controlled relay to reset the test
750 box and receive its serial output.
751
752 =item 3.
753
754 Run DHCP and TFTP server on developer's machine to PXE-boot NOVA from
755 it. E.g.
756
757  ./script --dhcp-tftp
758
759 When a PXE-bootable machine is connected via Ethernet to developer's
760 machine, it will boot the configuration described in I<script>.
761
762 =item 4.
763
764 Create bootable ISO images. E.g.
765
766  novaboot --iso -- script1 script2
767
768 The created ISO image will have GRUB bootloader installed on it and
769 the boot menu will allow selecting between I<script1> and I<script2>
770 configurations.
771
772 =back
773
774 =head1 PHASES AND OPTIONS
775
776 Novaboot perform its work in several phases. Each phase can be
777 influenced by several options, certain phases can be skipped. The list
778 of phases with the corresponding options follows.
779
780 =head2 Configuration reading phase
781
782 After starting, novaboot reads configuration files. By default, it
783 searches for files named F<.novaboot> starting from the directory of
784 the novaboot script (or working directory, see bellow) and continuing
785 upwards up to the root directory. The configuration files are read in
786 order from the root directory downwards with latter files overriding
787 settings from the former ones.
788
789 In certain cases, the location of the novaboot script cannot be
790 determined in this early phase. This happens either when the script is
791 read from the standard input or when novaboot is invoked explicitly
792 and options precede the script name, as in the example L</"4."> above.
793 In this case the current working directory is used as a starting point
794 for configuration file search.
795
796 =over 8
797
798 =item -c, --config=<filename>
799
800 Use the specified configuration file instead of the default one(s).
801
802 =back
803
804 =head2 Command line processing phase
805
806 =over 8
807
808 =item --dump-config
809
810 Dump the current configuration to stdout end exits. Useful as an
811 initial template for a configuration file.
812
813 =item -h, --help
814
815 Print short (B<-h>) or long (B<--help>) help.
816
817 =item -t, --target=<target>
818
819 This option serves as a user configurable shortcut for other novaboot
820 options. The effect of this option is the same as the options stored
821 in the C<%targets> configuration variable under key I<target>. See
822 also L</"CONFIGURATION FILE">.
823
824 =back
825
826 =head2 Script preprocessing phase
827
828 This phases allows to modify the parsed novaboot script before it is
829 used in the later phases.
830
831 =over 8
832
833 =item -a, --append=<parameters>
834
835 Appends a string to the first "filename" line in the novaboot script.
836 This can be used to append parameters to the kernel's or root task's
837 command line.
838
839 =item -b, --bender
840
841 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
842 ports and stores the information about them in the BIOS data area for
843 use by the kernel.
844
845 =item --dump
846
847 Prints the content of the novaboot script after removing comments and
848 evaluating all I<--scriptmod> expressions. Exit after reading (and
849 dumping) the script.
850
851 =item --scriptmod=I<perl expression>
852
853 When novaboot script is read, I<perl expression> is executed for every
854 line (in $_ variable). For example, C<novaboot
855 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
856 in the script with I<omega6>.
857
858 When this option is present, it overrides I<$script_modifier> variable
859 from the configuration file, which has the same effect. If this option
860 is given multiple times all expressions are evaluated in the command
861 line order.
862
863 =item --strip-rom
864
865 Strip I<rom://> prefix from command lines and generated config files.
866 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
867
868 =back
869
870 =head2 File generation phase
871
872 In this phase, files needed for booting are generated in a so called
873 I<build directory> (see TODO). In most cases configuration for a
874 bootloader is generated automatically by novaboot. It is also possible
875 to generate other files using I<heredoc> or I<"<"> syntax in novaboot
876 scripts. Finally, binaries can be generated in this phases by running
877 C<scons> or C<make>.
878
879 =over 8
880
881 =item --build-dir=<directory>
882
883 Overrides the default build directory location.
884
885 The default build directory location is determined as follows:
886
887 If there is a configuration file, the value specified in the
888 I<$builddir> variable is used. Otherwise, if the current working
889 directory is inside git work tree and there is F<build> directory at
890 the top of that tree, it is used. Otherwise, if directory
891 F<~/nul/build> exists, it is used. Otherwise, it is the directory that
892 contains the first processed novaboot script.
893
894 =item -g, --grub[=I<filename>]
895
896 Generates grub bootloader menu file. If the I<filename> is not
897 specified, F<menu.lst> is used. The I<filename> is relative to the
898 build directory (see B<--build-dir>).
899
900 =item --grub-preamble=I<prefix>
901
902 Specifies the I<preable> that is at the beginning of the generated
903 GRUB or GRUB2 config files. This is useful for specifying GRUB's
904 timeout.
905
906 =item --grub-prefix=I<prefix>
907
908 Specifies I<prefix> that is put in front of every file name in GRUB's
909 F<menu.lst>. The default value is the absolute path to the build directory.
910
911 If the I<prefix> contains string $NAME, it will be replaced with the
912 name of the novaboot script (see also B<--name>).
913
914 =item --grub2[=I<filename>]
915
916 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
917 specified F<grub.cfg> is used. The content of the menuentry can be
918 customized with B<--grub-preable>, B<--grub2-prolog> or
919 B<--grub_prefix> options.
920
921 In order to use the the generated menuentry on your development
922 machine that uses GRUB2, append the following snippet to
923 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
924 i.e. run update-grub on Debian/Ubuntu.
925
926   if [ -f /path/to/nul/build/grub.cfg ]; then
927     source /path/to/nul/build/grub.cfg
928   fi
929
930 =item --grub2-prolog=I<prolog>
931
932 Specifies text I<preable> that is put at the begiging of the entry
933 GRUB2 entry.
934
935 =item --name=I<string>
936
937 Use the name I<string> instead of the name of the novaboot script.
938 This name is used for things like a title of grub menu or for the
939 server directory where the boot files are copied to.
940
941 =item --no-file-gen
942
943 Do not generate files on the fly (i.e. "<" syntax) except for the
944 files generated via "<<WORD" syntax.
945
946 =item -p, --pulsar[=mac]
947
948 Generates pulsar bootloader configuration file whose name is based on
949 the MAC address specified either on the command line or taken from
950 I<.novaboot> configuration file.
951
952 =back
953
954 =head2 File deployment phase
955
956 In some setups, it is necessary to copy the files needed for booting
957 to a particular location, e.g. to a TFTP boot server or to the
958 F</boot> partition.
959
960 =over 8
961
962 =item -d, --dhcp-tftp
963
964 Turns your workstation into a DHCP and TFTP server so that NOVA
965 can be booted via PXE BIOS on a test machine directly connected by
966 a plain Ethernet cable to your workstation.
967
968 The DHCP and TFTP servers require root privileges and C<novaboot>
969 uses C<sudo> command to obtain those. You can put the following to
970 I</etc/sudoers> to allow running the necessary commands without
971 asking for password.
972
973  Cmnd_Alias NOVABOOT = /bin/ip a add 10.23.23.1/24 dev eth0, /bin/ip l set dev eth0 up, /usr/sbin/dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid, /usr/sbin/in.tftpd --foreground --secure -v -v -v *, /usr/bin/touch dhcpd.leases
974  your_login ALL=NOPASSWD: NOVABOOT
975
976 =item -i, --iso[=filename]
977
978 Generates the ISO image that boots NOVA system via GRUB. If no filename
979 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
980 of the novaboot script (see also B<--name>).
981
982 =item --server[=[[user@]server:]path]
983
984 Copy all files needed for booting to another location (implies B<-g>
985 unless B<--grub2> is given). The files will be copied (by B<rsync>
986 tool) to the directory I<path>. If the I<path> contains string $NAME,
987 it will be replaced with the name of the novaboot script (see also
988 B<--name>).
989
990 =item --concat
991
992 If B<--server> is used and its value ends with $NAME, then after
993 copying the files, a new bootloader configuration file (e.g. menu.lst)
994 is created at I<path-wo-name>, i.e. the path specified by B<--server>
995 with $NAME part removed. The content of the file is created by
996 concatenating all files of the same name from all subdirectories of
997 I<path-wo-name> found on the "server".
998
999 =item --rsync-flags=I<flags>
1000
1001 Specifies which I<flags> are appended to F<rsync> command line when
1002 copying files as a result of I<--server> option.
1003
1004 =item --scons[=scons command]
1005
1006 Runs I<scons> to build files that are not generated by novaboot
1007 itself.
1008
1009 =back
1010
1011 =head2 Target power-on and reset phase
1012
1013 =over 8
1014
1015 =item --iprelay=I<addr[:port]>
1016
1017 Use IP relay to reset the machine and to get the serial output. The IP
1018 address of the relay is given by I<addr> parameter.
1019
1020 Note: This option is expected to work with HWG-ER02a IP relays.
1021
1022 =item --on, --off
1023
1024 Switch on/off the target machine. Currently works only with
1025 B<--iprelay>.
1026
1027 =item -Q, --qemu=I<qemu-binary>
1028
1029 The name of qemu binary to use. The default is 'qemu'.
1030
1031 =item --qemu-append=I<flags>
1032
1033 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1034 C<-cpu coreduo -smp 2>).
1035
1036 =item -q, --qemu-flags=I<flags>
1037
1038 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1039 -smp 2>) with I<flags> specified here.
1040
1041 =back
1042
1043 =head2 Interaction with the bootloader on the target
1044
1045 See B<--serial>. There will be new options soon.
1046
1047 =head2 Target's output reception phase
1048
1049 =over 8
1050
1051 =item -s, --serial[=device]
1052
1053 Use serial line to control GRUB bootloader and to see the output
1054 serial output of the machine. The default value is F</dev/ttyUSB0>.
1055
1056 =back
1057
1058 See also B<--iprelay>.
1059
1060 =head2 Termination phase
1061
1062 Daemons that were spwned (F<dhcpd> and F<tftpd>) are killed here.
1063
1064 =head1 NOVABOOT SCRIPT SYNTAX
1065
1066 The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
1067
1068 Lines starting with "#" are ignored.
1069
1070 Lines that end with "\" are concatenated with the following line after
1071 removal of the final "\" and leading whitespace of the following line.
1072
1073 Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1074 expression) assign values to internal variables. See VARIABLES
1075 section.
1076
1077 Otherwise, the first word on the line represents the filename
1078 (relative to the build directory (see B<--build-dir>) of the module to
1079 load and the remaining words are passed as the command line
1080 parameters.
1081
1082 When the line ends with "<<WORD" then the subsequent lines until the
1083 line containing only WORD are copied literally to the file named on
1084 that line.
1085
1086 When the line ends with "< CMD" the command CMD is executed with
1087 C</bin/sh> and its standard output is stored in the file named on that
1088 line. The SRCDIR variable in CMD's environment is set to the absolute
1089 path of the directory containing the interpreted novaboot script.
1090
1091 Example:
1092   #!/usr/bin/env novaboot
1093   WVDESC=Example program
1094   bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1095     verbose hostkeyb:0,0x60,1,12,2
1096   bin/apps/hello.nul
1097   hello.nulconfig <<EOF
1098   sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1099   rom://bin/apps/hello.nul
1100   EOF
1101
1102 This example will load three modules: sigma0.nul, hello.nul and
1103 hello.nulconfig. sigma0 gets some command line parameters and
1104 hello.nulconfig file is generated on the fly from the lines between
1105 <<EOF and EOF.
1106
1107 =head2 VARIABLES
1108
1109 The following variables are interpreted in the novaboot script:
1110
1111 =over 8
1112
1113 =item WVDESC
1114
1115 Description of the wvtest-compliant program.
1116
1117 =item WVTEST_TIMEOUT
1118
1119 The timeout in seconds for WvTest harness. If no complete line appears
1120 in the test output within the time specified here, the test fails. It
1121 is necessary to specify this for long running tests that produce no
1122 intermediate output.
1123
1124 =item QEMU
1125
1126 Use a specific qemu binary (can be overriden with B<-Q>) and flags
1127 when booting this script under qemu. If QEMU_FLAGS variable is also
1128 specified flags specified in QEMU variable are replaced by those in
1129 QEMU_FLAGS.
1130
1131 =item QEMU_FLAGS
1132
1133 Use specific qemu flags (can be overriden with B<-q>).
1134
1135 =item HYPERVISOR_PARAMS
1136
1137 Parameters passed to hypervisor. The default value is "serial", unless
1138 overriden in configuration file.
1139
1140 =item KERNEL
1141
1142 The kernel to use instead of NOVA hypervisor specified in the
1143 configuration file. The value should contain the name of the kernel
1144 image as well as its command line parameters. If this variable is
1145 defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
1146
1147 =back
1148
1149 =head1 CONFIGURATION FILE
1150
1151 Novaboot can read its configuration from a file. Configuration file
1152 was necessary in early days of novaboot. Nowadays, an attempt is made
1153 to not use the configuration file because it makes certain novaboot
1154 scripts unusable on systems without (or with different) configuration
1155 file. The only recommended use of the configuration file is to specify
1156 custom_options (see bellow).
1157
1158 If you decide to use the configuration file, it is looked up, by
1159 default, in files named F<.novaboot> as described in L</Configuration
1160 reading phase>. Alternatively, its location can be specified with the
1161 B<-c> switch or with the NOVABOOT_CONFIG environment variable. The
1162 configuration file has perl syntax and should set values of certain
1163 Perl variables. The current configuration can be dumped with the
1164 B<--dump-config> switch. Some configuration variables can be overriden
1165 by environment variables (see below) or by command line switches.
1166
1167 Documentation of some configuration variables follows:
1168
1169 =over 8
1170
1171 =item @chainloaders
1172
1173 Custom chainloaders to load before hypervisor and files specified in
1174 novaboot script. E.g. ('bin/boot/bender promisc', 'bin/boot/zapp').
1175
1176 =item %targets
1177
1178 Hash of shortcuts to be used with the B<--target> option. If the hash
1179 contains, for instance, the following pair of values
1180
1181  'mybox' => '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1182
1183 then the following two commands are equivalent:
1184
1185  ./script --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1186  ./script -t mybox
1187
1188 =back
1189
1190 =head1 ENVIRONMENT VARIABLES
1191
1192 Some options can be specified not only via config file or command line
1193 but also through environment variables. Environment variables override
1194 the values from configuration file and command line parameters
1195 override the environment variables.
1196
1197 =over 8
1198
1199 =item NOVABOOT_CONFIG
1200
1201 Name of the novaboot configuration file to use instead of the default
1202 one(s).
1203
1204 =item NOVABOOT_BENDER
1205
1206 Defining this variable has the same meaning as B<--bender> option.
1207
1208 =back
1209
1210 =head1 AUTHORS
1211
1212 Michal Sojka <sojka@os.inf.tu-dresden.de>