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