X-Git-Url: http://rtime.felk.cvut.cz/gitweb/novaboot.git/blobdiff_plain/02f538346f2f5aef302113383b2fdd55b16ef853..aa4859ce992247034bf3accc7126d533ce65f505:/novaboot
diff --git a/novaboot b/novaboot
index 42743a7..c5c9153 100755
--- a/novaboot
+++ b/novaboot
@@ -13,6 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+## Initialization
+
use strict;
use warnings;
use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ());
@@ -25,7 +27,7 @@ use Time::HiRes("usleep");
use Socket;
use FileHandle;
use IPC::Open2;
-use POSIX qw(:errno_h);
+use POSIX qw(:errno_h sysconf);
use Cwd qw(getcwd abs_path);
use Expect;
@@ -34,6 +36,14 @@ $| = 1;
my $invocation_dir = $ENV{PWD} || getcwd();
+# We prefer using PWD, to use nicer paths names with symbolic links.
+# However, when executed from 'make -C dir', PWD may contain the make
+# invocation path, not the real invocation path with dir at the end.
+# We fix that here.
+if (abs_path($ENV{PWD}) ne abs_path(getcwd())) {
+ $invocation_dir = getcwd();
+}
+
## Configuration file handling
# Default configuration
@@ -41,19 +51,25 @@ $CFG::hypervisor = "";
$CFG::hypervisor_params = "serial";
$CFG::genisoimage = "genisoimage";
$CFG::qemu = 'qemu-system-i386 -cpu coreduo -smp 2';
-$CFG::default_target = 'qemu';
+$CFG::default_target = '';
%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 --iprelay=147.32.86.92:2324',
+ "novabox" => '--ssh=novabox@rtime.felk.cvut.cz',
"localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog=" set root=\'(hd0,msdos1)\'"',
"ryu" => '--uboot --uboot-init="mw f0000b00 \${psc_cfg}; sleep 1" --uboot-addr kernel=800000 --uboot-addr ramdisk=b00000 --uboot-addr fdt=7f0000',
- "ryuglab" => '--target ryu --server=pc-sojkam.felk.cvut.cz:/srv/tftp --remote-cmd="ssh -tt pc-sojkam.felk.cvut.cz \"sterm -d -s 115200 /dev/ttyUSB0\""',
+ "ryuglab" => '--target ryu --ssh=ryu@pc-sojkam.felk.cvut.cz',
"ryulocal" => '--target ryu --dhcp-tftp --serial --reset-cmd="if which dtrrts; then dtrrts $NB_SERIAL 0 1; sleep 0.1; dtrrts $NB_SERIAL 1 1; fi"',
);
-chomp(my $nproc = `nproc`);
-$CFG::scons = "scons -j$nproc";
-$CFG::make = "make -j$nproc";
+
+{
+ my %const;
+ $const{linux}->{_SC_NPROCESSORS_CONF} = 83;
+ my $nproc = sysconf($const{$^O}->{_SC_NPROCESSORS_CONF});
+
+ $CFG::scons = "scons -j$nproc";
+ $CFG::make = "make -j$nproc";
+}
my $builddir;
@@ -90,6 +106,10 @@ my @cfgs;
$dir = File::Spec->catdir(@dirs[0..$#dirs-1]);
}
@cfgs = reverse @cfgs;
+
+ my $xdg_config_home = $ENV{'XDG_CONFIG_HOME'} || $ENV{'HOME'}.'/.config';
+ unshift(@cfgs, "$xdg_config_home/novaboot") if -r "$xdg_config_home/novaboot";
+
$dir = $ENV{'NOVABOOT_CONFIG_DIR'} || '/etc/novaboot.d';
if (opendir(my $dh, $dir)) {
my @etccfg = map { "$dir/$_" } grep { /^[-_a-zA-Z0-9]+$/ && -f "$dir/$_" } readdir($dh);
@@ -105,21 +125,28 @@ read_config($_) foreach $cfg or @cfgs;
## Command line handling
-my $explicit_target;
+my $explicit_target = $ENV{'NOVABOOT_TARGET'};
GetOptions ("target|t=s" => \$explicit_target);
-my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $exiton_timeout, @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, $reset_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, $uboot_cmd, @uboot_init);
+# Variables for command line options
+my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $exiton_timeout, @expect_raw, $final_eol, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $ider, $interaction, $iprelay, $iprelay_cmd, $iso_image, $interactive, $kernel_opt, $make, $man, $netif, $no_file_gen, $off_opt, $on_opt, $pulsar, $pulsar_root, $qemu, $qemu_append, $qemu_flags_cmd, @remote_cmd, $remote_expect, $remote_expect_silent, $remote_expect_timeout, $reset, @reset_cmd, $reset_send, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, $uboot_cmd, @uboot_init, $uboot_stop_key);
+
+my ($target_reset, $target_power_on, $target_power_off);
+# Default values of certain command line options
%uboot_addr = (
'kernel' => '${kernel_addr_r}',
'ramdisk' => '${ramdisk_addr_r}',
'fdt' => '${fdt_addr_r}',
);
-
$rsync_flags = '';
$rom_prefix = 'rom://';
-$stty = 'raw -crtscts -onlcr 115200';
+$stty = 'raw -crtscts -onlcr -echo 115200';
$reset = 1; # Reset target by default
+$interaction = 1; # Perform target interaction by default
+$final_eol = 1;
+$netif = 'eth0';
+$remote_expect_timeout = -1;
my @expect_seen = ();
sub handle_expect
@@ -139,8 +166,28 @@ sub handle_send
@expect_seen = ();
}
-my %opt_spec;
-%opt_spec = (
+# Options which can be safely specified on the server (via --ssh),
+# i.e. which cannot cause unwanted local code execution etc.
+my %opt_spec_safe = (
+ "grub|g:s" => \$grub_config,
+ "grub-preamble=s"=> \$grub_preamble,
+ "grub2-prolog=s" => \$grub2_prolog,
+ "grub2:s" => \$grub2_config,
+ "prefix|grub-prefix=s" => \$grub_prefix,
+ "pulsar-root=s" => \$pulsar_root,
+ "pulsar|p:s" => \$pulsar,
+ "remote-expect=s"=> \$remote_expect,
+ "remote-expect-silent=s"=> sub { $remote_expect=$_[1]; $remote_expect_silent=1; },
+ "remote-expect-timeout=i"=> \$remote_expect_timeout,
+ "uboot-addr=s" => \%uboot_addr,
+ "uboot-cmd=s" => \$uboot_cmd,
+ "uboot-stop-key=s" => \$uboot_stop_key,
+ "uboot-init=s" => sub { push @uboot_init, { command => $_[1] }; },
+ "uboot:s" => \$uboot,
+ );
+
+my %opt_spec = (
+ %opt_spec_safe,
"amt=s" => \$amt,
"append|a=s" => \@append,
"bender|b" => \$bender,
@@ -156,31 +203,28 @@ my %opt_spec;
"expect=s" => \&handle_expect,
"expect-re=s" => \&handle_expect,
"expect-raw=s" => sub { my ($n, $v) = @_; unshift(@expect_raw, eval($v)); },
+ "final-eol!" => \$final_eol,
"gen-only" => \$gen_only,
- "grub|g:s" => \$grub_config,
- "grub-preamble=s"=> \$grub_preamble,
- "prefix|grub-prefix=s" => \$grub_prefix,
- "grub2:s" => \$grub2_config,
- "grub2-prolog=s" => \$grub2_prolog,
"ider" => \$ider,
+ "interaction!" => \$interaction,
"iprelay=s" => \$iprelay,
+ "iprelay-cmd=s" => \$iprelay_cmd,
"iso:s" => \$iso_image,
"kernel|k=s" => \$kernel_opt,
"interactive|i" => \$interactive,
"name=s" => \$config_name_opt,
"make|m:s" => \$make,
+ "netif=s" => \$netif,
"no-file-gen" => \$no_file_gen,
"off" => \$off_opt,
"on" => \$on_opt,
- "pulsar|p:s" => \$pulsar,
- "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,
+ "remote-cmd=s" => sub { @remote_cmd = ($_[1]); },
"reset!" => \$reset,
- "reset-cmd=s" => \$reset_cmd,
+ "reset-cmd=s" => sub { @reset_cmd = ($_[1]); },
+ "reset-send=s" => \$reset_send,
"rsync-flags=s" => \$rsync_flags,
"scons:s" => \$scons,
"scriptmod=s" => \@scriptmod,
@@ -188,18 +232,46 @@ my %opt_spec;
"sendcont=s" => \&handle_send,
"serial|s:s" => \$serial,
"server:s" => \$server,
+ "ssh:s" => \&handle_novaboot_server,
"strip-rom" => sub { $rom_prefix = ''; },
"stty=s" => \$stty,
"tftp" => \$tftp,
"tftp-port=i" => \$tftp_port,
- "uboot:s" => \$uboot,
- "uboot-addr=s" => \%uboot_addr,
- "uboot-cmd=s" => \$uboot_cmd,
- "uboot-init=s" => \@uboot_init,
+ "no-uboot" => sub { undef $uboot; },
"h" => \$help,
"help" => \$man,
);
+sub handle_novaboot_server
+{
+ my ($n, $val) = @_;
+ my $xdg_runtime_dir = $ENV{XDG_RUNTIME_DIR} || '/var/run';
+ my $ssh_ctl_path = "${xdg_runtime_dir}/novaboot$$";
+
+ @remote_cmd = ('ssh', '-tt', '-M', '-S', $ssh_ctl_path, $val, 'console');
+ $remote_expect = "novaboot-shell: Connected";
+ $server = "$val:";
+ $rsync_flags = "--rsh='ssh -S \'${ssh_ctl_path}\''";
+ ($grub_prefix = $val) =~ s|(.*)@.*|\/$1\/| if index($val, '@') != -1;
+ @reset_cmd = ('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'reset');
+
+ $target_power_off = sub { system_verbose('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'off'); };
+ $target_power_on = sub { system_verbose('ssh', '-tt', '-S', $ssh_ctl_path, $val, 'on'); };
+
+ my $cmd = "ssh '${val}' get-config";
+ print STDERR "novaboot: Running: $cmd\n";
+ my @target_config = qx($cmd < /dev/null);
+ if ($?) { die("Cannot get target configuration from the server"); }
+ printf "novaboot: Received configuration from the server:%s\n", (!@target_config) ? " empty" : "";
+ foreach (@target_config) { chomp; print " $_\n"; }
+
+ my $p = Getopt::Long::Parser->new;
+ $p->configure(qw/no_ignore_case no_pass_through/);
+ $p->getoptionsfromarray(\@target_config, %opt_spec_safe) or die("Error processing configuration from the server");
+
+ if (scalar @target_config) { die "Unsuported configuration received from the server: ".join(", ", @target_config); }
+}
+
# First process target options
{
my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
@@ -214,6 +286,10 @@ my %opt_spec;
push(@target_expanded, @$remaining_args);
$t = $explicit_target;
}
+
+ my @args = (@target_expanded, @ARGV);
+ print STDERR "novaboot: Effective options: @args\n";
+
Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
GetOptionsFromArray(\@target_expanded, %opt_spec) or die ("Error in target definition");
}
@@ -252,6 +328,17 @@ if ($ider) {
if (!defined $amt) { die "Error: --ider requires --amt"; }
}
+{
+ my %input_opts = ('--iprelay' => \$iprelay,
+ '--iprelay-cmd'=> \$iprelay_cmd,
+ '--serial' => \$serial,
+ '--remote-cmd' => (@remote_cmd ? \$remote_cmd[0] : undef),
+ '--amt' => \$amt);
+ my @opts = grep(defined(${$input_opts{$_}}) , keys %input_opts);
+
+ die("novaboot: More than one target connection option: ".join(', ', @opts)) if scalar @opts > 1;
+}
+
# Default options
if (defined $serial) {
$serial ||= "/dev/ttyUSB0";
@@ -265,7 +352,7 @@ my @scripts;
my $file;
my $EOF;
my $last_fn = '';
-my ($modules, $variables, $generated, $continuation) = ([], {}, []);
+my ($modules, $variables, $generated, $copy, $chainload, $continuation) = ([], {}, [], []);
my $skip_reading = defined($on_opt) || defined($off_opt);
while (!$skip_reading && ($_ = <>)) {
if ($ARGV ne $last_fn) { # New script
@@ -275,7 +362,10 @@ while (!$skip_reading && ($_ = <>)) {
push @scripts, { 'filename' => $ARGV,
'modules' => $modules = [],
'variables' => $variables = {},
- 'generated' => $generated = []};
+ 'generated' => $generated = [],
+ 'copy' => $copy = [],
+ 'chainload' => $chainload = [],
+ };
}
chomp();
@@ -306,38 +396,52 @@ while (!$skip_reading && ($_ = <>)) {
if (/^([A-Z_]+)=(.*)$/) { # Internal variable
$$variables{$1} = $2;
push(@exiton, $2) if ($1 eq "EXITON");
+ $interaction = $2 if ($1 eq "INTERACTION");
next;
}
- if (s/^load *//) { # Load line
+ sub process_load_copy($) {
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;
+ return "$1$2";
}
if (/^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
- push @$modules, "$1$2";
push @$generated, {filename => $1, command => $3};
- next;
+ return "$1$2";
}
- push @$modules, $_;
+ return $_;
+ }
+ if (s/^load *//) { # Load line
+ push @$modules, process_load_copy($_);
+ next;
+ }
+ if (s/^copy *//) { # Copy line
+ push @$copy, process_load_copy($_);
+ next;
+ }
+ if (s/^chld *//) { # Chainload line
+ push @$chainload, process_load_copy($_);
next;
}
if (/^run (.*)/) { # run line
push @$generated, {command => $1};
next;
}
- if (/^uboot(?::([0-9]+)s)? (.*)/) { # uboot line
+ if (/^uboot(?::([0-9]+)s)? +(< *)?(.*)/) { # uboot line
# TODO: If U-Boot supports some interactive menu, it might
# make sense to store uboot lines per novaboot script.
- if ($1) { # Command with explicit timeout
- push @uboot_init, { command => $2,
- timeout => $1 };
- } else { # Command without explicit timeout
- push @uboot_init, $2;
+ my ($timeout, $redir, $string, $dest) = ($1, $2, $3);
+ if ($string =~ /(.*) *> *(.*)/) {
+ $string = $1;
+ $dest = $2;
}
+ push @uboot_init, { command => $redir ? "" : $string,
+ system => $redir ? $string : "",
+ timeout => $timeout,
+ dest => $dest,
+ };
next;
}
@@ -388,7 +492,7 @@ sub generate_configs($$$) {
open(my $f, '>', $fn) || die("$fn: $!");
map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
close($f);
- print "novaboot: Created $fn\n";
+ print STDERR "novaboot: Created $fn\n";
} elsif (exists $$g{command} && ! $no_file_gen) {
$ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
if (exists $$g{filename}) {
@@ -488,26 +592,30 @@ sub generate_grub2_config($$$$;$$)
return $filename;
}
-sub generate_pulsar_config($$)
+sub generate_pulsar_config($$$)
{
- my ($filename, $modules_ref) = @_;
+ my ($filename, $modules_ref, $chainload_ref) = @_;
open(my $fg, '>', $filename) or die "$filename: $!";
print $fg "root $pulsar_root\n" if defined $pulsar_root;
- my $first = 1;
- my ($kbin, $kcmd);
- foreach (@$modules_ref) {
- if ($first) {
- $first = 0;
- ($kbin, $kcmd) = split(' ', $_, 2);
- $kcmd = '' if !defined $kcmd;
- } else {
- my @args = split;
- s|\brom://|$rom_prefix|g;
- print $fg "load $_\n";
+ if (scalar(@$chainload_ref) > 0) {
+ print $fg "chld $$chainload_ref[0]\n";
+ } else {
+ my $first = 1;
+ my ($kbin, $kcmd);
+ foreach (@$modules_ref) {
+ if ($first) {
+ $first = 0;
+ ($kbin, $kcmd) = split(' ', $_, 2);
+ $kcmd = '' if !defined $kcmd;
+ } else {
+ my @args = split;
+ s|\brom://|$rom_prefix|g;
+ print $fg "load $_\n";
+ }
}
+ # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
+ print $fg "exec $kbin $kcmd\n";
}
- # 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 $builddir/$filename\n");
return $filename;
@@ -515,32 +623,41 @@ sub generate_pulsar_config($$)
sub shell_cmd_string(@)
{
- return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
+ if (scalar(@_) == 1) {
+ return $_[0];
+ } else {
+ return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
+ }
}
sub exec_verbose(@)
{
- print "novaboot: Running: ".shell_cmd_string(@_)."\n";
+ print STDERR "novaboot: Running: ".shell_cmd_string(@_)."\n";
exec(@_);
exit(1); # should not be reached
}
-sub system_verbose($)
+sub system_verbose
{
- my $cmd = shift;
- print "novaboot: Running: $cmd\n";
- my $ret = system($cmd);
+ print STDERR "novaboot: Running: ".shell_cmd_string(@_)."\n";
+ my $ret = system(@_);
if ($ret & 0x007f) { die("Command terminated by a signal"); }
if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
if ($ret) { die("Command failure $ret"); }
}
+sub trim($) {
+ my ($str) = @_;
+ $str =~ s/^\s+|\s+$//g;
+ return $str
+}
+
## WvTest headline
if (exists $variables->{WVDESC}) {
- print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
+ print STDERR "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
} elsif ($last_fn =~ /\.wv$/) {
- print "Testing \"all\" in $last_fn:\n";
+ print STDERR "Testing \"all\" in $last_fn:\n";
}
## Connect to the target and check whether it is not occupied
@@ -553,28 +670,61 @@ if (exists $variables->{WVDESC}) {
my $exp; # Expect object to communicate with the target over serial line
-my ($target_reset, $target_power_on, $target_power_off);
+sub kill_exp_on_signal() {
+ # Sometimes, under unclear circumstances (e.g. when running under
+ # both Jenkins and Robotframework), novaboot does not terminate
+ # console command when killed. The console is then blocked by the
+ # stale process forever. Theoretically, this should not happen,
+ # because when novaboot is killed, console command's controlling
+ # terminal sends SIGHUP to the console command and the command
+ # should terminate. It seems that at least SSH sometimes ignores
+ # HUP and does not terminate. The code below seems to work around
+ # that problem by killing the process immediately with SIGTERM,
+ # which is not ignored.
+
+ sub kill_console { kill TERM => $exp->pid if $exp->pid; die "Terminated by SIG$_[0]"; };
+
+ # For our Jenkins/Robotframework use case, it was sufficient to
+ # handle the TERM signal, but to be on the safe side, we also
+ # catch other signals.
+ $SIG{TERM} = \&kill_console;
+ $SIG{HUP} = \&kill_console;
+ $SIG{INT} = \&kill_console;
+ $SIG{QUIT} = \&kill_console;
+}
+
+
my ($amt_user, $amt_password, $amt_host, $amt_port);
-if (defined $iprelay) {
- my $IPRELAY;
- $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
- my $addr = $1;
- my $port = $3 || 23;
- my $paddr = sockaddr_in($port, inet_aton($addr));
- my $proto = getprotobyname('tcp');
- socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
- print "novaboot: Connecting to IP relay... ";
- connect($IPRELAY, $paddr) || die "connect: $!";
- print "done\n";
- $exp = Expect->init(\*$IPRELAY);
- $exp->log_stdout(1);
+if (defined $iprelay || defined $iprelay_cmd) {
+ if (defined $iprelay) {
+ my $IPRELAY;
+ $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
+ my $addr = $1;
+ my $port = $3 || 23;
+ my $paddr = sockaddr_in($port, inet_aton($addr));
+ my $proto = getprotobyname('tcp');
+ socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
+ print STDERR "novaboot: Connecting to IP relay... ";
+ connect($IPRELAY, $paddr) || die "connect: $!";
+ print STDERR "done\n";
+ $exp = Expect->init(\*$IPRELAY);
+ $exp->log_stdout(1);
+ }
+ if (defined $iprelay_cmd) {
+ print STDERR "novaboot: Running: $iprelay_cmd\n";
+ $exp = new Expect;
+ $exp->raw_pty(1);
+ $exp->spawn($iprelay_cmd);
+ kill_exp_on_signal();
+ }
while (1) {
print $exp "\xFF\xF6"; # AYT
my $connected = $exp->expect(20, # Timeout in seconds
'',
- '-re', ']*>');
+ '-re', ']*>')
+ || die "iprelay connection: " . ($! || "timeout");
last if $connected;
}
@@ -599,7 +749,8 @@ if (defined $iprelay) {
$exp->log_stdout(0);
print $exp relaycmd($relay, $onoff);
my $confirmed = $exp->expect(20, # Timeout in seconds
- relayconf($relay, $onoff));
+ relayconf($relay, $onoff))
+ || die "iprelay command: " . ($! || "timeout");
if (!$confirmed) {
if ($can_giveup) {
print("Relay confirmation timeout - ignoring\n");
@@ -634,9 +785,10 @@ elsif ($serial) {
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 (@remote_cmd) {
+ print STDERR "novaboot: Running: ".shell_cmd_string(@remote_cmd)."\n";
+ $exp = Expect->spawn(@remote_cmd);
+ kill_exp_on_signal();
}
elsif (defined $amt) {
require LWP::UserAgent;
@@ -733,21 +885,36 @@ END
};
my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port";
- print "novaboot: Running: $cmd\n" =~ s/\Q$amt_password\E/???/r;
+ print STDERR "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";
-
+ $exp->expect(10, "RUN_SOL") || die "Expect for 'RUN_SOL': " . ($! || "timeout");
}
if ($remote_expect) {
$exp || die("No serial line connection");
- $exp->expect(180, $remote_expect) || die "Expect for '$remote_expect' timed out";
+ my $log = $exp->log_stdout;
+ if (defined $remote_expect_silent) {
+ $exp->log_stdout(0);
+ }
+ $exp->expect($remote_expect_timeout >= 0 ? $remote_expect_timeout : undef,
+ $remote_expect) || die "Expect for '$remote_expect':" . ($! || "timeout");;
+ if (defined $remote_expect_silent) {
+ $exp->log_stdout($log);
+ print $exp->after() if $log;
+ }
}
-if (defined $reset_cmd) {
+if (@reset_cmd) {
$target_reset = sub {
- system_verbose($reset_cmd);
+ system_verbose(@reset_cmd);
+ };
+}
+
+if (defined $reset_send) {
+ $target_reset = sub {
+ $reset_send =~ s/\\n/\n/g;
+ $exp->send($reset_send);
};
}
@@ -756,7 +923,7 @@ if (defined $on_opt && defined $target_power_on) {
exit;
}
if (defined $off_opt && defined $target_power_off) {
- print "novaboot: Switching the target off...\n";
+ print STDERR "novaboot: Switching the target off...\n";
&$target_power_off();
exit;
}
@@ -764,7 +931,7 @@ if (defined $off_opt && defined $target_power_off) {
$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";
+ print STDERR "novaboot: Entering directory `$builddir'\n";
} else {
$builddir = $invocation_dir;
}
@@ -786,7 +953,7 @@ foreach my $script (@scripts) {
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";
+ print STDERR "novaboot: Entering directory `$builddir'\n";
}
if ($grub_prefix) {
@@ -801,11 +968,16 @@ foreach my $script (@scripts) {
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('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
+ push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules, $chainload) if (defined $pulsar);
### Run scons or make
{
- my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
+ my @all;
+ push @all, @$modules;
+ push @all, @$copy;
+ push @all, @$chainload;
+ my @files = map({ ($file) = m/([^ ]*)/; $file; } @all);
+
# Filter-out generated files
my @to_build = grep({ my $file = $_; !scalar(grep($file eq ($$_{filename} || ''), @$generated)) } @files);
@@ -822,14 +994,16 @@ foreach my $script (@scripts) {
$path = $hostname;
$hostname = "";
}
- my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
- map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
+ my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs, @$copy)));
+ map({ my $file = (split)[0]; die "Not a file: $file: $!" if ! -e $file || -d $file; } @$modules);
my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
my $progress = $istty ? "--progress" : "";
- system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
- if ($server =~ m|/\$NAME$| && $concat) {
- my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
- system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
+ if ($files) {
+ system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
+ if ($server =~ m|/\$NAME$| && $concat) {
+ my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
+ system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
+ }
}
}
@@ -886,13 +1060,7 @@ if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $
}
if ($variables->{WVTEST_TIMEOUT}) {
- print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
-}
-
-sub trim($) {
- my ($str) = @_;
- $str =~ s/^\s+|\s+$//g;
- return $str
+ print STDERR "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
}
### Start in Qemu
@@ -929,9 +1097,11 @@ if (defined $qemu) {
push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
}
}
- push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
+ if (!grep /^-serial$/, @qemu_flags) {
+ push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
+ }
unshift(@qemu_flags, ('-name', $config_name));
- print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
+ print STDERR "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
$exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
}
@@ -947,7 +1117,7 @@ if (defined $dhcp_tftp)
system_verbose('mkdir -p tftpboot');
generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
open(my $fh, '>', 'dhcpd.conf');
- my $mac = `cat /sys/class/net/eth0/address`;
+ my $mac = `cat /sys/class/net/$netif/address`;
chomp $mac;
print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
range 10.23.23.10 10.23.23.100;
@@ -959,8 +1129,8 @@ host server {
fixed-address 10.23.23.1;
}";
close($fh);
- system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
- sudo ip l set dev eth0 up;
+ system_verbose("sudo ip a add 10.23.23.1/24 dev $netif;
+ sudo ip l set dev $netif up;
sudo touch dhcpd.leases");
# We run servers by forking ourselves, because the servers end up
@@ -987,7 +1157,7 @@ if (defined $dhcp_tftp || defined $tftp) {
### 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;
+ print STDERR "novaboot: Running: $ider_cmd\n" =~ s/\Q$amt_password\E/???/r;
my $ider_pid = fork();
if ($ider_pid == 0) {
exec($ider_cmd);
@@ -1001,32 +1171,50 @@ if (defined $ider) {
### Reset target (IP relay, AMT, ...)
if (defined $target_reset && $reset) {
- print "novaboot: Reseting the test box... ";
+ print STDERR "novaboot: Resetting the test box... ";
&$target_reset();
- print "done\n";
+ print STDERR "done\n";
+ if (defined $exp) {
+ # We don't want to output anything printed by the target
+ # before reset so we clear the buffers now. This is, however,
+ # not ideal because we may loose some data that were sent
+ # after the reset. If this is a problem, one should reset and
+ # connect to serial line in atomic manner. For example, if
+ # supported by hardware, use --remote-cmd 'sterm -d ...' and
+ # do not use separate --reset-cmd.
+ my $log = $exp->log_stdout;
+ $exp->log_stdout(0);
+ $exp->expect(0); # Read data from target
+ $exp->clear_accum(); # Clear the read data
+ $exp->log_stdout($log);
+ }
}
### U-boot conversation
if (defined $uboot) {
my $uboot_prompt = $uboot || '=> ';
- print "novaboot: Waiting for U-Boot prompt...\n";
+ print STDERR "novaboot: Waiting for U-Boot prompt...\n";
$exp || die("No serial line connection");
$exp->log_stdout(1);
#$exp->exp_internal(1);
$exp->expect(20,
- [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
+ [qr/Hit any key to stop autoboot:/, sub {
+ $exp->send($uboot_stop_key // "\n");
+ exp_continue; }],
$uboot_prompt) || die "No U-Boot prompt deteceted";
foreach my $cmdspec (@uboot_init) {
my ($cmd, $timeout);
- if (ref($cmdspec) eq "HASH") {
- $cmd = $cmdspec->{command};
- $timeout = $cmdspec->{timeout};
+ die "Internal error - please report a bug" unless ref($cmdspec) eq "HASH";
+
+ if ($cmdspec->{system}) {
+ $cmd = `$cmdspec->{system}`;
} else {
- $cmd = $cmdspec;
- $timeout = 10;
+ $cmd = $cmdspec->{command};
}
+ $timeout = $cmdspec->{timeout} // 10;
+
if ($cmd =~ /\$NB_MYIP/) {
- my $ip = (grep /inet /, `ip addr show eth0`)[0] || die "Problem determining our IP address";
+ my $ip = (grep /inet /, `ip addr show $netif`)[0] || die "Problem determining IP address of $netif";
$ip =~ s/\s*inet ([0-9.]*).*/$1/;
$cmd =~ s/\$NB_MYIP/$ip/g;
}
@@ -1037,99 +1225,120 @@ if (defined $uboot) {
}
chomp($cmd);
$exp->send("$cmd\n");
- $exp->expect($timeout, $uboot_prompt) || die "U-Boot prompt timeout";
+
+ my ($matched_pattern_position, $error,
+ $successfully_matching_string,
+ $before_match, $after_match) =
+ $exp->expect($timeout, $uboot_prompt);
+ die "No U-Boot prompt: $error" if $error;
+
+ if ($cmdspec->{dest}) {
+ open(my $fh, ">", $cmdspec->{dest}) or die "Cannot open '$cmdspec->{dest}': $!";
+ print $fh $before_match;
+ close($fh);
+ }
}
- # Boot the system if there are some load lines in the script
- if (scalar(@$modules) > 0 && !$variables->{NO_BOOT}) {
+ # Load files if there are some load lines in the script
+ if (scalar(@$modules) > 0 && !$variables->{NO_BOOT}) {
my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
my $dtb;
@$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
my $initrd = shift @$modules;
- die "No '--uboot-addr kernel' given" unless $uboot_addr{kernel};
- $exp->send("tftpboot $uboot_addr{kernel} $prefix$kbin\n");
- $exp->expect(10,
- [qr/##/, sub { exp_continue; }],
- $uboot_prompt) || die "Kernel load timeout";
+ if (defined $kbin && $kbin ne '/dev/null') {
+ die "No '--uboot-addr kernel' given" unless $uboot_addr{kernel};
+ $exp->send("tftpboot $uboot_addr{kernel} $prefix$kbin\n");
+ $exp->expect(15,
+ [qr/##/, sub { exp_continue; }],
+ $uboot_prompt) || die "Kernel load: " . ($! || "timeout");
+ }
if (defined $dtb) {
die "No '--uboot-addr fdt' given" unless $uboot_addr{fdt};
$exp->send("tftpboot $uboot_addr{fdt} $prefix$dtb\n");
- $exp->expect(10,
+ $exp->expect(15,
[qr/##/, sub { exp_continue; }],
- $uboot_prompt) || die "Device tree load timeout";
+ $uboot_prompt) || die "Device tree load: " . ($! || "timeout");
+ } else {
+ $uboot_addr{fdt} = '';
}
if (defined $initrd) {
die "No '--uboot-addr ramdisk' given" unless $uboot_addr{ramdisk};
$exp->send("tftpboot $uboot_addr{ramdisk} $prefix$initrd\n");
- $exp->expect(10,
+ $exp->expect(15,
[qr/##/, sub { exp_continue; }],
- $uboot_prompt) || die "Initrd load timeout";
+ $uboot_prompt) || die "Initrd load: " . ($! || "timeout");
} else {
$uboot_addr{ramdisk} = '-';
}
- $exp->send("echo $kcmd\n");
- $exp->expect(1, '-re', qr{echo .*\n(.*)\n$uboot_prompt}) || die "Command line test timeout";
- my $args = ($exp->matchlist)[0];
- if ($args =~ /^setenv\s+bootargs/) {
- $exp->send("$args\n");
- } else {
- $exp->send("setenv bootargs $kcmd\n");
- }
- $exp->expect(5, $uboot_prompt) || die "U-Boot prompt timeout";
- $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
+ $kcmd //= '';
+ $exp->send("setenv bootargs $kcmd\n");
+ $exp->expect(5, $uboot_prompt) || die "U-Boot prompt: " . ($! || "timeout");
+
+ }
+ $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr';
+ if (!$variables->{NO_BOOT} && $uboot_cmd ne '') {
$uboot_cmd =~ s/\$kernel_addr/$uboot_addr{kernel}/g;
$uboot_cmd =~ s/\$ramdisk_addr/$uboot_addr{ramdisk}/g;
$uboot_cmd =~ s/\$fdt_addr/$uboot_addr{fdt}/g;
$exp->send($uboot_cmd . "\n");
- $exp->expect(5, "\n") || die "U-Boot command timeout";
+ $exp->expect(5, "\n") || die "U-Boot command: " . ($! || "timeout");
}
}
### Serial line interaction
-if (defined $exp) {
+if ($interaction && 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";
+ print STDERR "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
$exp->log_stdout(1);
if (@exiton) {
- $exp->expect($exiton_timeout, @expect_raw, @exiton) || die("exiton timeout");
+ $exp->expect($exiton_timeout, @expect_raw, @exiton) || die("exiton: " . ($! || "timeout"));
} 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);
+ 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 STDERR "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;
+
+ $^W = 0; # Suppress Expect warning: handle id(3) is not a tty. Not changing mode at /usr/share/perl5/Expect.pm line 393, <> line 8.
Expect::interconnect(@inputs) unless defined($exp->exitstatus);
+ $^W = 1;
}
}
+# When exp-spawned command ignores SIGHUP, Expect waits 5 seconds
+# before killing it. We kill it by SIGTERM immediately.
+kill TERM => $exp->pid if defined $exp && $exp->pid;
+
## Kill dhcpc or tftpd
if (defined $dhcp_tftp || defined $tftp) {
die("novaboot: This should kill servers on background\n");
}
+# Always finish novaboot output with newline
+print "\n" if $final_eol;
+
## Documentation
+=encoding utf8
+
=head1 NAME
novaboot - Boots a locally compiled operating system on a remote
@@ -1145,68 +1354,133 @@ B<./script> [option]...
=head1 DESCRIPTION
-This program makes booting of a locally compiled operating system (OS)
+Novaboot makes booting of a locally compiled operating system (OS)
(e.g. NOVA or Linux) on remote targets as simple as running a program
locally. It automates things like copying OS images to a TFTP server,
generation of bootloader configuration files, resetting of target
hardware or redirection of target's serial line to stdin/out. Novaboot
-is highly configurable and it makes it easy to boot a single image on
+is highly configurable and makes it easy to boot a single image on
different targets or different images on a single target.
-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">).
-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.
+Novaboot operation is controlled by configuration files, 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">). The 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.
Novaboot uses configuration files to, among other things, define
command line options needed for different targets. Users typically use
only the B<-t>/B<--target> command line option to select the target.
Internally, this option expands to the pre-configured options.
-Configuration files are searched at multiple places, which allows to
-have per-system, per-user or per-project configurations. Configuration
-file syntax is described in section L"CONFIGURATION FILE">.
+Novaboot searches configuration files at multiple places, which allows
+having per-system, per-user or per-project configurations.
+Configuration file syntax is described in section L"CONFIGURATION
+FILES">.
+
+Novaboot newcomers may be confused by a large number of configuration
+options. Understanding all these options is not always needed,
+depending on the used setup. The L