]> rtime.felk.cvut.cz Git - novaboot.git/blob - novaboot
Implement user configuration file
[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 GetOptionsFromArray);
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 = $ENV{PWD} || 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-system-i386 -cpu coreduo -smp 2';
44 $CFG::default_target = 'qemu';
45 %CFG::targets = (
46     'qemu' => '--qemu',
47     "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',
48     "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',
49     "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog="  set root=\'(hd0,msdos1)\'"',
50     "ryu" =>  '--uboot --uboot-init="mw f0000b00 \${psc_cfg}; sleep 1" --uboot-addr kernel=800000 --uboot-addr ramdisk=b00000 --uboot-addr fdt=7f0000',
51     "ryuglab" => '--target ryu --server=pc-sojkam.felk.cvut.cz:/srv/tftp --remote-cmd="ssh -tt pc-sojkam.felk.cvut.cz \"sterm -d -s 115200 /dev/ttyUSB0\""',
52     "ryulocal" => '--target ryu --dhcp-tftp --serial --reset-cmd="if which dtrrts; then dtrrts $NB_SERIAL 0 1; sleep 0.1; dtrrts $NB_SERIAL 1 1; fi"',
53     );
54 chomp(my $nproc = `nproc`);
55 $CFG::scons = "scons -j$nproc";
56 $CFG::make = "make -j$nproc";
57
58 my $builddir;
59
60 sub read_config($) {
61     my ($cfg) = @_;
62     {
63         package CFG; # Put config data into a separate namespace
64
65         my $rc = do($cfg);
66
67         # Check for errors
68         if ($@) {
69             die("ERROR: Failure compiling '$cfg' - $@");
70         } elsif (! defined($rc)) {
71             die("ERROR: Failure reading '$cfg' - $!");
72         } elsif (! $rc) {
73             die("ERROR: Failure processing '$cfg'");
74         }
75     }
76     $builddir = File::Spec->rel2abs($CFG::builddir, dirname($cfg)) if defined $CFG::builddir;
77     print STDERR "novaboot: Read $cfg\n";
78 }
79
80 my @cfgs;
81 {
82     # We don't use $0 here, because it points to the novaboot itself and
83     # not to the novaboot script. The problem with this approach is that
84     # when a script is run as "novaboot <options> <script>" then $ARGV[0]
85     # contains the first option. Hence the -f check.
86     my $dir = File::Spec->rel2abs($ARGV[0] && -f $ARGV[0] ? dirname($ARGV[0]) : '', $invocation_dir);
87     while ((-d $dir || -l $dir ) && $dir ne "/") {
88         push @cfgs, "$dir/.novaboot" if -r "$dir/.novaboot";
89         my @dirs = File::Spec->splitdir($dir);
90         $dir = File::Spec->catdir(@dirs[0..$#dirs-1]);
91     }
92     @cfgs = reverse @cfgs;
93
94     my $xdg_config_home = $ENV{'XDG_CONFIG_HOME'} || $ENV{'HOME'}.'/.config';
95     unshift(@cfgs, "$xdg_config_home/novaboot") if -r "$xdg_config_home/novaboot";
96
97     $dir = $ENV{'NOVABOOT_CONFIG_DIR'} || '/etc/novaboot.d';
98     if (opendir(my $dh, $dir)) {
99         my @etccfg = map { "$dir/$_" } grep { /^[-_a-zA-Z0-9]+$/ && -f "$dir/$_" } readdir($dh);
100         closedir $dh;
101         @etccfg = sort(@etccfg);
102         @cfgs = ( @etccfg, @cfgs );
103     }
104 }
105 my $cfg = $ENV{'NOVABOOT_CONFIG'};
106 Getopt::Long::Configure(qw/no_ignore_case pass_through/);
107 GetOptions ("config|c=s" => \$cfg);
108 read_config($_) foreach $cfg or @cfgs;
109
110 ## Command line handling
111
112 my $explicit_target;
113 GetOptions ("target|t=s" => \$explicit_target);
114
115 my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $exiton_timeout, @expect_raw, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $ider, $iprelay, $iso_image, $interactive, $kernel_opt, $make, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $pulsar_root, $qemu, $qemu_append, $qemu_flags_cmd, $remote_cmd, $remote_expect, $reset, $reset_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, $uboot_cmd, @uboot_init);
116
117 %uboot_addr = (
118     'kernel'  => '${kernel_addr_r}',
119     'ramdisk' => '${ramdisk_addr_r}',
120     'fdt'     => '${fdt_addr_r}',
121     );
122
123 $rsync_flags = '';
124 $rom_prefix = 'rom://';
125 $stty = 'raw -crtscts -onlcr 115200';
126 $reset = 1;                     # Reset target by default
127
128 my @expect_seen = ();
129 sub handle_expect
130 {
131     my ($n, $v) = @_;
132     push(@expect_seen, '-re') if $n eq "expect-re";
133     push(@expect_seen, $v);
134 }
135
136 sub handle_send
137 {
138     my ($n, $v) = @_;
139     unless (@expect_seen) { die("No --expect before --send"); }
140     my $ret = ($n eq "sendcont") ? exp_continue : 0;
141     unshift(@expect_raw, sub { shift->send(eval("\"$v\"")); $ret; });
142     unshift(@expect_raw, @expect_seen);
143     @expect_seen = ();
144 }
145
146 my %opt_spec;
147 %opt_spec = (
148     "amt=s"          => \$amt,
149     "append|a=s"     => \@append,
150     "bender|b"       => \$bender,
151     "build-dir=s"    => sub { my ($n, $v) = @_; $builddir = File::Spec->rel2abs($v); },
152     "concat"         => \$concat,
153     "chainloader=s"  => \@chainloaders,
154     "dhcp-tftp|d"    => \$dhcp_tftp,
155     "dump"           => \$dump_opt,
156     "dump-config"    => \$dump_config,
157     "exiton=s"       => \@exiton,
158     "exiton-timeout=i"=> \$exiton_timeout,
159     "exiton-re=s"    => sub { my ($n, $v) = @_; push(@exiton, '-re', $v); },
160     "expect=s"       => \&handle_expect,
161     "expect-re=s"    => \&handle_expect,
162     "expect-raw=s"   => sub { my ($n, $v) = @_; unshift(@expect_raw, eval($v)); },
163     "gen-only"       => \$gen_only,
164     "grub|g:s"       => \$grub_config,
165     "grub-preamble=s"=> \$grub_preamble,
166     "prefix|grub-prefix=s" => \$grub_prefix,
167     "grub2:s"        => \$grub2_config,
168     "grub2-prolog=s" => \$grub2_prolog,
169     "ider"           => \$ider,
170     "iprelay=s"      => \$iprelay,
171     "iso:s"          => \$iso_image,
172     "kernel|k=s"     => \$kernel_opt,
173     "interactive|i"  => \$interactive,
174     "name=s"         => \$config_name_opt,
175     "make|m:s"       => \$make,
176     "no-file-gen"    => \$no_file_gen,
177     "off"            => \$off_opt,
178     "on"             => \$on_opt,
179     "pulsar|p:s"     => \$pulsar,
180     "pulsar-root=s"  => \$pulsar_root,
181     "qemu|Q:s"       => \$qemu,
182     "qemu-append=s"  => \$qemu_append,
183     "qemu-flags|q=s" => \$qemu_flags_cmd,
184     "remote-cmd=s"   => \$remote_cmd,
185     "remote-expect=s"=> \$remote_expect,
186     "reset!"         => \$reset,
187     "reset-cmd=s"    => \$reset_cmd,
188     "rsync-flags=s"  => \$rsync_flags,
189     "scons:s"        => \$scons,
190     "scriptmod=s"    => \@scriptmod,
191     "send=s"         => \&handle_send,
192     "sendcont=s"     => \&handle_send,
193     "serial|s:s"     => \$serial,
194     "server:s"       => \$server,
195     "strip-rom"      => sub { $rom_prefix = ''; },
196     "stty=s"         => \$stty,
197     "tftp"           => \$tftp,
198     "tftp-port=i"    => \$tftp_port,
199     "uboot:s"        => \$uboot,
200     "uboot-addr=s"   => \%uboot_addr,
201     "uboot-cmd=s"    => \$uboot_cmd,
202     "uboot-init=s"   => \@uboot_init,
203     "h"              => \$help,
204     "help"           => \$man,
205     );
206
207 # First process target options
208 {
209     my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
210     my @target_expanded;
211     Getopt::Long::Configure(qw/no_ignore_case pass_through/);
212     while ($t) {
213         exists $CFG::targets{$t} or die("Unknown target '$t' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
214
215         undef $explicit_target;
216         my ($ret, $remaining_args) = GetOptionsFromString ($CFG::targets{$t}, ("target|t=s" => \$explicit_target));
217         if (!$ret) { die "Error parsing target $t option"; }
218         push(@target_expanded, @$remaining_args);
219         $t = $explicit_target;
220     }
221     Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
222     GetOptionsFromArray(\@target_expanded, %opt_spec) or die ("Error in target definition");
223 }
224
225 # Then process other command line options - some of them may override
226 # what was specified by the target
227 GetOptions %opt_spec or die("Error in command line arguments");
228 pod2usage(1) if $help;
229 pod2usage(-exitstatus => 0, -verbose => 2) if $man;
230
231 ### Dump sanitized configuration (if requested)
232
233 if ($dump_config) {
234     use Data::Dumper;
235     $Data::Dumper::Indent=1;
236     print "# This file is in perl syntax.\n";
237     foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1)
238         if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); }
239         if (        @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); }
240         if (        %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); }
241     }
242     print "1;\n";
243     exit;
244 }
245
246 ### Sanitize configuration
247
248 if ($interactive && !-t STDIN) {
249     die("novaboot: Interactive mode not supported when not on terminal");
250 }
251
252 if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
253
254 if ($ider) {
255     $iso_image //= ''; # IDE-R needs an ISO image
256     if (!defined $amt) { die "Error: --ider requires --amt"; }
257 }
258
259 # Default options
260 if (defined $serial) {
261     $serial ||= "/dev/ttyUSB0";
262     $ENV{NB_SERIAL} = $serial;
263 }
264 if (defined $grub_config) { $grub_config ||= "menu.lst"; }
265 if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
266
267 ## Parse the novaboot script(s)
268 my @scripts;
269 my $file;
270 my $EOF;
271 my $last_fn = '';
272 my ($modules, $variables, $generated, $continuation) = ([], {}, []);
273 my $skip_reading = defined($on_opt) || defined($off_opt);
274 while (!$skip_reading && ($_ = <>)) {
275     if ($ARGV ne $last_fn) { # New script
276         die "Missing EOF in $last_fn" if $file;
277         die "Unfinished line in $last_fn" if $continuation;
278         $last_fn = $ARGV;
279         push @scripts, { 'filename' => $ARGV,
280                          'modules' => $modules = [],
281                          'variables' => $variables = {},
282                          'generated' => $generated = []};
283
284     }
285     chomp();
286     next if /^#/ || /^\s*$/;    # Skip comments and empty lines
287
288     $_ =~ s/^[[:space:]]*// if ($continuation);
289
290     if (/\\$/) {                # Line continuation
291         $continuation .= substr($_, 0, length($_)-1);
292         next;
293     }
294
295     if ($continuation) {        # Last continuation line
296         $_ = $continuation . $_;
297         $continuation = '';
298     }
299
300     foreach my $mod(@scriptmod) { eval $mod; }
301
302     if ($file && $_ eq $EOF) {  # Heredoc end
303         undef $file;
304         next;
305     }
306     if ($file) {                # Heredoc content
307         push @{$file}, "$_\n";
308         next;
309     }
310     if (/^([A-Z_]+)=(.*)$/) {   # Internal variable
311         $$variables{$1} = $2;
312         push(@exiton, $2) if ($1 eq "EXITON");
313         next;
314     }
315     if (s/^load *//) {          # Load line
316         die("novaboot: '$last_fn' line $.: Missing file name\n") unless /^[^ <]+/;
317         if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
318             push @$modules, "$1$2";
319             $file = [];
320             push @$generated, {filename => $1, content => $file};
321             $EOF = $3;
322             next;
323         }
324         if (/^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
325             push @$modules, "$1$2";
326             push @$generated, {filename => $1, command => $3};
327             next;
328         }
329         push @$modules, $_;
330         next;
331     }
332     if (/^run (.*)/) {          # run line
333         push @$generated, {command => $1};
334         next;
335     }
336     if (/^uboot(?::([0-9]+)s)? (.*)/) { # uboot line
337         # TODO: If U-Boot supports some interactive menu, it might
338         # make sense to store uboot lines per novaboot script.
339         if ($1) {               # Command with explicit timeout
340             push @uboot_init, { command => $2,
341                                 timeout => $1 };
342         } else {                # Command without explicit timeout
343             push @uboot_init, $2;
344         }
345         next;
346     }
347
348     die("novaboot: Cannot parse script '$last_fn' line $.. Didn't you forget 'load' keyword?\n");
349 }
350 # use Data::Dumper;
351 # print Dumper(\@scripts);
352
353 foreach my $script (@scripts) {
354     $modules = $$script{modules};
355     @$modules[0] =~ s/^[^ ]*/$kernel_opt/ if $kernel_opt;
356     @$modules[0] .= ' ' . join(' ', @append) if @append;
357
358     my $kernel;
359     if (exists $variables->{KERNEL}) {
360         $kernel = $variables->{KERNEL};
361     } else {
362         if ($CFG::hypervisor) {
363             $kernel = $CFG::hypervisor . " ";
364             if (exists $variables->{HYPERVISOR_PARAMS}) {
365                 $kernel .= $variables->{HYPERVISOR_PARAMS};
366             } else {
367                 $kernel .= $CFG::hypervisor_params;
368             }
369         }
370     }
371     @$modules = ($kernel, @$modules) if $kernel;
372     @$modules = (@chainloaders, @$modules);
373     @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
374 }
375
376 if ($dump_opt) {
377     foreach my $script (@scripts) {
378         print join("\n", @{$$script{modules}})."\n";
379     }
380     exit(0);
381 }
382
383 ## Helper functions
384
385 sub generate_configs($$$) {
386     my ($base, $generated, $filename) = @_;
387     if ($base) { $base = "$base/"; };
388     foreach my $g(@$generated) {
389       if (exists $$g{content}) {
390         my $config = $$g{content};
391         my $fn = $$g{filename};
392         open(my $f, '>', $fn) || die("$fn: $!");
393         map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
394         close($f);
395         print "novaboot: Created $fn\n";
396       } elsif (exists $$g{command} && ! $no_file_gen) {
397         $ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
398         if (exists $$g{filename}) {
399             system_verbose("( $$g{command} ) > $$g{filename}");
400         } else {
401             system_verbose($$g{command});
402         }
403       }
404     }
405 }
406
407 sub generate_grub_config($$$$;$)
408 {
409     my ($filename, $title, $base, $modules_ref, $preamble) = @_;
410     if ($base) { $base = "$base/"; };
411     open(my $fg, '>', $filename) or die "$filename: $!";
412     print $fg "$preamble\n" if $preamble;
413     print $fg "title $title\n" if $title;
414     #print $fg "root $base\n"; # root doesn't really work for (nd)
415     my $first = 1;
416     foreach (@$modules_ref) {
417         if ($first) {
418             $first = 0;
419             my ($kbin, $kcmd) = split(' ', $_, 2);
420             $kcmd = '' if !defined $kcmd;
421             print $fg "kernel ${base}$kbin $kcmd\n";
422         } else {
423             s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
424             print $fg "module $base$_\n";
425         }
426     }
427     close($fg);
428     print("novaboot: Created $builddir/$filename\n");
429     return $filename;
430 }
431
432 sub generate_syslinux_config($$$$)
433 {
434     my ($filename, $title, $base, $modules_ref) = @_;
435     if ($base && $base !~ /\/$/) { $base = "$base/"; };
436     open(my $fg, '>', $filename) or die "$filename: $!";
437     print $fg "LABEL $title\n";
438     #TODO print $fg "MENU LABEL $human_readable_title\n";
439
440     my ($kbin, $kcmd) = split(' ', @$modules_ref[0], 2);
441
442     if (system("file $kbin|grep 'Linux kernel'") == 0) {
443         my $initrd = @$modules_ref[1];
444         die('To many "load" lines for Linux kernel') if (scalar @$modules_ref > 2);
445         print $fg "LINUX $base$kbin\n";
446         print $fg "APPEND $kcmd\n";
447         print $fg "INITRD $base$initrd\n";
448     } else {
449         print $fg "KERNEL mboot.c32\n";
450         my @append;
451         foreach (@$modules_ref) {
452             s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
453             push @append, "$base$_";
454             print $fg "APPEND ".join(' --- ', @append)."\n";
455         }
456     }
457     #TODO print $fg "TEXT HELP\n";
458     #TODO print $fg "some help here\n";
459     #TODO print $fg "ENDTEXT\n";
460     close($fg);
461     print("novaboot: Created $builddir/$filename\n");
462     return $filename;
463 }
464
465 sub generate_grub2_config($$$$;$$)
466 {
467     my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
468     if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
469     open(my $fg, '>', $filename) or die "$filename: $!";
470     print $fg "$preamble\n" if $preamble;
471     $title ||= 'novaboot';
472     print $fg "menuentry $title {\n";
473     print $fg "$prolog\n" if $prolog;
474     my $first = 1;
475     foreach (@$modules_ref) {
476         if ($first) {
477             $first = 0;
478             my ($kbin, $kcmd) = split(' ', $_, 2);
479             $kcmd = '' if !defined $kcmd;
480             print $fg "  multiboot ${base}$kbin $kcmd\n";
481         } else {
482             my @args = split;
483             # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
484             $_ = join(' ', ($args[0], @args));
485             s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
486             print $fg "  module $base$_\n";
487         }
488     }
489     print $fg "}\n";
490     close($fg);
491     print("novaboot: Created $builddir/$filename\n");
492     return $filename;
493 }
494
495 sub generate_pulsar_config($$)
496 {
497     my ($filename, $modules_ref) = @_;
498     open(my $fg, '>', $filename) or die "$filename: $!";
499     print $fg "root $pulsar_root\n" if defined $pulsar_root;
500     my $first = 1;
501     my ($kbin, $kcmd);
502     foreach (@$modules_ref) {
503         if ($first) {
504             $first = 0;
505             ($kbin, $kcmd) = split(' ', $_, 2);
506             $kcmd = '' if !defined $kcmd;
507         } else {
508             my @args = split;
509             s|\brom://|$rom_prefix|g;
510             print $fg "load $_\n";
511         }
512     }
513     # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
514     print $fg "exec $kbin $kcmd\n";
515     close($fg);
516     print("novaboot: Created $builddir/$filename\n");
517     return $filename;
518 }
519
520 sub shell_cmd_string(@)
521 {
522     return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
523 }
524
525 sub exec_verbose(@)
526 {
527     print "novaboot: Running: ".shell_cmd_string(@_)."\n";
528     exec(@_);
529     exit(1); # should not be reached
530 }
531
532 sub system_verbose($)
533 {
534     my $cmd = shift;
535     print "novaboot: Running: $cmd\n";
536     my $ret = system($cmd);
537     if ($ret & 0x007f) { die("Command terminated by a signal"); }
538     if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
539     if ($ret) { die("Command failure $ret"); }
540 }
541
542 ## WvTest headline
543
544 if (exists $variables->{WVDESC}) {
545     print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
546 } elsif ($last_fn =~ /\.wv$/) {
547     print "Testing \"all\" in $last_fn:\n";
548 }
549
550 ## Connect to the target and check whether it is not occupied
551
552 # We have to do this before file generation phase, because file
553 # generation is intermixed with file deployment phase and we want to
554 # check whether the target is not used by somebody else before
555 # deploying files. Otherwise, we may rewrite other user's files on a
556 # boot server.
557
558 my $exp; # Expect object to communicate with the target over serial line
559
560 my ($target_reset, $target_power_on, $target_power_off);
561 my ($amt_user, $amt_password, $amt_host, $amt_port);
562
563 if (defined $iprelay) {
564     my $IPRELAY;
565     $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
566     my $addr = $1;
567     my $port = $3 || 23;
568     my $paddr   = sockaddr_in($port, inet_aton($addr));
569     my $proto   = getprotobyname('tcp');
570     socket($IPRELAY, PF_INET, SOCK_STREAM, $proto)  || die "socket: $!";
571     print "novaboot: Connecting to IP relay... ";
572     connect($IPRELAY, $paddr)    || die "connect: $!";
573     print "done\n";
574     $exp = Expect->init(\*$IPRELAY);
575     $exp->log_stdout(1);
576
577     while (1) {
578         print $exp "\xFF\xF6";  # AYT
579         my $connected = $exp->expect(20, # Timeout in seconds
580                                      '<iprelayd: connected>',
581                                      '-re', '<WEB51 HW[^>]*>');
582         last if $connected;
583     }
584
585     sub relaycmd($$) {
586         my ($relay, $onoff) = @_;
587         die unless ($relay == 1 || $relay == 2);
588
589         my $cmd = ($relay == 1 ? 0x5 : 0x6) | ($onoff ? 0x20 : 0x10);
590         return "\xFF\xFA\x2C\x32".chr($cmd)."\xFF\xF0";
591     }
592
593     sub relayconf($$) {
594         my ($relay, $onoff) = @_;
595         die unless ($relay == 1 || $relay == 2);
596         my $cmd = ($relay == 1 ? 0xdf : 0xbf) | ($onoff ? 0x00 : 0xff);
597         return "\xFF\xFA\x2C\x97".chr($cmd)."\xFF\xF0";
598     }
599
600     sub relay($$;$) {
601         my ($relay, $onoff, $can_giveup) = @_;
602         my $confirmation = '';
603         $exp->log_stdout(0);
604         print $exp relaycmd($relay, $onoff);
605         my $confirmed = $exp->expect(20, # Timeout in seconds
606                                      relayconf($relay, $onoff));
607         if (!$confirmed) {
608             if ($can_giveup) {
609                 print("Relay confirmation timeout - ignoring\n");
610             } else {
611                 die "Relay confirmation timeout";
612             }
613         }
614         $exp->log_stdout(1);
615     }
616
617     $target_reset = sub {
618         relay(2, 1, 1); # Reset the machine
619         usleep(100000);
620         relay(2, 0);
621     };
622
623     $target_power_off = sub {
624         relay(1, 1);            # Press power button
625         usleep(6000000);        # Long press to switch off
626         relay(1, 0);
627     };
628
629     $target_power_on = sub {
630         relay(1, 1);            # Press power button
631         usleep(100000);         # Short press
632         relay(1, 0);
633     };
634 }
635 elsif ($serial) {
636     my $CONN;
637     system_verbose("stty -F $serial $stty");
638     open($CONN, "+<", $serial) || die "open $serial: $!";
639     $exp = Expect->init(\*$CONN);
640 }
641 elsif ($remote_cmd) {
642     print "novaboot: Running: $remote_cmd\n";
643     $exp = Expect->spawn($remote_cmd);
644 }
645 elsif (defined $amt) {
646     require LWP::UserAgent;
647     require LWP::Authen::Digest;
648
649     sub genXML {
650         my ($host, $username, $password, $schema, $className, $pstate) = @_;
651         #AMT numbers for PowerStateChange (MNI => bluescreen on windows;-)
652         my %pstates = ("on"        => 2,
653                        "standby"   => 4,
654                        "hibernate" => 7,
655                        "off"       => 8,
656                        "reset"     => 10,
657                        "MNI"       => 11);
658         return <<END;
659                 <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
660                 <s:Header><a:To>http://$host:16992/wsman</a:To>
661                 <w:ResourceURI s:mustUnderstand="true">$schema</w:ResourceURI>
662                 <a:ReplyTo><a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address></a:ReplyTo>
663                 <a:Action s:mustUnderstand="true">$schema$className</a:Action>
664                 <w:MaxEnvelopeSize s:mustUnderstand="true">153600</w:MaxEnvelopeSize>
665                 <a:MessageID>uuid:709072C9-609C-4B43-B301-075004043C7C</a:MessageID>
666                 <w:Locale xml:lang="en-US" s:mustUnderstand="false" />
667                 <w:OperationTimeout>PT60.000S</w:OperationTimeout>
668                 <w:SelectorSet><w:Selector Name="Name">Intel(r) AMT Power Management Service</w:Selector></w:SelectorSet>
669                 </s:Header><s:Body>
670                 <p:RequestPowerStateChange_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService">
671                 <p:PowerState>$pstates{$pstate}</p:PowerState>
672                 <p:ManagedElement><a:Address>http://$host:16992/wsman</a:Address>
673                 <a:ReferenceParameters><w:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</w:ResourceURI>
674                 <w:SelectorSet><w:Selector Name="Name">ManagedSystem</w:Selector></w:SelectorSet>
675                 </a:ReferenceParameters></p:ManagedElement>
676                 </p:RequestPowerStateChange_INPUT>
677                 </s:Body></s:Envelope>
678 END
679     }
680
681     sub sendPOST {
682         my ($host, $username, $password, $content) = @_;
683
684         my $ua = LWP::UserAgent->new();
685         $ua->agent("novaboot");
686
687         my $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
688         my $res = $ua->request($req);
689         die ("Unexpected AMT response: " . $res->status_line) unless $res->code == 401;
690
691         my ($realm) = $res->header("WWW-Authenticate") =~ /Digest realm="(.*?)"/;
692         $ua->credentials("$host:16992", $realm, $username => $password);
693
694         # Create a request
695         $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
696         $req->content_type('application/x-www-form-urlencoded');
697         $req->content($content);
698         $res = $ua->request($req);
699         die ("AMT power change request failed: " . $res->status_line) unless $res->is_success;
700         $res->content() =~ /<g:ReturnValue>(\d+)<\/g:ReturnValue>/;
701         return $1;
702     }
703
704     sub powerChange  {
705         my ($host, $username, $password, $pstate)=@_;
706         my $schema="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService";
707         my $className="/RequestPowerStateChange";
708         my $content = genXML($host, $username, $password ,$schema, $className, $pstate);
709         return sendPOST($host, $username, $password, $content);
710     }
711
712     ($amt_user,$amt_password,$amt_host,$amt_port) = ($amt =~ /(?:(.*?)(?::(.*))?@)?([^:]*)(?::([0-9]*))?/);;
713     $amt_user ||= "admin";
714     $amt_password ||= $ENV{'AMT_PASSWORD'} || die "AMT password not specified";
715     $amt_host || die "AMT host not specified";
716     $amt_port ||= 16994;
717
718
719     $target_power_off = sub {
720         $exp->close();
721         my $result = powerChange($amt_host,$amt_user,$amt_password, "off");
722         die "AMT power off failed (ReturnValue $result)" if $result != 0;
723     };
724
725     $target_power_on = sub {
726         my $result = powerChange($amt_host,$amt_user,$amt_password, "on");
727         die "AMT power on failed (ReturnValue $result)" if $result != 0;
728     };
729
730     $target_reset = sub {
731         my $result = powerChange($amt_host,$amt_user,$amt_password, "reset");
732         if ($result != 0) {
733             print STDERR "Warning: Cannot reset $amt_host, trying power on. ";
734             $result = powerChange($amt_host,$amt_user,$amt_password, "on");
735         }
736         die "AMT reset failed (ReturnValue $result)" if $result != 0;
737     };
738
739     my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port";
740     print "novaboot: Running: $cmd\n" =~ s/\Q$amt_password\E/???/r;
741     $exp = Expect->spawn($cmd);
742     $exp->expect(10, "RUN_SOL") || die "Expect for 'RUN_SOL' timed out";
743
744 }
745
746
747 if ($remote_expect) {
748     $exp || die("No serial line connection");
749     $exp->expect(180, $remote_expect) || die "Expect for '$remote_expect' timed out";
750 }
751
752 if (defined $reset_cmd) {
753     $target_reset = sub {
754         system_verbose($reset_cmd);
755     };
756 }
757
758 if (defined $on_opt && defined $target_power_on) {
759     &$target_power_on();
760     exit;
761 }
762 if (defined $off_opt && defined $target_power_off) {
763     print "novaboot: Switching the target off...\n";
764     &$target_power_off();
765     exit;
766 }
767
768 $builddir ||= dirname(File::Spec->rel2abs( ${$scripts[0]}{filename})) if scalar @scripts;
769 if (defined $builddir) {
770     chdir($builddir) or die "Can't change directory to $builddir: $!";
771     print "novaboot: Entering directory `$builddir'\n";
772 } else {
773     $builddir = $invocation_dir;
774 }
775
776 ## File generation phase
777 my (%files_iso, $menu_iso, $filename);
778 my $config_name = '';
779 my $prefix = '';
780
781 foreach my $script (@scripts) {
782     $filename = $$script{filename};
783     $modules = $$script{modules};
784     $generated = $$script{generated};
785     $variables = $$script{variables};
786
787     ($config_name = $filename) =~ s#.*/##;
788     $config_name = $config_name_opt if (defined $config_name_opt);
789
790     if (exists $variables->{BUILDDIR}) {
791         $builddir = File::Spec->rel2abs($variables->{BUILDDIR});
792         chdir($builddir) or die "Can't change directory to $builddir: $!";
793         print "novaboot: Entering directory `$builddir'\n";
794     }
795
796     if ($grub_prefix) {
797         $prefix = $grub_prefix;
798         $prefix =~ s/\$NAME/$config_name/;
799         $prefix =~ s/\$BUILDDIR/$builddir/;
800     }
801     # TODO: use $grub_prefix as first parameter if some switch is given
802     generate_configs('', $generated, $filename);
803
804 ### Generate bootloader configuration files
805     my @bootloader_configs;
806     push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
807     push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
808     push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
809
810 ### Run scons or make
811     {
812         my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
813         # Filter-out generated files
814         my @to_build = grep({ my $file = $_; !scalar(grep($file eq ($$_{filename} || ''), @$generated)) } @files);
815
816         system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
817         system_verbose($make  || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
818     }
819
820 ### Copy files (using rsync)
821     if (defined $server && !defined($gen_only)) {
822         (my $real_server = $server) =~ s/\$NAME/$config_name/;
823
824         my ($hostname, $path) = split(":", $real_server, 2);
825         if (! defined $path) {
826             $path = $hostname;
827             $hostname = "";
828         }
829         my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
830         map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
831         my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
832         my $progress = $istty ? "--progress" : "";
833         system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
834         if ($server =~ m|/\$NAME$| && $concat) {
835             my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
836             system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
837         }
838     }
839
840 ### Prepare ISO image generation
841     if (defined $iso_image) {
842         generate_configs("(cd)", $generated, $filename);
843         my $menu;
844         generate_syslinux_config(\$menu, $config_name, "/", $modules);
845         $menu_iso .= "$menu\n";
846         map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
847     }
848 }
849
850 ## Generate ISO image
851 if (defined $iso_image) {
852     system_verbose("mkdir -p isolinux");
853
854     my @files;
855     if (-f '/usr/lib/ISOLINUX/isolinux.bin') {
856         # Newer ISOLINUX version
857         @files = qw(/usr/lib/ISOLINUX/isolinux.bin /usr/lib/syslinux/modules/bios/mboot.c32 /usr/lib/syslinux/modules/bios/libcom32.c32 /usr/lib/syslinux/modules/bios/menu.c32 /usr/lib/syslinux/modules/bios/ldlinux.c32);
858     } else {
859         # Older ISOLINUX version
860         @files = qw(/usr/lib/syslinux/isolinux.bin /usr/lib/syslinux/mboot.c32 /usr/lib/syslinux/menu.c32);
861     }
862     system_verbose("cp @files isolinux");
863     open(my $fh, ">isolinux/isolinux.cfg");
864     if ($#scripts) {
865         print $fh "TIMEOUT 50\n";
866         print $fh "DEFAULT menu\n";
867     } else {
868         print $fh "DEFAULT $config_name\n";
869     }
870     print $fh "$menu_iso";
871     close($fh);
872
873     my $files = join(" ", map("$_=$_", (keys(%files_iso), 'isolinux/isolinux.cfg', map(s|.*/|isolinux/|r, @files))));
874     $iso_image ||= "$config_name.iso";
875
876     # Note: We use -U flag below to "Allow 'untranslated' filenames,
877     # completely violating the ISO9660 standards". Without this
878     # option, isolinux is not able to read files names for example
879     # bzImage-3.0.
880     system_verbose("$CFG::genisoimage -R -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -hide-rr-moved -U -o $iso_image -graft-points $files");
881     print("ISO image created: $builddir/$iso_image\n");
882 }
883
884 exit(0) if defined $gen_only;
885
886 ## Boot the system using various methods and send serial output to stdout
887
888 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
889     die "You cannot do this with multiple scripts simultaneously";
890 }
891
892 if ($variables->{WVTEST_TIMEOUT}) {
893     print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
894 }
895
896 sub trim($) {
897     my ($str) = @_;
898     $str =~ s/^\s+|\s+$//g;
899     return $str
900 }
901
902 ### Start in Qemu
903
904 if (defined $qemu) {
905     # Qemu
906     $qemu ||= $variables->{QEMU} || $CFG::qemu;
907     my @qemu_flags = split(" ", $qemu);
908     $qemu = shift(@qemu_flags);
909
910     @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
911     @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
912     push(@qemu_flags, split(/ +/, trim($qemu_append || '')));
913
914     if (defined $iso_image) {
915         # Boot NOVA with grub (and test the iso image)
916         push(@qemu_flags, ('-cdrom', $iso_image));
917     } else {
918         # Boot NOVA without GRUB
919
920         # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
921         foreach (@$modules) {s/,/+/g;}
922         generate_configs("", $generated, $filename);
923
924         if (scalar @$modules) {
925             my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
926             $kcmd = '' if !defined $kcmd;
927             my $dtb;
928             @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
929             my $initrd = join ",", @$modules;
930
931             push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
932             push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
933             push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
934         }
935     }
936     push(@qemu_flags,  qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
937     unshift(@qemu_flags, ('-name', $config_name));
938     print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
939     $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
940 }
941
942 ### Local DHCPD and TFTPD
943
944 my ($dhcpd_pid, $tftpd_pid);
945
946 $tftp=1 if $tftp_port;
947
948 if (defined $dhcp_tftp)
949 {
950     generate_configs("(nd)", $generated, $filename);
951     system_verbose('mkdir -p tftpboot');
952     generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
953     open(my $fh, '>', 'dhcpd.conf');
954     my $mac = `cat /sys/class/net/eth0/address`;
955     chomp $mac;
956     print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
957                       range 10.23.23.10 10.23.23.100;
958                       filename \"bin/boot/grub/pxegrub.pxe\";
959                       next-server 10.23.23.1;
960 }
961 host server {
962         hardware ethernet $mac;
963         fixed-address 10.23.23.1;
964 }";
965     close($fh);
966     system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
967             sudo ip l set dev eth0 up;
968             sudo touch dhcpd.leases");
969
970     # We run servers by forking ourselves, because the servers end up
971     # in our process group and get killed by signals sent to the
972     # process group (e.g. Ctrl-C on terminal).
973     $dhcpd_pid = fork();
974     exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
975 }
976
977 if (defined $dhcp_tftp || defined $tftp) {
978     $tftp_port ||= 69;
979     # Unfortunately, tftpd requires root privileges even with
980     # non-privileged (>1023) port due to initgroups().
981     system_verbose("sudo in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid  --address :$tftp_port $builddir");
982
983     # Kill server when we die
984     $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid') if (defined $dhcp_tftp);
985                           system_verbose('sudo pkill --pidfile=tftpd.pid'); };
986
987     # We have to kill tftpd explicitely, because it is not in our process group
988     $SIG{INT} = sub { system_verbose('sudo pkill --pidfile=tftpd.pid'); exit(0); };
989 }
990
991 ### AMT IDE-R
992 if (defined $ider) {
993     my $ider_cmd= "amtider -c $iso_image -u $amt_user -p $amt_password $amt_host $amt_port"  ;
994     print "novaboot: Running: $ider_cmd\n" =~ s/\Q$amt_password\E/???/r;
995     my $ider_pid = fork();
996     if ($ider_pid == 0) {
997         exec($ider_cmd);
998         die "IDE redirection failed";
999     }
1000     # FIXME: This collides with --tftp option. Hopefully, nobody needs
1001     # to use both simultaneously.
1002     $SIG{__DIE__} = sub { system_verbose('kill $ider_pid'); };
1003 }
1004
1005 ### Reset target (IP relay, AMT, ...)
1006
1007 if (defined $target_reset && $reset) {
1008     print "novaboot: Reseting the test box... ";
1009     &$target_reset();
1010     print "done\n";
1011 }
1012
1013 ### U-boot conversation
1014 if (defined $uboot) {
1015     my $uboot_prompt = $uboot || '=> ';
1016     print "novaboot: Waiting for U-Boot prompt...\n";
1017     $exp || die("No serial line connection");
1018     $exp->log_stdout(1);
1019     #$exp->exp_internal(1);
1020     $exp->expect(20,
1021                  [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
1022                  $uboot_prompt) || die "No U-Boot prompt deteceted";
1023     foreach my $cmdspec (@uboot_init) {
1024         my ($cmd, $timeout);
1025         if (ref($cmdspec) eq "HASH") {
1026             $cmd = $cmdspec->{command};
1027             $timeout = $cmdspec->{timeout};
1028         } else {
1029             $cmd = $cmdspec;
1030             $timeout = 10;
1031         }
1032         if ($cmd =~ /\$NB_MYIP/) {
1033             my $ip = (grep /inet /, `ip addr show eth0`)[0] || die "Problem determining our IP address";
1034             $ip =~ s/\s*inet ([0-9.]*).*/$1/;
1035             $cmd =~ s/\$NB_MYIP/$ip/g;
1036         }
1037         if ($cmd =~ /\$NB_PREFIX/) {
1038             my $p = $prefix;
1039             $p =~ s|/*$||;
1040             $cmd =~ s/\$NB_PREFIX/$p/g;
1041         }
1042         chomp($cmd);
1043         $exp->send("$cmd\n");
1044         $exp->expect($timeout, $uboot_prompt) || die "U-Boot prompt timeout";
1045     }
1046
1047     # Boot the system if there are some load lines in the script
1048     if (scalar(@$modules) > 0 && !$variables->{NO_BOOT}) {
1049         my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
1050         my $dtb;
1051         @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
1052         my $initrd = shift @$modules;
1053
1054         die "No '--uboot-addr kernel' given" unless $uboot_addr{kernel};
1055         $exp->send("tftpboot $uboot_addr{kernel} $prefix$kbin\n");
1056         $exp->expect(10,
1057                      [qr/##/, sub { exp_continue; }],
1058                      $uboot_prompt) || die "Kernel load timeout";
1059         if (defined $dtb) {
1060             die "No '--uboot-addr fdt' given" unless $uboot_addr{fdt};
1061             $exp->send("tftpboot $uboot_addr{fdt} $prefix$dtb\n");
1062             $exp->expect(10,
1063                          [qr/##/, sub { exp_continue; }],
1064                          $uboot_prompt) || die "Device tree load timeout";
1065         }
1066         if (defined $initrd) {
1067             die "No '--uboot-addr ramdisk' given" unless $uboot_addr{ramdisk};
1068             $exp->send("tftpboot $uboot_addr{ramdisk} $prefix$initrd\n");
1069             $exp->expect(10,
1070                          [qr/##/, sub { exp_continue; }],
1071                          $uboot_prompt) || die "Initrd load timeout";
1072         } else {
1073             $uboot_addr{ramdisk} = '-';
1074         }
1075
1076         $kcmd //= '';
1077         $exp->send("setenv bootargs $kcmd\n");
1078         $exp->expect(5, $uboot_prompt)  || die "U-Boot prompt timeout";
1079
1080         $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
1081         $uboot_cmd =~ s/\$kernel_addr/$uboot_addr{kernel}/g;
1082         $uboot_cmd =~ s/\$ramdisk_addr/$uboot_addr{ramdisk}/g;
1083         $uboot_cmd =~ s/\$fdt_addr/$uboot_addr{fdt}/g;
1084
1085         $exp->send($uboot_cmd . "\n");
1086         $exp->expect(5, "\n")  || die "U-Boot command timeout";
1087     }
1088 }
1089
1090 ### Serial line interaction
1091 if (defined $exp) {
1092     # Serial line of the target is available
1093     my $interrupt = 'Ctrl-C';
1094     if ($interactive && !@exiton) {
1095         $interrupt = '"~~."';
1096     }
1097     my $note = (-t STDIN) ? '' : '- only target->host ';
1098     print "novaboot: Serial line interaction $note(press $interrupt to interrupt)...\n";
1099     $exp->log_stdout(1);
1100     if (@exiton) {
1101         $exp->expect($exiton_timeout, @expect_raw, @exiton) || die("exiton timeout");
1102         print "\n";
1103     } else {
1104         my @inputs = ($exp);
1105         if (-t STDIN) { # Set up bi-directional communication if we run on terminal
1106             my $infile = new IO::File;
1107             $infile->IO::File::fdopen(*STDIN,'r');
1108             my $in_object = Expect->exp_init($infile);
1109             $in_object->set_group($exp);
1110
1111             if ($interactive) {
1112                 $in_object->set_seq('~~\.', sub { print "novaboot: Escape sequence detected\r\n"; undef; });
1113                 $in_object->manual_stty(0);       # Use raw terminal mode
1114             } else {
1115                 $in_object->manual_stty(1);       # Do not modify terminal settings
1116             }
1117             push(@inputs, $in_object);
1118         }
1119         #use Data::Dumper;
1120         #print Dumper(\@expect_raw);
1121         $exp->expect(undef, @expect_raw) if @expect_raw;
1122         Expect::interconnect(@inputs) unless defined($exp->exitstatus);
1123     }
1124 }
1125
1126 ## Kill dhcpc or tftpd
1127 if (defined $dhcp_tftp || defined $tftp) {
1128     die("novaboot: This should kill servers on background\n");
1129 }
1130
1131 ## Documentation
1132
1133 =head1 NAME
1134
1135 novaboot - Boots a locally compiled operating system on a remote
1136 target or in qemu
1137
1138 =head1 SYNOPSIS
1139
1140 B<novaboot> --help
1141
1142 B<novaboot> [option]... [--] script...
1143
1144 B<./script> [option]...
1145
1146 =head1 DESCRIPTION
1147
1148 This program makes booting of a locally compiled operating system (OS)
1149 (e.g. NOVA or Linux) on remote targets as simple as running a program
1150 locally. It automates things like copying OS images to a TFTP server,
1151 generation of bootloader configuration files, resetting of target
1152 hardware or redirection of target's serial line to stdin/out. Novaboot
1153 is highly configurable and it makes it easy to boot a single image on
1154 different targets or different images on a single target.
1155
1156 Novaboot operation is controlled by command line options and by a so
1157 called novaboot script, which can be thought as a generalization of
1158 bootloader configuration files (see L</"NOVABOOT SCRIPT SYNTAX">).
1159 Typical way of using novaboot is to make the novaboot script
1160 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
1161 booting a particular OS configuration becomes the same as executing a
1162 local program - the novaboot script.
1163
1164 Novaboot uses configuration files to, among other things, define
1165 command line options needed for different targets. Users typically use
1166 only the B<-t>/B<--target> command line option to select the target.
1167 Internally, this option expands to the pre-configured options.
1168 Configuration files are searched at multiple places, which allows to
1169 have per-system, per-user or per-project configurations. Configuration
1170 file syntax is described in section L</"CONFIGURATION FILE">.
1171
1172 Simple examples of using C<novaboot>:
1173
1174 =over 3
1175
1176 =item 1.
1177
1178 Run an OS in Qemu. This is the default action when no other action is
1179 specified by command line switches. Thus running C<novaboot myos> (or
1180 C<./myos> as described above) will run Qemu and make it boot the
1181 configuration specified in the F<myos> script.
1182
1183 =item 2.
1184
1185 Create a bootloader configuration file (currently supported
1186 bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar and U-Boot) and copy it
1187 with all other files needed for booting to a remote boot server. Then
1188 use a TCP/IP-controlled relay/serial-to-TCP converter to reset the
1189 target and receive its serial output.
1190
1191  ./myos --grub2 --server=192.168.1.1:/tftp --iprelay=192.168.1.2
1192
1193 =item 3.
1194
1195 Run DHCP and TFTP server on developer's machine to boot the target
1196 from it.
1197
1198  ./myos --dhcp-tftp
1199
1200 This is useful when no network infrastructure is in place and
1201 the target is connected directly to developer's box.
1202
1203 =item 4.
1204
1205 Create bootable ISO image.
1206
1207  novaboot --iso -- script1 script2
1208
1209 The created ISO image will have ISOLINUX bootloader installed on it
1210 and the boot menu will allow selecting between I<script1> and
1211 I<script2> configurations.
1212
1213 =back
1214
1215 =head1 PHASES AND OPTIONS
1216
1217 Novaboot performs its work in several phases. Each phase can be
1218 influenced by several command line options, certain phases can be
1219 skipped. The list of phases (in the execution order) and the
1220 corresponding options follows.
1221
1222 =head2 Configuration reading phase
1223
1224 After starting, novaboot reads configuration files. Their content is
1225 described in section L</"CONFIGURATION FILE">. By default,
1226 configuration is read from multiple locations. First from the system
1227 configuration directory, second from the user configuration file and
1228 third from F<.novaboot> files along the path to the current directory.
1229 Alternatively, a single configuration file specified with the B<-c>
1230 switch or with the C<NOVABOOT_CONFIG> environment variable. The latter
1231 read files override settings from the former ones.
1232
1233 The system configuration directory is determined by the content of
1234 NOVABOOT_CONFIG_DIR environment variable and defaults to
1235 F</etc/novaboot.d>. Files in this directory with names consisting
1236 solely of English letters, numbers, dashes '-' and underscores '_'
1237 (note that dot '.' is not included) are read in alphabetical order.
1238
1239 Then, the user configuration file is read from
1240 F<$XDG_CONFIG_HOME/novaboot>. If C<$XDG_CONFIG_HOME> environemnt
1241 variable is not set F<~/.config/novaboot> is read instead.
1242
1243 Finally, novaboot searches for files named F<.novaboot> starting from the
1244 directory of the novaboot script (or working directory, see bellow)
1245 and continuing upwards up to the root directory. The found
1246 configuration files are then read in the opposite order (i.e. from the
1247 root directory downwards). This allows to have, for example, a project
1248 specific configuration in F<~/project/.novaboot>.
1249
1250 Note the difference between F<~/.config/novaboot> and F<~/.novaboot>.
1251 The former one is read always, whereas the latter only when novaboot
1252 script or working directory is under the C<$HOME> directory.
1253
1254 In certain cases, the location of the novaboot script cannot be
1255 determined in this early phase. This happens either when the script is
1256 read from the standard input or when novaboot is invoked explicitly as
1257 in the example L</"4."> above. In this case the current working
1258 directory is used as a starting point for configuration file search
1259 instead of the novaboot script directory.
1260
1261 =over 8
1262
1263 =item -c, --config=I<filename>
1264
1265 Use the specified configuration file instead of the default one(s).
1266
1267 =back
1268
1269 =head2 Command line processing phase
1270
1271 =over 8
1272
1273 =item --dump-config
1274
1275 Dump the current configuration to stdout end exit. Useful as an
1276 initial template for a configuration file.
1277
1278 =item -h, --help
1279
1280 Print short (B<-h>) or long (B<--help>) help.
1281
1282 =item -t, --target=I<target>
1283
1284 This option serves as a user configurable shortcut for other novaboot
1285 options. The effect of this option is the same as the options stored
1286 in the C<%targets> configuration variable under key I<target>. See
1287 also L</"CONFIGURATION FILE">.
1288
1289 =back
1290
1291 =head2 Script preprocessing phase
1292
1293 This phases allows to modify the parsed novaboot script before it is
1294 used in the later phases.
1295
1296 =over 8
1297
1298 =item -a, --append=I<parameters>
1299
1300 Append a string to the first C<load> line in the novaboot script. This
1301 can be used to append parameters to the kernel's or root task's
1302 command line. This option can appear multiple times.
1303
1304 =item -b, --bender
1305
1306 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
1307 ports and stores the information about them in the BIOS data area for
1308 use by the kernel.
1309
1310 =item --chainloader=I<chainloader>
1311
1312 Specifies a chainloader that is loaded before the kernel and other
1313 files specified in the novaboot script. E.g. 'bin/boot/bender
1314 promisc'.
1315
1316 =item --dump
1317
1318 Print the modules to boot and their parameters after this phase
1319 finishes. Then exit. This is useful for seeing the effect of other
1320 options in this section.
1321
1322 =item -k, --kernel=F<file>
1323
1324 Replace the first word on the first C<load> line in the novaboot
1325 script with F<file>.
1326
1327 =item --scriptmod=I<perl expression>
1328
1329 When novaboot script is read, I<perl expression> is executed for every
1330 line (in $_ variable). For example, C<novaboot
1331 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
1332 in the script with I<omega6>.
1333
1334 When this option is present, it overrides I<$script_modifier> variable
1335 from the configuration file, which has the same effect. If this option
1336 is given multiple times all expressions are evaluated in the command
1337 line order.
1338
1339 =back
1340
1341 =head2 File generation phase
1342
1343 In this phase, files needed for booting are generated in a so called
1344 I<build directory> (see L</--build-dir>). In most cases configuration
1345 for a bootloader is generated automatically by novaboot. It is also
1346 possible to generate other files using I<heredoc> or I<"<"> syntax in
1347 novaboot scripts. Finally, binaries can be generated in this phases by
1348 running C<scons> or C<make>.
1349
1350 =over 8
1351
1352 =item --build-dir=I<directory>
1353
1354 Overrides the default build directory location.
1355
1356 The default build directory location is determined as follows: If the
1357 configuration file defines the C<$builddir> variable, its value is
1358 used. Otherwise, it is the directory that contains the first processed
1359 novaboot script.
1360
1361 See also L</BUILDDIR> variable.
1362
1363 =item -g, --grub[=I<filename>]
1364
1365 Generates grub bootloader menu file. If the I<filename> is not
1366 specified, F<menu.lst> is used. The I<filename> is relative to the
1367 build directory (see B<--build-dir>).
1368
1369 =item --grub-preamble=I<prefix>
1370
1371 Specifies the I<preable> that is at the beginning of the generated
1372 GRUB or GRUB2 config files. This is useful for specifying GRUB's
1373 timeout.
1374
1375 =item --prefix=I<prefix>
1376
1377 Specifies I<prefix> (e.g. F</srv/tftp>) that is put in front of every
1378 file name in generated bootloader configuration files (or in U-Boot
1379 commands).
1380
1381 If the I<prefix> contains string $NAME, it will be replaced with the
1382 name of the novaboot script (see also B<--name>).
1383
1384 If the I<prefix> contains string $BUILDDIR, it will be replaced with
1385 the build directory (see also B<--build-dir>).
1386
1387 =item --grub-prefix
1388
1389 Alias for B<--prefix>.
1390
1391 =item --grub2[=I<filename>]
1392
1393 Generate GRUB2 menu entry in I<filename>. If I<filename> is not
1394 specified F<grub.cfg> is used. The content of the menu entry can be
1395 customized with B<--grub-preamble>, B<--grub2-prolog> or
1396 B<--grub_prefix> options.
1397
1398 In order to use the the generated menu entry on your development
1399 machine that uses GRUB2, append the following snippet to
1400 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
1401 i.e. run update-grub on Debian/Ubuntu.
1402
1403   if [ -f /path/to/nul/build/grub.cfg ]; then
1404     source /path/to/nul/build/grub.cfg
1405   fi
1406
1407 =item --grub2-prolog=I<prolog>
1408
1409 Specifies text that is put at the beginning of the GRUB2 menu entry.
1410
1411 =item -m, --make[=make command]
1412
1413 Runs C<make> to build files that are not generated by novaboot itself.
1414
1415 =item --name=I<string>
1416
1417 Use the name I<string> instead of the name of the novaboot script.
1418 This name is used for things like a title of grub menu or for the
1419 server directory where the boot files are copied to.
1420
1421 =item --no-file-gen
1422
1423 Do not run external commands to generate files (i.e. "<" syntax and
1424 C<run> keyword). This switch does not influence generation of files
1425 specified with "<<WORD" syntax.
1426
1427 =item -p, --pulsar[=mac]
1428
1429 Generates pulsar bootloader configuration file named F<config-I<mac>>
1430 The I<mac> string is typically a MAC address and defaults to
1431 I<novaboot>.
1432
1433 =item --scons[=scons command]
1434
1435 Runs C<scons> to build files that are not generated by novaboot
1436 itself.
1437
1438 =item --strip-rom
1439
1440 Strip I<rom://> prefix from command lines and generated config files.
1441 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
1442
1443 =item --gen-only
1444
1445 Exit novaboot after file generation phase.
1446
1447 =back
1448
1449 =head2 Target connection check
1450
1451 If supported by the target, the connection to it is made and it is
1452 checked whether the target is not occupied by another novaboot
1453 user/instance.
1454
1455 =over 8
1456
1457 =item --amt=I<"[user[:password]@]host[:port]>
1458
1459 Use Intel AMT technology to control the target machine. WS management
1460 is used to powercycle it and Serial-Over-Lan (SOL) for input/output.
1461 The hostname or (IP address) is given by the I<host> parameter. If
1462 I<password> is not specified, environment variable AMT_PASSWORD is
1463 used. The I<port> specifies a TCP port for SOL. If not specified, the
1464 default is 16992. Default I<user> is admin.
1465
1466 =item --iprelay=I<addr[:port]>
1467
1468 Use TCP/IP relay and serial port to access the target's serial port
1469 and powercycle it. The IP address of the relay is given by I<addr>
1470 parameter. If I<port> is not specified, it default to 23.
1471
1472 Note: This option is supposed to work with HWG-ER02a IP relays.
1473
1474 =item -s, --serial[=device]
1475
1476 Target's serial line is connected to host's serial line (device). The
1477 default value for device is F</dev/ttyUSB0>.
1478
1479 The value of this option is exported in NB_NOVABOOT environment
1480 variable to all subprocesses run by C<novaboot>.
1481
1482 =item --stty=I<settings>
1483
1484 Specifies settings passed to C<stty> invoked on the serial line
1485 specified with B<--serial> option. If this option is not given,
1486 C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
1487
1488 =item --remote-cmd=I<cmd>
1489
1490 Command that mediates connection to the target's serial line. For
1491 example C<ssh server 'cu -l /dev/ttyS0'>.
1492
1493 =item --remote-expect=I<string>
1494
1495 Wait for reception of I<string> after establishing the the remote
1496 connection before continuing.
1497
1498
1499 =back
1500
1501 =head2 File deployment phase
1502
1503 In some setups, it is necessary to copy the files needed for booting
1504 to a particular location, e.g. to a TFTP boot server or to the
1505 F</boot> partition.
1506
1507 =over 8
1508
1509 =item -d, --dhcp-tftp
1510
1511 Turns your workstation into a DHCP and TFTP server so that the OS can
1512 be booted via PXE BIOS (or similar mechanism) on the test machine
1513 directly connected by a plain Ethernet cable to your workstation.
1514
1515 The DHCP and TFTP servers requires root privileges and C<novaboot>
1516 uses C<sudo> command to obtain those. You can put the following to
1517 I</etc/sudoers> to allow running the necessary commands without asking
1518 for password.
1519
1520  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 --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/touch dhcpd.leases, /usr/bin/pkill --pidfile=dhcpd.pid, /usr/bin/pkill --pidfile=tftpd.pid
1521  your_login ALL=NOPASSWD: NOVABOOT
1522
1523 =item --tftp
1524
1525 Starts a TFTP server on your workstation. This is similar to
1526 B<--dhcp-tftp> except that DHCP server is not started.
1527
1528 The TFTP server require root privileges and C<novaboot> uses C<sudo>
1529 command to obtain those. You can put the following to I</etc/sudoers>
1530 to allow running the necessary commands without asking for password.
1531
1532  Cmnd_Alias NOVABOOT =  /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/pkill --pidfile=tftpd.pid
1533  your_login ALL=NOPASSWD: NOVABOOT
1534
1535 =item --tftp-port=I<port>
1536
1537 Port to run the TFTP server on. Implies B<--tftp>.
1538
1539 =item --iso[=filename]
1540
1541 Generates the ISO image that boots NOVA system via GRUB. If no filename
1542 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
1543 of the novaboot script (see also B<--name>).
1544
1545 =item --server[=[[user@]server:]path]
1546
1547 Copy all files needed for booting to another location. The files will
1548 be copied (by B<rsync> tool) to the directory I<path>. If the I<path>
1549 contains string $NAME, it will be replaced with the name of the
1550 novaboot script (see also B<--name>).
1551
1552 =item --rsync-flags=I<flags>
1553
1554 Specifies which I<flags> are appended to F<rsync> command line when
1555 copying files as a result of I<--server> option.
1556
1557 =item --concat
1558
1559 If B<--server> is used and its value ends with $NAME, then after
1560 copying the files, a new bootloader configuration file (e.g. menu.lst)
1561 is created at I<path-wo-name>, i.e. the path specified by B<--server>
1562 with $NAME part removed. The content of the file is created by
1563 concatenating all files of the same name from all subdirectories of
1564 I<path-wo-name> found on the "server".
1565
1566 =item --ider
1567
1568 Use Intel AMT technology for IDE redirection. This allows the target
1569 machine to boot from novaboot created ISO image. Implies B<--iso>.
1570
1571 The experimental C<amtider> utility needed by this option can be
1572 obtained from https://github.com/wentasah/amtterm.
1573
1574 =back
1575
1576 =head2 Target power-on and reset phase
1577
1578 At this point, the target is reset (or switched on/off). There is
1579 several ways how this can be accomplished. Resetting a physical target
1580 can currently be accomplished by the following options: B<--amt>,
1581 B<--iprelay>, B<--reset-cmd>.
1582
1583 =over 8
1584
1585 =item --on, --off
1586
1587 Switch on/off the target machine and exit. The script (if any) is
1588 completely ignored. Currently it works only with B<--iprelay> or
1589 B<--amt>.
1590
1591 =item -Q, --qemu[=I<qemu-binary>]
1592
1593 Boot the configuration in qemu. Optionally, the name of qemu binary
1594 can be specified as a parameter.
1595
1596 =item --qemu-append=I<flags>
1597
1598 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1599 C<-cpu coreduo -smp 2>).
1600
1601 =item -q, --qemu-flags=I<flags>
1602
1603 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1604 -smp 2>) with I<flags> specified here.
1605
1606 =item --reset-cmd=I<cmd>
1607
1608 Command that resets the target.
1609
1610 =item --no-reset, --reset
1611
1612 Disable/enable resetting of the target.
1613
1614 =back
1615
1616 =head2 Interaction with the bootloader on the target
1617
1618 =over 8
1619
1620 =item --uboot[=I<prompt>]
1621
1622 Interact with U-Boot bootloader to boot the thing described in the
1623 novaboot script. I<prompt> specifies the U-Boot's prompt (default is
1624 "=> ", other common prompts are "U-Boot> " or "U-Boot# ").
1625 Implementation of this option is currently tied to a particular board
1626 that we use. It may be subject to changes in the future!
1627
1628 =item --uboot-init
1629
1630 Command(s) to send the U-Boot bootloader before loading the images and
1631 booting them. This option can be given multiple times. After sending
1632 commands from each option novaboot waits for U-Boot I<prompt>.
1633
1634 If the command contains string I<$NB_MYIP> then this string is
1635 replaced by IPv4 address of eth0 interface. Similarly I<$NB_PREFIX> is
1636 replaced with prefix given by B<--prefix>.
1637
1638 See also C<uboot> keyword in L</"NOVABOOT SCRIPT SYNTAX">).
1639
1640 =item --uboot-addr I<name>=I<address>
1641
1642 Load address of U-Boot's C<tftpboot> command for loading I<name>,
1643 where name is one of I<kernel>, I<ramdisk> or I<fdt> (flattened device
1644 tree).
1645
1646 The default addresses are ${I<name>_addr_r}, i.e. U-Boot environment
1647 variables used by convention for this purpose.
1648
1649 =item --uboot-cmd=I<command>
1650
1651 Specifies U-Boot command used to execute the OS. If the command
1652 contains strings C<$kernel_addr>, C<$ramdisk_addr>, C<$fdt_addr>,
1653 these are replaced with the addresses configured with B<--uboot-addr>.
1654
1655 The default value is
1656
1657     bootm $kernel_addr $ramdisk_addr $fdt_addr
1658
1659 or the C<UBOOT_CMD> variable if defined in the novaboot script.
1660
1661 =back
1662
1663 =head2 Target interaction phase
1664
1665 In this phase, target's serial output is redirected to stdout and if
1666 stdin is a TTY, it is redirected to the target's serial input allowing
1667 interactive work with the target.
1668
1669 =over 8
1670
1671 =item --exiton=I<string>
1672
1673 When I<string> is sent by the target, novaboot exits. This option can
1674 be specified multiple times, in which case novaboot exits whenever
1675 either of the specified strings is sent.
1676
1677 If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
1678 treated as regular expression. For example:
1679
1680     --exiton -re --exiton 'error:.*failed'
1681
1682 =item --exiton-re=I<regex>
1683
1684 The same as --exiton -re --exiton I<regex>.
1685
1686 =item --exiton-timeout=I<seconds>
1687
1688 By default B<--exiton> waits for the string match forever. When this
1689 option is specified, "exiton" timeouts after the specifies number of
1690 seconds and novaboot returns non-zero exit code.
1691
1692 =item -i, --interactive
1693
1694 Setup things for interactive use of target. Your terminal will be
1695 switched to raw mode. In raw mode, your system does not process input
1696 in any way (no echoing of entered characters, no interpretation
1697 special characters). This, among others, means that Ctrl-C is passed
1698 to the target and does no longer interrupt novaboot. Use "~~."
1699 sequence to exit novaboot.
1700
1701 =item --expect=I<string>
1702
1703 When I<string> is received from the target, send the string specified
1704 with the subsequent B<--send*> option to the target.
1705
1706 =item --expect-re=I<regex>
1707
1708 When target's output matches regular expression I<regex>, send the
1709 string specified with the subsequent B<--send*> option to the target.
1710
1711 =item --expect-raw=I<perl-code>
1712
1713 Provides direct control over Perl's Expect module.
1714
1715 =item --send=I<string>
1716
1717 Send I<string> to the target after the previously specified
1718 B<--expect*> was matched in the target's output. The I<string> may
1719 contain escape sequences such as "\n".
1720
1721 Note that I<string> is actually interpreted by Perl, so it can contain
1722 much more that escape sequences. This behavior may change in the
1723 future.
1724
1725 Example: C<--expect='login: ' --send='root\n'>
1726
1727 =item --sendcont=I<string>
1728
1729 Similar to B<--send> but continue expecting more input.
1730
1731 Example: C<--expect='Continue?' --sendcont='yes\n'>
1732
1733 =back
1734
1735 =head1 NOVABOOT SCRIPT SYNTAX
1736
1737 The syntax tries to mimic POSIX shell syntax. The syntax is defined
1738 with the following rules.
1739
1740 Lines starting with "#" and empty lines are ignored.
1741
1742 Lines that end with "\" are concatenated with the following line after
1743 removal of the final "\" and leading whitespace of the following line.
1744
1745 Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1746 expression) assign values to internal variables. See L</VARIABLES>
1747 section.
1748
1749 Lines starting with C<load> keyword represent modules to boot. The
1750 word after C<load> is a file name (relative to the build directory
1751 (see B<--build-dir>) of the module to load and the remaining words are
1752 passed to it as the command line parameters.
1753
1754 When the C<load> line ends with "<<WORD" then the subsequent lines
1755 until the line containing solely WORD are copied literally to the file
1756 named on that line. This is similar to shell's heredoc feature.
1757
1758 When the C<load> line ends with "< CMD" then command CMD is executed
1759 with F</bin/sh> and its standard output is stored in the file named on
1760 that line. The SRCDIR variable in CMD's environment is set to the
1761 absolute path of the directory containing the interpreted novaboot
1762 script.
1763
1764 Lines starting with C<run> keyword contain shell commands that are run
1765 during file generation phase. This is the same as the "< CMD" syntax
1766 for C<load> keyboard except that the command's output is not
1767 redirected to a file. The ordering of commands is the same as they
1768 appear in the novaboot script.
1769
1770 Lines starting with C<uboot> represent U-Boot commands that are sent
1771 to the target if B<--uboot> option is given. Having a U-Boot line in
1772 the novaboot script is the same as passing an equivalent
1773 B<--uboot-init> option to novaboot. The C<uboot> keyword can be
1774 suffixed with timeout specification. The syntax is C<uboot:Ns>, where
1775 C<N> is the whole number of seconds. If the U-Boot command prompt does
1776 not appear before the timeout, novaboot fails. The default timeout is
1777 10 seconds.
1778
1779 Example (Linux):
1780
1781   #!/usr/bin/env novaboot
1782   load bzImage console=ttyS0,115200
1783   run  make -C buildroot
1784   load rootfs.cpio < gen_cpio buildroot/images/rootfs.cpio "myapp->/etc/init.d/S99myapp"
1785
1786 Example (NOVA User Land - NUL):
1787
1788   #!/usr/bin/env novaboot
1789   WVDESC=Example program
1790   load bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1791                            verbose hostkeyb:0,0x60,1,12,2
1792   load bin/apps/hello.nul
1793   load hello.nulconfig <<EOF
1794   sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1795   rom://bin/apps/hello.nul
1796   EOF
1797
1798 This example will load three modules: F<sigma0.nul>, F<hello.nul> and
1799 F<hello.nulconfig>. sigma0 receives some command line parameters and
1800 F<hello.nulconfig> file is generated on the fly from the lines between
1801 C<<<EOF> and C<EOF>.
1802
1803 =head2 VARIABLES
1804
1805 The following variables are interpreted in the novaboot script:
1806
1807 =over 8
1808
1809 =item BUILDDIR
1810
1811 Novaboot chdir()s to this directory before file generation phase. The
1812 directory name specified here is relative to the build directory
1813 specified by other means (see L</--build-dir>).
1814
1815 =item EXITON
1816
1817 Assigning this variable has the same effect as specifying L</--exiton>
1818 option.
1819
1820 =item HYPERVISOR_PARAMS
1821
1822 Parameters passed to hypervisor. The default value is "serial", unless
1823 overridden in configuration file.
1824
1825 =item KERNEL
1826
1827 The kernel to use instead of the hypervisor specified in the
1828 configuration file with the C<$hypervisor> variable. The value should
1829 contain the name of the kernel image as well as its command line
1830 parameters. If this variable is defined and non-empty, the variable
1831 HYPERVISOR_PARAMS is not used.
1832
1833 =item NO_BOOT
1834
1835 If this variable is 1, the system is not booted. This is currently
1836 only implemented for U-Boot bootloader where it is useful for
1837 interacting with the bootloader without booting the system - e.g. for
1838 flashing.
1839
1840 =item QEMU
1841
1842 Use a specific qemu binary (can be overridden with B<-Q>) and flags
1843 when booting this script under qemu. If QEMU_FLAGS variable is also
1844 specified flags specified in QEMU variable are replaced by those in
1845 QEMU_FLAGS.
1846
1847 =item QEMU_FLAGS
1848
1849 Use specific qemu flags (can be overridden with B<-q>).
1850
1851 =item UBOOT_CMD
1852
1853 See L</--uboot-cmd>.
1854
1855 =item WVDESC
1856
1857 Description of the WvTest-compliant program.
1858
1859 =item WVTEST_TIMEOUT
1860
1861 The timeout in seconds for WvTest harness. If no complete line appears
1862 in the test output within the time specified here, the test fails. It
1863 is necessary to specify this for long running tests that produce no
1864 intermediate output.
1865
1866 =back
1867
1868 =head1 CONFIGURATION FILE
1869
1870 Novaboot can read its configuration from one or more files. By
1871 default, novaboot looks for files in F</etc/novaboot.d>, file
1872 F<~/.config/novaboot> and files named F<.novaboot> as described in
1873 L</Configuration reading phase>. Alternatively, configuration file
1874 location can be specified with the B<-c> switch or with the
1875 NOVABOOT_CONFIG environment variable. The configuration file has Perl
1876 syntax (i.e. it is better to put C<1;> as the last line) and should set
1877 values of certain Perl variables. The current configuration can be
1878 dumped with the B<--dump-config> switch. Some configuration variables
1879 can be overridden by environment variables (see below) or by command
1880 line switches.
1881
1882 Supported configuration variables include:
1883
1884 =over 8
1885
1886 =item $builddir
1887
1888 Build directory location relative to the location of the configuration
1889 file.
1890
1891 =item $default_target
1892
1893 Default target (see below) to use when no target is explicitly
1894 specified on command line with the B<--target> option.
1895
1896 =item %targets
1897
1898 Hash of target definitions to be used with the B<--target> option. The
1899 key is the identifier of the target, the value is the string with
1900 command line options. For instance, if the configuration file contains:
1901
1902  $targets{'mybox'} = '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1903
1904 then the following two commands are equivalent:
1905
1906  ./myos --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1907  ./myos -t mybox
1908
1909 =back
1910
1911 =head1 ENVIRONMENT VARIABLES
1912
1913 Some options can be specified not only via config file or command line
1914 but also through environment variables. Environment variables override
1915 the values from configuration file and command line parameters
1916 override the environment variables.
1917
1918 =over 8
1919
1920 =item NOVABOOT_CONFIG
1921
1922 Name of the novaboot configuration file to use instead of the default
1923 one(s).
1924
1925 =item NOVABOOT_CONFIG_DIR
1926
1927 Name of the novaboot configuration directory. When not specified
1928 F</etc/novaboot.d> is used.
1929
1930 =item NOVABOOT_BENDER
1931
1932 Defining this variable has the same meaning as B<--bender> option.
1933
1934 =back
1935
1936 =head1 AUTHORS
1937
1938 Michal Sojka <sojka@os.inf.tu-dresden.de>
1939
1940 =cut
1941
1942 # LocalWords:  novaboot Novaboot NOVABOOT TFTP PXE DHCP filename stty
1943 # LocalWords:  chainloader stdout Qemu qemu preprocessing ISOLINUX bootable
1944 # LocalWords:  config subprocesses sudo sudoers tftp dhcp IDE stdin
1945 # LocalWords:  subdirectories TTY whitespace heredoc POSIX WvTest