X-Git-Url: http://rtime.felk.cvut.cz/gitweb/novaboot.git/blobdiff_plain/8cad6bbc32843ea5577275686e774f5bf33f2e8d..e156afd58ba9a5fd169965fa8c49503fe103ddc8:/novaboot diff --git a/novaboot b/novaboot index 1fab53d..f4fcdc9 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,26 @@ $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::netif = 'eth0'; %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; @@ -109,22 +126,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, $interaction, $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 = $CFG::netif; +$remote_expect_timeout = -1; my @expect_seen = (); sub handle_expect @@ -144,8 +167,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, @@ -161,32 +204,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, @@ -194,19 +233,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, "no-uboot" => sub { undef $uboot; }, - "uboot-addr=s" => \%uboot_addr, - "uboot-cmd=s" => \$uboot_cmd, - "uboot-init=s" => \@uboot_init, "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; @@ -221,6 +287,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"); } @@ -261,8 +331,9 @@ if ($ider) { { 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); @@ -282,7 +353,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 @@ -292,7 +363,10 @@ while (!$skip_reading && ($_ = <>)) { push @scripts, { 'filename' => $ARGV, 'modules' => $modules = [], 'variables' => $variables = {}, - 'generated' => $generated = []}; + 'generated' => $generated = [], + 'copy' => $copy = [], + 'chainload' => $chainload = [], + }; } chomp(); @@ -323,38 +397,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; } @@ -405,7 +493,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}) { @@ -505,26 +593,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; @@ -532,32 +624,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 @@ -570,28 +671,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; } @@ -616,7 +750,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"); @@ -651,9 +786,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; @@ -750,21 +886,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); }; } @@ -773,7 +924,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; } @@ -781,7 +932,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; } @@ -803,7 +954,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) { @@ -818,11 +969,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); @@ -839,14 +995,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); + } } } @@ -903,13 +1061,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 @@ -946,9 +1098,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: $!"); } @@ -964,7 +1118,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; @@ -976,8 +1130,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 @@ -1004,7 +1158,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); @@ -1018,32 +1172,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; } @@ -1054,49 +1226,66 @@ 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} = '-'; } $kcmd //= ''; $exp->send("setenv bootargs $kcmd\n"); - $exp->expect(5, $uboot_prompt) || die "U-Boot prompt timeout"; + $exp->expect(5, $uboot_prompt) || die "U-Boot prompt: " . ($! || "timeout"); - $uboot_cmd //= $variables->{UBOOT_CMD} // 'bootm $kernel_addr $ramdisk_addr $fdt_addr'; + } + $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"); } } @@ -1107,42 +1296,50 @@ if ($interaction && defined $exp) { 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"); - print "\n"; + $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 @@ -1158,68 +1355,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). -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). 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. +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. + +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
+shows different setups that vary in how much effort is needed +configure novaboot for them. The setups are: + +=over 3 + +=item A: Laptop and target device only + +This requires to configure everything on the laptop side, including a +serial line connection (L, L, ...), power +on/off/reset commands (L, ...), TFTP server +(L, L...), device IP addresses, etc. + +=item B: Laptop, target device and external TFTP server -Simple examples of using C: +Like the previous setup, but the TFTP (and maybe DHCP) configuration +is handled by a server. Novaboot users need to understand where to +copy their files to the TFTP server (L) and which IP +addresses their target will get, but do not need to configure the +servers themselves. + +=item C: Novaboot server running novaboot-shell + +With this setup, the configuration is done on the server. Users only +need to know the SSH account (L) used to communicate between +novaboot and novaboot server. The server is implemented as a +restricted shell (L) on the server. No need to give +full shell access to novaboot users on the server. + +=back + +=head2 Simple examples of using C: + +To boot Linux (files F and F in current +directory), create F file with this content: + + #!/usr/bin/env novaboot + load bzImage console=ttyS0,115200 + load rootfs.cpio =over 3 =item 1. -Run an OS in Qemu. This is the default action when no other action is -specified by command line switches. Thus running C (or -C<./myos> as described above) will run Qemu and make it boot the -configuration specified in the F script. +Booting an OS in Qemu can be accomplished by giving the B<--qemu> option. +Thus running + + novaboot --qemu mylinux + +(or C<./mylinux --qemu> as described above) will run Qemu and make it +boot the configuration specified in the F script. How is qemu +started can be configured in various ways (see below). =item 2. Create a bootloader configuration file (currently supported -bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar and U-Boot) and copy it -with all other files needed for booting to a remote boot server. Then +bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar, and U-Boot) and copy it +with all other files needed for booting to a remote TFTP server. Then use a TCP/IP-controlled relay/serial-to-TCP converter to reset the target and receive its serial output. - ./myos --grub2 --server=192.168.1.1:/tftp --iprelay=192.168.1.2 + ./mylinux --grub2 --server=192.168.1.1:/tftp --iprelay=192.168.1.2 + +Alternatively, you can put these switches to the configuration file +and run: + + ./mylinux --target mytarget =item 3. +Specifying all the options needed by novaboot to successfully control +the target, either on command line or in configuration files, can be +difficult for users. Novaboot supports configuring the target +centrally via L on a server. With such a +configuration, users only need to use the B<--ssh> option to specify +where to boot their OS: + + ./mylinux --ssh myboard@example.com + +Typically, the server is the computer connected to and controlling the +target board and running the TFTP server. + +=item 4. + Run DHCP and TFTP server on developer's machine to boot the target from it. - ./myos --dhcp-tftp + ./mylinux --dhcp-tftp -This is useful when no network infrastructure is in place and +This usage is useful when no network infrastructure is in place, and the target is connected directly to developer's box. -=item 4. +=item 5. Create bootable ISO image. novaboot --iso -- script1 script2 -The created ISO image will have ISOLINUX bootloader installed on it +The created ISO image will have ISOLINUX bootloader installed on it, and the boot menu will allow selecting between I and I configurations. @@ -1227,15 +1489,39 @@ I configurations. =head1 PHASES AND OPTIONS -Novaboot performs its work in several phases. Each phase can be -influenced by several command line options, certain phases can be -skipped. The list of phases (in the execution order) and the -corresponding options follows. +Novaboot performs its work in several phases. Command line options +described bellow influence the execution of each phase or allow their +skipping. The list of phases (in the execution order) is as follows. + +=over + +=item 1. L + +=item 2. L + +=item 3. L