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