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