use strict;
use warnings;
+use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ());
use Getopt::Long qw(GetOptionsFromString);
use Pod::Usage;
use File::Basename;
use IPC::Open2;
use POSIX qw(:errno_h);
use Cwd qw(getcwd abs_path);
+use Expect;
# always flush
$| = 1;
-my $invocation_dir = getcwd();
-
-chomp(my $gittop = `git rev-parse --show-toplevel 2>/dev/null`);
+my $invocation_dir = $ENV{PWD} || getcwd();
## Configuration file handling
$CFG::hypervisor = "";
$CFG::hypervisor_params = "serial";
$CFG::genisoimage = "genisoimage";
-$CFG::iprelay_addr = '141.76.48.80:2324'; #'141.76.48.252';
-$CFG::qemu = 'qemu';
-@CFG::chainloaders = (); #('bin/boot/bender promisc'); # TODO: convert to option
-$CFG::pulsar_root = ''; # TODO: convert to option
-$CFG::pulsar_mac = '52-54-00-12-34-56';
+$CFG::qemu = 'qemu-system-i386 -cpu coreduo -smp 2';
+$CFG::default_target = 'qemu';
%CFG::targets = (
+ 'qemu' => '--qemu',
"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',
- "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=novaboot --iprelay=147.32.86.92:2324',
+ "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',
"localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog=" set root=\'(hd0,msdos1)\'"',
+ "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\""',
+ "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"',
+
);
-$CFG::scons = "scons -j2";
+chomp(my $nproc = `nproc`);
+$CFG::scons = "scons -j$nproc";
+$CFG::make = "make -j$nproc";
+
+my $builddir;
-my @qemu_flags = qw(-cpu coreduo -smp 2);
sub read_config($) {
my ($cfg) = @_;
{
die("ERROR: Failure processing '$cfg'");
}
}
- print "novaboot: Read $cfg\n";
+ $builddir = File::Spec->rel2abs($CFG::builddir, dirname($cfg)) if defined $CFG::builddir;
+ print STDERR "novaboot: Read $cfg\n";
}
my @cfgs;
# not to the novaboot script. The problem with this approach is that
# when a script is run as "novaboot <options> <script>" then $ARGV[0]
# contains the first option. Hence the -f check.
- my $dir = abs_path($invocation_dir . ((-f $ARGV[0]) ? '/'.dirname($ARGV[0]) : ''));
- while (-d $dir && $dir ne "/") {
+ my $dir = File::Spec->rel2abs($ARGV[0] && -f $ARGV[0] ? dirname($ARGV[0]) : '', $invocation_dir);
+ while ((-d $dir || -l $dir ) && $dir ne "/") {
push @cfgs, "$dir/.novaboot" if -r "$dir/.novaboot";
- $dir = abs_path($dir."/..");
+ my @dirs = File::Spec->splitdir($dir);
+ $dir = File::Spec->catdir(@dirs[0..$#dirs-1]);
}
}
my $cfg = $ENV{'NOVABOOT_CONFIG'};
## Command line handling
-my ($append, $bender, $builddir, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $iprelay, $iso_image, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $qemu, $qemu_append, $qemu_flags_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server);
+my $explicit_target;
+GetOptions ("target|t=s" => \$explicit_target);
+
+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);
$rsync_flags = '';
$rom_prefix = 'rom://';
+$stty = 'raw -crtscts -onlcr 115200';
+
+my @expect_seen = ();
+sub handle_expect
+{
+ my ($n, $v) = @_;
+ push(@expect_seen, '-re') if $n eq "expect-re";
+ push(@expect_seen, $v);
+}
+
+sub handle_send
+{
+ my ($n, $v) = @_;
+ unless (@expect_seen) { die("No --expect before --send"); }
+ my $ret = ($n eq "sendcont") ? exp_continue : 0;
+ unshift(@expect_raw, sub { shift->send(eval("\"$v\"")); $ret; });
+ unshift(@expect_raw, @expect_seen);
+ @expect_seen = ();
+}
Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
my %opt_spec;
%opt_spec = (
- "append|a=s" => \$append,
+ "amt=s" => \$amt,
+ "append|a=s" => \@append,
"bender|b" => \$bender,
- "build-dir=s" => \$builddir,
+ "build-dir=s" => sub { my ($n, $v) = @_; $builddir = File::Spec->rel2abs($v); },
"concat" => \$concat,
+ "chainloader=s" => \@chainloaders,
"dhcp-tftp|d" => \$dhcp_tftp,
"dump" => \$dump_opt,
"dump-config" => \$dump_config,
+ "exiton=s" => \@exiton,
+ "expect=s" => \&handle_expect,
+ "expect-re=s" => \&handle_expect,
+ "expect-raw=s" => sub { my ($n, $v) = @_; unshift(@expect_raw, eval($v)); },
+ "gen-only" => \$gen_only,
"grub|g:s" => \$grub_config,
"grub-preamble=s"=> \$grub_preamble,
"grub-prefix=s" => \$grub_prefix,
"grub2:s" => \$grub2_config,
"grub2-prolog=s" => \$grub2_prolog,
- "iprelay:s" => \$iprelay,
- "iso|i:s" => \$iso_image,
+ "ider" => \$ider,
+ "iprelay=s" => \$iprelay,
+ "iso:s" => \$iso_image,
+ "kernel|k=s" => \$kernel_opt,
+ "interactive|i" => \$interactive,
"name=s" => \$config_name_opt,
+ "make|m:s" => \$make,
"no-file-gen" => \$no_file_gen,
"off" => \$off_opt,
"on" => \$on_opt,
"pulsar|p:s" => \$pulsar,
- "qemu|Q=s" => \$qemu,
- "qemu-append:s" => \$qemu_append,
+ "pulsar-root=s" => \$pulsar_root,
+ "qemu|Q:s" => \$qemu,
+ "qemu-append=s" => \$qemu_append,
"qemu-flags|q=s" => \$qemu_flags_cmd,
+ "remote-cmd=s" => \$remote_cmd,
+ "remote-expect=s"=> \$remote_expect,
+ "reset-cmd=s" => \$reset_cmd,
"rsync-flags=s" => \$rsync_flags,
"scons:s" => \$scons,
"scriptmod=s" => \@scriptmod,
+ "send=s" => \&handle_send,
+ "sendcont=s" => \&handle_send,
"serial|s:s" => \$serial,
"server:s" => \$server,
"strip-rom" => sub { $rom_prefix = ''; },
- "target|t=s" => sub { my ($opt_name, $opt_value) = @_;
- exists $CFG::targets{$opt_value} or die("Unknown target '$opt_value' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
- GetOptionsFromString($CFG::targets{$opt_value}, %opt_spec); },
+ "stty=s" => \$stty,
+ "tftp" => \$tftp,
+ "tftp-port=i" => \$tftp_port,
+ "uboot:s" => \$uboot,
+ "uboot-init=s" => \@uboot_init,
"h" => \$help,
"help" => \$man,
);
-GetOptions %opt_spec or pod2usage(2);
-pod2usage(1) if $help;
-pod2usage(-exitstatus => 0, -verbose => 2) if $man;
-
-### Sanitize configuration
-
-$CFG::iprelay_addr = $ENV{'NOVABOOT_IPRELAY'} if $ENV{'NOVABOOT_IPRELAY'};
-if ($iprelay && $iprelay ne "on" && $iprelay ne "off") { $CFG::iprelay_addr = $iprelay; }
-if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
-
-if ($qemu) { $CFG::qemu = $qemu; }
-$qemu_append ||= '';
-
-if ($pulsar) {
- $CFG::pulsar_mac = $pulsar;
+# First process target options
+{
+ my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
+ if ($t) {
+ exists $CFG::targets{$t} or die("Unknown target '$t' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
+ GetOptionsFromString($CFG::targets{$t}, %opt_spec);
+ }
}
-if ($scons) { $CFG::scons = $scons; }
+# Then process other command line options - some of them may override
+# what was specified by the target
+GetOptions %opt_spec or die("Error in command line arguments");
+pod2usage(1) if $help;
+pod2usage(-exitstatus => 0, -verbose => 2) if $man;
### Dump sanitized configuration (if requested)
if ($dump_config) {
use Data::Dumper;
- $Data::Dumper::Indent=0;
+ $Data::Dumper::Indent=1;
print "# This file is in perl syntax.\n";
foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1)
if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); }
if ( @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); }
if ( %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); }
- print "\n";
}
print "1;\n";
exit;
}
-if (defined $serial) {
- $serial ||= "/dev/ttyUSB0";
-}
+### Sanitize configuration
-if (defined $grub_config) {
- $grub_config ||= "menu.lst";
+if ($interactive && !-t STDIN) {
+ die("novaboot: Interactive mode not supported when not on terminal");
}
-if (defined $grub2_config) {
- $grub2_config ||= "grub.cfg";
+if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
+
+if ($ider) {
+ $iso_image //= ''; # IDE-R needs an ISO image
+ if (!defined $amt) { die "Error: --ider requires --amt"; }
}
-if ($on_opt) { $iprelay="on"; }
-if ($off_opt) { $iprelay="off"; }
+# Default options
+if (defined $serial) {
+ $serial ||= "/dev/ttyUSB0";
+ $ENV{NB_SERIAL} = $serial;
+}
+if (defined $grub_config) { $grub_config ||= "menu.lst"; }
+if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
## Parse the novaboot script(s)
my @scripts;
my $file;
-my $line;
my $EOF;
my $last_fn = '';
my ($modules, $variables, $generated, $continuation);
-while (<>) {
+my $skip_reading = defined($on_opt) || defined($off_opt);
+while (!$skip_reading && ($_ = <>)) {
if ($ARGV ne $last_fn) { # New script
die "Missing EOF in $last_fn" if $file;
- die "Unfinished line in $last_fn" if $line;
+ die "Unfinished line in $last_fn" if $continuation;
$last_fn = $ARGV;
push @scripts, { 'filename' => $ARGV,
'modules' => $modules = [],
chomp();
next if /^#/ || /^\s*$/; # Skip comments and empty lines
- foreach my $mod(@scriptmod) { eval $mod; }
-
- print "$_\n" if $dump_opt;
+ $_ =~ s/^[[:space:]]*// if ($continuation);
- if (/^([A-Z_]+)=(.*)$/) { # Internal variable
- $$variables{$1} = $2;
+ if (/\\$/) { # Line continuation
+ $continuation .= substr($_, 0, length($_)-1);
next;
}
- if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
- push @$modules, "$1$2";
- $file = [];
- push @$generated, {filename => $1, content => $file};
- $EOF = $3;
- next;
+
+ if ($continuation) { # Last continuation line
+ $_ = $continuation . $_;
+ $continuation = '';
}
+
+ foreach my $mod(@scriptmod) { eval $mod; }
+
if ($file && $_ eq $EOF) { # Heredoc end
undef $file;
next;
push @{$file}, "$_\n";
next;
}
- $_ =~ s/^[[:space:]]*// if ($continuation);
- if (/\\$/) { # Line continuation
- $line .= substr($_, 0, length($_)-1);
- $continuation = 1;
+ if (/^([A-Z_]+)=(.*)$/) { # Internal variable
+ $$variables{$1} = $2;
+ push(@exiton, $2) if ($1 eq "EXITON");
+ next;
+ }
+ if (s/^load *//) { # Load line
+ die("novaboot: '$last_fn' line $.: Missing file name\n") unless /^[^ <]+/;
+ if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
+ push @$modules, "$1$2";
+ $file = [];
+ push @$generated, {filename => $1, content => $file};
+ $EOF = $3;
+ next;
+ }
+ if (/^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
+ push @$modules, "$1$2";
+ push @$generated, {filename => $1, command => $3};
+ next;
+ }
+ push @$modules, $_;
next;
}
- $continuation = 0;
- $line .= $_;
- $line .= " $append" if ($append && scalar(@$modules) == 0);
-
- if ($line =~ /^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
- push @$modules, "$1$2";
- push @$generated, {filename => $1, command => $3};
- $line = '';
+ if (/^run (.*)/) { # run line
+ push @$generated, {command => $1};
next;
}
- push @$modules, $line;
- $line = '';
+
+ die("novaboot: Cannot parse script '$last_fn' line $.. Didn't you forget 'load' keyword?\n");
+}
+# use Data::Dumper;
+# print Dumper(\@scripts);
+
+foreach my $script (@scripts) {
+ $modules = $$script{modules};
+ @$modules[0] =~ s/^[^ ]*/$kernel_opt/ if $kernel_opt;
+ @$modules[0] .= ' ' . join(' ', @append) if @append;
+
+ my $kernel;
+ if (exists $variables->{KERNEL}) {
+ $kernel = $variables->{KERNEL};
+ } else {
+ if ($CFG::hypervisor) {
+ $kernel = $CFG::hypervisor . " ";
+ if (exists $variables->{HYPERVISOR_PARAMS}) {
+ $kernel .= $variables->{HYPERVISOR_PARAMS};
+ } else {
+ $kernel .= $CFG::hypervisor_params;
+ }
+ }
+ }
+ @$modules = ($kernel, @$modules) if $kernel;
+ @$modules = (@chainloaders, @$modules);
+ @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
}
-#use Data::Dumper;
-#print Dumper(\@scripts);
-exit if $dump_opt;
+if ($dump_opt) {
+ foreach my $script (@scripts) {
+ print join("\n", @{$$script{modules}})."\n";
+ }
+ exit(0);
+}
## Helper functions
print "novaboot: Created $fn\n";
} elsif (exists $$g{command} && ! $no_file_gen) {
$ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
- system_verbose("( $$g{command} ) > $$g{filename}");
+ if (exists $$g{filename}) {
+ system_verbose("( $$g{command} ) > $$g{filename}");
+ } else {
+ system_verbose($$g{command});
+ }
}
}
}
if ($base) { $base = "$base/"; };
open(my $fg, '>', $filename) or die "$filename: $!";
print $fg "$preamble\n" if $preamble;
- my $endmark = ($serial || defined $iprelay) ? ';' : '';
- print $fg "title $title$endmark\n" if $title;
+ print $fg "title $title\n" if $title;
#print $fg "root $base\n"; # root doesn't really work for (nd)
my $first = 1;
foreach (@$modules_ref) {
}
}
close($fg);
- print("novaboot: Created $CFG::builddir/$filename\n");
+ print("novaboot: Created $builddir/$filename\n");
+ return $filename;
+}
+
+sub generate_syslinux_config($$$$)
+{
+ my ($filename, $title, $base, $modules_ref) = @_;
+ if ($base && $base !~ /\/$/) { $base = "$base/"; };
+ open(my $fg, '>', $filename) or die "$filename: $!";
+ print $fg "LABEL $title\n";
+ #TODO print $fg "MENU LABEL $human_readable_title\n";
+
+ my ($kbin, $kcmd) = split(' ', @$modules_ref[0], 2);
+
+ if (system("file $kbin|grep 'Linux kernel'") == 0) {
+ my $initrd = @$modules_ref[1];
+ die('To many "load" lines for Linux kernel') if (scalar @$modules_ref > 2);
+ print $fg "LINUX $base$kbin\n";
+ print $fg "APPEND $kcmd\n";
+ print $fg "INITRD $base$initrd\n";
+ } else {
+ print $fg "KERNEL mboot.c32\n";
+ my @append;
+ foreach (@$modules_ref) {
+ s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
+ push @append, "$base$_";
+ print $fg "APPEND ".join(' --- ', @append)."\n";
+ }
+ }
+ #TODO print $fg "TEXT HELP\n";
+ #TODO print $fg "some help here\n";
+ #TODO print $fg "ENDTEXT\n";
+ close($fg);
+ print("novaboot: Created $builddir/$filename\n");
return $filename;
}
if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
open(my $fg, '>', $filename) or die "$filename: $!";
print $fg "$preamble\n" if $preamble;
- my $endmark = ($serial || defined $iprelay) ? ';' : '';
$title ||= 'novaboot';
- print $fg "menuentry $title$endmark {\n";
+ print $fg "menuentry $title {\n";
print $fg "$prolog\n" if $prolog;
my $first = 1;
foreach (@$modules_ref) {
}
print $fg "}\n";
close($fg);
- print("novaboot: Created $CFG::builddir/$filename\n");
+ print("novaboot: Created $builddir/$filename\n");
return $filename;
}
{
my ($filename, $modules_ref) = @_;
open(my $fg, '>', $filename) or die "$filename: $!";
- print $fg "root $CFG::pulsar_root\n" if $CFG::pulsar_root;
+ print $fg "root $pulsar_root\n" if defined $pulsar_root;
my $first = 1;
my ($kbin, $kcmd);
foreach (@$modules_ref) {
# Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
print $fg "exec $kbin $kcmd\n";
close($fg);
- print("novaboot: Created $CFG::builddir/$filename\n");
+ print("novaboot: Created $builddir/$filename\n");
return $filename;
}
+sub shell_cmd_string(@)
+{
+ return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
+}
+
sub exec_verbose(@)
{
- print "novaboot: Running: ".join(' ', map("'$_'", @_))."\n";
+ print "novaboot: Running: ".shell_cmd_string(@_)."\n";
exec(@_);
+ exit(1); # should not be reached
}
sub system_verbose($)
print "Testing \"all\" in $last_fn:\n";
}
-## Handle reset and power on/off
+## Connect to the target and check whether it is not occupied
+
+# We have to do this before file generation phase, because file
+# generation is intermixed with file deployment phase and we want to
+# check whether the target is not used by somebody else before
+# deploying files. Otherwise, we may rewrite other user's files on a
+# boot server.
+
+my $exp; # Expect object to communicate with the target over serial line
+
+my ($target_reset, $target_power_on, $target_power_off);
+my ($amt_user, $amt_password, $amt_host, $amt_port);
-my $IPRELAY;
if (defined $iprelay) {
- $CFG::iprelay_addr =~ /([.0-9]+)(:([0-9]+))?/;
+ my $IPRELAY;
+ $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
my $addr = $1;
my $port = $3 || 23;
my $paddr = sockaddr_in($port, inet_aton($addr));
print "novaboot: Connecting to IP relay... ";
connect($IPRELAY, $paddr) || die "connect: $!";
print "done\n";
- $IPRELAY->autoflush(1);
+ $exp = Expect->init(\*$IPRELAY);
+ $exp->log_stdout(1);
while (1) {
- print $IPRELAY "\xFF\xF6";
- alarm(20);
- local $SIG{ALRM} = sub { die "Relay AYT timeout"; };
- my $ayt_reponse = "";
- my $read = sysread($IPRELAY, $ayt_reponse, 100);
- alarm(0);
-
- chomp($ayt_reponse);
- print "$ayt_reponse\n";
- if ($ayt_reponse =~ /<iprelayd: not connected/) {
- sleep(10);
- next;
- }
- last;
+ print $exp "\xFF\xF6"; # AYT
+ my $connected = $exp->expect(20, # Timeout in seconds
+ '<iprelayd: connected>',
+ '-re', '<WEB51 HW[^>]*>');
+ last if $connected;
}
sub relaycmd($$) {
sub relay($$;$) {
my ($relay, $onoff, $can_giveup) = @_;
my $confirmation = '';
- print $IPRELAY relaycmd($relay, $onoff);
-
- # We use non-blocking I/O and polling here because for some
- # reason read() on blocking FD returns only after all
- # requested data is available. If we get during the first
- # read() only a part of confirmation, we may get the rest
- # after the system boots and print someting, which may be too
- # long.
- $IPRELAY->blocking(0);
-
- alarm(20); # Timeout in seconds
- my $giveup = 0;
- local $SIG{ALRM} = sub {
- if ($can_giveup) { print("Relay confirmation timeout - ignoring\n"); $giveup = 1;}
- else {die "Relay confirmation timeout";}
- };
- my $index;
- while (($index=index($confirmation, relayconf($relay, $onoff))) < 0 && !$giveup) {
- my $read = read($IPRELAY, $confirmation, 70, length($confirmation));
- if (!defined($read)) {
- die("IP relay: $!") unless $! == EAGAIN;
- usleep(10000);
- next;
+ $exp->log_stdout(0);
+ print $exp relaycmd($relay, $onoff);
+ my $confirmed = $exp->expect(20, # Timeout in seconds
+ relayconf($relay, $onoff));
+ if (!$confirmed) {
+ if ($can_giveup) {
+ print("Relay confirmation timeout - ignoring\n");
+ } else {
+ die "Relay confirmation timeout";
}
- #use MIME::QuotedPrint;
- #print "confirmation = ".encode_qp($confirmation)."\n";
}
- alarm(0);
- $IPRELAY->blocking(1);
+ $exp->log_stdout(1);
}
-}
-if ($iprelay && ($iprelay eq "on" || $iprelay eq "off")) {
- relay(1, 1); # Press power button
- if ($iprelay eq "on") {
- usleep(100000); # Short press
- } else {
+ $target_reset = sub {
+ relay(2, 1, 1); # Reset the machine
+ usleep(100000);
+ relay(2, 0);
+ };
+
+ $target_power_off = sub {
+ relay(1, 1); # Press power button
usleep(6000000); # Long press to switch off
- }
- print $IPRELAY relay(1, 0);
- exit;
+ relay(1, 0);
+ };
+
+ $target_power_on = sub {
+ relay(1, 1); # Press power button
+ usleep(100000); # Short press
+ relay(1, 0);
+ };
+}
+elsif ($serial) {
+ my $CONN;
+ system_verbose("stty -F $serial $stty");
+ open($CONN, "+<", $serial) || die "open $serial: $!";
+ $exp = Expect->init(\*$CONN);
+}
+elsif ($remote_cmd) {
+ print "novaboot: Running: $remote_cmd\n";
+ $exp = Expect->spawn($remote_cmd);
}
+elsif (defined $amt) {
+ require LWP::UserAgent;
+ require LWP::Authen::Digest;
+
+ sub genXML {
+ my ($host, $username, $password, $schema, $className, $pstate) = @_;
+ #AMT numbers for PowerStateChange (MNI => bluescreen on windows;-)
+ my %pstates = ("on" => 2,
+ "standby" => 4,
+ "hibernate" => 7,
+ "off" => 8,
+ "reset" => 10,
+ "MNI" => 11);
+ return <<END;
+ <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">
+ <s:Header><a:To>http://$host:16992/wsman</a:To>
+ <w:ResourceURI s:mustUnderstand="true">$schema</w:ResourceURI>
+ <a:ReplyTo><a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address></a:ReplyTo>
+ <a:Action s:mustUnderstand="true">$schema$className</a:Action>
+ <w:MaxEnvelopeSize s:mustUnderstand="true">153600</w:MaxEnvelopeSize>
+ <a:MessageID>uuid:709072C9-609C-4B43-B301-075004043C7C</a:MessageID>
+ <w:Locale xml:lang="en-US" s:mustUnderstand="false" />
+ <w:OperationTimeout>PT60.000S</w:OperationTimeout>
+ <w:SelectorSet><w:Selector Name="Name">Intel(r) AMT Power Management Service</w:Selector></w:SelectorSet>
+ </s:Header><s:Body>
+ <p:RequestPowerStateChange_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService">
+ <p:PowerState>$pstates{$pstate}</p:PowerState>
+ <p:ManagedElement><a:Address>http://$host:16992/wsman</a:Address>
+ <a:ReferenceParameters><w:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</w:ResourceURI>
+ <w:SelectorSet><w:Selector Name="Name">ManagedSystem</w:Selector></w:SelectorSet>
+ </a:ReferenceParameters></p:ManagedElement>
+ </p:RequestPowerStateChange_INPUT>
+ </s:Body></s:Envelope>
+END
+ }
-## Figure out the location of builddir and chdir() there
-if ($builddir) {
- $CFG::builddir = $builddir;
-} else {
- if (! defined $CFG::builddir) {
- $CFG::builddir = ( $gittop || $ENV{'HOME'}."/nul" ) . "/build";
- if (! -d $CFG::builddir) {
- $CFG::builddir = $ENV{SRCDIR} = dirname(File::Spec->rel2abs( ${$scripts[0]}{filename}, $invocation_dir ));
- }
+ sub sendPOST {
+ my ($host, $username, $password, $content) = @_;
+
+ my $ua = LWP::UserAgent->new();
+ $ua->agent("novaboot");
+
+ my $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
+ my $res = $ua->request($req);
+ die ("Unexpected AMT response: " . $res->status_line) unless $res->code == 401;
+
+ my ($realm) = $res->header("WWW-Authenticate") =~ /Digest realm="(.*?)"/;
+ $ua->credentials("$host:16992", $realm, $username => $password);
+
+ # Create a request
+ $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
+ $req->content_type('application/x-www-form-urlencoded');
+ $req->content($content);
+ $res = $ua->request($req);
+ die ("AMT power change request failed: " . $res->status_line) unless $res->is_success;
+ $res->content() =~ /<g:ReturnValue>(\d+)<\/g:ReturnValue>/;
+ return $1;
+ }
+
+ sub powerChange {
+ my ($host, $username, $password, $pstate)=@_;
+ my $schema="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService";
+ my $className="/RequestPowerStateChange";
+ my $content = genXML($host, $username, $password ,$schema, $className, $pstate);
+ return sendPOST($host, $username, $password, $content);
}
+
+ ($amt_user,$amt_password,$amt_host,$amt_port) = ($amt =~ /(?:(.*?)(?::(.*))?@)?([^:]*)(?::([0-9]*))?/);;
+ $amt_user ||= "admin";
+ $amt_password ||= $ENV{'AMT_PASSWORD'} || die "AMT password not specified";
+ $amt_host || die "AMT host not specified";
+ $amt_port ||= 16994;
+
+
+ $target_power_off = sub {
+ $exp->close();
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "off");
+ die "AMT power off failed (ReturnValue $result)" if $result != 0;
+ };
+
+ $target_power_on = sub {
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "on");
+ die "AMT power on failed (ReturnValue $result)" if $result != 0;
+ };
+
+ $target_reset = sub {
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "reset");
+ if ($result != 0) {
+ print STDERR "Warning: Cannot reset $amt_host, trying power on. ";
+ $result = powerChange($amt_host,$amt_user,$amt_password, "on");
+ }
+ die "AMT reset failed (ReturnValue $result)" if $result != 0;
+ };
+
+ my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port";
+ print "novaboot: Running: $cmd\n" =~ s/\Q$amt_password\E/???/r;
+ $exp = Expect->spawn($cmd);
+ $exp->expect(10, "RUN_SOL") || die "Expect for 'RUN_SOL' timed out";
+
+}
+
+
+if ($remote_expect) {
+ $exp->expect(10, $remote_expect) || die "Expect for '$remote_expect' timed out";
+}
+
+if (defined $reset_cmd) {
+ $target_reset = sub {
+ system_verbose($reset_cmd);
+ };
+}
+
+if (defined $on_opt && defined $target_power_on) {
+ &$target_power_on();
+ exit;
+}
+if (defined $off_opt && defined $target_power_off) {
+ print "novaboot: Switching the target off...\n";
+ &$target_power_off();
+ exit;
}
-chdir($CFG::builddir) or die "Can't change directory to $CFG::builddir: $!";
-print "novaboot: Entering directory `$CFG::builddir'\n";
+$builddir ||= dirname(File::Spec->rel2abs( ${$scripts[0]}{filename})) if scalar @scripts;
+if (defined $builddir) {
+ chdir($builddir) or die "Can't change directory to $builddir: $!";
+ print "novaboot: Entering directory `$builddir'\n";
+}
## File generation phase
-my (%files_iso, $menu_iso, $config_name, $filename);
+my (%files_iso, $menu_iso, $filename);
+my $config_name = '';
foreach my $script (@scripts) {
$filename = $$script{filename};
($config_name = $filename) =~ s#.*/##;
$config_name = $config_name_opt if (defined $config_name_opt);
- my $kernel;
- if (exists $variables->{KERNEL}) {
- $kernel = $variables->{KERNEL};
- } else {
- if ($CFG::hypervisor) {
- $kernel = $CFG::hypervisor . " ";
- if (exists $variables->{HYPERVISOR_PARAMS}) {
- $kernel .= $variables->{HYPERVISOR_PARAMS};
- } else {
- $kernel .= $CFG::hypervisor_params;
- }
- }
+ if (exists $variables->{BUILDDIR}) {
+ $builddir = File::Spec->rel2abs($variables->{BUILDDIR});
+ chdir($builddir) or die "Can't change directory to $builddir: $!";
+ print "novaboot: Entering directory `$builddir'\n";
}
- @$modules = ($kernel, @$modules) if $kernel;
- @$modules = (@CFG::chainloaders, @$modules);
- @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
my $prefix;
($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
- $prefix ||= $CFG::builddir;
+ $prefix ||= $builddir;
# TODO: use $grub_prefix as first parameter if some switch is given
generate_configs('', $generated, $filename);
### Generate bootloader configuration files
- my $pulsar_config;
my @bootloader_configs;
push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
- push @bootloader_configs, generate_pulsar_config($pulsar_config = "config-$CFG::pulsar_mac", $modules) if (defined $pulsar);
+ push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
-### Run scons
- if (defined $scons) {
+### Run scons or make
+ {
my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
# Filter-out generated files
- my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
- system_verbose($CFG::scons." ".join(" ", @to_build));
+ my @to_build = grep({ my $file = $_; !scalar(grep($file eq ($$_{filename} || ''), @$generated)) } @files);
+
+ system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
+ system_verbose($make || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
}
### Copy files (using rsync)
- if (defined $server) {
+ if (defined $server && !defined($gen_only)) {
(my $real_server = $server) =~ s/\$NAME/$config_name/;
my ($hostname, $path) = split(":", $real_server, 2);
if (defined $iso_image) {
generate_configs("(cd)", $generated, $filename);
my $menu;
- generate_grub_config(\$menu, $config_name, "(cd)", $modules);
+ generate_syslinux_config(\$menu, $config_name, "/", $modules);
$menu_iso .= "$menu\n";
map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
}
## Generate ISO image
if (defined $iso_image) {
- open(my $fh, ">menu-iso.lst");
- print $fh "timeout 5\n\n$menu_iso";
+ system_verbose("mkdir -p isolinux");
+ system_verbose('cp /usr/lib/syslinux/isolinux.bin /usr/lib/syslinux/mboot.c32 /usr/lib/syslinux/menu.c32 isolinux');
+ open(my $fh, ">isolinux/isolinux.cfg");
+ if ($#scripts) {
+ print $fh "TIMEOUT 50\n";
+ print $fh "DEFAULT menu\n";
+ } else {
+ print $fh "DEFAULT $config_name\n";
+ }
+ print $fh "$menu_iso";
close($fh);
- my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
+
+ my $files = join(" ", map("$_=$_", (keys(%files_iso), 'isolinux/isolinux.bin', 'isolinux/isolinux.cfg', 'isolinux/mboot.c32', 'isolinux/menu.c32')));
$iso_image ||= "$config_name.iso";
- system_verbose("$CFG::genisoimage -R -b stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table -hide-rr-moved -J -joliet-long -o $iso_image -graft-points bin/boot/grub/ $files");
- print("ISO image created: $CFG::builddir/$iso_image\n");
+
+ # Note: We use -U flag below to "Allow 'untranslated' filenames,
+ # completely violating the ISO9660 standards". Without this
+ # option, isolinux is not able to read files names for example
+ # bzImage-3.0.
+ 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");
+ print("ISO image created: $builddir/$iso_image\n");
}
+exit(0) if defined $gen_only;
+
## Boot the system using various methods and send serial output to stdout
if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
return $str
}
-### Qemu
+### Start in Qemu
-if (!(defined $dhcp_tftp || defined $serial || defined $iprelay || defined $server || defined $iso_image)) {
+if (defined $qemu) {
# Qemu
- if (!$qemu && $variables->{QEMU}) {
- @qemu_flags = split(" ", $variables->{QEMU});
- $CFG::qemu = shift(@qemu_flags);
- }
+ $qemu ||= $variables->{QEMU} || $CFG::qemu;
+ my @qemu_flags = split(" ", $qemu);
+ $qemu = shift(@qemu_flags);
@qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
@qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
- push(@qemu_flags, split(/ +/, trim($qemu_append)));
+ push(@qemu_flags, split(/ +/, trim($qemu_append || '')));
if (defined $iso_image) {
# Boot NOVA with grub (and test the iso image)
- push(@qemu_flags, ('-cdrom', "$config_name.iso"));
+ push(@qemu_flags, ('-cdrom', $iso_image));
} else {
# Boot NOVA without GRUB
foreach (@$modules) {s/,/+/g;}
generate_configs("", $generated, $filename);
- my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
- $kcmd = '' if !defined $kcmd;
- my $dtb;
- @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
- my $initrd = join ",", @$modules;
+ if (scalar @$modules) {
+ my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
+ $kcmd = '' if !defined $kcmd;
+ my $dtb;
+ @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
+ my $initrd = join ",", @$modules;
- push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
- push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
- push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
+ push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
+ push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
+ push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
+ }
}
push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
- exec_verbose(($CFG::qemu, '-name', $config_name, @qemu_flags));
+ unshift(@qemu_flags, ('-name', $config_name));
+ print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
+ $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
}
### Local DHCPD and TFTPD
my ($dhcpd_pid, $tftpd_pid);
+$tftp=1 if $tftp_port;
+
if (defined $dhcp_tftp)
{
generate_configs("(nd)", $generated, $filename);
sudo ip l set dev eth0 up;
sudo touch dhcpd.leases");
+ # We run servers by forking ourselves, because the servers end up
+ # in our process group and get killed by signals sent to the
+ # process group (e.g. Ctrl-C on terminal).
$dhcpd_pid = fork();
- if ($dhcpd_pid == 0) {
- # This way, the spawned server are killed when this script is killed.
- exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid");
- }
- $tftpd_pid = fork();
- if ($tftpd_pid == 0) {
- exec_verbose("sudo in.tftpd --foreground --secure -v -v -v $CFG::builddir");
+ exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
+}
+
+if (defined $dhcp_tftp || defined $tftp) {
+ $tftp_port ||= 69;
+ # Unfortunately, tftpd requires root privileges even with
+ # non-privileged (>1023) port due to initgroups().
+ system_verbose("sudo in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid --address :$tftp_port $builddir");
+
+ # Kill server when we die
+ $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid') if (defined $dhcp_tftp);
+ system_verbose('sudo pkill --pidfile=tftpd.pid'); };
+
+ # We have to kill tftpd explicitely, because it is not in our process group
+ $SIG{INT} = sub { system_verbose('sudo pkill --pidfile=tftpd.pid'); exit(0); };
+}
+
+### AMT IDE-R
+if (defined $ider) {
+ my $ider_cmd= "amtider -c $iso_image -u $amt_user -p $amt_password $amt_host $amt_port" ;
+ print "novaboot: Running: $ider_cmd\n" =~ s/\Q$amt_password\E/???/r;
+ my $ider_pid = fork();
+ if ($ider_pid == 0) {
+ exec($ider_cmd);
+ die "IDE redirection failed";
}
- $SIG{TERM} = sub { print "CHILDS KILLED\n"; kill 15, $dhcpd_pid, $tftpd_pid; };
+ # FIXME: This collides with --tftp option. Hopefully, nobody needs
+ # to use both simultaneously.
+ $SIG{__DIE__} = sub { system_verbose('kill $ider_pid'); };
}
-### Serial line or IP relay
+### Reset target (IP relay, AMT, ...)
-if ($serial || defined $iprelay) {
- my $CONN;
- if (defined $iprelay) {
- print "novaboot: Reseting the test box... ";
- relay(2, 1, 1); # Reset the machine
- usleep(100000);
- relay(2, 0);
- print "done\n";
+if (defined $target_reset) {
+ print "novaboot: Reseting the test box... ";
+ &$target_reset();
+ print "done\n";
+}
- $CONN = $IPRELAY;
- } elsif ($serial) {
- system("stty -F $serial raw -crtscts -onlcr 115200");
- open($CONN, "+<", $serial) || die "open $serial: $!";
- $CONN->autoflush(1);
+### U-boot conversation
+if (defined $uboot) {
+ my $uboot_prompt = $uboot || '=> ';
+ print "novaboot: Waiting for uBoot prompt...\n";
+ $exp->log_stdout(1);
+ #$exp->exp_internal(1);
+ $exp->expect(20,
+ [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
+ $uboot_prompt) || die "No uBoot prompt deteceted";
+ while (@uboot_init) {
+ my $cmd = shift @uboot_init;
+ $exp->send("$cmd\n");
+ $exp->expect(10, $uboot_prompt) || die "uBoot prompt timeout";
+ }
+
+ # Boot the system if there are some load lines in the script
+ if (scalar(@$modules) > 0) {
+ my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
+ my $dtb;
+ @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
+ my $initrd = shift @$modules;
+
+ my $kern_addr = '800000';
+ my $initrd_addr = '-';
+ my $dtb_addr = '';
+
+ $exp->send("tftp $kern_addr $kbin\n");
+ $exp->expect(10,
+ [qr/#/, sub { exp_continue; }],
+ $uboot_prompt) || die "Kernel load failed";
+ if (defined $dtb) {
+ $dtb_addr = '7f0000';
+ $exp->send("tftp $dtb_addr $dtb\n");
+ $exp->expect(10,
+ [qr/#/, sub { exp_continue; }],
+ $uboot_prompt) || die "Device tree load failed";
+ }
+ if (defined $initrd) {
+ $initrd_addr = 'b00000';
+ $exp->send("tftp $initrd_addr $initrd\n");
+ $exp->expect(10,
+ [qr/#/, sub { exp_continue; }],
+ $uboot_prompt) || die "Initrd load failed";
+ }
+ $exp->send("set bootargs '$kcmd'\n");
+ $exp->expect(5, $uboot_prompt) || die "uBoot prompt timeout";
+ $exp->send("bootm $kern_addr $initrd_addr $dtb_addr\n");
+ $exp->expect(5, "\n") || die "uBoot command timeout";
}
+}
- # Pass the NOVA output to stdout.
- while (<$CONN>) {
- print;
+### Serial line interaction
+if (defined $exp) {
+ # Serial line of the target is available
+ my $interrupt = 'Ctrl-C';
+ if ($interactive && !@exiton) {
+ $interrupt = '"~~."';
+ }
+ my $note = (-t STDIN) ? '' : '- only target->host ';
+ print "novaboot: Serial line interaction $note(press $interrupt to interrupt)...\n";
+ $exp->log_stdout(1);
+ if (@exiton) {
+ $exp->expect(undef, @expect_raw, @exiton);
+ } else {
+ my @inputs = ($exp);
+ if (-t STDIN) { # Set up bi-directional communication if we run on terminal
+ my $infile = new IO::File;
+ $infile->IO::File::fdopen(*STDIN,'r');
+ my $in_object = Expect->exp_init($infile);
+ $in_object->set_group($exp);
+
+ if ($interactive) {
+ $in_object->set_seq('~~\.', sub { print "novaboot: Escape sequence detected\r\n"; undef; });
+ $in_object->manual_stty(0); # Use raw terminal mode
+ } else {
+ $in_object->manual_stty(1); # Do not modify terminal settings
+ }
+ push(@inputs, $in_object);
+ }
+ #use Data::Dumper;
+ #print Dumper(\@expect_raw);
+ $exp->expect(undef, @expect_raw) if @expect_raw;
+ Expect::interconnect(@inputs) unless defined($exp->exitstatus);
}
- kill 15, $dhcpd_pid, $tftpd_pid if ($dhcp_tftp);
- exit;
}
-### Wait for dhcpc or tftpd
-if (defined $dhcp_tftp) {
- my $pid = wait();
- if ($pid == $dhcpd_pid) { print "dhcpd exited!\n"; }
- elsif ($pid == $tftpd_pid) { print "tftpd exited!\n"; }
- else { print "wait returned: $pid\n"; }
- kill(15, 0); # Kill current process group i.e. all remaining children
+## Kill dhcpc or tftpd
+if (defined $dhcp_tftp || defined $tftp) {
+ die("novaboot: This should kill servers on background\n");
}
## Documentation
=head1 SYNOPSIS
-B<novaboot> [ options ] [--] script...
+B<novaboot> --help
-B<./script> [ options ]
+B<novaboot> [option]... [--] script...
-B<novaboot> --help
+B<./script> [option]...
=head1 DESCRIPTION
-This program makes it easier to boot NOVA or other operating system
-(OS) in different environments. It reads a so called novaboot script
-and uses it either to boot the OS in an emulator (e.g. in qemu) or to
-generate the configuration for a specific bootloader and optionally to
-copy the necessary binaries and other needed files to proper
-locations, perhaps on a remote server. In case the system is actually
-booted, its serial output is redirected to standard output if that is
-possible.
-
-A typical way of using novaboot is to make the novaboot script
+This program makes booting of an operating system (e.g. NOVA or Linux)
+as simple as running a local program. It facilitates booting on local
+or remote hosts or in emulators such as qemu. Novaboot operation is
+controlled by command line options and by a so called novaboot script,
+which can be thought as a generalization of bootloader configuration
+files (see L</"NOVABOOT SCRIPT SYNTAX">). Based on this input,
+novaboot setups everything for the target host to boot the desired
+configuration, i.e. it generates the bootloader configuration file in
+the proper format, deploy the binaries and other needed files to
+required locations, perhaps on a remote boot server and reset the
+target host. Finally, target host's serial output is redirected to
+standard output if that is possible.
+
+Typical way of using novaboot is to make the novaboot script
executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
booting a particular OS configuration becomes the same as executing a
local program - the novaboot script.
-With C<novaboot> you can:
+For example, with C<novaboot> you can:
=over 3
Run an OS in Qemu. This is the default action when no other action is
specified by command line switches. Thus running C<novaboot ./script>
(or C<./script> as described above) will run Qemu and make it boot the
-configuration specified in the I<script>.
+configuration specified in the F<script>.
=item 2.
Create a bootloader configuration file (currently supported
-bootloaders are GRUB, GRUB2 and Pulsar) and copy it with all other
-files needed for booting to another, perhaps remote, location.
+bootloaders are GRUB, GRUB2, Pulsar and U-Boot) and copy it with all
+other files needed for booting to a remote boot server.
- ./script --server --iprelay
+ ./script --server=192.168.1.1:/tftp --iprelay=192.168.1.2
-This command copies files to a TFTP server specified in the
-configuration file and uses TCP/IP-controlled relay to reset the test
-box and receive its serial output.
+This command copies files to the TFTP server and uses
+TCP/IP-controlled relay to reset the target host and receive its
+serial output.
=item 3.
-Run DHCP and TFTP server on developer's machine to PXE-boot NOVA from
-it. E.g.
+Run DHCP and TFTP server on developer's machine to PXE-boot the target
+host from it. E.g.
./script --dhcp-tftp
=back
+Note that the options needed for a specific target can be stored in a
+L</"CONFIGURATION FILE">. Then it is sufficient to use only the B<-t>
+option to specify the name of the target.
+
=head1 PHASES AND OPTIONS
-Novaboot perform its work in several phases. Each phase can be
+Novaboot performs its work in several phases. Each phase can be
influenced by several options, certain phases can be skipped. The list
-of phases with the corresponding options follows.
+of phases (in the execution order) and the corresponding options
+follow.
=head2 Configuration reading phase
=over 8
-=item -c, --config=<filename>
+=item -c, --config=I<filename>
Use the specified configuration file instead of the default one(s).
Print short (B<-h>) or long (B<--help>) help.
-=item -t, --target=<target>
+=item -t, --target=I<target>
This option serves as a user configurable shortcut for other novaboot
options. The effect of this option is the same as the options stored
=over 8
-=item -a, --append=<parameters>
+=item -a, --append=I<parameters>
-Appends a string to the first "filename" line in the novaboot script.
-This can be used to append parameters to the kernel's or root task's
-command line.
+Append a string to the first C<load> line in the novaboot script. This
+can be used to append parameters to the kernel's or root task's
+command line. Can appear multiple times.
=item -b, --bender
ports and stores the information about them in the BIOS data area for
use by the kernel.
+=item --chainloader=I<chainloader>
+
+Chainloader that is loaded before the kernel and other files specified
+in the novaboot script. E.g. 'bin/boot/bender promisc'.
+
=item --dump
-Prints the content of the novaboot script after removing comments and
-evaluating all I<--scriptmod> expressions. Exit after reading (and
-dumping) the script.
+Print the modules to boot and their parameters after this phase
+finishes. Then exit. This is useful for seeing the effect of other
+options in this section.
+
+=item -k, --kernel=F<file>
+
+Replace the first word on the first C<load> line in the novaboot
+script with F<file>.
=item --scriptmod=I<perl expression>
is given multiple times all expressions are evaluated in the command
line order.
-=item --strip-rom
-
-Strip I<rom://> prefix from command lines and generated config files.
-The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
-
=back
=head2 File generation phase
In this phase, files needed for booting are generated in a so called
-I<build directory> (see TODO). In most cases configuration for a
-bootloader is generated automatically by novaboot. It is also possible
-to generate other files using I<heredoc> or I<"<"> syntax in novaboot
-scripts. Finally, binaries can be generated in this phases by running
-C<scons> or C<make>.
+I<build directory> (see L</--build-dir>). In most cases configuration
+for a bootloader is generated automatically by novaboot. It is also
+possible to generate other files using I<heredoc> or I<"<"> syntax in
+novaboot scripts. Finally, binaries can be generated in this phases by
+running C<scons> or C<make>.
=over 8
-=item --build-dir=<directory>
+=item --build-dir=I<directory>
Overrides the default build directory location.
-The default build directory location is determined as follows:
+The default build directory location is determined as follows: If the
+configuration file defines the C<$builddir> variable, its value is
+used. Otherwise, it is the directory that contains the first processed
+novaboot script.
-If there is a configuration file, the value specified in the
-I<$builddir> variable is used. Otherwise, if the current working
-directory is inside git work tree and there is F<build> directory at
-the top of that tree, it is used. Otherwise, if directory
-F<~/nul/build> exists, it is used. Otherwise, it is the directory that
-contains the first processed novaboot script.
+See also L</BUILDDIR> variable.
=item -g, --grub[=I<filename>]
=item --grub2-prolog=I<prolog>
-Specifies text I<preable> that is put at the begiging of the entry
+Specifies text I<preable> that is put at the beginning of the entry
GRUB2 entry.
+=item -m, --make[=make command]
+
+Runs C<make> to build files that are not generated by novaboot itself.
+
=item --name=I<string>
Use the name I<string> instead of the name of the novaboot script.
=item --no-file-gen
-Do not generate files on the fly (i.e. "<" syntax) except for the
-files generated via "<<WORD" syntax.
+Do not run external commands to generate files (i.e. "<" syntax and
+C<run> keyword). This switch does not influence generation of files
+specified with "<<WORD" syntax.
=item -p, --pulsar[=mac]
-Generates pulsar bootloader configuration file whose name is based on
-the MAC address specified either on the command line or taken from
-I<.novaboot> configuration file.
+Generates pulsar bootloader configuration file named F<config-I<mac>>
+The I<mac> string is typically a MAC address and defaults to
+I<novaboot>.
+
+=item --scons[=scons command]
+
+Runs C<scons> to build files that are not generated by novaboot
+itself.
+
+=item --strip-rom
+
+Strip I<rom://> prefix from command lines and generated config files.
+The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
+
+=item --gen-only
+
+Exit novaboot after file generation phase.
+
+=back
+
+=head2 Target connection check
+
+If supported by the target, the connection to it is made and it is
+checked whether the target is not occupied by another novaboot
+user/instance.
+
+=over 8
+
+=item --amt=I<"[user[:password]@]host[:port]>
+
+Use Intel AMT technology to control the target machine. WS management
+is used to powercycle it and Serial-Over-Lan (SOL) for input/output.
+The hostname or (IP address) is given by the I<host> parameter. If
+I<password> is not specified, environment variable AMT_PASSWORD is
+used. The I<port> specifies a TCP port for SOL. If not specified, the
+default is 16992. Default I<user> is admin.
+
+=item --ider
+
+Use Intel AMT technology for IDE redirection. This allows the target
+machine to boot from nonvaboot created ISO image.
+
+The experimental I<amtider> utility needed by this option can be
+obtained from https://github.com/wentasah/amtterm.
+
+=item --iprelay=I<addr[:port]>
+
+Use TCP/IP relay and serial port to access the target's serial port
+and powercycle it. The IP address of the relay is given by I<addr>
+parameter. If I<port> is not specified, it default to 23.
+
+Note: This option is supposed to work with HWG-ER02a IP relays.
+
+=item -s, --serial[=device]
+
+Target's serial line is connected to host's serial line (device). The
+default value for device is F</dev/ttyUSB0>.
+
+The value of this option is exported in NB_NOVABOOT environment
+variable to all subprocesses run by C<novaboot>.
+
+=item --stty=I<settings>
+
+Specifies settings passed to C<stty> invoked on the serial line
+specified with B<--serial> option. If this option is not given,
+C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
+
+=item --remote-cmd=I<cmd>
+
+Command that mediates connection to the target's serial line. For
+example C<ssh server 'cu -l /dev/ttyS0'>.
+
+=item --remote-expect=I<string>
+
+Wait for reception of I<string> after establishing the the remote
+connection before continuing.
+
=back
=item -d, --dhcp-tftp
-Turns your workstation into a DHCP and TFTP server so that NOVA
-can be booted via PXE BIOS on a test machine directly connected by
-a plain Ethernet cable to your workstation.
+Turns your workstation into a DHCP and TFTP server so that the OS can
+be booted via PXE BIOS (or similar mechanism) on the test machine
+directly connected by a plain Ethernet cable to your workstation.
-The DHCP and TFTP servers require root privileges and C<novaboot>
+The DHCP and TFTP servers requires root privileges and C<novaboot>
uses C<sudo> command to obtain those. You can put the following to
-I</etc/sudoers> to allow running the necessary commands without
-asking for password.
+I</etc/sudoers> to allow running the necessary commands without asking
+for password.
+
+ 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
+ your_login ALL=NOPASSWD: NOVABOOT
- 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 *, /usr/bin/touch dhcpd.leases
+=item --tftp
+
+Starts a TFTP server on your workstation. This is similar to
+B<--dhcp-tftp> except that DHCP server is not started.
+
+The TFTP server require root privileges and C<novaboot> uses C<sudo>
+command to obtain those. You can put the following to I</etc/sudoers>
+to allow running the necessary commands without asking for password.
+
+ Cmnd_Alias NOVABOOT = /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/pkill --pidfile=tftpd.pid
your_login ALL=NOPASSWD: NOVABOOT
-=item -i, --iso[=filename]
+=item --tftp-port=I<port>
+
+Port to run the TFTP server on. Implies B<--tftp>.
+
+=item --iso[=filename]
Generates the ISO image that boots NOVA system via GRUB. If no filename
is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
Specifies which I<flags> are appended to F<rsync> command line when
copying files as a result of I<--server> option.
-=item --scons[=scons command]
-
-Runs I<scons> to build files that are not generated by novaboot
-itself.
-
=back
=head2 Target power-on and reset phase
=over 8
-=item --iprelay[=addr or cmd]
-
-If no I<cmd> is given, use IP relay to reset the machine and to get
-the serial output. The IP address of the relay is given by I<addr>
-parameter if specified or by $iprelay_addr variable in the
-configuration file.
-
-If I<cmd> is one of "on" or "off", the IP relay is used to press power
-button for a short (in case of "on") or long (in case of "off") time.
-Then, novaboot exits.
-
-Note: This option is expected to work with HWG-ER02a IP relays.
-
=item --on, --off
-Synonym for --iprelay=on/off.
+Switch on/off the target machine and exit. The script (if any) is
+completely ignored. Currently it works only with B<--iprelay> or
+B<--amt>.
-=item -Q, --qemu=I<qemu-binary>
+=item -Q, --qemu[=I<qemu-binary>]
-The name of qemu binary to use. The default is 'qemu'.
+Boot the configuration in qemu. Optionally, the name of qemu binary
+can be specified as a parameter.
=item --qemu-append=I<flags>
Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
-smp 2>) with I<flags> specified here.
+=item --reset-cmd=I<cmd>
+
+Command that resets the target.
+
=back
=head2 Interaction with the bootloader on the target
-See B<--serial>. There will be new options soon.
+=over 8
-=head2 Target's output reception phase
+=item --uboot[=I<prompt>]
-=over 8
+Interact with uBoot bootloader to boot the thing described in the
+novaboot script. I<prompt> specifies the U-Boot's prompt (default is
+"=> ", other common prompts are "U-Boot> " or "U-Boot# ").
+Implementation of this option is currently tied to a particular board
+that we use. It may be subject to changes in the future!
-=item -s, --serial[=device]
+=item --uboot-init
-Use serial line to control GRUB bootloader and to see the output
-serial output of the machine. The default value is F</dev/ttyUSB0>.
+Command(s) to send the U-Boot bootloader before loading the images and
+booting them. This option can be given multiple times. After sending
+commands from each option novaboot waits for U-Boot I<prompt>.
=back
-See also B<--iprelay>.
+=head2 Target interaction phase
+
+In this phase, target's serial output is redirected to stdout and if
+stdin is a TTY, it is redirected to the target's serial input allowing
+interactive work with the target.
+
+=over 8
+
+=item --exiton=I<string>
+
+When I<string> is sent by the target, novaboot exits. This option can
+be specified multiple times.
+
+If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
+treated as regular expression. For example:
+
+ --exiton -re --exiton 'error:.*failed'
+
+=item -i, --interactive
+
+Setup things for interactive use of target. Your terminal will be
+switched to raw mode. In raw mode, your system does not process input
+in any way (no echoing of entered characters, no interpretation
+special characters). This, among others, means that Ctrl-C is passed
+to the target and does no longer interrupt novaboot. Use "~~."
+sequence to exit novaboot.
+
+=item --expect=I<string>
+
+When I<string> is received from the target, send the string specified
+with the subsequent B<--send*> option to the target.
+
+=item --expect-re=I<regex>
+
+When target's output matches regular expression I<regex>, send the
+string specified with the subsequent B<--send*> option to the target.
+
+=item --expect-raw=I<perl-code>
+
+Provides direct control over Perl's Expect module.
+
+=item --send=I<string>
+
+Send I<string> to the target after the previously specified
+B<--expect*> was matched in the target's output. The I<string> may
+contain escape sequences such as "\n".
-=head2 Termination phase
+Note that I<string> is actually interpreted by Perl, so it can contain
+much more that escape sequences. This behavior may change in the
+future.
-Daemons that were spwned (F<dhcpd> and F<tftpd>) are killed here.
+Example: C<--expect='login: ' --send='root\n'>
+
+=item --sendcont=I<string>
+
+Similar to B<--send> but continue expecting more input.
+
+Example: C<--expect='Continue?' --sendcont='yes\n'>
+
+=back
=head1 NOVABOOT SCRIPT SYNTAX
-The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
+The syntax tries to mimic POSIX shell syntax. The syntax is defined
+with the following rules.
-Lines starting with "#" are ignored.
+Lines starting with "#" and empty lines are ignored.
Lines that end with "\" are concatenated with the following line after
removal of the final "\" and leading whitespace of the following line.
-Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
-expression) assign values to internal variables. See VARIABLES
+Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
+expression) assign values to internal variables. See L</VARIABLES>
section.
-Otherwise, the first word on the line represents the filename
-(relative to the build directory (see B<--build-dir>) of the module to
-load and the remaining words are passed as the command line
-parameters.
+Lines starting with C<load> keyword represent modules to boot. The
+word after C<load> is a file name (relative to the build directory
+(see B<--build-dir>) of the module to load and the remaining words are
+passed to it as the command line parameters.
-When the line ends with "<<WORD" then the subsequent lines until the
-line containing only WORD are copied literally to the file named on
-that line.
+When the C<load> line ends with "<<WORD" then the subsequent lines
+until the line containing solely WORD are copied literally to the file
+named on that line. This is similar to shell's heredoc feature.
+
+When the C<load> line ends with "< CMD" then command CMD is executed
+with F</bin/sh> and its standard output is stored in the file named on
+that line. The SRCDIR variable in CMD's environment is set to the
+absolute path of the directory containing the interpreted novaboot
+script.
+
+Lines starting with C<run> keyword contain shell commands that are run
+during file generation phase. This is the same as the "< CMD" syntax
+for C<load> keyboard except that the command's output is not
+redirected to a file. The ordering of commands is the same as they
+appear in the novaboot script.
+
+Example (Linux):
+
+ #!/usr/bin/env novaboot
+ load bzImage console=ttyS0,115200
+ run make -C buildroot
+ load rootfs.cpio < gen_cpio buildroot/images/rootfs.cpio "myapp->/etc/init.d/S99myapp"
-When the line ends with "< CMD" the command CMD is executed with
-C</bin/sh> and its standard output is stored in the file named on that
-line. The SRCDIR variable in CMD's environment is set to the absolute
-path of the directory containing the interpreted novaboot script.
+Example (NOVA User Land - NUL):
-Example:
#!/usr/bin/env novaboot
WVDESC=Example program
- bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
- verbose hostkeyb:0,0x60,1,12,2
- bin/apps/hello.nul
- hello.nulconfig <<EOF
+ load bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
+ verbose hostkeyb:0,0x60,1,12,2
+ load bin/apps/hello.nul
+ load hello.nulconfig <<EOF
sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
rom://bin/apps/hello.nul
EOF
-This example will load three modules: sigma0.nul, hello.nul and
-hello.nulconfig. sigma0 gets some command line parameters and
-hello.nulconfig file is generated on the fly from the lines between
-<<EOF and EOF.
+This example will load three modules: F<sigma0.nul>, F<hello.nul> and
+F<hello.nulconfig>. sigma0 receives some command line parameters and
+F<hello.nulconfig> file is generated on the fly from the lines between
+C<<<EOF> and C<EOF>.
=head2 VARIABLES
=over 8
-=item WVDESC
+=item BUILDDIR
-Description of the wvtest-compliant program.
+Novaboot chdir()s to this directory before file generation phase. The
+directory name specified here is relative to the build directory
+specified by other means (see L</--build-dir>).
-=item WVTEST_TIMEOUT
+=item EXITON
-The timeout in seconds for WvTest harness. If no complete line appears
-in the test output within the time specified here, the test fails. It
-is necessary to specify this for long running tests that produce no
-intermediate output.
+Assigning this variable has the same effect as specifying L</--exiton>
+option.
+
+=item HYPERVISOR_PARAMS
+
+Parameters passed to hypervisor. The default value is "serial", unless
+overridden in configuration file.
+
+=item KERNEL
+
+The kernel to use instead of the hypervisor specified in the
+configuration file with the C<$hypervisor> variable. The value should
+contain the name of the kernel image as well as its command line
+parameters. If this variable is defined and non-empty, the variable
+HYPERVISOR_PARAMS is not used.
=item QEMU
-Use a specific qemu binary (can be overriden with B<-Q>) and flags
+Use a specific qemu binary (can be overridden with B<-Q>) and flags
when booting this script under qemu. If QEMU_FLAGS variable is also
specified flags specified in QEMU variable are replaced by those in
QEMU_FLAGS.
=item QEMU_FLAGS
-Use specific qemu flags (can be overriden with B<-q>).
+Use specific qemu flags (can be overridden with B<-q>).
-=item HYPERVISOR_PARAMS
+=item WVDESC
-Parameters passed to hypervisor. The default value is "serial", unless
-overriden in configuration file.
+Description of the wvtest-compliant program.
-=item KERNEL
+=item WVTEST_TIMEOUT
-The kernel to use instead of NOVA hypervisor specified in the
-configuration file. The value should contain the name of the kernel
-image as well as its command line parameters. If this variable is
-defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
+The timeout in seconds for WvTest harness. If no complete line appears
+in the test output within the time specified here, the test fails. It
+is necessary to specify this for long running tests that produce no
+intermediate output.
=back
=head1 CONFIGURATION FILE
-Novaboot can read its configuration from a file. Configuration file
-was necessary in early days of novaboot. Nowadays, an attempt is made
-to not use the configuration file because it makes certain novaboot
-scripts unusable on systems without (or with different) configuration
-file. The only recommended use of the configuration file is to specify
-custom_options (see bellow).
-
-If you decide to use the configuration file, it is looked up, by
-default, in files named F<.novaboot> as described in L</Configuration
-reading phase>. Alternatively, its location can be specified with the
-B<-c> switch or with the NOVABOOT_CONFIG environment variable. The
-configuration file has perl syntax and should set values of certain
-Perl variables. The current configuration can be dumped with the
-B<--dump-config> switch. Some configuration variables can be overriden
-by environment variables (see below) or by command line switches.
+Novaboot can read its configuration from one or more files. By
+default, novaboot looks for files named F<.novaboot> as described in
+L</Configuration reading phase>. Alternatively, its location can be
+specified with the B<-c> switch or with the NOVABOOT_CONFIG
+environment variable. The configuration file has perl syntax and
+should set values of certain Perl variables. The current configuration
+can be dumped with the B<--dump-config> switch. Some configuration
+variables can be overridden by environment variables (see below) or by
+command line switches.
-Documentation of some configuration variables follows:
+Supported configuration variables include:
=over 8
-=item @chainloaders
+=item $builddir
-Custom chainloaders to load before hypervisor and files specified in
-novaboot script. E.g. ('bin/boot/bender promisc', 'bin/boot/zapp').
+Build directory location relative to the location of the configuration
+file.
+
+=item $default_target
+
+Default target (see below) to use when no target is explicitly
+specified on command line with the B<--target> option.
=item %targets
Defining this variable has the same meaning as B<--bender> option.
-=item NOVABOOT_IPRELAY
-
-The IP address (and optionally the port) of the IP relay. This
-overrides $iprelay_addr variable from the configuration file.
-
=back
=head1 AUTHORS