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