]> rtime.felk.cvut.cz Git - novaboot.git/blob - novaboot
698655772beaa8d1ba2514cb9d8d584a06f0e08d
[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 config(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     @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
613     @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
614     push(@qemu_flags, split(/ +/, trim($qemu_append)));
615
616     if (defined $iso_image) {
617         # Boot NOVA with grub (and test the iso image)
618         push(@qemu_flags, ('-cdrom', "$config_name.iso"));
619     } else {
620         # Boot NOVA without GRUB
621
622         # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
623         foreach (@$modules) {s/,/+/g;}
624         generate_configs("", $generated, $filename);
625
626         my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
627         $kcmd = '' if !defined $kcmd;
628         my $initrd = join ",", @$modules;
629
630         push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
631         push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
632     }
633     push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
634     exec_verbose(($CFG::qemu,  '-name', $config_name, @qemu_flags));
635 }
636
637 ### Local DHCPD and TFTPD
638
639 my ($dhcpd_pid, $tftpd_pid);
640
641 if (defined $dhcp_tftp)
642 {
643     generate_configs("(nd)", $generated, $filename);
644     system_verbose('mkdir -p tftpboot');
645     generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
646     open(my $fh, '>', 'dhcpd.conf');
647     my $mac = `cat /sys/class/net/eth0/address`;
648     chomp $mac;
649     print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
650                       range 10.23.23.10 10.23.23.100;
651                       filename \"bin/boot/grub/pxegrub.pxe\";
652                       next-server 10.23.23.1;
653 }
654 host server {
655         hardware ethernet $mac;
656         fixed-address 10.23.23.1;
657 }";
658     close($fh);
659     system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
660             sudo ip l set dev eth0 up;
661             sudo touch dhcpd.leases");
662
663     $dhcpd_pid = fork();
664     if ($dhcpd_pid == 0) {
665         # This way, the spawned server are killed when this script is killed.
666         exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid");
667     }
668     $tftpd_pid = fork();
669     if ($tftpd_pid == 0) {
670         exec_verbose("sudo in.tftpd --foreground --secure -v -v -v $CFG::builddir");
671     }
672     $SIG{TERM} = sub { print "CHILDS KILLED\n"; kill 15, $dhcpd_pid, $tftpd_pid; };
673 }
674
675 ### Serial line or IP relay
676
677 if ($serial || defined $iprelay) {
678     my $CONN;
679     if (defined $iprelay) {
680         print "novaboot: Reseting the test box... ";
681         relay(2, 1, 1); # Reset the machine
682         usleep(100000);
683         relay(2, 0);
684         print "done\n";
685
686         $CONN = $IPRELAY;
687     } elsif ($serial) {
688         system("stty -F $serial raw -crtscts -onlcr 115200");
689         open($CONN, "+<", $serial) || die "open $serial: $!";
690         $CONN->autoflush(1);
691     }
692     if (!defined $dhcp_tftp && $CFG::grub_keys) {
693         # Control grub via serial line
694         print "Waiting for GRUB's serial output... ";
695         while (<$CONN>) {
696             if (/Press any key to continue/) { print $CONN "\n"; last; }
697         }
698         $CFG::grub_keys =~ s/\$NAME/$config_name;/;
699         my @characters = split(//, $CFG::grub_keys);
700         foreach (@characters) {
701             print $CONN $_;
702             usleep($_ eq "\n" ? 100000 : 10000);
703         }
704         print $CONN "\n";
705         print "done\n";
706     }
707     # Pass the NOVA output to stdout.
708     while (<$CONN>) {
709         print;
710     }
711     kill 15, $dhcpd_pid, $tftpd_pid if ($dhcp_tftp);
712     exit;
713 }
714
715 ### Wait for dhcpc or tftpd
716 if (defined $dhcp_tftp) {
717     my $pid = wait();
718     if ($pid == $dhcpd_pid) { print "dhcpd exited!\n"; }
719     elsif ($pid == $tftpd_pid) { print "tftpd exited!\n"; }
720     else { print "wait returned: $pid\n"; }
721     kill(15, 0); # Kill current process group i.e. all remaining children
722 }
723
724 ## Documentation
725
726 =head1 NAME
727
728 novaboot - A tool for booting various operating systems on various hardware or in qemu
729
730 =head1 SYNOPSIS
731
732 B<novaboot> [ options ] [--] script...
733
734 B<./script> [ options ]
735
736 B<novaboot> --help
737
738 =head1 DESCRIPTION
739
740 This program makes it easier to boot NOVA or other operating system
741 (OS) in different environments. It reads a so called novaboot script
742 and uses it either to boot the OS in an emulator (e.g. in qemu) or to
743 generate the configuration for a specific bootloader and optionally to
744 copy the necessary binaries and other needed files to proper
745 locations, perhaps on a remote server. In case the system is actually
746 booted, its serial output is redirected to standard output if that is
747 possible.
748
749 A typical way of using novaboot is to make the novaboot script
750 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
751 booting a particular OS configuration becomes the same as executing a
752 local program - the novaboot script.
753
754 With C<novaboot> you can:
755
756 =over 3
757
758 =item 1.
759
760 Run an OS in Qemu. This is the default action when no other action is
761 specified by command line switches. Thus running C<novaboot ./script>
762 (or C<./script> as described above) will run Qemu and make it boot the
763 configuration specified in the I<script>.
764
765 =item 2.
766
767 Create a bootloader configuration file (currently supported
768 bootloaders are GRUB, GRUB2 and Pulsar) and copy it with all other
769 files needed for booting to another, perhaps remote, location.
770
771  ./script --server --iprelay
772
773 This command copies files to a TFTP server specified in the
774 configuration file and uses TCP/IP-controlled relay to reset the test
775 box and receive its serial output.
776
777 =item 3.
778
779 Run DHCP and TFTP server on developer's machine to PXE-boot NOVA from
780 it. E.g.
781
782  ./script --dhcp-tftp
783
784 When a PXE-bootable machine is connected via Ethernet to developer's
785 machine, it will boot the configuration described in I<script>.
786
787 =item 4.
788
789 Create bootable ISO images. E.g.
790
791  novaboot --iso -- script1 script2
792
793 The created ISO image will have GRUB bootloader installed on it and
794 the boot menu will allow selecting between I<script1> and I<script2>
795 configurations.
796
797 =back
798
799 =head1 OPTIONS
800
801 =over 8
802
803 =item -a, --append=<parameters>
804
805 Appends a string to the root task's command line.
806
807 =item -b, --bender
808
809 Boot bender tool before the kernel to find PCI serial ports.
810
811 =item --build-dir=<directory>
812
813 Overrides the default build directory location.
814
815 The default build directory location is determined as follows:
816
817 If there is a configuration file, the value specified in I<$builddir>
818 variable is used. Otherwise, if the current working directory is
819 inside git work tree and there is F<build> directory at the top of
820 that tree, it is used. Otherwise, if directory F<~/nul/build> exists,
821 it is used. Otherwise, it is the directory that contains the first
822 processed novaboot script.
823
824 =item -c, --config=<filename>
825
826 Use a different configuration file than the default one (i.e.
827 F<~/.novaboot>).
828
829 =item -d, --dhcp-tftp
830
831 Turns your workstation into a DHCP and TFTP server so that NOVA
832 can be booted via PXE BIOS on a test machine directly connected by
833 a plain Ethernet cable to your workstation.
834
835 The DHCP and TFTP servers require root privileges and C<novaboot>
836 uses C<sudo> command to obtain those. You can put the following to
837 I</etc/sudoers> to allow running the necessary commands without
838 asking for password.
839
840  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
841  your_login ALL=NOPASSWD: NOVABOOT
842
843 =item --dump
844
845 Prints the content of the novaboot script after removing comments and
846 evaluating all I<--scriptmod> expressions.
847
848 =item --dump-config
849
850 Dumps current configuration to stdout end exits. Useful as an initial
851 template for a configuration file.
852
853 =item -g, --grub[=I<filename>]
854
855 Generates grub menu file. If the I<filename> is not specified,
856 F<menu.lst> is used. The I<filename> is relative to NUL build
857 directory.
858
859 =item --grub-prefix=I<prefix>
860
861 Specifies I<prefix> that is put before every file in GRUB's menu.lst.
862 This overrides the value of I<$server_grub_prefix> from the
863 configuration file.
864
865 =item --grub2[=I<filename>]
866
867 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
868 specified grub.cfg is used. The content of the menuentry can be
869 customized by I<$grub2_prolog> and I<$server_grub_prefix>
870 configuration variables.
871
872 In order to use the the generated menuentry on your development
873 machine that uses GRUB2, append the following snippet to
874 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
875 i.e. run update-grub on Debian/Ubuntu.
876
877   if [ -f /path/to/nul/build/grub.cfg ]; then
878     source /path/to/nul/build/grub.cfg
879   fi
880
881
882 =item -h, --help
883
884 Print short (B<-h>) or long (B<--help>) help.
885
886 =item --iprelay[=addr or cmd]
887
888 If no I<cmd> is given, use IP relay to reset the machine and to get
889 the serial output. The IP address of the relay is given by I<addr>
890 parameter if specified or by $iprelay_addr variable in the
891 configuration file.
892
893 If I<cmd> is one of "on" or "off", the IP relay is used to press power
894 button for a short (in case of "on") or long (in case of "off") time.
895 Then, novaboot exits.
896
897 Note: This option is expected to work with HWG-ER02a IP relays.
898
899 =item -i, --iso[=filename]
900
901 Generates the ISO image that boots NOVA system via GRUB. If no filename
902 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
903 of novaboot script (see also B<--name>).
904
905 =item -I
906
907 This is an alias (see C<%custom_options> below) defined in the default
908 configuration. When used, it causes novaboot to use Michal's remotely
909 controllable test bed.
910
911 =item -J
912
913 This is an alias (see C<%custom_options> below) defined in the default
914 configuration. When used, it causes novaboot to use another remotely
915 controllable test bed.
916
917 =item --on, --off
918
919 Synonym for --iprelay=on/off.
920
921 =item --name=I<string>
922
923 Use the name I<string> instead of the name of the novaboot script.
924 This name is used for things like a title of grub menu or for the
925 server directory where the boot files are copied to.
926
927 =item --no-file-gen
928
929 Do not generate files on the fly (i.e. "<" syntax) except for the
930 files generated via "<<WORD" syntax.
931
932 =item -p, --pulsar[=mac]
933
934 Generates pulsar bootloader configuration file whose name is based on
935 the MAC address specified either on the command line or taken from
936 I<.novaboot> configuration file.
937
938 =item -Q, --qemu=I<qemu-binary>
939
940 Use specific version of qemu binary. The default is 'qemu'.
941
942 =item --qemu-append=I<flags>
943
944 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
945 C<-cpu coreduo -smp 2>).
946
947 =item -q, --qemu-flags=I<flags>
948
949 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
950 -smp 2>) with I<flags> specified here.
951
952 =item --rsync-flags=I<flags>
953
954 Specifies which I<flags> are appended to F<rsync> command line when
955 copying files as a result of I<--server> option.
956
957 =item --scons[=scons command]
958
959 Runs I<scons> to build files that are not generated by novaboot
960 itself.
961
962 =item --scriptmod=I<perl expression>
963
964 When novaboot script is read, I<perl expression> is executed for every
965 line (in $_ variable). For example, C<novaboot
966 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
967 in the script with I<omega6>.
968
969 When this option is present, it overrides I<$script_modifier> variable
970 from the configuration file, which has the same effect. If this option
971 is given multiple times all expressions are evaluated in the command
972 line order.
973
974 =item --server[=[[user@]server:]path]
975
976 Copy all files needed for booting to a server (implies B<-g> unless
977 B<--grub2> is given). The files will be copied to the directory
978 I<path>. If the I<path> contains string $NAME, it will be replaced
979 with the name of the novaboot script (see also B<--name>).
980
981 Additionally, if $NAME is the last component of the I<path>, a file
982 named I<path>/menu.lst (with $NAME removed from the I<path>) will be
983 created on the server by concatenating all I<path>/*/menu.lst (with
984 $NAME removed from the I<path>) files found on the server.
985
986 =item -s, --serial[=device]
987
988 Use serial line to control GRUB bootloader and to get the serial
989 output of the machine. The default value is /dev/ttyUSB0.
990
991 =item --strip-rom
992
993 Strip I<rom://> prefix from command lines and generated config files.
994 This is needed for NRE.
995
996 =back
997
998 =head1 NOVABOOT SCRIPT SYNTAX
999
1000 The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
1001
1002 Lines starting with "#" are ignored.
1003
1004 Lines that end with "\" are concatenated with the following line after
1005 removal of the final "\" and leading whitespace of the following line.
1006
1007 Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1008 expression) assign values to internal variables. See VARIABLES
1009 section.
1010
1011 Otherwise, the first word on the line represents the filename
1012 (relative to the build directory (see I<--build-dir>) of the module to
1013 load and the remaining words are passed as the command line
1014 parameters.
1015
1016 When the line ends with "<<WORD" then the subsequent lines until the
1017 line containing only WORD are copied literally to the file named on
1018 that line.
1019
1020 When the line ends with "< CMD" the command CMD is executed with
1021 C</bin/sh> and its standard output is stored in the file named on that
1022 line. The SRCDIR variable in CMD's environment is set to the absolute
1023 path of the directory containing the interpreted novaboot script.
1024
1025 Example:
1026   #!/usr/bin/env novaboot
1027   WVDESC=Example program
1028   bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1029     verbose hostkeyb:0,0x60,1,12,2
1030   bin/apps/hello.nul
1031   hello.nulconfig <<EOF
1032   sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1033   rom://bin/apps/hello.nul
1034   EOF
1035
1036 This example will load three modules: sigma0.nul, hello.nul and
1037 hello.nulconfig. sigma0 gets some command line parameters and
1038 hello.nulconfig file is generated on the fly from the lines between
1039 <<EOF and EOF.
1040
1041 =head2 VARIABLES
1042
1043 The following variables are interpreted in the novaboot script:
1044
1045 =over 8
1046
1047 =item WVDESC
1048
1049 Description of the wvtest-compliant program.
1050
1051 =item WVTEST_TIMEOUT
1052
1053 The timeout in seconds for WvTest harness. If no complete line appears
1054 in the test output within the time specified here, the test fails. It
1055 is necessary to specify this for long running tests that produce no
1056 intermediate output.
1057
1058 =item QEMU_FLAGS
1059
1060 Use specific qemu flags (can be overriden by B<-q>).
1061
1062 =item HYPERVISOR_PARAMS
1063
1064 Parameters passed to hypervisor. The default value is "serial", unless
1065 overriden in configuration file.
1066
1067 =item KERNEL
1068
1069 The kernel to use instead of NOVA hypervisor specified in the
1070 configuration file. The value should contain the name of the kernel
1071 image as well as its command line parameters. If this variable is
1072 defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
1073
1074 =back
1075
1076 =head1 CONFIGURATION FILE
1077
1078 novaboot can read its configuration from ~/.novaboot file (or another
1079 file specified with B<-c> parameter or NOVABOOT_CONFIG environment
1080 variable). It is a file with perl syntax, which sets values of certain
1081 variables. The current configuration can be dumped with
1082 B<--dump-config> switch. Use
1083
1084     novaboot --dump-config > ~/.novaboot
1085
1086 to create a default configuration file and modify it to your needs.
1087 Some configuration variables can be overriden by environment variables
1088 (see below) or by command line switches.
1089
1090 Documentation of some configuration variables follows:
1091
1092 =over 8
1093
1094 =item @chainloaders
1095
1096 Custom chainloaders to load before hypervisor and files specified in
1097 novaboot script. E.g. ('bin/boot/bender promisc', 'bin/boot/zapp').
1098
1099 =item %custom_options
1100
1101 Defines custom command line options that can serve as aliases for
1102 other options. E.g. 'S' => '--server=boot:/tftproot
1103 --serial=/dev/ttyUSB0'.
1104
1105 =back
1106
1107 =head1 ENVIRONMENT VARIABLES
1108
1109 Some options can be specified not only via config file or command line
1110 but also through environment variables. Environment variables override
1111 the values from configuration file and command line parameters
1112 override the environment variables.
1113
1114 =over 8
1115
1116 =item NOVABOOT_CONFIG
1117
1118 A name of default novaboot configuration file.
1119
1120 =item NOVABOOT_BENDER
1121
1122 Defining this variable has the same meaning as B<--bender> option.
1123
1124 =item NOVABOOT_IPRELAY
1125
1126 The IP address (and optionally the port) of the IP relay. This
1127 overrides $iprelay_addr variable from the configuration file.
1128
1129 =back
1130
1131 =head1 AUTHORS
1132
1133 Michal Sojka <sojka@os.inf.tu-dresden.de>