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