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