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