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