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