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