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