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