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