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