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