use strict;
use warnings;
use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ());
-use Getopt::Long qw(GetOptionsFromString);
+use Getopt::Long qw(GetOptionsFromString GetOptionsFromArray);
use Pod::Usage;
use File::Basename;
use File::Spec;
$CFG::hypervisor = "";
$CFG::hypervisor_params = "serial";
$CFG::genisoimage = "genisoimage";
-$CFG::qemu = 'qemu -cpu coreduo -smp 2';
+$CFG::qemu = 'qemu-system-i386 -cpu coreduo -smp 2';
$CFG::default_target = 'qemu';
%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)\'"',
- "ryuglab" => '--server=pc-sojkam.felk.cvut.cz:/srv/tftp --uboot --uboot-init="mw f0000b00 \${psc_cfg}; sleep 1" --remote-cmd="ssh -t pc-sojkam.felk.cvut.cz \"cu -l /dev/ttyUSB0 -s 115200\"" --remote-expect="Connected." --reset-cmd="ssh -t pc-sojkam.felk.cvut.cz \"dtrrts /dev/ttyUSB0 1 1\""',
- "ryulocal" => '--dhcp-tftp --serial --uboot --uboot-init="dhcp; mw f0000b00 \${psc_cfg}; sleep 1" --reset-cmd="if which dtrrts; then dtrrts $NB_SERIAL 0 1; sleep 0.1; dtrrts $NB_SERIAL 1 1; fi"',
-
+ "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\""',
+ "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";
my @dirs = File::Spec->splitdir($dir);
$dir = File::Spec->catdir(@dirs[0..$#dirs-1]);
}
+ @cfgs = reverse @cfgs;
+ $dir = $ENV{'NOVABOOT_CONFIG_DIR'} || '/etc/novaboot.d';
+ if (opendir(my $dh, $dir)) {
+ my @etccfg = map { "$dir/$_" } grep { /^[-_a-zA-Z0-9]+$/ && -f "$dir/$_" } readdir($dh);
+ closedir $dh;
+ @etccfg = sort(@etccfg);
+ @cfgs = ( @etccfg, @cfgs );
+ }
}
my $cfg = $ENV{'NOVABOOT_CONFIG'};
Getopt::Long::Configure(qw/no_ignore_case pass_through/);
GetOptions ("config|c=s" => \$cfg);
-read_config($_) foreach $cfg or reverse @cfgs;
+read_config($_) foreach $cfg or @cfgs;
## Command line handling
my $explicit_target;
GetOptions ("target|t=s" => \$explicit_target);
-my (@append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, @expect_raw, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $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_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $uboot, $uboot_init);
+my ($amt, @append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, @expect_raw, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $ider, $iprelay, $iso_image, $interactive, $kernel_opt, $make, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $pulsar_root, $qemu, $qemu_append, $qemu_flags_cmd, $remote_cmd, $remote_expect, $reset, $reset_cmd, $rom_prefix, $rsync_flags, @scriptmod, $scons, $serial, $server, $stty, $tftp, $tftp_port, $uboot, %uboot_addr, @uboot_init);
$rsync_flags = '';
$rom_prefix = 'rom://';
$stty = 'raw -crtscts -onlcr 115200';
+$reset = 1; # Reset target by default
my @expect_seen = ();
sub handle_expect
@expect_seen = ();
}
-Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
my %opt_spec;
%opt_spec = (
+ "amt=s" => \$amt,
"append|a=s" => \@append,
"bender|b" => \$bender,
"build-dir=s" => sub { my ($n, $v) = @_; $builddir = File::Spec->rel2abs($v); },
"gen-only" => \$gen_only,
"grub|g:s" => \$grub_config,
"grub-preamble=s"=> \$grub_preamble,
- "grub-prefix=s" => \$grub_prefix,
+ "prefix|grub-prefix=s" => \$grub_prefix,
"grub2:s" => \$grub2_config,
"grub2-prolog=s" => \$grub2_prolog,
+ "ider" => \$ider,
"iprelay=s" => \$iprelay,
"iso:s" => \$iso_image,
"kernel|k=s" => \$kernel_opt,
"qemu-flags|q=s" => \$qemu_flags_cmd,
"remote-cmd=s" => \$remote_cmd,
"remote-expect=s"=> \$remote_expect,
+ "reset!" => \$reset,
"reset-cmd=s" => \$reset_cmd,
"rsync-flags=s" => \$rsync_flags,
"scons:s" => \$scons,
"server:s" => \$server,
"strip-rom" => sub { $rom_prefix = ''; },
"stty=s" => \$stty,
- "uboot" => \$uboot,
- "uboot-init=s" => \$uboot_init,
+ "tftp" => \$tftp,
+ "tftp-port=i" => \$tftp_port,
+ "uboot:s" => \$uboot,
+ "uboot-addr=s" => \%uboot_addr,
+ "uboot-init=s" => \@uboot_init,
"h" => \$help,
"help" => \$man,
);
# First process target options
{
my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
- if ($t) {
+ my @target_expanded;
+ Getopt::Long::Configure(qw/no_ignore_case pass_through/);
+ while ($t) {
exists $CFG::targets{$t} or die("Unknown target '$t' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
- GetOptionsFromString($CFG::targets{$t}, %opt_spec);
+
+ undef $explicit_target;
+ my ($ret, $remaining_args) = GetOptionsFromString ($CFG::targets{$t}, ("target|t=s" => \$explicit_target));
+ if (!$ret) { die "Error parsing target $t option"; }
+ push(@target_expanded, @$remaining_args);
+ $t = $explicit_target;
}
+ Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
+ GetOptionsFromArray(\@target_expanded, %opt_spec) or die ("Error in target definition");
}
# Then process other command line options - some of them may override
if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
+if ($ider) {
+ $iso_image //= ''; # IDE-R needs an ISO image
+ if (!defined $amt) { die "Error: --ider requires --amt"; }
+}
+
# Default options
if (defined $serial) {
$serial ||= "/dev/ttyUSB0";
my $file;
my $EOF;
my $last_fn = '';
-my ($modules, $variables, $generated, $continuation);
-while (<>) {
+my ($modules, $variables, $generated, $continuation) = ([], {}, []);
+my $skip_reading = defined($on_opt) || defined($off_opt);
+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;
foreach my $mod(@scriptmod) { eval $mod; }
+ if ($file && $_ eq $EOF) { # Heredoc end
+ undef $file;
+ next;
+ }
+ if ($file) { # Heredoc content
+ push @{$file}, "$_\n";
+ next;
+ }
if (/^([A-Z_]+)=(.*)$/) { # Internal variable
$$variables{$1} = $2;
push(@exiton, $2) if ($1 eq "EXITON");
next;
}
if (s/^load *//) { # Load line
+ die("novaboot: '$last_fn' line $.: Missing file name\n") unless /^[^ <]+/;
if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
push @$modules, "$1$2";
$file = [];
push @$modules, $_;
next;
}
- if ($file && $_ eq $EOF) { # Heredoc end
- undef $file;
- next;
- }
- if ($file) { # Heredoc content
- push @{$file}, "$_\n";
+ if (/^run (.*)/) { # run line
+ push @$generated, {command => $1};
next;
}
print "novaboot: Created $fn\n";
} elsif (exists $$g{command} && ! $no_file_gen) {
$ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
- system_verbose("( $$g{command} ) > $$g{filename}");
+ if (exists $$g{filename}) {
+ system_verbose("( $$g{command} ) > $$g{filename}");
+ } else {
+ system_verbose($$g{command});
+ }
}
}
}
return $filename;
}
+sub generate_syslinux_config($$$$)
+{
+ my ($filename, $title, $base, $modules_ref) = @_;
+ if ($base && $base !~ /\/$/) { $base = "$base/"; };
+ open(my $fg, '>', $filename) or die "$filename: $!";
+ print $fg "LABEL $title\n";
+ #TODO print $fg "MENU LABEL $human_readable_title\n";
+
+ my ($kbin, $kcmd) = split(' ', @$modules_ref[0], 2);
+
+ 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);
+ print $fg "LINUX $base$kbin\n";
+ print $fg "APPEND $kcmd\n";
+ print $fg "INITRD $base$initrd\n";
+ } else {
+ print $fg "KERNEL mboot.c32\n";
+ my @append;
+ foreach (@$modules_ref) {
+ s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
+ push @append, "$base$_";
+ print $fg "APPEND ".join(' --- ', @append)."\n";
+ }
+ }
+ #TODO print $fg "TEXT HELP\n";
+ #TODO print $fg "some help here\n";
+ #TODO print $fg "ENDTEXT\n";
+ close($fg);
+ print("novaboot: Created $builddir/$filename\n");
+ return $filename;
+}
+
sub generate_grub2_config($$$$;$$)
{
my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
{
print "novaboot: Running: ".shell_cmd_string(@_)."\n";
exec(@_);
+ exit(1); # should not be reached
}
sub system_verbose($)
if ($ret) { die("Command failure $ret"); }
}
-## WvTest handline
+## WvTest headline
if (exists $variables->{WVDESC}) {
print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
print "Testing \"all\" in $last_fn:\n";
}
-## Connect to the target and check whether is not occupied
+## Connect to the target and check whether it is not occupied
# We have to do this before file generation phase, because file
# generation is intermixed with file deployment phase and we want to
my $exp; # Expect object to communicate with the target over serial line
my ($target_reset, $target_power_on, $target_power_off);
+my ($amt_user, $amt_password, $amt_host, $amt_port);
if (defined $iprelay) {
my $IPRELAY;
system_verbose("stty -F $serial $stty");
open($CONN, "+<", $serial) || die "open $serial: $!";
$exp = Expect->init(\*$CONN);
-} elsif ($remote_cmd) {
+}
+elsif ($remote_cmd) {
print "novaboot: Running: $remote_cmd\n";
$exp = Expect->spawn($remote_cmd);
}
+elsif (defined $amt) {
+ require LWP::UserAgent;
+ require LWP::Authen::Digest;
+
+ sub genXML {
+ my ($host, $username, $password, $schema, $className, $pstate) = @_;
+ #AMT numbers for PowerStateChange (MNI => bluescreen on windows;-)
+ my %pstates = ("on" => 2,
+ "standby" => 4,
+ "hibernate" => 7,
+ "off" => 8,
+ "reset" => 10,
+ "MNI" => 11);
+ return <<END;
+ <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
+ <s:Header><a:To>http://$host:16992/wsman</a:To>
+ <w:ResourceURI s:mustUnderstand="true">$schema</w:ResourceURI>
+ <a:ReplyTo><a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address></a:ReplyTo>
+ <a:Action s:mustUnderstand="true">$schema$className</a:Action>
+ <w:MaxEnvelopeSize s:mustUnderstand="true">153600</w:MaxEnvelopeSize>
+ <a:MessageID>uuid:709072C9-609C-4B43-B301-075004043C7C</a:MessageID>
+ <w:Locale xml:lang="en-US" s:mustUnderstand="false" />
+ <w:OperationTimeout>PT60.000S</w:OperationTimeout>
+ <w:SelectorSet><w:Selector Name="Name">Intel(r) AMT Power Management Service</w:Selector></w:SelectorSet>
+ </s:Header><s:Body>
+ <p:RequestPowerStateChange_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService">
+ <p:PowerState>$pstates{$pstate}</p:PowerState>
+ <p:ManagedElement><a:Address>http://$host:16992/wsman</a:Address>
+ <a:ReferenceParameters><w:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</w:ResourceURI>
+ <w:SelectorSet><w:Selector Name="Name">ManagedSystem</w:Selector></w:SelectorSet>
+ </a:ReferenceParameters></p:ManagedElement>
+ </p:RequestPowerStateChange_INPUT>
+ </s:Body></s:Envelope>
+END
+ }
+
+ sub sendPOST {
+ my ($host, $username, $password, $content) = @_;
+
+ my $ua = LWP::UserAgent->new();
+ $ua->agent("novaboot");
+
+ my $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
+ my $res = $ua->request($req);
+ die ("Unexpected AMT response: " . $res->status_line) unless $res->code == 401;
+
+ my ($realm) = $res->header("WWW-Authenticate") =~ /Digest realm="(.*?)"/;
+ $ua->credentials("$host:16992", $realm, $username => $password);
+
+ # Create a request
+ $req = HTTP::Request->new(POST => "http://$host:16992/wsman");
+ $req->content_type('application/x-www-form-urlencoded');
+ $req->content($content);
+ $res = $ua->request($req);
+ die ("AMT power change request failed: " . $res->status_line) unless $res->is_success;
+ $res->content() =~ /<g:ReturnValue>(\d+)<\/g:ReturnValue>/;
+ return $1;
+ }
+
+ sub powerChange {
+ my ($host, $username, $password, $pstate)=@_;
+ my $schema="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService";
+ my $className="/RequestPowerStateChange";
+ my $content = genXML($host, $username, $password ,$schema, $className, $pstate);
+ return sendPOST($host, $username, $password, $content);
+ }
+
+ ($amt_user,$amt_password,$amt_host,$amt_port) = ($amt =~ /(?:(.*?)(?::(.*))?@)?([^:]*)(?::([0-9]*))?/);;
+ $amt_user ||= "admin";
+ $amt_password ||= $ENV{'AMT_PASSWORD'} || die "AMT password not specified";
+ $amt_host || die "AMT host not specified";
+ $amt_port ||= 16994;
+
+
+ $target_power_off = sub {
+ $exp->close();
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "off");
+ die "AMT power off failed (ReturnValue $result)" if $result != 0;
+ };
+
+ $target_power_on = sub {
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "on");
+ die "AMT power on failed (ReturnValue $result)" if $result != 0;
+ };
+
+ $target_reset = sub {
+ my $result = powerChange($amt_host,$amt_user,$amt_password, "reset");
+ if ($result != 0) {
+ print STDERR "Warning: Cannot reset $amt_host, trying power on. ";
+ $result = powerChange($amt_host,$amt_user,$amt_password, "on");
+ }
+ die "AMT reset failed (ReturnValue $result)" if $result != 0;
+ };
+
+ my $cmd = "amtterm -u $amt_user -p $amt_password $amt_host $amt_port";
+ print "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";
+
+}
+
if ($remote_expect) {
$exp->expect(10, $remote_expect) || die "Expect for '$remote_expect' timed out";
if (defined $builddir) {
chdir($builddir) or die "Can't change directory to $builddir: $!";
print "novaboot: Entering directory `$builddir'\n";
+} else {
+ $builddir = $invocation_dir;
}
## File generation phase
my (%files_iso, $menu_iso, $filename);
my $config_name = '';
+my $prefix = '';
foreach my $script (@scripts) {
$filename = $$script{filename};
print "novaboot: Entering directory `$builddir'\n";
}
- my $prefix;
- ($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
- $prefix ||= $builddir;
+ if ($grub_prefix) {
+ $prefix = $grub_prefix;
+ $prefix =~ s/\$NAME/$config_name/;
+ $prefix =~ s/\$BUILDDIR/$builddir/;
+ }
# TODO: use $grub_prefix as first parameter if some switch is given
generate_configs('', $generated, $filename);
{
my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
# Filter-out generated files
- my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
+ my @to_build = grep({ my $file = $_; !scalar(grep($file eq ($$_{filename} || ''), @$generated)) } @files);
system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
system_verbose($make || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
if (defined $iso_image) {
generate_configs("(cd)", $generated, $filename);
my $menu;
- generate_grub_config(\$menu, $config_name, "(cd)", $modules);
+ generate_syslinux_config(\$menu, $config_name, "/", $modules);
$menu_iso .= "$menu\n";
map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
}
## Generate ISO image
if (defined $iso_image) {
- open(my $fh, ">menu-iso.lst");
- print $fh "timeout 5\n\n$menu_iso";
+ system_verbose("mkdir -p isolinux");
+
+ my @files;
+ 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 {
+ # Older ISOLINUX version
+ @files = qw(/usr/lib/syslinux/isolinux.bin /usr/lib/syslinux/mboot.c32 /usr/lib/syslinux/menu.c32);
+ }
+ system_verbose("cp @files isolinux");
+ open(my $fh, ">isolinux/isolinux.cfg");
+ if ($#scripts) {
+ print $fh "TIMEOUT 50\n";
+ print $fh "DEFAULT menu\n";
+ } else {
+ print $fh "DEFAULT $config_name\n";
+ }
+ print $fh "$menu_iso";
close($fh);
- my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
+
+ my $files = join(" ", map("$_=$_", (keys(%files_iso), 'isolinux/isolinux.cfg', map(s|.*/|isolinux/|r, @files))));
$iso_image ||= "$config_name.iso";
- system_verbose("$CFG::genisoimage -R -b stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table -hide-rr-moved -J -joliet-long -o $iso_image -graft-points bin/boot/grub/ $files");
+
+ # Note: We use -U flag below to "Allow 'untranslated' filenames,
+ # completely violating the ISO9660 standards". Without this
+ # option, isolinux is not able to read files names for example
+ # bzImage-3.0.
+ system_verbose("$CFG::genisoimage -R -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -boot-info-table -hide-rr-moved -U -o $iso_image -graft-points $files");
print("ISO image created: $builddir/$iso_image\n");
}
return $str
}
-### Qemu
+### Start in Qemu
if (defined $qemu) {
# Qemu
if (defined $iso_image) {
# Boot NOVA with grub (and test the iso image)
- push(@qemu_flags, ('-cdrom', "$config_name.iso"));
+ push(@qemu_flags, ('-cdrom', $iso_image));
} else {
# Boot NOVA without GRUB
my ($dhcpd_pid, $tftpd_pid);
+$tftp=1 if $tftp_port;
+
if (defined $dhcp_tftp)
{
generate_configs("(nd)", $generated, $filename);
# process group (e.g. Ctrl-C on terminal).
$dhcpd_pid = fork();
exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
- $tftpd_pid = fork();
- exec_verbose("sudo in.tftpd --foreground --secure -v -v -v --pidfile tftpd.pid $builddir") if ($tftpd_pid == 0);
+}
+
+if (defined $dhcp_tftp || defined $tftp) {
+ $tftp_port ||= 69;
+ # 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");
# Kill server when we die
- $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid');
+ $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid') if (defined $dhcp_tftp);
system_verbose('sudo pkill --pidfile=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); };
+}
+
+### 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;
+ my $ider_pid = fork();
+ if ($ider_pid == 0) {
+ exec($ider_cmd);
+ die "IDE redirection failed";
+ }
+ # FIXME: This collides with --tftp option. Hopefully, nobody needs
+ # to use both simultaneously.
+ $SIG{__DIE__} = sub { system_verbose('kill $ider_pid'); };
}
-### Serial line or IP relay
+### Reset target (IP relay, AMT, ...)
-if (defined $target_reset) {
+if (defined $target_reset && $reset) {
print "novaboot: Reseting the test box... ";
&$target_reset();
print "done\n";
}
+### U-boot conversation
if (defined $uboot) {
- print "novaboot: Waiting for uBoot prompt...\n";
+ my $uboot_prompt = $uboot || '=> ';
+ print "novaboot: Waiting for U-Boot prompt...\n";
$exp->log_stdout(1);
#$exp->exp_internal(1);
$exp->expect(20,
[qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
- '=> ') || die "No uBoot prompt deteceted";
- $exp->send("$uboot_init\n") if $uboot_init;
- $exp->expect(10, '=> ') || die "uBoot prompt timeout";
-
- my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
- my $dtb;
- @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
- my $initrd = shift @$modules;
-
- my $kern_addr = '800000';
- my $initrd_addr = '-';
- my $dtb_addr = '';
-
- $exp->send("tftp $kern_addr $kbin\n");
- $exp->expect(10,
- [qr/#/, sub { exp_continue; }],
- '=> ') || die "Kernel load failed";
- if (defined $dtb) {
- $dtb_addr = '7f0000';
- $exp->send("tftp $dtb_addr $dtb\n");
- $exp->expect(10,
- [qr/#/, sub { exp_continue; }],
- '=> ') || die "Device tree load failed";
+ $uboot_prompt) || die "No U-Boot prompt deteceted";
+ foreach my $cmd (@uboot_init) {
+ if ($cmd =~ /\$NB_MYIP/) {
+ my $ip = (grep /inet /, `ip addr show eth0`)[0] || die "Problem determining our IP address";
+ $ip =~ s/\s*inet ([0-9.]*).*/$1/;
+ $cmd =~ s/\$NB_MYIP/$ip/g;
+ }
+ chomp($cmd);
+ $exp->send("$cmd\n");
+ $exp->expect(10, $uboot_prompt) || die "U-Boot prompt timeout";
}
- if (defined $initrd) {
- $initrd_addr = 'b00000';
- $exp->send("tftp $initrd_addr $initrd\n");
+
+ # Boot the system if there are some load lines in the script
+ if (scalar(@$modules) > 0) {
+ my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
+ my $dtb;
+ @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
+ my $initrd = shift @$modules;
+
+ my ($ramdisk_addr, $fdt_addr) = ('-', '');
+
+ 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; }],
- '=> ') || die "Initrd load failed";
+ [qr/##/, sub { exp_continue; }],
+ $uboot_prompt) || die "Kernel load timeout";
+ if (defined $dtb) {
+ die "No '--uboot-addr fdt' given" unless $uboot_addr{fdt};
+ $fdt_addr = $uboot_addr{fdt};
+ $exp->send("tftpboot $fdt_addr $prefix$dtb\n");
+ $exp->expect(10,
+ [qr/##/, sub { exp_continue; }],
+ $uboot_prompt) || die "Device tree load timeout";
+ }
+ if (defined $initrd) {
+ die "No '--uboot-addr ramdisk' given" unless $uboot_addr{ramdisk};
+ $ramdisk_addr = $uboot_addr{ramdisk};
+ $exp->send("tftpboot $ramdisk_addr $prefix$initrd\n");
+ $exp->expect(10,
+ [qr/#/, sub { exp_continue; }],
+ $uboot_prompt) || die "Initrd load timeout";
+ }
+ $exp->send("echo $kcmd\n");
+ $exp->expect(1, '-re', qr{echo .*\n(.*)\n$uboot_prompt}) || die "Command line test timeout";
+ my $args = ($exp->matchlist)[0];
+ if ($args =~ /^setenv\s+bootargs/) {
+ $exp->send("$args\n");
+ } else {
+ $exp->send("setenv bootargs $kcmd\n");
+ }
+ $exp->expect(5, $uboot_prompt) || die "U-Boot prompt timeout";
+ $exp->send("bootm $uboot_addr{kernel} $ramdisk_addr $fdt_addr\n");
+ $exp->expect(5, "\n") || die "U-Boot command timeout";
}
- $exp->send("set bootargs '$kcmd'\n");
- $exp->expect(5, '=> ') || die "uBoot prompt timeout";
- $exp->send("bootm $kern_addr $initrd_addr $dtb_addr\n");
- $exp->expect(5, "\n") || die "uBoot command timeout";
}
+### Serial line interaction
if (defined $exp) {
# Serial line of the target is available
my $interrupt = 'Ctrl-C';
if ($interactive && !@exiton) {
$interrupt = '"~~."';
}
- print "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
+ my $note = (-t STDIN) ? '' : '- only target->host ';
+ print "novaboot: Serial line interaction $note(press $interrupt to interrupt)...\n";
$exp->log_stdout(1);
if (@exiton) {
$exp->expect(undef, @expect_raw, @exiton);
}
## Kill dhcpc or tftpd
-if (defined $dhcp_tftp) {
+if (defined $dhcp_tftp || defined $tftp) {
die("novaboot: This should kill servers on background\n");
}
=head1 DESCRIPTION
-This program makes it easier to boot NOVA or other operating system
-(OS) on different targets (machines or emulators). It reads a so
-called novaboot script, that specifies the boot configuration, and
-setups the target to boot that configuration. Setting up the target
-means to generate the bootloader configuration files, deploy the
-binaries and other needed files to proper locations, perhaps on a
-remote boot server and reset the target. Then, target's serial output
-is redirected to standard output if that is possible.
-
-A typical way of using novaboot is to make the novaboot script
+This program makes booting of an operating system (e.g. NOVA or Linux)
+as simple as running a local program. It facilitates booting on local
+or remote hosts or in emulators such as qemu. Novaboot operation is
+controlled by command line options and by a so called novaboot script,
+which can be thought as a generalization of bootloader configuration
+files (see L</"NOVABOOT SCRIPT SYNTAX">). Based on this input,
+novaboot setups everything for the target host to boot the desired
+configuration, i.e. it generates the bootloader configuration file in
+the proper format, deploy the binaries and other needed files to
+required locations, perhaps on a remote boot server and reset the
+target host. Finally, target host's serial output is redirected to
+standard output if that is possible.
+
+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.
Run an OS in Qemu. This is the default action when no other action is
specified by command line switches. Thus running C<novaboot ./script>
(or C<./script> as described above) will run Qemu and make it boot the
-configuration specified in the I<script>.
+configuration specified in the F<script>.
=item 2.
Create a bootloader configuration file (currently supported
-bootloaders are GRUB, GRUB2, Pulsar and uBoot) and copy it with all
-other files needed for booting to another, perhaps remote, location.
+bootloaders are GRUB, GRUB2, ISOLINUX, Pulsar and U-Boot) and copy it
+with all other files needed for booting to a remote boot server.
./script --server=192.168.1.1:/tftp --iprelay=192.168.1.2
This command copies files to the TFTP server and uses
-TCP/IP-controlled relay to reset the test box and receive its serial
-output.
+TCP/IP-controlled relay to reset the target host and receive its
+serial output.
=item 3.
-Run DHCP and TFTP server on developer's machine to PXE-boot the OS
-from it. E.g.
+Run DHCP and TFTP server on developer's machine to PXE-boot the target
+machine from it. E.g.
./script --dhcp-tftp
When a PXE-bootable machine is connected via Ethernet to developer's
-machine, it will boot the configuration described in I<script>.
+machine, it will boot the configuration described in the I<script>.
=item 4.
novaboot --iso -- script1 script2
-The created ISO image will have GRUB bootloader installed on it and
+The created ISO image will use ISOLINUX bootloader installed on it and
the boot menu will allow selecting between I<script1> and I<script2>
configurations.
=back
Note that the options needed for a specific target can be stored in a
-L</"CONFIGURATION FILE"> and then it is sufficient to use only the
-B<-t> option to specify the name of the target.
+L</"CONFIGURATION FILE">. Then it is sufficient to use only the B<-t>
+option to specify the name of the target.
=head1 PHASES AND OPTIONS
Novaboot performs its work in several phases. Each phase can be
-influenced by several options, certain phases can be skipped. The list
-of phases (in the execution order) and the corresponding options
-follow.
+influenced by several command line options, certain phases can be
+skipped. The list of phases (in the execution order) and the
+corresponding options follow.
=head2 Configuration reading phase
-After starting, novaboot reads configuration files. By default, it
-searches for files named F<.novaboot> starting from the directory of
-the novaboot script (or working directory, see bellow) and continuing
-upwards up to the root directory. The configuration files are read in
-order from the root directory downwards with latter files overriding
-settings from the former ones.
+After starting, novaboot reads configuration files. Their content is
+described in section L</"CONFIGURATION FILE">. By default,
+configuration is read from two locations. First from the configuration
+directory and second from F<.novaboot> files along the path to the
+current directory. The later read files override settings from the
+former ones.
+
+Configuration directory is determined by the content of
+NOVABOOT_CONFIG_DIR environment variable defaulting to
+F</etc/novaboot.d>. Files in this directory with names consisting
+solely from English letters, numbers, dashes '-' and underscores '_'
+(note that dot '.' is not included) are read in alphabetical order.
+
+Then novaboot searches for files named F<.novaboot> starting from the
+directory of the novaboot script (or working directory, see bellow)
+and continuing upwards up to the root directory. The found
+configuration files are then read in the opposite order (i.e. from the
+root directory downwards). This allows to have, for example, user
+specific configuration in F<~/.novaboot> and project specific one in
+F<~/project/.novaboot>.
In certain cases, the location of the novaboot script cannot be
determined in this early phase. This happens either when the script is
-read from the standard input or when novaboot is invoked explicitly
-and options precede the script name, as in the example L</"4."> above.
-In this case the current working directory is used as a starting point
-for configuration file search.
+read from the standard input or when novaboot is invoked explicitly as
+in the example L</"4."> above. In this case the current working
+directory is used as a starting point for configuration file search
+instead of the novaboot script directory.
=over 8
Append a string to the first C<load> line in the novaboot script. This
can be used to append parameters to the kernel's or root task's
-command line. Can appear multiple times.
+command line. This option can appear multiple times.
=item -b, --bender
=item --chainloader=I<chainloader>
-Chainloader that is loaded before the kernel and other files specified
-in the novaboot script. E.g. 'bin/boot/bender promisc'.
+Specifies a chainloader that is loaded before the kernel and other
+files specified in the novaboot script. E.g. 'bin/boot/bender
+promisc'.
=item --dump
=head2 File generation phase
In this phase, files needed for booting are generated in a so called
-I<build directory> (see L<--build-dir>). In most cases configuration
+I<build directory> (see L</--build-dir>). In most cases configuration
for a bootloader is generated automatically by novaboot. It is also
possible to generate other files using I<heredoc> or I<"<"> syntax in
novaboot scripts. Finally, binaries can be generated in this phases by
used. Otherwise, it is the directory that contains the first processed
novaboot script.
-See also L<BUILDDIR> variable.
+See also L</BUILDDIR> variable.
=item -g, --grub[=I<filename>]
GRUB or GRUB2 config files. This is useful for specifying GRUB's
timeout.
-=item --grub-prefix=I<prefix>
+=item --prefix=I<prefix>
-Specifies I<prefix> that is put in front of every file name in GRUB's
-F<menu.lst>. The default value is the absolute path to the build directory.
+Specifies I<prefix> (e.g. F</srv/tftp>) that is put in front of every
+file name in generated bootloader configuration files (or in U-Boot
+commands).
If the I<prefix> contains string $NAME, it will be replaced with the
name of the novaboot script (see also B<--name>).
+If the I<prefix> contains string $BUILDDIR, it will be replaced with
+the build directory (see also B<--build-dir>).
+
+=item --grub-prefix
+
+Alias for B<--prefix>.
+
=item --grub2[=I<filename>]
-Generate GRUB2 menuentry in I<filename>. If I<filename> is not
-specified F<grub.cfg> is used. The content of the menuentry can be
-customized with B<--grub-preable>, B<--grub2-prolog> or
+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
+customized with B<--grub-preamble>, B<--grub2-prolog> or
B<--grub_prefix> options.
-In order to use the the generated menuentry on your development
+In order to use the 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,
i.e. run update-grub on Debian/Ubuntu.
=item --grub2-prolog=I<prolog>
-Specifies text I<preable> that is put at the beginning of the entry
-GRUB2 entry.
+Specifies text that is put at the beginning of the GRUB2 menu entry.
=item -m, --make[=make command]
=item --no-file-gen
-Do not generate files on the fly (i.e. "<" syntax) except for the
-files generated via "<<WORD" syntax.
+Do not run external commands to generate files (i.e. "<" syntax and
+C<run> keyword). This switch does not influence generation of files
+specified with "<<WORD" syntax.
=item -p, --pulsar[=mac]
=over 8
+=item --amt=I<"[user[:password]@]host[:port]>
+
+Use Intel AMT technology to control the target machine. WS management
+is used to powercycle it and Serial-Over-Lan (SOL) for input/output.
+The hostname or (IP address) is given by the I<host> parameter. If
+I<password> is not specified, environment variable AMT_PASSWORD is
+used. The I<port> specifies a TCP port for SOL. If not specified, the
+default is 16992. Default I<user> is admin.
+
=item --iprelay=I<addr[:port]>
Use TCP/IP relay and serial port to access the target's serial port
Wait for reception of I<string> after establishing the the remote
connection before continuing.
+
=back
=head2 File deployment phase
be booted via PXE BIOS (or similar mechanism) on the test machine
directly connected by a plain Ethernet cable to your workstation.
-The DHCP and TFTP servers require root privileges and C<novaboot>
+The DHCP and TFTP servers requires root privileges and C<novaboot>
uses C<sudo> command to obtain those. You can put the following to
-I</etc/sudoers> to allow running the necessary commands without
-asking for password.
+I</etc/sudoers> to allow running the necessary commands without asking
+for password.
- Cmnd_Alias NOVABOOT = /bin/ip a add 10.23.23.1/24 dev eth0, /bin/ip l set dev eth0 up, /usr/sbin/dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid, /usr/sbin/in.tftpd --foreground --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/touch dhcpd.leases, /usr/bin/pkill --pidfile=dhcpd.pid, /usr/bin/pkill --pidfile=tftpd.pid
+ Cmnd_Alias NOVABOOT = /bin/ip a add 10.23.23.1/24 dev eth0, /bin/ip l set dev eth0 up, /usr/sbin/dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid, /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/touch dhcpd.leases, /usr/bin/pkill --pidfile=dhcpd.pid, /usr/bin/pkill --pidfile=tftpd.pid
your_login ALL=NOPASSWD: NOVABOOT
+=item --tftp
+
+Starts a TFTP server on your workstation. This is similar to
+B<--dhcp-tftp> except that DHCP server is not started.
+
+The TFTP server require root privileges and C<novaboot> uses C<sudo>
+command to obtain those. You can put the following to I</etc/sudoers>
+to allow running the necessary commands without asking for password.
+
+ Cmnd_Alias NOVABOOT = /usr/sbin/in.tftpd --listen --secure -v -v -v --pidfile tftpd.pid *, /usr/bin/pkill --pidfile=tftpd.pid
+ your_login ALL=NOPASSWD: NOVABOOT
+
+=item --tftp-port=I<port>
+
+Port to run the TFTP server on. Implies B<--tftp>.
+
=item --iso[=filename]
Generates the ISO image that boots NOVA system via GRUB. If no filename
=item --server[=[[user@]server:]path]
-Copy all files needed for booting to another location (implies B<-g>
-unless B<--grub2> is given). 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 novaboot script (see also
-B<--name>).
+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
+novaboot script (see also B<--name>).
+
+=item --rsync-flags=I<flags>
+
+Specifies which I<flags> are appended to F<rsync> command line when
+copying files as a result of I<--server> option.
=item --concat
concatenating all files of the same name from all subdirectories of
I<path-wo-name> found on the "server".
-=item --rsync-flags=I<flags>
+=item --ider
-Specifies which I<flags> are appended to F<rsync> command line when
-copying files as a result of I<--server> option.
+Use Intel AMT technology for IDE redirection. This allows the target
+machine to boot from novaboot created ISO image. Implies B<--iso>.
+
+The experimental C<amtider> utility needed by this option can be
+obtained from https://github.com/wentasah/amtterm.
=back
=head2 Target power-on and reset phase
+At this point, the target is reset (or switched on/off). There is
+several ways how this can be accomplished. Resetting a physical target
+can currently be accomplished by the following options: B<--amt>,
+B<--iprelay>, B<--reset-cmd>.
+
=over 8
=item --on, --off
-Switch on/off the target machine. Currently works only with
-B<--iprelay>.
+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>.
=item -Q, --qemu[=I<qemu-binary>]
Command that resets the target.
+=item --no-reset, --reset
+
+Disable/enable reseting of the target.
+
=back
=head2 Interaction with the bootloader on the target
=over 8
-=item --uboot
+=item --uboot[=I<prompt>]
-Interact with uBoot bootloader to boot the thing described in the
-novaboot script. Implementation of this option is currently tied to a
-particular board that we use. It may be subject to changes in the
-future!
+Interact with U-Boot bootloader to boot the thing described in the
+novaboot script. I<prompt> specifies the U-Boot's prompt (default is
+"=> ", other common prompts are "U-Boot> " or "U-Boot# ").
+Implementation of this option is currently tied to a particular board
+that we use. It may be subject to changes in the future!
=item --uboot-init
Command(s) to send the U-Boot bootloader before loading the images and
-booting them.
+booting them. This option can be given multiple times. After sending
+commands from each option novaboot waits for U-Boot I<prompt>.
+
+If the command contains string I<$NB_MYIP> then this string is
+replaced by IPv4 address of eth0 interface.
+
+=item --uboot-addr I<name>=I<address>
+
+Load address of U-Boot's C<tftpboot> command for loading I<name>,
+where name is one of I<kernel>, I<ramdisk> or I<fdt> (flattened device
+tree).
=back
removal of the final "\" and leading whitespace of the following line.
Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
-expression) assign values to internal variables. See L<VARIABLES>
+expression) assign values to internal variables. See L</VARIABLES>
section.
Lines starting with C<load> keyword represent modules to boot. The
named on that line. This is similar to shell's heredoc feature.
When the C<load> line ends with "< CMD" then command CMD is executed
-with C</bin/sh> and its standard output is stored in the file named on
+with F</bin/sh> and its standard output is stored in the file named on
that line. The SRCDIR variable in CMD's environment is set to the
absolute path of the directory containing the interpreted novaboot
script.
-Example:
+Lines starting with C<run> keyword contain shell commands that are run
+during file generation phase. This is the same as the "< CMD" syntax
+for C<load> keyboard except that the command's output is not
+redirected to a file. The ordering of commands is the same as they
+appear in the novaboot script.
+
+Example (Linux):
+
+ #!/usr/bin/env novaboot
+ load bzImage console=ttyS0,115200
+ run make -C buildroot
+ load rootfs.cpio < gen_cpio buildroot/images/rootfs.cpio "myapp->/etc/init.d/S99myapp"
+
+Example (NOVA User Land - NUL):
+
#!/usr/bin/env novaboot
WVDESC=Example program
load bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
- verbose hostkeyb:0,0x60,1,12,2
+ verbose hostkeyb:0,0x60,1,12,2
load bin/apps/hello.nul
load hello.nulconfig <<EOF
sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
=item WVDESC
-Description of the wvtest-compliant program.
+Description of the WvTest-compliant program.
=item WVTEST_TIMEOUT
Novaboot can read its configuration from one or more files. By
default, novaboot looks for files named F<.novaboot> as described in
-L</Configuration reading phase>. Alternatively, its location can be
-specified with the B<-c> switch or with the NOVABOOT_CONFIG
-environment variable. The configuration file has perl syntax and
-should set values of certain Perl variables. The current configuration
-can be dumped with the B<--dump-config> switch. Some configuration
-variables can be overridden by environment variables (see below) or by
-command line switches.
+L</Configuration reading phase>. Alternatively, configuration file
+location can be specified with the B<-c> switch or with the
+NOVABOOT_CONFIG environment variable. The configuration file has Perl
+syntax and should set values of certain Perl variables. The current
+configuration can be dumped with the B<--dump-config> switch. Some
+configuration variables can be overridden by environment variables
+(see below) or by command line switches.
Supported configuration variables include:
=item %targets
-Hash of shortcuts to be used with the B<--target> option. If the hash
-contains, for instance, the following pair of values
+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:
- 'mybox' => '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
+ $targets{'mybox'} = '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
then the following two commands are equivalent:
Name of the novaboot configuration file to use instead of the default
one(s).
+=item NOVABOOT_CONFIG_DIR
+
+Name of the novaboot configuration directory. When not specified
+F</etc/novaboot.d> is used.
+
=item NOVABOOT_BENDER
Defining this variable has the same meaning as B<--bender> option.
=head1 AUTHORS
Michal Sojka <sojka@os.inf.tu-dresden.de>
+
+=cut
+
+# LocalWords: novaboot Novaboot NOVABOOT TFTP PXE DHCP filename stty
+# LocalWords: chainloader stdout Qemu qemu preprocessing ISOLINUX bootable
+# LocalWords: config subprocesses sudo sudoers tftp dhcp IDE stdin
+# LocalWords: subdirectories TTY whitespace heredoc POSIX WvTest