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