]> rtime.felk.cvut.cz Git - novaboot.git/blob - novaboot
Allow specifying target in NOVABOOT_TARGET environment variable
[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 = $ENV{'NOVABOOT_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         defined $uboot_cmd) {
1088         my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
1089         my $dtb;
1090         @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
1091         my $initrd = shift @$modules;
1092
1093         if (defined $kbin) {
1094             die "No '--uboot-addr kernel' given" unless $uboot_addr{kernel};
1095             $exp->send("tftpboot $uboot_addr{kernel} $prefix$kbin\n");
1096             $exp->expect(10,
1097                          [qr/##/, sub { exp_continue; }],
1098                          $uboot_prompt) || die "Kernel load timeout";
1099         }
1100         if (defined $dtb) {
1101             die "No '--uboot-addr fdt' given" unless $uboot_addr{fdt};
1102             $exp->send("tftpboot $uboot_addr{fdt} $prefix$dtb\n");
1103             $exp->expect(10,
1104                          [qr/##/, sub { exp_continue; }],
1105                          $uboot_prompt) || die "Device tree load timeout";
1106         }
1107         if (defined $initrd) {
1108             die "No '--uboot-addr ramdisk' given" unless $uboot_addr{ramdisk};
1109             $exp->send("tftpboot $uboot_addr{ramdisk} $prefix$initrd\n");
1110             $exp->expect(10,
1111                          [qr/##/, sub { exp_continue; }],
1112                          $uboot_prompt) || die "Initrd load timeout";
1113         } else {
1114             $uboot_addr{ramdisk} = '-';
1115         }
1116
1117         $kcmd //= '';
1118         $exp->send("setenv bootargs $kcmd\n");
1119         $exp->expect(5, $uboot_prompt)  || die "U-Boot prompt timeout";
1120
1121         $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
1122         $uboot_cmd =~ s/\$kernel_addr/$uboot_addr{kernel}/g;
1123         $uboot_cmd =~ s/\$ramdisk_addr/$uboot_addr{ramdisk}/g;
1124         $uboot_cmd =~ s/\$fdt_addr/$uboot_addr{fdt}/g;
1125
1126         $exp->send($uboot_cmd . "\n");
1127         $exp->expect(5, "\n")  || die "U-Boot command timeout";
1128     }
1129 }
1130
1131 ### Serial line interaction
1132 if ($interaction && defined $exp) {
1133     # Serial line of the target is available
1134     my $interrupt = 'Ctrl-C';
1135     if ($interactive && !@exiton) {
1136         $interrupt = '"~~."';
1137     }
1138     print STDERR "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
1139     $exp->log_stdout(1);
1140     if (@exiton) {
1141         $exp->expect($exiton_timeout, @expect_raw, @exiton) || die("exiton timeout");
1142     } else {
1143         my @inputs = ($exp);
1144         my $infile = new IO::File;
1145         $infile->IO::File::fdopen(*STDIN,'r');
1146         my $in_object = Expect->exp_init($infile);
1147         $in_object->set_group($exp);
1148
1149         if ($interactive) {
1150             $in_object->set_seq('~~\.', sub { print STDERR "novaboot: Escape sequence detected\r\n"; undef; });
1151             $in_object->manual_stty(0);   # Use raw terminal mode
1152         } else {
1153             $in_object->manual_stty(1);   # Do not modify terminal settings
1154         }
1155         push(@inputs, $in_object);
1156         #use Data::Dumper;
1157         #print Dumper(\@expect_raw);
1158         $exp->expect(undef, @expect_raw) if @expect_raw;
1159         Expect::interconnect(@inputs) unless defined($exp->exitstatus);
1160     }
1161 }
1162
1163 ## Kill dhcpc or tftpd
1164 if (defined $dhcp_tftp || defined $tftp) {
1165     die("novaboot: This should kill servers on background\n");
1166 }
1167
1168 # Always finish novaboot output with newline
1169 print "\n";
1170
1171 ## Documentation
1172
1173 =head1 NAME
1174
1175 novaboot - Boots a locally compiled operating system on a remote
1176 target or in qemu
1177
1178 =head1 SYNOPSIS
1179
1180 B<novaboot> --help
1181
1182 B<novaboot> [option]... [--] script...
1183
1184 B<./script> [option]...
1185
1186 =head1 DESCRIPTION
1187
1188 This program makes booting of a locally compiled operating system (OS)
1189 (e.g. NOVA or Linux) on remote targets as simple as running a program
1190 locally. It automates things like copying OS images to a TFTP server,
1191 generation of bootloader configuration files, resetting of target
1192 hardware or redirection of target's serial line to stdin/out. Novaboot
1193 is highly configurable and it makes it easy to boot a single image on
1194 different targets or different images on a single target.
1195
1196 Novaboot operation is controlled by command line options and by a so
1197 called novaboot script, which can be thought as a generalization of
1198 bootloader configuration files (see L</"NOVABOOT SCRIPT SYNTAX">).
1199 Typical way of using novaboot is to make the novaboot script
1200 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
1201 booting a particular OS configuration becomes the same as executing a
1202 local program - the novaboot script.
1203
1204 Novaboot uses configuration files to, among other things, define
1205 command line options needed for different targets. Users typically use
1206 only the B<-t>/B<--target> command line option to select the target.
1207 Internally, this option expands to the pre-configured options.
1208 Configuration files are searched at multiple places, which allows to
1209 have per-system, per-user or per-project configurations. Configuration
1210 file syntax is described in section L</"CONFIGURATION FILE">.
1211
1212 Simple examples of using C<novaboot>:
1213
1214 =over 3
1215
1216 =item 1.
1217
1218 Run an OS in Qemu. This is the default action when no other action is
1219 specified by command line switches. Thus running C<novaboot myos> (or
1220 C<./myos> as described above) will run Qemu and make it boot the
1221 configuration specified in the F<myos> script.
1222
1223 =item 2.
1224
1225 Create a bootloader configuration file (currently supported
1226 bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar and U-Boot) and copy it
1227 with all other files needed for booting to a remote boot server. Then
1228 use a TCP/IP-controlled relay/serial-to-TCP converter to reset the
1229 target and receive its serial output.
1230
1231  ./myos --grub2 --server=192.168.1.1:/tftp --iprelay=192.168.1.2
1232
1233 =item 3.
1234
1235 Run DHCP and TFTP server on developer's machine to boot the target
1236 from it.
1237
1238  ./myos --dhcp-tftp
1239
1240 This is useful when no network infrastructure is in place and
1241 the target is connected directly to developer's box.
1242
1243 =item 4.
1244
1245 Create bootable ISO image.
1246
1247  novaboot --iso -- script1 script2
1248
1249 The created ISO image will have ISOLINUX bootloader installed on it
1250 and the boot menu will allow selecting between I<script1> and
1251 I<script2> configurations.
1252
1253 =back
1254
1255 =head1 PHASES AND OPTIONS
1256
1257 Novaboot performs its work in several phases. Each phase can be
1258 influenced by several command line options, certain phases can be
1259 skipped. The list of phases (in the execution order) and the
1260 corresponding options follows.
1261
1262 =head2 Configuration reading phase
1263
1264 After starting, novaboot reads configuration files. Their content is
1265 described in section L</"CONFIGURATION FILE">. By default,
1266 configuration is read from multiple locations. First from the system
1267 configuration directory (F</etc/novaboot.d/>), second from the user
1268 configuration file (F<~/.config/novaboot>) and third from F<.novaboot>
1269 files along the path to the current directory. Alternatively, a single
1270 configuration file specified with the B<-c> switch or with the
1271 C<NOVABOOT_CONFIG> environment variable is read. The latter read files
1272 override settings from the former ones.
1273
1274 The system configuration directory is determined by the content of
1275 NOVABOOT_CONFIG_DIR environment variable and defaults to
1276 F</etc/novaboot.d>. Files in this directory with names consisting
1277 solely of English letters, numbers, dashes '-' and underscores '_'
1278 (note that dot '.' is not included) are read in alphabetical order.
1279
1280 Then, the user configuration file is read from
1281 F<$XDG_CONFIG_HOME/novaboot>. If C<$XDG_CONFIG_HOME> environemnt
1282 variable is not set F<~/.config/novaboot> is read instead.
1283
1284 Finally, novaboot searches for files named F<.novaboot> starting from the
1285 directory of the novaboot script (or working directory, see bellow)
1286 and continuing upwards up to the root directory. The found
1287 configuration files are then read in the opposite order (i.e. from the
1288 root directory downwards). This allows to have, for example, a project
1289 specific configuration in F<~/project/.novaboot>.
1290
1291 Note the difference between F<~/.config/novaboot> and F<~/.novaboot>.
1292 The former one is read always, whereas the latter only when novaboot
1293 script or working directory is under the C<$HOME> directory.
1294
1295 In certain cases, the location of the novaboot script cannot be
1296 determined in this early phase. This happens either when the script is
1297 read from the standard input or when novaboot is invoked explicitly as
1298 in the example L</"4."> above. In this case the current working
1299 directory is used as a starting point for configuration file search
1300 instead of the novaboot script directory.
1301
1302 =over 8
1303
1304 =item -c, --config=I<filename>
1305
1306 Use the specified configuration file instead of the default one(s).
1307
1308 =back
1309
1310 =head2 Command line processing phase
1311
1312 =over 8
1313
1314 =item --dump-config
1315
1316 Dump the current configuration to stdout end exit. Useful as an
1317 initial template for a configuration file.
1318
1319 =item -h, --help
1320
1321 Print short (B<-h>) or long (B<--help>) help.
1322
1323 =item -t, --target=I<target>
1324
1325 This option serves as a user configurable shortcut for other novaboot
1326 options. The effect of this option is the same as specifying the
1327 options stored in the C<%targets> configuration variable under key
1328 I<target>. See also L</"CONFIGURATION FILE">.
1329
1330 When this option is not given, novaboot tries to determine the target
1331 to use from either B<NOVABOOT_TARGET> environment variable or from
1332 B<$default_target> configuration file variable.
1333
1334 =back
1335
1336 =head2 Script preprocessing phase
1337
1338 This phases allows to modify the parsed novaboot script before it is
1339 used in the later phases.
1340
1341 =over 8
1342
1343 =item -a, --append=I<parameters>
1344
1345 Append a string to the first C<load> line in the novaboot script. This
1346 can be used to append parameters to the kernel's or root task's
1347 command line. This option can appear multiple times.
1348
1349 =item -b, --bender
1350
1351 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
1352 ports and stores the information about them in the BIOS data area for
1353 use by the kernel.
1354
1355 =item --chainloader=I<chainloader>
1356
1357 Specifies a chainloader that is loaded before the kernel and other
1358 files specified in the novaboot script. E.g. 'bin/boot/bender
1359 promisc'.
1360
1361 =item --dump
1362
1363 Print the modules to boot and their parameters after this phase
1364 finishes. Then exit. This is useful for seeing the effect of other
1365 options in this section.
1366
1367 =item -k, --kernel=F<file>
1368
1369 Replace the first word on the first C<load> line in the novaboot
1370 script with F<file>.
1371
1372 =item --scriptmod=I<perl expression>
1373
1374 When novaboot script is read, I<perl expression> is executed for every
1375 line (in $_ variable). For example, C<novaboot
1376 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
1377 in the script with I<omega6>.
1378
1379 When this option is present, it overrides I<$script_modifier> variable
1380 from the configuration file, which has the same effect. If this option
1381 is given multiple times all expressions are evaluated in the command
1382 line order.
1383
1384 =back
1385
1386 =head2 File generation phase
1387
1388 In this phase, files needed for booting are generated in a so called
1389 I<build directory> (see L</--build-dir>). In most cases configuration
1390 for a bootloader is generated automatically by novaboot. It is also
1391 possible to generate other files using I<heredoc> or I<"<"> syntax in
1392 novaboot scripts. Finally, binaries can be generated in this phases by
1393 running C<scons> or C<make>.
1394
1395 =over 8
1396
1397 =item --build-dir=I<directory>
1398
1399 Overrides the default build directory location.
1400
1401 The default build directory location is determined as follows: If the
1402 configuration file defines the C<$builddir> variable, its value is
1403 used. Otherwise, it is the directory that contains the first processed
1404 novaboot script.
1405
1406 See also L</BUILDDIR> variable.
1407
1408 =item -g, --grub[=I<filename>]
1409
1410 Generates grub bootloader menu file. If the I<filename> is not
1411 specified, F<menu.lst> is used. The I<filename> is relative to the
1412 build directory (see B<--build-dir>).
1413
1414 =item --grub-preamble=I<prefix>
1415
1416 Specifies the I<preable> that is at the beginning of the generated
1417 GRUB or GRUB2 config files. This is useful for specifying GRUB's
1418 timeout.
1419
1420 =item --prefix=I<prefix>
1421
1422 Specifies I<prefix> (e.g. F</srv/tftp>) that is put in front of every
1423 file name in generated bootloader configuration files (or in U-Boot
1424 commands).
1425
1426 If the I<prefix> contains string $NAME, it will be replaced with the
1427 name of the novaboot script (see also B<--name>).
1428
1429 If the I<prefix> contains string $BUILDDIR, it will be replaced with
1430 the build directory (see also B<--build-dir>).
1431
1432 =item --grub-prefix
1433
1434 Alias for B<--prefix>.
1435
1436 =item --grub2[=I<filename>]
1437
1438 Generate GRUB2 menu entry in I<filename>. If I<filename> is not
1439 specified F<grub.cfg> is used. The content of the menu entry can be
1440 customized with B<--grub-preamble>, B<--grub2-prolog> or
1441 B<--grub_prefix> options.
1442
1443 In order to use the the generated menu entry on your development
1444 machine that uses GRUB2, append the following snippet to
1445 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
1446 i.e. run update-grub on Debian/Ubuntu.
1447
1448   if [ -f /path/to/nul/build/grub.cfg ]; then
1449     source /path/to/nul/build/grub.cfg
1450   fi
1451
1452 =item --grub2-prolog=I<prolog>
1453
1454 Specifies text that is put at the beginning of the GRUB2 menu entry.
1455
1456 =item -m, --make[=make command]
1457
1458 Runs C<make> to build files that are not generated by novaboot itself.
1459
1460 =item --name=I<string>
1461
1462 Use the name I<string> instead of the name of the novaboot script.
1463 This name is used for things like a title of grub menu or for the
1464 server directory where the boot files are copied to.
1465
1466 =item --no-file-gen
1467
1468 Do not run external commands to generate files (i.e. "<" syntax and
1469 C<run> keyword). This switch does not influence generation of files
1470 specified with "<<WORD" syntax.
1471
1472 =item -p, --pulsar[=mac]
1473
1474 Generates pulsar bootloader configuration file named F<config-I<mac>>
1475 The I<mac> string is typically a MAC address and defaults to
1476 I<novaboot>.
1477
1478 =item --scons[=scons command]
1479
1480 Runs C<scons> to build files that are not generated by novaboot
1481 itself.
1482
1483 =item --strip-rom
1484
1485 Strip I<rom://> prefix from command lines and generated config files.
1486 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
1487
1488 =item --gen-only
1489
1490 Exit novaboot after file generation phase.
1491
1492 =back
1493
1494 =head2 Target connection check
1495
1496 If supported by the target, the connection to it is made and it is
1497 checked whether the target is not occupied by another novaboot
1498 user/instance.
1499
1500 =over 8
1501
1502 =item --amt=I<"[user[:password]@]host[:port]>
1503
1504 Use Intel AMT technology to control the target machine. WS management
1505 is used to powercycle it and Serial-Over-Lan (SOL) for input/output.
1506 The hostname or (IP address) is given by the I<host> parameter. If
1507 I<password> is not specified, environment variable AMT_PASSWORD is
1508 used. The I<port> specifies a TCP port for SOL. If not specified, the
1509 default is 16992. Default I<user> is admin.
1510
1511 =item --iprelay=I<addr[:port]>
1512
1513 Use TCP/IP relay and serial port to access the target's serial port
1514 and powercycle it. The IP address of the relay is given by I<addr>
1515 parameter. If I<port> is not specified, it default to 23.
1516
1517 Note: This option is supposed to work with HWG-ER02a IP relays.
1518
1519 =item -s, --serial[=device]
1520
1521 Target's serial line is connected to host's serial line (device). The
1522 default value for device is F</dev/ttyUSB0>.
1523
1524 The value of this option is exported in NB_NOVABOOT environment
1525 variable to all subprocesses run by C<novaboot>.
1526
1527 =item --stty=I<settings>
1528
1529 Specifies settings passed to C<stty> invoked on the serial line
1530 specified with B<--serial> option. If this option is not given,
1531 C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
1532
1533 =item --remote-cmd=I<cmd>
1534
1535 Command that mediates connection to the target's serial line. For
1536 example C<ssh server 'cu -l /dev/ttyS0'>.
1537
1538 =item --remote-expect=I<string>
1539
1540 Wait for reception of I<string> after establishing the remote
1541 connection.
1542
1543 =item --remote-expect-silent=I<string>
1544
1545 The same as B<--remote-expect> except that the remote output is not
1546 echoed to stdout while waiting for the I<string>. Everything after the
1547 matched string is printed to stdout, so you may want to include line
1548 end characters in the I<string> as well.
1549
1550 =back
1551
1552 =head2 File deployment phase
1553
1554 In some setups, it is necessary to copy the files needed for booting
1555 to a particular location, e.g. to a TFTP boot server or to the
1556 F</boot> partition.
1557
1558 =over 8
1559
1560 =item -d, --dhcp-tftp
1561
1562 Turns your workstation into a DHCP and TFTP server so that the OS can
1563 be booted via PXE BIOS (or similar mechanism) on the test machine
1564 directly connected by a plain Ethernet cable to your workstation.
1565
1566 The DHCP and TFTP servers requires root privileges and C<novaboot>
1567 uses C<sudo> command to obtain those. You can put the following to
1568 I</etc/sudoers> to allow running the necessary commands without asking
1569 for password.
1570
1571  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
1572  your_login ALL=NOPASSWD: NOVABOOT
1573
1574 =item --tftp
1575
1576 Starts a TFTP server on your workstation. This is similar to
1577 B<--dhcp-tftp> except that DHCP server is not started.
1578
1579 The TFTP server require root privileges and C<novaboot> uses C<sudo>
1580 command to obtain those. You can put the following to I</etc/sudoers>
1581 to allow running the necessary commands without asking for password.
1582
1583  Cmnd_Alias NOVABOOT =  /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/pkill --pidfile=tftpd.pid
1584  your_login ALL=NOPASSWD: NOVABOOT
1585
1586 =item --tftp-port=I<port>
1587
1588 Port to run the TFTP server on. Implies B<--tftp>.
1589
1590 =item --iso[=filename]
1591
1592 Generates the ISO image that boots NOVA system via GRUB. If no filename
1593 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
1594 of the novaboot script (see also B<--name>).
1595
1596 =item --server[=[[user@]server:]path]
1597
1598 Copy all files needed for booting to another location. The files will
1599 be copied (by B<rsync> tool) to the directory I<path>. If the I<path>
1600 contains string $NAME, it will be replaced with the name of the
1601 novaboot script (see also B<--name>).
1602
1603 =item --rsync-flags=I<flags>
1604
1605 Specifies which I<flags> are appended to F<rsync> command line when
1606 copying files as a result of I<--server> option.
1607
1608 =item --concat
1609
1610 If B<--server> is used and its value ends with $NAME, then after
1611 copying the files, a new bootloader configuration file (e.g. menu.lst)
1612 is created at I<path-wo-name>, i.e. the path specified by B<--server>
1613 with $NAME part removed. The content of the file is created by
1614 concatenating all files of the same name from all subdirectories of
1615 I<path-wo-name> found on the "server".
1616
1617 =item --ider
1618
1619 Use Intel AMT technology for IDE redirection. This allows the target
1620 machine to boot from novaboot created ISO image. Implies B<--iso>.
1621
1622 The experimental C<amtider> utility needed by this option can be
1623 obtained from https://github.com/wentasah/amtterm.
1624
1625 =back
1626
1627 =head2 Target power-on and reset phase
1628
1629 At this point, the target is reset (or switched on/off). There is
1630 several ways how this can be accomplished. Resetting a physical target
1631 can currently be accomplished by the following options: B<--amt>,
1632 B<--iprelay>, B<--reset-cmd>.
1633
1634 =over 8
1635
1636 =item --on, --off
1637
1638 Switch on/off the target machine and exit. The script (if any) is
1639 completely ignored. Currently it works only with B<--iprelay> or
1640 B<--amt>.
1641
1642 =item -Q, --qemu[=I<qemu-binary>]
1643
1644 Boot the configuration in qemu. Optionally, the name of qemu binary
1645 can be specified as a parameter.
1646
1647 =item --qemu-append=I<flags>
1648
1649 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1650 C<-cpu coreduo -smp 2>).
1651
1652 =item -q, --qemu-flags=I<flags>
1653
1654 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1655 -smp 2>) with I<flags> specified here.
1656
1657 =item --reset-cmd=I<cmd>
1658
1659 Command that resets the target.
1660
1661 =item --no-reset, --reset
1662
1663 Disable/enable resetting of the target.
1664
1665 =back
1666
1667 =head2 Interaction with the bootloader on the target
1668
1669 =over 8
1670
1671 =item --uboot[=I<prompt>]
1672
1673 Interact with U-Boot bootloader to boot the thing described in the
1674 novaboot script. I<prompt> specifies the U-Boot's prompt (default is
1675 "=> ", other common prompts are "U-Boot> " or "U-Boot# ").
1676 Implementation of this option is currently tied to a particular board
1677 that we use. It may be subject to changes in the future!
1678
1679 =item --no-uboot
1680
1681 Disable U-Boot interaction previously enabled with B<--uboot>.
1682
1683 =item --uboot-init
1684
1685 Command(s) to send the U-Boot bootloader before loading the images and
1686 booting them. This option can be given multiple times. After sending
1687 commands from each option novaboot waits for U-Boot I<prompt>.
1688
1689 If the command contains string I<$NB_MYIP> then this string is
1690 replaced by IPv4 address of eth0 interface. Similarly I<$NB_PREFIX> is
1691 replaced with prefix given by B<--prefix>.
1692
1693 See also C<uboot> keyword in L</"NOVABOOT SCRIPT SYNTAX">).
1694
1695 =item --uboot-addr I<name>=I<address>
1696
1697 Load address of U-Boot's C<tftpboot> command for loading I<name>,
1698 where name is one of I<kernel>, I<ramdisk> or I<fdt> (flattened device
1699 tree).
1700
1701 The default addresses are ${I<name>_addr_r}, i.e. U-Boot environment
1702 variables used by convention for this purpose.
1703
1704 =item --uboot-cmd=I<command>
1705
1706 Specifies U-Boot command used to execute the OS. If the command
1707 contains strings C<$kernel_addr>, C<$ramdisk_addr>, C<$fdt_addr>,
1708 these are replaced with the addresses configured with B<--uboot-addr>.
1709
1710 The default value is
1711
1712     bootm $kernel_addr $ramdisk_addr $fdt_addr
1713
1714 or the C<UBOOT_CMD> variable if defined in the novaboot script.
1715
1716 =back
1717
1718 =head2 Target interaction phase
1719
1720 In this phase, target's serial output is redirected to stdout and if
1721 stdin is a TTY, it is redirected to the target's serial input allowing
1722 interactive work with the target.
1723
1724 =over 8
1725
1726 =item --exiton=I<string>
1727
1728 When I<string> is sent by the target, novaboot exits. This option can
1729 be specified multiple times, in which case novaboot exits whenever
1730 either of the specified strings is sent.
1731
1732 If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
1733 treated as regular expression. For example:
1734
1735     --exiton -re --exiton 'error:.*failed'
1736
1737 =item --exiton-re=I<regex>
1738
1739 The same as --exiton -re --exiton I<regex>.
1740
1741 =item --exiton-timeout=I<seconds>
1742
1743 By default B<--exiton> waits for the string match forever. When this
1744 option is specified, "exiton" timeouts after the specifies number of
1745 seconds and novaboot returns non-zero exit code.
1746
1747 =item -i, --interactive
1748
1749 Setup things for interactive use of target. Your terminal will be
1750 switched to raw mode. In raw mode, your system does not process input
1751 in any way (no echoing of entered characters, no interpretation
1752 special characters). This, among others, means that Ctrl-C is passed
1753 to the target and does no longer interrupt novaboot. Use "~~."
1754 sequence to exit novaboot.
1755
1756 =item --no-interaction, --interaction
1757
1758 Skip resp. force target interaction phase. When skipped, novaboot exits
1759 immediately when boot is initiated.
1760
1761 =item --expect=I<string>
1762
1763 When I<string> is received from the target, send the string specified
1764 with the subsequent B<--send*> option to the target.
1765
1766 =item --expect-re=I<regex>
1767
1768 When target's output matches regular expression I<regex>, send the
1769 string specified with the subsequent B<--send*> option to the target.
1770
1771 =item --expect-raw=I<perl-code>
1772
1773 Provides direct control over Perl's Expect module.
1774
1775 =item --send=I<string>
1776
1777 Send I<string> to the target after the previously specified
1778 B<--expect*> was matched in the target's output. The I<string> may
1779 contain escape sequences such as "\n".
1780
1781 Note that I<string> is actually interpreted by Perl, so it can contain
1782 much more that escape sequences. This behavior may change in the
1783 future.
1784
1785 Example: C<--expect='login: ' --send='root\n'>
1786
1787 =item --sendcont=I<string>
1788
1789 Similar to B<--send> but continue expecting more input.
1790
1791 Example: C<--expect='Continue?' --sendcont='yes\n'>
1792
1793 =back
1794
1795 =head1 NOVABOOT SCRIPT SYNTAX
1796
1797 The syntax tries to mimic POSIX shell syntax. The syntax is defined
1798 with the following rules.
1799
1800 Lines starting with "#" and empty lines are ignored.
1801
1802 Lines that end with "\" are concatenated with the following line after
1803 removal of the final "\" and leading whitespace of the following line.
1804
1805 Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1806 expression) assign values to internal variables. See L</VARIABLES>
1807 section.
1808
1809 Lines starting with C<load> keyword represent modules to boot. The
1810 word after C<load> is a file name (relative to the build directory
1811 (see B<--build-dir>) of the module to load and the remaining words are
1812 passed to it as the command line parameters.
1813
1814 When the C<load> line ends with "<<WORD" then the subsequent lines
1815 until the line containing solely WORD are copied literally to the file
1816 named on that line. This is similar to shell's heredoc feature.
1817
1818 When the C<load> line ends with "< CMD" then command CMD is executed
1819 with F</bin/sh> and its standard output is stored in the file named on
1820 that line. The SRCDIR variable in CMD's environment is set to the
1821 absolute path of the directory containing the interpreted novaboot
1822 script.
1823
1824 Lines starting with C<run> keyword contain shell commands that are run
1825 during file generation phase. This is the same as the "< CMD" syntax
1826 for C<load> keyboard except that the command's output is not
1827 redirected to a file. The ordering of commands is the same as they
1828 appear in the novaboot script.
1829
1830 Lines starting with C<uboot> represent U-Boot commands that are sent
1831 to the target if B<--uboot> option is given. Having a U-Boot line in
1832 the novaboot script is the same as passing an equivalent
1833 B<--uboot-init> option to novaboot. The C<uboot> keyword can be
1834 suffixed with timeout specification. The syntax is C<uboot:Ns>, where
1835 C<N> is the whole number of seconds. If the U-Boot command prompt does
1836 not appear before the timeout, novaboot fails. The default timeout is
1837 10 seconds.
1838
1839 Example (Linux):
1840
1841   #!/usr/bin/env novaboot
1842   load bzImage console=ttyS0,115200
1843   run  make -C buildroot
1844   load rootfs.cpio < gen_cpio buildroot/images/rootfs.cpio "myapp->/etc/init.d/S99myapp"
1845
1846 Example (NOVA User Land - NUL):
1847
1848   #!/usr/bin/env novaboot
1849   WVDESC=Example program
1850   load bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1851                            verbose hostkeyb:0,0x60,1,12,2
1852   load bin/apps/hello.nul
1853   load hello.nulconfig <<EOF
1854   sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1855   rom://bin/apps/hello.nul
1856   EOF
1857
1858 This example will load three modules: F<sigma0.nul>, F<hello.nul> and
1859 F<hello.nulconfig>. sigma0 receives some command line parameters and
1860 F<hello.nulconfig> file is generated on the fly from the lines between
1861 C<<<EOF> and C<EOF>.
1862
1863 =head2 VARIABLES
1864
1865 The following variables are interpreted in the novaboot script:
1866
1867 =over 8
1868
1869 =item BUILDDIR
1870
1871 Novaboot chdir()s to this directory before file generation phase. The
1872 directory name specified here is relative to the build directory
1873 specified by other means (see L</--build-dir>).
1874
1875 =item EXITON
1876
1877 Assigning this variable has the same effect as specifying L</--exiton>
1878 option.
1879
1880 =item HYPERVISOR_PARAMS
1881
1882 Parameters passed to hypervisor. The default value is "serial", unless
1883 overridden in configuration file.
1884
1885 =item KERNEL
1886
1887 The kernel to use instead of the hypervisor specified in the
1888 configuration file with the C<$hypervisor> variable. The value should
1889 contain the name of the kernel image as well as its command line
1890 parameters. If this variable is defined and non-empty, the variable
1891 HYPERVISOR_PARAMS is not used.
1892
1893 =item NO_BOOT
1894
1895 If this variable is 1, the system is not booted. This is currently
1896 only implemented for U-Boot bootloader where it is useful for
1897 interacting with the bootloader without booting the system - e.g. for
1898 flashing.
1899
1900 =item QEMU
1901
1902 Use a specific qemu binary (can be overridden with B<-Q>) and flags
1903 when booting this script under qemu. If QEMU_FLAGS variable is also
1904 specified flags specified in QEMU variable are replaced by those in
1905 QEMU_FLAGS.
1906
1907 =item QEMU_FLAGS
1908
1909 Use specific qemu flags (can be overridden with B<-q>).
1910
1911 =item UBOOT_CMD
1912
1913 See L</--uboot-cmd>.
1914
1915 =item WVDESC
1916
1917 Description of the WvTest-compliant program.
1918
1919 =item WVTEST_TIMEOUT
1920
1921 The timeout in seconds for WvTest harness. If no complete line appears
1922 in the test output within the time specified here, the test fails. It
1923 is necessary to specify this for long running tests that produce no
1924 intermediate output.
1925
1926 =back
1927
1928 =head1 CONFIGURATION FILE
1929
1930 Novaboot can read its configuration from one or more files. By
1931 default, novaboot looks for files in F</etc/novaboot.d>, file
1932 F<~/.config/novaboot> and files named F<.novaboot> as described in
1933 L</Configuration reading phase>. Alternatively, configuration file
1934 location can be specified with the B<-c> switch or with the
1935 NOVABOOT_CONFIG environment variable. The configuration file has Perl
1936 syntax (i.e. it is better to put C<1;> as the last line) and should set
1937 values of certain Perl variables. The current configuration can be
1938 dumped with the B<--dump-config> switch. Some configuration variables
1939 can be overridden by environment variables (see below) or by command
1940 line switches.
1941
1942 Supported configuration variables include:
1943
1944 =over 8
1945
1946 =item $builddir
1947
1948 Build directory location relative to the location of the configuration
1949 file.
1950
1951 =item $default_target
1952
1953 Default target (see below) to use when no target is explicitly
1954 specified with the B<--target> command line option or
1955 B<NOVABOOT_TARGET> environment variable.
1956
1957 =item %targets
1958
1959 Hash of target definitions to be used with the B<--target> option. The
1960 key is the identifier of the target, the value is the string with
1961 command line options. For instance, if the configuration file contains:
1962
1963  $targets{'mybox'} = '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1964
1965 then the following two commands are equivalent:
1966
1967  ./myos --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1968  ./myos -t mybox
1969
1970 =back
1971
1972 =head1 ENVIRONMENT VARIABLES
1973
1974 Some options can be specified not only via config file or command line
1975 but also through environment variables. Environment variables override
1976 the values from configuration file and command line parameters
1977 override the environment variables.
1978
1979 =over 8
1980
1981 =item NOVABOOT_CONFIG
1982
1983 Name of the novaboot configuration file to use instead of the default
1984 one(s).
1985
1986 =item NOVABOOT_CONFIG_DIR
1987
1988 Name of the novaboot configuration directory. When not specified
1989 F</etc/novaboot.d> is used.
1990
1991 =item NOVABOOT_TARGET
1992
1993 Name of the novaboot target to use. This overrides the value of
1994 B<$default_target> from the configuration file and can be overriden
1995 with the B<--target> command line option.
1996
1997 =item NOVABOOT_BENDER
1998
1999 Defining this variable has the same meaning as B<--bender> option.
2000
2001 =back
2002
2003 =head1 AUTHORS
2004
2005 Michal Sojka <sojka@os.inf.tu-dresden.de>
2006
2007 =cut
2008
2009 # LocalWords:  novaboot Novaboot NOVABOOT TFTP PXE DHCP filename stty
2010 # LocalWords:  chainloader stdout Qemu qemu preprocessing ISOLINUX bootable
2011 # LocalWords:  config subprocesses sudo sudoers tftp dhcp IDE stdin
2012 # LocalWords:  subdirectories TTY whitespace heredoc POSIX WvTest