]> rtime.felk.cvut.cz Git - novaboot.git/blob - novaboot
Give more structure to the documentation
[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 (        @{$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 ## File generation phase
471 my (%files_iso, $menu_iso, $config_name, $filename);
472
473 foreach my $script (@scripts) {
474     $filename = $$script{filename};
475     $modules = $$script{modules};
476     $generated = $$script{generated};
477     $variables = $$script{variables};
478     my ($server_grub_prefix);
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 ### Generate bootloader configuration files
501     if (defined $grub_config) {
502         generate_configs("", $generated, $filename);
503         generate_grub_config($grub_config, $config_name, "", $modules);
504         print("GRUB menu created: $CFG::builddir/$grub_config\n");
505         exit;
506     }
507
508     if (defined $grub2_config && !defined $server) {
509         generate_configs('', $generated, $filename);
510         generate_grub2_config($grub2_config, $config_name, $CFG::builddir, $modules);
511         print("GRUB2 configuration created: $CFG::builddir/$grub2_config\n");
512         exit;
513     }
514
515     my $pulsar_config;
516     if (defined $pulsar) {
517         $pulsar_config = "config-$CFG::pulsar_mac";
518         generate_configs('', $generated, $filename);
519         generate_pulsar_config($pulsar_config, $modules);
520         if (!defined $server) {
521             print("Pulsar configuration created: $CFG::builddir/$pulsar_config\n");
522             exit;
523         }
524     }
525
526 ### Run scons
527     if (defined $scons) {
528         my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
529         # Filter-out generated files
530         my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
531         system_verbose($CFG::scons." ".join(" ", @to_build));
532     }
533
534 ### Copy files (using rsync)
535     if (defined $server) {
536         ($server_grub_prefix = $CFG::server_grub_prefix) =~ s/\$NAME/$config_name/;
537         ($server = $CFG::server)                         =~ s/\$NAME/$config_name/;
538         my $bootloader_config;
539         if ($grub2_config) {
540             generate_configs('', $generated, $filename);
541             $bootloader_config ||= "grub.cfg";
542             generate_grub2_config($grub2_config, $config_name, $server_grub_prefix, $modules);
543         } elsif (defined $pulsar) {
544             $bootloader_config = $pulsar_config;
545         } else {
546             generate_configs($server_grub_prefix, $generated, $filename);
547             $bootloader_config ||= "menu.lst";
548             if (!grep { $_ eq $bootloader_config } @$modules) {
549                 generate_grub_config($bootloader_config, $config_name, $server_grub_prefix, $modules,
550                                      $server_grub_prefix eq $CFG::server_grub_prefix ? "timeout 0" : undef);
551             }
552         }
553         my ($hostname, $path) = split(":", $server, 2);
554         if (! defined $path) {
555             $path = $hostname;
556             $hostname = "";
557         }
558         my $files = "$bootloader_config " . join(" ", map({ ($file) = m/([^ ]*)/; $file; } @$modules));
559         my $combined_menu_lst = ($CFG::server =~ m|/\$NAME$|);
560         map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
561         my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
562         my $progress = $istty ? "--progress" : "";
563         system_verbose("rsync $progress -RLp $rsync_flags $files $server");
564         my $cmd = "cd $path/.. && cat */menu.lst > menu.lst";
565         if ($combined_menu_lst) { system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd); }
566     }
567
568 ### Prepare ISO image generation
569     if (defined $iso_image) {
570         generate_configs("(cd)", $generated, $filename);
571         my $menu;
572         generate_grub_config(\$menu, $config_name, "(cd)", $modules);
573         $menu_iso .= "$menu\n";
574         map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
575     }
576 }
577
578 ## Generate ISO image
579 if (defined $iso_image) {
580     open(my $fh, ">menu-iso.lst");
581     print $fh "timeout 5\n\n$menu_iso";
582     close($fh);
583     my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
584     $iso_image ||= "$config_name.iso";
585     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");
586     print("ISO image created: $CFG::builddir/$iso_image\n");
587 }
588
589 ## Boot the system using various methods and send serial output to stdout
590
591 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
592     die "You cannot do this with multiple scripts simultaneously";
593 }
594
595 if ($variables->{WVTEST_TIMEOUT}) {
596     print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
597 }
598
599 sub trim($) {
600     my ($str) = @_;
601     $str =~ s/^\s+|\s+$//g;
602     return $str
603 }
604
605 ### Qemu
606
607 if (!(defined $dhcp_tftp || defined $serial || defined $iprelay || defined $server || defined $iso_image)) {
608     # Qemu
609     if (!$qemu && $variables->{QEMU}) {
610         @qemu_flags = split(" ", $variables->{QEMU});
611         $CFG::qemu = shift(@qemu_flags);
612     }
613
614     @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
615     @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
616     push(@qemu_flags, split(/ +/, trim($qemu_append)));
617
618     if (defined $iso_image) {
619         # Boot NOVA with grub (and test the iso image)
620         push(@qemu_flags, ('-cdrom', "$config_name.iso"));
621     } else {
622         # Boot NOVA without GRUB
623
624         # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
625         foreach (@$modules) {s/,/+/g;}
626         generate_configs("", $generated, $filename);
627
628         my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
629         $kcmd = '' if !defined $kcmd;
630         my $dtb;
631         @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
632         my $initrd = join ",", @$modules;
633
634         push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
635         push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
636         push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
637     }
638     push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
639     exec_verbose(($CFG::qemu,  '-name', $config_name, @qemu_flags));
640 }
641
642 ### Local DHCPD and TFTPD
643
644 my ($dhcpd_pid, $tftpd_pid);
645
646 if (defined $dhcp_tftp)
647 {
648     generate_configs("(nd)", $generated, $filename);
649     system_verbose('mkdir -p tftpboot');
650     generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
651     open(my $fh, '>', 'dhcpd.conf');
652     my $mac = `cat /sys/class/net/eth0/address`;
653     chomp $mac;
654     print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
655                       range 10.23.23.10 10.23.23.100;
656                       filename \"bin/boot/grub/pxegrub.pxe\";
657                       next-server 10.23.23.1;
658 }
659 host server {
660         hardware ethernet $mac;
661         fixed-address 10.23.23.1;
662 }";
663     close($fh);
664     system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
665             sudo ip l set dev eth0 up;
666             sudo touch dhcpd.leases");
667
668     $dhcpd_pid = fork();
669     if ($dhcpd_pid == 0) {
670         # This way, the spawned server are killed when this script is killed.
671         exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid");
672     }
673     $tftpd_pid = fork();
674     if ($tftpd_pid == 0) {
675         exec_verbose("sudo in.tftpd --foreground --secure -v -v -v $CFG::builddir");
676     }
677     $SIG{TERM} = sub { print "CHILDS KILLED\n"; kill 15, $dhcpd_pid, $tftpd_pid; };
678 }
679
680 ### Serial line or IP relay
681
682 if ($serial || defined $iprelay) {
683     my $CONN;
684     if (defined $iprelay) {
685         print "novaboot: Reseting the test box... ";
686         relay(2, 1, 1); # Reset the machine
687         usleep(100000);
688         relay(2, 0);
689         print "done\n";
690
691         $CONN = $IPRELAY;
692     } elsif ($serial) {
693         system("stty -F $serial raw -crtscts -onlcr 115200");
694         open($CONN, "+<", $serial) || die "open $serial: $!";
695         $CONN->autoflush(1);
696     }
697     if (!defined $dhcp_tftp && $CFG::grub_keys) {
698         # Control grub via serial line
699         print "Waiting for GRUB's serial output... ";
700         while (<$CONN>) {
701             if (/Press any key to continue/) { print $CONN "\n"; last; }
702         }
703         $CFG::grub_keys =~ s/\$NAME/$config_name;/;
704         my @characters = split(//, $CFG::grub_keys);
705         foreach (@characters) {
706             print $CONN $_;
707             usleep($_ eq "\n" ? 100000 : 10000);
708         }
709         print $CONN "\n";
710         print "done\n";
711     }
712     # Pass the NOVA output to stdout.
713     while (<$CONN>) {
714         print;
715     }
716     kill 15, $dhcpd_pid, $tftpd_pid if ($dhcp_tftp);
717     exit;
718 }
719
720 ### Wait for dhcpc or tftpd
721 if (defined $dhcp_tftp) {
722     my $pid = wait();
723     if ($pid == $dhcpd_pid) { print "dhcpd exited!\n"; }
724     elsif ($pid == $tftpd_pid) { print "tftpd exited!\n"; }
725     else { print "wait returned: $pid\n"; }
726     kill(15, 0); # Kill current process group i.e. all remaining children
727 }
728
729 ## Documentation
730
731 =head1 NAME
732
733 novaboot - A tool for booting various operating systems on various hardware or in qemu
734
735 =head1 SYNOPSIS
736
737 B<novaboot> [ options ] [--] script...
738
739 B<./script> [ options ]
740
741 B<novaboot> --help
742
743 =head1 DESCRIPTION
744
745 This program makes it easier to boot NOVA or other operating system
746 (OS) in different environments. It reads a so called novaboot script
747 and uses it either to boot the OS in an emulator (e.g. in qemu) or to
748 generate the configuration for a specific bootloader and optionally to
749 copy the necessary binaries and other needed files to proper
750 locations, perhaps on a remote server. In case the system is actually
751 booted, its serial output is redirected to standard output if that is
752 possible.
753
754 A typical way of using novaboot is to make the novaboot script
755 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
756 booting a particular OS configuration becomes the same as executing a
757 local program - the novaboot script.
758
759 With C<novaboot> you can:
760
761 =over 3
762
763 =item 1.
764
765 Run an OS in Qemu. This is the default action when no other action is
766 specified by command line switches. Thus running C<novaboot ./script>
767 (or C<./script> as described above) will run Qemu and make it boot the
768 configuration specified in the I<script>.
769
770 =item 2.
771
772 Create a bootloader configuration file (currently supported
773 bootloaders are GRUB, GRUB2 and Pulsar) and copy it with all other
774 files needed for booting to another, perhaps remote, location.
775
776  ./script --server --iprelay
777
778 This command copies files to a TFTP server specified in the
779 configuration file and uses TCP/IP-controlled relay to reset the test
780 box and receive its serial output.
781
782 =item 3.
783
784 Run DHCP and TFTP server on developer's machine to PXE-boot NOVA from
785 it. E.g.
786
787  ./script --dhcp-tftp
788
789 When a PXE-bootable machine is connected via Ethernet to developer's
790 machine, it will boot the configuration described in I<script>.
791
792 =item 4.
793
794 Create bootable ISO images. E.g.
795
796  novaboot --iso -- script1 script2
797
798 The created ISO image will have GRUB bootloader installed on it and
799 the boot menu will allow selecting between I<script1> and I<script2>
800 configurations.
801
802 =back
803
804 =head1 PHASES AND OPTIONS
805
806 Novaboot perform its work in several phases. Each phase can be
807 influenced by several options, certain phases can be skipped. The list
808 of phases with the corresponding options follows.
809
810 =head2 Command line processing phase
811
812 =over 8
813
814 =item -c, --config=<filename>
815
816 Use a different configuration file than the default one (i.e.
817 F<~/.novaboot>).
818
819 =item --dump-config
820
821 Dumps current configuration to stdout end exits. Useful as an initial
822 template for a configuration file.
823
824 =item -h, --help
825
826 Print short (B<-h>) or long (B<--help>) help.
827
828 =item -I
829
830 This is an alias (see C<%custom_options> below) defined in the default
831 configuration. When used, it causes novaboot to use Michal's remotely
832 controllable test bed.
833
834 =item -J
835
836 This is an alias (see C<%custom_options> below) defined in the default
837 configuration. When used, it causes novaboot to use another remotely
838 controllable test bed.
839
840 =back
841
842 =head2 Script preprocessing phase
843
844 This phases allows to modify the parsed novaboot script before it is
845 used in the later phases.
846
847 =over 8
848
849 =item -a, --append=<parameters>
850
851 Appends a string to the first "filename" line in the novaboot script.
852 This can be used to append parameters to the kernel's or root task's
853 command line.
854
855 =item -b, --bender
856
857 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
858 ports and stores the information about them in the BIOS data area for
859 use by the kernel.
860
861 =item --dump
862
863 Prints the content of the novaboot script after removing comments and
864 evaluating all I<--scriptmod> expressions. Exit after reading (and
865 dumping) the script.
866
867 =item --scriptmod=I<perl expression>
868
869 When novaboot script is read, I<perl expression> is executed for every
870 line (in $_ variable). For example, C<novaboot
871 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
872 in the script with I<omega6>.
873
874 When this option is present, it overrides I<$script_modifier> variable
875 from the configuration file, which has the same effect. If this option
876 is given multiple times all expressions are evaluated in the command
877 line order.
878
879 =item --strip-rom
880
881 Strip I<rom://> prefix from command lines and generated config files.
882 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
883
884 =back
885
886 =head2 File generation phase
887
888 In this phase, files needed for booting are generated in a so called
889 I<build directory> (see TODO). In most cases configuration for a
890 bootloader is generated automatically by novaboot. It is also possible
891 to generate other files using I<heredoc> or I<"<"> syntax in novaboot
892 scripts. Finally, binaries can be generated in this phases by running
893 C<scons> or C<make>.
894
895 =over 8
896
897 =item --build-dir=<directory>
898
899 Overrides the default build directory location.
900
901 The default build directory location is determined as follows:
902
903 If there is a configuration file, the value specified in the
904 I<$builddir> variable is used. Otherwise, if the current working
905 directory is inside git work tree and there is F<build> directory at
906 the top of that tree, it is used. Otherwise, if directory
907 F<~/nul/build> exists, it is used. Otherwise, it is the directory that
908 contains the first processed novaboot script.
909
910 =item -g, --grub[=I<filename>]
911
912 Generates grub bootloader menu file. If the I<filename> is not
913 specified, F<menu.lst> is used. The I<filename> is relative to the
914 build directory (see B<--build-dir>).
915
916 =item --grub-prefix=I<prefix>
917
918 Specifies I<prefix> that is put before every file in GRUB's F<menu.lst>.
919 This overrides the value of I<$server_grub_prefix> from the
920 configuration file.
921
922 =item --grub2[=I<filename>]
923
924 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
925 specified F<grub.cfg> is used. The content of the menuentry can be
926 customized by I<$grub2_prolog> and I<$server_grub_prefix>
927 configuration variables.
928
929 In order to use the the generated menuentry on your development
930 machine that uses GRUB2, append the following snippet to
931 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
932 i.e. run update-grub on Debian/Ubuntu.
933
934   if [ -f /path/to/nul/build/grub.cfg ]; then
935     source /path/to/nul/build/grub.cfg
936   fi
937
938 =item --name=I<string>
939
940 Use the name I<string> instead of the name of the novaboot script.
941 This name is used for things like a title of grub menu or for the
942 server directory where the boot files are copied to.
943
944 =item --no-file-gen
945
946 Do not generate files on the fly (i.e. "<" syntax) except for the
947 files generated via "<<WORD" syntax.
948
949 =item -p, --pulsar[=mac]
950
951 Generates pulsar bootloader configuration file whose name is based on
952 the MAC address specified either on the command line or taken from
953 I<.novaboot> configuration file.
954
955 =back
956
957 =head2 File deployment phase
958
959 In some setups, it is necessary to copy the files needed for booting
960 to a particular location, e.g. to a TFTP boot server or to the
961 F</boot> partition.
962
963 =over 8
964
965 =item -d, --dhcp-tftp
966
967 Turns your workstation into a DHCP and TFTP server so that NOVA
968 can be booted via PXE BIOS on a test machine directly connected by
969 a plain Ethernet cable to your workstation.
970
971 The DHCP and TFTP servers require root privileges and C<novaboot>
972 uses C<sudo> command to obtain those. You can put the following to
973 I</etc/sudoers> to allow running the necessary commands without
974 asking for password.
975
976  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
977  your_login ALL=NOPASSWD: NOVABOOT
978
979 =item -i, --iso[=filename]
980
981 Generates the ISO image that boots NOVA system via GRUB. If no filename
982 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
983 of the novaboot script (see also B<--name>).
984
985 =item --server[=[[user@]server:]path]
986
987 Copy all files needed for booting to another location (implies B<-g>
988 unless B<--grub2> is given). The files will be copied (by B<rsync>
989 tool) to the directory I<path>. If the I<path> contains string $NAME,
990 it will be replaced with the name of the novaboot script (see also
991 B<--name>).
992
993 Additionally, if $NAME is the last component of the I<path>, a file
994 named I<path>/menu.lst (with $NAME removed from the I<path>) will be
995 created on the server by concatenating all I<path>/*/menu.lst (with
996 $NAME removed from the I<path>) files found on the server.
997
998 =item --rsync-flags=I<flags>
999
1000 Specifies which I<flags> are appended to F<rsync> command line when
1001 copying files as a result of I<--server> option.
1002
1003 =item --scons[=scons command]
1004
1005 Runs I<scons> to build files that are not generated by novaboot
1006 itself.
1007
1008 =back
1009
1010 =head2 Target power-on and reset phase
1011
1012 =over 8
1013
1014 =item --iprelay[=addr or cmd]
1015
1016 If no I<cmd> is given, use IP relay to reset the machine and to get
1017 the serial output. The IP address of the relay is given by I<addr>
1018 parameter if specified or by $iprelay_addr variable in the
1019 configuration file.
1020
1021 If I<cmd> is one of "on" or "off", the IP relay is used to press power
1022 button for a short (in case of "on") or long (in case of "off") time.
1023 Then, novaboot exits.
1024
1025 Note: This option is expected to work with HWG-ER02a IP relays.
1026
1027 =item --on, --off
1028
1029 Synonym for --iprelay=on/off.
1030
1031 =item -Q, --qemu=I<qemu-binary>
1032
1033 The name of qemu binary to use. The default is 'qemu'.
1034
1035 =item --qemu-append=I<flags>
1036
1037 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1038 C<-cpu coreduo -smp 2>).
1039
1040 =item -q, --qemu-flags=I<flags>
1041
1042 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1043 -smp 2>) with I<flags> specified here.
1044
1045 =back
1046
1047 =head2 Interaction with the bootloader on the target
1048
1049 See B<--serial>. There will be new options soon.
1050
1051 =head2 Reception of target's output (e.g. console) on the host system
1052
1053 =item -s, --serial[=device]
1054
1055 Use serial line to control GRUB bootloader and to see the output
1056 serial output of the machine. The default value is F</dev/ttyUSB0>.
1057
1058 =head2 Termination phase
1059
1060 Daemons that were spwned (F<dhcpd> and F<tftpd>) are killed here.
1061
1062 =head1 NOVABOOT SCRIPT SYNTAX
1063
1064 The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
1065
1066 Lines starting with "#" are ignored.
1067
1068 Lines that end with "\" are concatenated with the following line after
1069 removal of the final "\" and leading whitespace of the following line.
1070
1071 Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1072 expression) assign values to internal variables. See VARIABLES
1073 section.
1074
1075 Otherwise, the first word on the line represents the filename
1076 (relative to the build directory (see B<--build-dir>) of the module to
1077 load and the remaining words are passed as the command line
1078 parameters.
1079
1080 When the line ends with "<<WORD" then the subsequent lines until the
1081 line containing only WORD are copied literally to the file named on
1082 that line.
1083
1084 When the line ends with "< CMD" the command CMD is executed with
1085 C</bin/sh> and its standard output is stored in the file named on that
1086 line. The SRCDIR variable in CMD's environment is set to the absolute
1087 path of the directory containing the interpreted novaboot script.
1088
1089 Example:
1090   #!/usr/bin/env novaboot
1091   WVDESC=Example program
1092   bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1093     verbose hostkeyb:0,0x60,1,12,2
1094   bin/apps/hello.nul
1095   hello.nulconfig <<EOF
1096   sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1097   rom://bin/apps/hello.nul
1098   EOF
1099
1100 This example will load three modules: sigma0.nul, hello.nul and
1101 hello.nulconfig. sigma0 gets some command line parameters and
1102 hello.nulconfig file is generated on the fly from the lines between
1103 <<EOF and EOF.
1104
1105 =head2 VARIABLES
1106
1107 The following variables are interpreted in the novaboot script:
1108
1109 =over 8
1110
1111 =item WVDESC
1112
1113 Description of the wvtest-compliant program.
1114
1115 =item WVTEST_TIMEOUT
1116
1117 The timeout in seconds for WvTest harness. If no complete line appears
1118 in the test output within the time specified here, the test fails. It
1119 is necessary to specify this for long running tests that produce no
1120 intermediate output.
1121
1122 =item QEMU
1123
1124 Use a specific qemu binary (can be overriden with B<-Q>) and flags
1125 when booting this script under qemu. If QEMU_FLAGS variable is also
1126 specified flags specified in QEMU variable are replaced by those in
1127 QEMU_FLAGS.
1128
1129 =item QEMU_FLAGS
1130
1131 Use specific qemu flags (can be overriden with B<-q>).
1132
1133 =item HYPERVISOR_PARAMS
1134
1135 Parameters passed to hypervisor. The default value is "serial", unless
1136 overriden in configuration file.
1137
1138 =item KERNEL
1139
1140 The kernel to use instead of NOVA hypervisor specified in the
1141 configuration file. The value should contain the name of the kernel
1142 image as well as its command line parameters. If this variable is
1143 defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
1144
1145 =back
1146
1147 =head1 CONFIGURATION FILE
1148
1149 Novaboot can read its configuration from a file. Configuration file
1150 was necessary in early days of novaboot. Nowadays, an attempt is made
1151 to not use the configuration file because it makes certain novaboot
1152 scripts unusable on systems without (or with different) configuration
1153 file. The only recommended use of the configuration file is to specify
1154 custom_options (see bellow).
1155
1156 If you decide to use the configuration file, its default location is
1157 ~/.novaboot, other location can be specified with the B<-c> switch or
1158 with the NOVABOOT_CONFIG environment variable. The configuration file
1159 has perl syntax and should set values of certain Perl variables. The
1160 current configuration can be dumped with the B<--dump-config> switch.
1161 Some configuration variables can be overriden by environment variables
1162 (see below) or by command line switches.
1163
1164 Documentation of some configuration variables follows:
1165
1166 =over 8
1167
1168 =item @chainloaders
1169
1170 Custom chainloaders to load before hypervisor and files specified in
1171 novaboot script. E.g. ('bin/boot/bender promisc', 'bin/boot/zapp').
1172
1173 =item %custom_options
1174
1175 Defines custom command line options that can serve as aliases for
1176 other options. E.g. 'S' => '--server=boot:/tftproot
1177 --serial=/dev/ttyUSB0'.
1178
1179 =back
1180
1181 =head1 ENVIRONMENT VARIABLES
1182
1183 Some options can be specified not only via config file or command line
1184 but also through environment variables. Environment variables override
1185 the values from configuration file and command line parameters
1186 override the environment variables.
1187
1188 =over 8
1189
1190 =item NOVABOOT_CONFIG
1191
1192 A name of default novaboot configuration file.
1193
1194 =item NOVABOOT_BENDER
1195
1196 Defining this variable has the same meaning as B<--bender> option.
1197
1198 =item NOVABOOT_IPRELAY
1199
1200 The IP address (and optionally the port) of the IP relay. This
1201 overrides $iprelay_addr variable from the configuration file.
1202
1203 =back
1204
1205 =head1 AUTHORS
1206
1207 Michal Sojka <sojka@os.inf.tu-dresden.de>