GetOptions ("target|t=s" => \$explicit_target);
# 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);
+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);
my ($target_reset, $target_power_on, $target_power_off);
@expect_seen = ();
}
-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-%r@%h%p";
-
- $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/@.*// 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"); };
-}
+# 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,
+ "uboot-addr=s" => \%uboot_addr,
+ "uboot-cmd=s" => \$uboot_cmd,
+ "uboot-init=s" => sub { push @uboot_init, { command => $_[1] }; },
+ "uboot:s" => \$uboot,
+ );
-my %opt_spec;
-%opt_spec = (
+my %opt_spec = (
+ %opt_spec_safe,
"amt=s" => \$amt,
"append|a=s" => \@append,
"bender|b" => \$bender,
"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,
"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-cmd=s" => sub { @remote_cmd = ($_[1]); },
"remote-expect=s"=> \$remote_expect,
"remote-expect-silent=s"=> sub { $remote_expect=$_[1]; $remote_expect_silent=1; },
"remote-expect-timeout=i"=> \$remote_expect_timeout,
"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,
"stty=s" => \$stty,
"tftp" => \$tftp,
"tftp-port=i" => \$tftp_port,
- "uboot:s" => \$uboot,
"no-uboot" => sub { undef $uboot; },
- "uboot-addr=s" => \%uboot_addr,
- "uboot-cmd=s" => \$uboot_cmd,
- "uboot-init=s" => sub { push @uboot_init, { command => $_[1] }; },
"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;
my %input_opts = ('--iprelay' => \$iprelay,
'--iprelay-cmd'=> \$iprelay_cmd,
'--serial' => \$serial,
- '--remote-cmd' => \$remote_cmd,
+ '--remote-cmd' => (@remote_cmd ? \$remote_cmd[0] : undef),
'--amt' => \$amt);
my @opts = grep(defined(${$input_opts{$_}}) , keys %input_opts);
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(@)
exit(1); # should not be reached
}
-sub system_verbose($)
+sub system_verbose
{
- my $cmd = shift;
- print STDERR "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"); }
my $exp; # Expect object to communicate with the target over serial line
+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 || defined $iprelay_cmd) {
$exp = new Expect;
$exp->raw_pty(1);
$exp->spawn($iprelay_cmd);
+ kill_exp_on_signal();
}
while (1) {
open($CONN, "+<", $serial) || die "open $serial: $!";
$exp = Expect->init(\*$CONN);
}
-elsif ($remote_cmd) {
- print STDERR "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;
}
}
-if (defined $reset_cmd) {
+if (@reset_cmd) {
$target_reset = sub {
- system_verbose($reset_cmd);
+ system_verbose(@reset_cmd);
};
}
}
}
+# 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");
Using this option is the same as specifying B<--remote-cmd>,
B<--remote-expect>, B<--server> B<--rsync-flags>, B<--prefix> and
-B<--reset-cmd> manually in a way compatible with V<novaboot-shell>.
+B<--reset-cmd> manually in a way compatible with C<novaboot-shell>.
+The server can be configured to provide other, safe bootloader-related
+options, to the client. When this happens, novaboot prints them to
+stdout.
Currently, this in an initial experimental implementation. We plan to
change/extend this feature soon!