]> rtime.felk.cvut.cz Git - novaboot.git/blobdiff - novaboot
nix: Update flake inputs
[novaboot.git] / novaboot
index b06971cf2fc0e4e5cc039cc7deef076de149b73a..cd59748d35e6cb9633f345f6ce2d6d504ec4c149 100755 (executable)
--- 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
 
 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;
@@ -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
@@ -44,13 +55,14 @@ $CFG::hypervisor_params = "serial";
 $CFG::genisoimage = "genisoimage";
 $CFG::qemu = 'qemu-system-i386 -cpu coreduo -smp 2';
 $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=novabox@rtime.felk.cvut.cz:/srv/tftp/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --pulsar --iprelay-cmd="ssh -tt novabox@rtime.felk.cvut.cz nc localhost 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"',
     );
 
@@ -120,7 +132,10 @@ read_config($_) foreach $cfg or @cfgs;
 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, $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);
+# 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 = (
@@ -130,12 +145,12 @@ my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp
     );
 $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 = 180;
+$netif = $CFG::netif;
+$remote_expect_timeout = -1;
 
 my @expect_seen = ();
 sub handle_expect
@@ -155,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,
@@ -174,11 +209,6 @@ my %opt_spec;
     "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,
@@ -192,17 +222,12 @@ my %opt_spec;
     "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-expect-timeout=i"=> \$remote_expect_timeout,
+    "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,
@@ -211,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"   => 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;
@@ -284,7 +337,7 @@ 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);
 
@@ -297,7 +350,7 @@ 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;
@@ -306,7 +359,7 @@ my $EOF;
 my $last_fn = '';
 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;
@@ -348,6 +401,7 @@ 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;
     }
     sub process_load_copy($) {
@@ -362,6 +416,7 @@ while (!$skip_reading && ($_ = <>)) {
            push @$generated, {filename => $1, command => $3};
            return "$1$2";
        }
+       s/\s*$//;               # Strip trailing whitespace
        return $_;
     }
     if (s/^load *//) {         # Load line
@@ -492,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";
@@ -517,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";
@@ -574,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(@)
@@ -584,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"); }
@@ -618,7 +688,30 @@ 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 || defined $iprelay_cmd) {
@@ -641,6 +734,7 @@ if (defined $iprelay || defined $iprelay_cmd) {
        $exp = new Expect;
        $exp->raw_pty(1);
        $exp->spawn($iprelay_cmd);
+       kill_exp_on_signal();
     }
 
     while (1) {
@@ -709,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;
@@ -828,9 +923,9 @@ if ($remote_expect) {
     }
 }
 
-if (defined $reset_cmd) {
+if (@reset_cmd) {
     $target_reset = sub {
-       system_verbose($reset_cmd);
+       system_verbose(@reset_cmd);
     };
 }
 
@@ -918,11 +1013,11 @@ foreach my $script (@scripts) {
            $hostname = "";
        }
        my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs, @$copy)));
-       map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
+       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" : "";
        if ($files) {
-           system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
+           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);
@@ -948,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";
@@ -1042,10 +1143,19 @@ if (defined $dhcp_tftp)
     open(my $fh, '>', 'dhcpd.conf');
     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;
@@ -1065,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
@@ -1094,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) {
@@ -1121,7 +1243,9 @@ 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);
@@ -1167,17 +1291,18 @@ if (defined $uboot) {
        @$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");
        } else  {
@@ -1186,7 +1311,7 @@ if (defined $uboot) {
        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");
        } else {
@@ -1219,7 +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"));
+       $exp->expect($exiton_timeout, @exiton, @expect_raw) || die("exiton: " . ($! || "timeout"));
     } else {
        my @inputs = ($exp);
        my $infile = new IO::File;
@@ -1244,6 +1369,10 @@ if ($interaction && defined $exp) {
     }
 }
 
+# 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");
@@ -1255,6 +1384,7 @@ print "\n" if $final_eol;
 ## Documentation
 
 =encoding utf8
+
 =head1 NAME
 
 novaboot - Boots a locally compiled operating system on a remote
@@ -1295,46 +1425,102 @@ having per-system, per-user or per-project configurations.
 Configuration file syntax is described in section L</"CONFIGURATION
 FILES">.
 
-Simple examples of using C<novaboot>:
+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<figure from the doc directory
+|https://github.com/wentasah/novaboot/blob/master/doc/typical-setups.svg>
+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</--serial>, L</--remote-cmd>, ...), power
+on/off/reset commands (L</--reset-cmd>, ...), TFTP server
+(L</--copy>, L</--prefix>...), 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</--copy>) 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</--ssh>) used to communicate between
+novaboot and novaboot server. The server is implemented as a
+restricted shell (L<novaboot-shell(1)>) on the server. No need to give
+full shell access to novaboot users on the server.
+
+=back
+
+=head2 Simple examples of using C<novaboot>:
+
+To boot Linux (files F<bzImage> and F<rootfs.cpio> in current
+directory), create F<mylinux> file with this content:
+
+    #!/usr/bin/env novaboot
+    load bzImage console=ttyS0,115200
+    load rootfs.cpio
 
 =over 3
 
 =item 1.
 
-Running an OS in Qemu can be accomplished by giving the B<--qemu> option.
+Booting an OS in Qemu can be accomplished by giving the B<--qemu> option.
 Thus running
 
- novaboot --qemu myos
+ novaboot --qemu mylinux
 
-(or C<./myos --qemu> as described above) will run Qemu and make it
-boot the configuration specified in the F<myos> script.
+(or C<./mylinux --qemu> as described above) will run Qemu and make it
+boot the configuration specified in the F<mylinux> 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
+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:
 
- ./myos --target mytarget
+ ./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<novaboot-shell(1)> 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 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.
 
@@ -1346,9 +1532,9 @@ I<script2> configurations.
 
 =back
 
-=head1 PHASES AND OPTIONS
+=head1 OPTIONS AND PHASES
 
-Novaboot performs its work in several phases. Command like options
+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.
 
@@ -1449,6 +1635,21 @@ When this option is not given, novaboot tries to determine the target
 to use from either B<NOVABOOT_TARGET> environment variable or
 B<$default_target> configuration file variable.
 
+=item --ssh=I<user@hostname>
+
+Configures novaboot to control the target via C<novaboot-shell>
+running remotely via SSH.
+
+Using this option is the same as specifying B<--remote-cmd>,
+B<--remote-expect>, B<--copy> B<--rsync-flags>, B<--prefix> and
+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!
+
 =back
 
 =head2 Script preprocessing phase
@@ -1555,10 +1756,14 @@ Alias for B<--prefix>.
 =item --grub2[=I<filename>]
 
 Generate GRUB2 menu entry in I<filename>. If I<filename> is not
-specified F<grub.cfg> is used. The content of the menu entry can be
+specified F<./boot/grub/grub.cfg> is used. The content of the menu entry can be
 customized with B<--grub-preamble>, B<--grub2-prolog> or
 B<--grub_prefix> options.
 
+GRUB2 can boot multiboot-compliant kernels and a few kernels with specific
+support. L</BOOT_METHOD> could be used to specify the command used by GRUB2 to
+load the kernel. See L<GNU GRUB Manual|https://www.gnu.org/software/grub/manual/grub/grub.html#Booting>.
+
 To use the generated menu entry on your development
 machine that uses GRUB2, append the following snippet to
 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
@@ -1661,10 +1866,23 @@ example C<ssh server 'cu -l /dev/ttyS0'>.
 
 =item --remote-expect=I<string>
 
-Wait for reception of I<string> after establishing the remote
-connection. This option is needed when novaboot should wait for
-confirmation before deploying files to the target, e.g. to not
-overwrite other user's files when they are using the target.
+Wait for reception of I<string> after establishing the remote serial
+line connection. Novaboot assumes that after establishing the serial
+line connection, the user running novaboot has exclusive access to the
+target. If establishing of the serial line connection happens
+asynchronously (e.g. running a command remotely via SSH), we need this
+option to wait until the exclusive access is confirmed by the remote
+side.
+
+Depending on target configuration, this option can solve two practical
+problems: 1) Overwriting of files deployed by another user currently
+using the target. 2) Resetting the target board before serial line
+connection is established and thus missing bootloader interaction.
+
+Example of usage with the L<sterm
+tool|https://rtime.felk.cvut.cz/gitweb/sojka/sterm.git>:
+
+  --remote-cmd='ssh -tt example.com sterm -v /dev/ttyUSB0' --remote-expect='sterm: Connected'
 
 =item --remote-expect-silent=I<string>
 
@@ -1677,7 +1895,7 @@ end characters in the I<string> as well.
 
 Timeout in seconds for B<--remote-expect> or
 B<--remote-expect-seconds>. When negative, waits forever. The default
-is 180 seconds.
+is -1 seconds.
 
 =back
 
@@ -1721,10 +1939,11 @@ Port to run the TFTP server on. Implies B<--tftp>.
 
 =item --netif=I<network interface>
 
-Network interface used to deploy files to the target. The default value is
-I<eth0>. This option influences the configuration of the DHCP server started
-by B<--dhcp-tftp> and the value that B<$NB_MYIP> get replaced with during
-U-Boot conversation.
+Network interface used to deploy files to the target. This option
+influences the configuration of the DHCP server started by
+B<--dhcp-tftp> and the value that B<$NB_MYIP> get replaced with during
+U-Boot conversation. The default value is C<$netif> variable from
+configuration files, which defaults to I<eth0>.
 
 =item --iso[=filename]
 
@@ -1734,6 +1953,10 @@ of the novaboot script (see also B<--name>).
 
 =item --server[=[[user@]server:]path]
 
+Alias of B<--copy> (kept for backward compatibility).
+
+=item --copy[=[[user@]server:]path]
+
 Copy all files needed for booting to another location. The files will
 be copied (by B<rsync> tool) to the directory I<path>. If the I<path>
 contains string $NAME, it will be replaced with the name of the
@@ -1742,13 +1965,13 @@ novaboot script (see also B<--name>).
 =item --rsync-flags=I<flags>
 
 Specifies I<flags> to append to F<rsync> command line when
-copying files as a result of I<--server> option.
+copying files as a result of I<--copy> option.
 
 =item --concat
 
-If B<--server> is used and its value ends with $NAME, then after
+If B<--copy> is used and its value ends with $NAME, then after
 copying the files, a new bootloader configuration file (e.g. menu.lst)
-is created at I<path-wo-name>, i.e. the path specified by B<--server>
+is created at I<path-wo-name>, i.e. the path specified by B<--copy>
 with $NAME part removed. The content of the file is created by
 concatenating all files of the same name from all subdirectories of
 I<path-wo-name> found on the "server".
@@ -1775,8 +1998,8 @@ B<--iprelay>, B<--reset-cmd> and B<--reset-send>.
 =item --on, --off
 
 Switch on/off the target machine and exit. The script (if any) is
-completely ignored. Currently, it works only with B<--iprelay> or
-B<--amt>.
+completely ignored. Currently, it works only with the following
+options: B<--iprelay>, B<--amt>, B<--ssh>.
 
 =item -Q, --qemu[=I<qemu-binary>]
 
@@ -1822,6 +2045,12 @@ novaboot script. I<prompt> specifies the U-Boot's prompt (default is
 
 Disable U-Boot interaction previously enabled with B<--uboot>.
 
+=item --uboot-stop-key=I<key>
+
+Character, which is sent as a response to U-Boot's "Hit any key to
+stop autoboot" message. The default value is newline, but some devices
+(e.g. TP-Link TD-W8970) require a specific key to be pressed.
+
 =item --uboot-init
 
 Command(s) to send the U-Boot bootloader before loading the images and
@@ -1883,7 +2112,7 @@ The same as --exiton -re --exiton I<regex>.
 =item --exiton-timeout=I<seconds>
 
 By default B<--exiton> waits for the string match forever. When this
-option is specified, "exiton" timeouts after the specifies the number of
+option is specified, "exiton" timeouts after the specified number of
 seconds and novaboot returns non-zero exit code.
 
 =item -i, --interactive
@@ -1968,9 +2197,16 @@ word after C<load> is a file name (relative to the build directory
 (see B<--build-dir>) of the module to load and the remaining words are
 passed to it as the command line parameters.
 
+When booting Linux, the first C<load> line usually refers to the
+kernel image and its command line parameters (unless you use some
+special pre-loader). Other C<load> lines may refer to an initramfs
+image and/or a device tree blob. Their order is not important, as the
+device tree is recognized as the file name ending with C<.dtb>.
+
 When the C<load> line ends with "<<WORD" then the subsequent lines
 until the line containing solely WORD are copied literally to the file
-named on that line. This is similar to shell's heredoc feature.
+named on that line. This is similar to the heredoc feature of UNIX
+shells.
 
 When the C<load> line ends with "< CMD" then command CMD is executed
 with F</bin/sh> and its standard output is stored in the file named on
@@ -2084,6 +2320,11 @@ The following variables are interpreted in the novaboot script:
 
 =over 8
 
+=item BOOT_METHOD
+
+Specifies the way GRUB2 boots the kernel. For kernels with multiboot
+support use C<multiboot> method (the default). For Linux kernel use C<linux> method.
+
 =item BUILDDIR
 
 Novaboot chdir()s to this directory before file generation phase. The
@@ -2095,6 +2336,12 @@ specified by other means (see L</--build-dir>).
 Assigning this variable has the same effect as specifying L</--exiton>
 option.
 
+=item INTERACTION
+
+Setting this variable to zero is the same as giving
+L</--no-interaction>, specifying to one corresponds to
+L</--interaction>.
+
 =item HYPERVISOR_PARAMS
 
 Parameters passed to the hypervisor. The default value is "serial", unless
@@ -2172,17 +2419,22 @@ Default target (see below) to use when no target is explicitly
 specified with the B<--target> command line option or
 B<NOVABOOT_TARGET> environment variable.
 
+=item $netif
+
+Default value for the B<--netif> option. If not specified, it defaults
+to I<eth0>.
+
 =item %targets
 
 Hash of target definitions to be used with the B<--target> option. The
 key is the identifier of the target, the value is the string with
 command line options. For instance, if the configuration file contains:
 
- $targets{'mybox'} = '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
+ $targets{'mybox'} = '--copy=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
 
 then the following two commands are equivalent:
 
- ./myos --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
+ ./myos --copy=boot:/tftproot --serial=/dev/ttyUSB0 --grub
  ./myos -t mybox
 
 =back