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