X-Git-Url: http://rtime.felk.cvut.cz/gitweb/novaboot.git/blobdiff_plain/ec03313b05f52f743b4068fe8881d77c23a2dc42..HEAD:/novaboot diff --git a/novaboot b/novaboot index 2b38717..cd59748 100755 --- a/novaboot +++ b/novaboot @@ -1,4 +1,4 @@ -#!/usr/bin/perl -w +#!/usr/bin/env perl # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,17 +17,20 @@ use strict; use warnings; -use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ()); +use warnings (exists $ENV{NOVABOOT_TEST} ? + (FATAL => 'all') : + (FATAL => qw(inplace))); # Open warnings in <<>> are fatal use Getopt::Long qw(GetOptionsFromString GetOptionsFromArray); use Pod::Usage; use File::Basename; use File::Spec; +use File::Path qw(make_path); use IO::Handle; 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; @@ -36,6 +39,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 @@ -43,19 +54,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', - "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog=" set root=\'(hd0,msdos1)\'"', + "tud" => '--copy=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" => '--ssh=novabox@rtime.felk.cvut.cz', + "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --copy=/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; @@ -111,22 +129,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, $remote_expect_silent, $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 @@ -146,8 +170,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, @@ -163,33 +207,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-expect-silent=s"=> sub { $remote_expect=$_[1]; $remote_expect_silent=1; }, + "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, @@ -197,19 +236,47 @@ my %opt_spec; "sendcont=s" => \&handle_send, "serial|s:s" => \$serial, "server:s" => \$server, + "copy: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; @@ -224,6 +291,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"); } @@ -264,8 +335,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); @@ -278,16 +350,16 @@ if (defined $serial) { $ENV{NB_SERIAL} = $serial; } if (defined $grub_config) { $grub_config ||= "menu.lst"; } -if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; } +if (defined $grub2_config) { $grub2_config ||= "./boot/grub/grub.cfg"; } ## Parse the novaboot script(s) 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 && ($_ = <>)) { +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 $continuation; @@ -295,7 +367,10 @@ while (!$skip_reading && ($_ = <>)) { push @scripts, { 'filename' => $ARGV, 'modules' => $modules = [], 'variables' => $variables = {}, - 'generated' => $generated = []}; + 'generated' => $generated = [], + 'copy' => $copy = [], + 'chainload' => $chainload = [], + }; } chomp(); @@ -326,38 +401,53 @@ 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, $_; + s/\s*$//; # Strip trailing whitespace + 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; } @@ -457,7 +547,7 @@ sub generate_syslinux_config($$$$) 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); + die('Too 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"; @@ -482,24 +572,36 @@ sub generate_grub2_config($$$$;$$) { my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_; if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; }; + my $dir = dirname($filename); + make_path($dir, { + chmod => 0755, + }); open(my $fg, '>', $filename) or die "$filename: $!"; print $fg "$preamble\n" if $preamble; $title ||= 'novaboot'; print $fg "menuentry $title {\n"; print $fg "$prolog\n" if $prolog; my $first = 1; + my $boot_method = $variables->{BOOT_METHOD} // "multiboot"; + my $module_load_method = "module"; + if ($boot_method eq "linux") { + $module_load_method = "initrd"; + die('Too many "load" lines for Linux kernel') if (scalar(@$modules_ref) > 2); + } foreach (@$modules_ref) { if ($first) { $first = 0; my ($kbin, $kcmd) = split(' ', $_, 2); $kcmd = '' if !defined $kcmd; - print $fg " multiboot ${base}$kbin $kcmd\n"; + print $fg " $boot_method ${base}$kbin $kcmd\n"; } else { my @args = split; - # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here - $_ = join(' ', ($args[0], @args)); - s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2 - print $fg " module $base$_\n"; + if ($boot_method eq "multiboot") { + # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here + $_ = join(' ', ($args[0], @args)); + s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2 + } + print $fg " $module_load_method $base$_\n"; } } print $fg "}\n"; @@ -508,26 +610,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; @@ -535,7 +641,11 @@ 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(@) @@ -545,11 +655,10 @@ 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"); } @@ -579,28 +688,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 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 || 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; } @@ -625,7 +767,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"); @@ -660,9 +803,10 @@ elsif ($serial) { 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; @@ -761,8 +905,7 @@ END my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port"; 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"); } @@ -772,16 +915,24 @@ if ($remote_expect) { if (defined $remote_expect_silent) { $exp->log_stdout(0); } - $exp->expect(180, $remote_expect) || die "Expect for '$remote_expect' timed out"; + $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); }; } @@ -835,11 +986,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); @@ -856,14 +1012,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 -RL --chmod=ugo=rwX $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); + } } } @@ -885,11 +1043,17 @@ if (defined $iso_image) { if (-f '/usr/lib/ISOLINUX/isolinux.bin') { # Newer ISOLINUX version @files = qw(/usr/lib/ISOLINUX/isolinux.bin /usr/lib/syslinux/modules/bios/mboot.c32 /usr/lib/syslinux/modules/bios/libcom32.c32 /usr/lib/syslinux/modules/bios/menu.c32 /usr/lib/syslinux/modules/bios/ldlinux.c32); - } else { + } elsif (-f '/usr/lib/syslinux/isolinux.bin') { # Older ISOLINUX version @files = qw(/usr/lib/syslinux/isolinux.bin /usr/lib/syslinux/mboot.c32 /usr/lib/syslinux/menu.c32); + } else { + # NixOS and maybe others + my $syslinux = `which syslinux` || die "Cannot find syslinux"; + chomp $syslinux; + $syslinux =~ s,/bin/syslinux$,,; + @files = ("$syslinux/share/syslinux/isolinux.bin", "$syslinux/share/syslinux/mboot.c32", "$syslinux/share/syslinux/libcom32.c32", "$syslinux/share/syslinux/menu.c32", "$syslinux/share/syslinux/ldlinux.c32"); } - system_verbose("cp @files isolinux"); + system_verbose("cp @files isolinux && chmod +w isolinux/*"); open(my $fh, ">isolinux/isolinux.cfg"); if ($#scripts) { print $fh "TIMEOUT 50\n"; @@ -957,7 +1121,9 @@ 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 STDERR "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n"; $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!"); @@ -975,20 +1141,29 @@ 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; - filename \"bin/boot/grub/pxegrub.pxe\"; - next-server 10.23.23.1; + print $fh " +subnet 10.23.23.0 netmask 255.255.255.0 { + range 10.23.23.10 10.23.23.100; + next-server 10.23.23.1; +} +class \"pxe-clients\" { + match option vendor-class-identifier; +} +subclass \"pxe-clients\" \"PXEClient:Arch:00000:UNDI:002001\" { + option bootfile-name \"boot/grub/i386-pc/core.0\"; +} +subclass \"pxe-clients\" \"PXEClient:Arch:00007:UNDI:003016\" { + option bootfile-name \"boot/grub/x86_64-efi/core.efi\"; } host server { hardware ethernet $mac; 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 @@ -1000,16 +1175,28 @@ host server { if (defined $dhcp_tftp || defined $tftp) { $tftp_port ||= 69; + my $tftp_root = "$builddir"; + $tftp_root = "$server" if(defined $server); + + # Prepare a GRUB netboot directory + system_verbose("grub-mknetdir --net-directory=$tftp_root") if (defined $grub2_config); + + # Generate TFTP mapfile + open(my $fh, '>', "$tftp_root/mapfile"); + print $fh "# Some PXE clients (mainly UEFI) have bug. They add zero byte to the end of the +# path name. This rule removes it +r \\.efi.* \\.efi"; + close($fh); # 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"); + system_verbose("sudo in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid -m mapfile --address :$tftp_port $tftp_root"); # 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'); }; + system_verbose("sudo pkill --pidfile=$tftp_root/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); }; + $SIG{INT} = sub { system_verbose("sudo pkill --pidfile=$tftp_root/tftpd.pid"); exit(0); }; } ### AMT IDE-R @@ -1029,7 +1216,7 @@ if (defined $ider) { ### Reset target (IP relay, AMT, ...) if (defined $target_reset && $reset) { - print STDERR "novaboot: Reseting the test box... "; + print STDERR "novaboot: Resetting the test box... "; &$target_reset(); print STDERR "done\n"; if (defined $exp) { @@ -1056,19 +1243,23 @@ if (defined $uboot) { $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; } @@ -1079,52 +1270,67 @@ 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}) || - defined $uboot_cmd) { + # 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; - if (defined $kbin) { + 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(10, - [qr/##/, sub { exp_continue; }], - $uboot_prompt) || die "Kernel load timeout"; + $exp->expect(15, + $uboot_prompt, + [qr/#/, sub { exp_continue; }] + ) || 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"); } } @@ -1138,8 +1344,7 @@ if ($interaction && defined $exp) { 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 STDERR "\n"; + $exp->expect($exiton_timeout, @exiton, @expect_raw) || die("exiton: " . ($! || "timeout")); } else { my @inputs = ($exp); my $infile = new IO::File; @@ -1157,17 +1362,29 @@ if ($interaction && defined $exp) { #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 @@ -1183,84 +1400,173 @@ 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 +to 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 + +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. -Simple examples of using C: +=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 --copy=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. =back -=head1 PHASES AND OPTIONS +=head1 OPTIONS AND PHASES + +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