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