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