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