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