#!/usr/bin/perl -w use strict; use warnings; use Getopt::Long qw(GetOptionsFromString); use Pod::Usage; use File::Basename; use File::Spec; use IO::Handle; use Time::HiRes("usleep"); use Socket; use FileHandle; use IPC::Open2; use POSIX qw(:errno_h); use Cwd; # always flush $| = 1; my $invocation_dir = getcwd(); chomp(my $gittop = `git rev-parse --show-toplevel 2>/dev/null`); # Default configuration $CFG::hypervisor = "bin/apps/hypervisor"; $CFG::hypervisor_params = "serial"; $CFG::server = "erwin.inf.tu-dresden.de:boot/novaboot/\$NAME"; $CFG::server_grub_prefix = "(nd)/tftpboot/sojka/novaboot/\$NAME"; $CFG::grub_keys = ''; #"/novaboot\n\n/\$NAME\n\n"; $CFG::grub2_prolog = " set root='(hd0,msdos1)'"; $CFG::genisoimage = "genisoimage"; $CFG::iprelay_addr = '141.76.48.80:2324'; #'141.76.48.252'; $CFG::qemu = 'qemu'; $CFG::script_modifier = ''; # Depricated, use --scriptmod commandline option or custom_options. @CFG::chainloaders = (); #('bin/boot/bender promisc'); $CFG::pulsar_root = ''; $CFG::pulsar_mac = '52-54-00-12-34-56'; %CFG::custom_options = ('I' => '--server=erwin.inf.tu-dresden.de:~passive/boot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --grub-prefix=(nd)/tftpboot/passive/ --iprelay=141.76.48.80:2324 --scriptmod=s/\\\\bhostserial\\\\b/hostserialpci/g', 'J' => '--server=rtime.felk.cvut.cz:/srv/tftp/novaboot --rsync-flags="--chmod=Dg+s,ug+w,o-w,+rX --rsync-path=\"umask 002 && rsync\"" --pulsar=novaboot --iprelay=147.32.86.92:2324'); $CFG::scons = "scons -j2"; my @qemu_flags = qw(-cpu coreduo -smp 2); sub read_config($) { my ($cfg) = @_; { package CFG; # Put config data into a separate namespace my $rc = do($cfg); # Check for errors if ($@) { die("ERROR: Failure compiling '$cfg' - $@"); } elsif (! defined($rc)) { die("ERROR: Failure reading '$cfg' - $!"); } elsif (! $rc) { die("ERROR: Failure processing '$cfg'"); } } } my $cfg = $ENV{'NOVABOOT_CONFIG'} || $ENV{'HOME'}."/.novaboot"; Getopt::Long::Configure(qw/no_ignore_case pass_through/); GetOptions ("config|c=s" => \$cfg); if (-s $cfg) { read_config($cfg); } # Command line my ($append, $bender, $builddir, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, $grub_config, $grub_prefix, $grub2_config, $help, $iprelay, $iso_image, $man, $no_file_gen, $off_opt, $on_opt, $pulsar, $qemu, $qemu_append, $qemu_flags_cmd, $rsync_flags, @scriptmod, $scons, $serial, $server); $rsync_flags = ''; Getopt::Long::Configure(qw/no_ignore_case no_pass_through/); my %opt_spec = ( "append|a=s" => \$append, "bender|b" => \$bender, "build-dir=s" => \$builddir, "dhcp-tftp|d" => \$dhcp_tftp, "dump" => \$dump_opt, "dump-config" => \$dump_config, "grub|g:s" => \$grub_config, "grub-prefix=s" => \$grub_prefix, "grub2:s" => \$grub2_config, "iprelay:s" => \$iprelay, "iso|i:s" => \$iso_image, "name=s" => \$config_name_opt, "no-file-gen" => \$no_file_gen, "off" => \$off_opt, "on" => \$on_opt, "pulsar|p:s" => \$pulsar, "qemu|Q=s" => \$qemu, "qemu-append=s" => \$qemu_append, "qemu-flags|q=s" => \$qemu_flags_cmd, "rsync-flags=s" => \$rsync_flags, "scons:s" => \$scons, "scriptmod=s" => \@scriptmod, "serial|s:s" => \$serial, "server:s" => \$server, "h" => \$help, "help" => \$man, ); foreach my $opt(keys(%CFG::custom_options)) { $opt_spec{$opt} = sub { GetOptionsFromString($CFG::custom_options{$opt}, %opt_spec); }; } GetOptions %opt_spec or pod2usage(2); pod2usage(1) if $help; pod2usage(-exitstatus => 0, -verbose => 2) if $man; $CFG::iprelay_addr = $ENV{'NOVABOOT_IPRELAY'} if $ENV{'NOVABOOT_IPRELAY'}; if ($iprelay && $iprelay ne "on" && $iprelay ne "off") { $CFG::iprelay_addr = $iprelay; } if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; } if ($server) { $CFG::server = $server; } if ($qemu) { $CFG::qemu = $qemu; } $qemu_append ||= ''; if (defined $pulsar) { $CFG::pulsar_mac = $pulsar; } if ($scons) { $CFG::scons = $scons; } if (!@scriptmod && $CFG::script_modifier) { @scriptmod = ( $CFG::script_modifier ); } if (defined $grub_prefix) { $CFG::server_grub_prefix = $grub_prefix; } if ($dump_config) { use Data::Dumper; $Data::Dumper::Indent=0; print "# This file is in perl syntax.\n"; foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1) if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); } if (defined @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); } if ( %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); } print "\n"; } print "1;\n"; exit; } if (defined $serial) { $serial ||= "/dev/ttyUSB0"; } if (defined $grub_config) { $grub_config ||= "menu.lst"; } if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; } if ($on_opt) { $iprelay="on"; } if ($off_opt) { $iprelay="off"; } # Parse the config(s) my @scripts; my $file; my $line; my $EOF; my $last_fn = ''; my ($modules, $variables, $generated, $continuation); while (<>) { if ($ARGV ne $last_fn) { # New script die "Missing EOF in $last_fn" if $file; die "Unfinished line in $last_fn" if $line; $last_fn = $ARGV; push @scripts, { 'filename' => $ARGV, 'modules' => $modules = [], 'variables' => $variables = {}, 'generated' => $generated = []}; } chomp(); next if /^#/ || /^\s*$/; # Skip comments and empty lines foreach my $mod(@scriptmod) { eval $mod; } print "$_\n" if $dump_opt; if (/^([A-Z_]+)=(.*)$/) { # Internal variable $$variables{$1} = $2; next; } if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start push @$modules, "$1$2"; $file = []; push @$generated, {filename => $1, content => $file}; $EOF = $3; next; } if ($file && $_ eq $EOF) { # Heredoc end undef $file; next; } if ($file) { # Heredoc content push @{$file}, "$_\n"; next; } $_ =~ s/^[[:space:]]*// if ($continuation); if (/\\$/) { # Line continuation $line .= substr($_, 0, length($_)-1); $continuation = 1; next; } $continuation = 0; $line .= $_; $line .= " $append" if ($append && scalar(@$modules) == 0); if ($line =~ /^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution push @$modules, "$1$2"; push @$generated, {filename => $1, command => $3}; $line = ''; next; } push @$modules, $line; $line = ''; } #use Data::Dumper; #print Dumper(\@scripts); exit if $dump_opt; sub generate_configs($$$) { my ($base, $generated, $filename) = @_; if ($base) { $base = "$base/"; }; foreach my $g(@$generated) { if (exists $$g{content}) { my $config = $$g{content}; my $fn = $$g{filename}; open(my $f, '>', $fn) || die("$fn: $!"); map { s|\brom://([^ ]*)|rom://$base$1|g; print $f "$_"; } @{$config}; close($f); 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}"); } } } sub generate_grub_config($$$$;$) { my ($filename, $title, $base, $modules_ref, $prepend) = @_; if ($base) { $base = "$base/"; }; open(my $fg, '>', $filename) or die "$filename: $!"; print $fg "$prepend\n" if $prepend; my $endmark = ($serial || defined $iprelay) ? ';' : ''; print $fg "title $title$endmark\n" if $title; #print $fg "root $base\n"; # root doesn't really work for (nd) my $first = 1; foreach (@$modules_ref) { if ($first) { $first = 0; my ($kbin, $kcmd) = split(' ', $_, 2); $kcmd = '' if !defined $kcmd; print $fg "kernel ${base}$kbin $kcmd\n"; } else { s|\brom://([^ ]*)|rom://$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0 print $fg "module $base$_\n"; } } close($fg); } sub generate_grub2_config($$$$;$) { my ($filename, $title, $base, $modules_ref, $prepend) = @_; if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; }; open(my $fg, '>', $filename) or die "$filename: $!"; print $fg "$prepend\n" if $prepend; my $endmark = ($serial || defined $iprelay) ? ';' : ''; $title ||= 'novaboot'; print $fg "menuentry $title$endmark {\n"; print $fg "$CFG::grub2_prolog\n"; my $first = 1; foreach (@$modules_ref) { if ($first) { $first = 0; my ($kbin, $kcmd) = split(' ', $_, 2); $kcmd = '' if !defined $kcmd; print $fg " multiboot ${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://$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0 print $fg " module $base$_\n"; } } print $fg "}\n"; close($fg); } sub generate_pulsar_config($$) { my ($filename, $modules_ref) = @_; open(my $fg, '>', $filename) or die "$filename: $!"; print $fg "root $CFG::pulsar_root\n" if $CFG::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; 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"; close($fg); } sub exec_verbose(@) { print "novaboot: Running: ".join(' ', map("'$_'", @_))."\n"; exec(@_); } sub system_verbose($) { my $cmd = shift; print "novaboot: Running: $cmd\n"; my $ret = system($cmd); 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"); } } if (exists $variables->{WVDESC}) { print "Testing \"$variables->{WVDESC}\" in $last_fn:\n"; } elsif ($last_fn =~ /\.wv$/) { print "Testing \"all\" in $last_fn:\n"; } my $IPRELAY; if (defined $iprelay) { $CFG::iprelay_addr =~ /([.0-9]+)(:([0-9]+))?/; my $addr = $1; my $port = $3 || 23; my $paddr = sockaddr_in($port, inet_aton($addr)); my $proto = getprotobyname('tcp'); socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; print "novaboot: Connecting to IP relay... "; connect($IPRELAY, $paddr) || die "connect: $!"; print "done\n"; $IPRELAY->autoflush(1); while (1) { print $IPRELAY "\xFF\xF6"; alarm(20); local $SIG{ALRM} = sub { die "Relay AYT timeout"; }; my $ayt_reponse = ""; my $read = sysread($IPRELAY, $ayt_reponse, 100); alarm(0); chomp($ayt_reponse); print "$ayt_reponse\n"; if ($ayt_reponse =~ /blocking(0); alarm(20); # Timeout in seconds my $giveup = 0; local $SIG{ALRM} = sub { if ($can_giveup) { print("Relay confirmation timeout - ignoring\n"); $giveup = 1;} else {die "Relay confirmation timeout";} }; my $index; while (($index=index($confirmation, relayconf($relay, $onoff))) < 0 && !$giveup) { my $read = read($IPRELAY, $confirmation, 70, length($confirmation)); if (!defined($read)) { die("IP relay: $!") unless $! == EAGAIN; usleep(10000); next; } #use MIME::QuotedPrint; #print "confirmation = ".encode_qp($confirmation)."\n"; } alarm(0); $IPRELAY->blocking(1); } } if ($iprelay && ($iprelay eq "on" || $iprelay eq "off")) { relay(1, 1); # Press power button if ($iprelay eq "on") { usleep(100000); # Short press } else { usleep(6000000); # Long press to switch off } print $IPRELAY relay(1, 0); exit; } if ($builddir) { $CFG::builddir = $builddir; } else { if (! defined $CFG::builddir) { $CFG::builddir = ( $gittop || $ENV{'HOME'}."/nul" ) . "/build"; if (! -d $CFG::builddir) { $CFG::builddir = $ENV{SRCDIR} = dirname(File::Spec->rel2abs( ${$scripts[0]}{filename}, $invocation_dir )); } } } chdir($CFG::builddir) or die "Can't change directory to $CFG::builddir: $!"; print "novaboot: Entering directory `$CFG::builddir'\n"; my (%files_iso, $menu_iso, $config_name, $filename); foreach my $script (@scripts) { $filename = $$script{filename}; $modules = $$script{modules}; $generated = $$script{generated}; $variables = $$script{variables}; my ($server_grub_prefix); if (defined $config_name_opt) { $config_name = $config_name_opt; } else { ($config_name = $filename) =~ s#.*/##; } my $kernel; if (exists $variables->{KERNEL}) { $kernel = $variables->{KERNEL}; } else { $kernel = $CFG::hypervisor . " "; if (exists $variables->{HYPERVISOR_PARAMS}) { $kernel .= $variables->{HYPERVISOR_PARAMS}; } else { $kernel .= $CFG::hypervisor_params; } } @$modules = ($kernel, @$modules); @$modules = (@CFG::chainloaders, @$modules); @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'}); if (defined $grub_config) { generate_configs("", $generated, $filename); generate_grub_config($grub_config, $config_name, "", $modules); print("GRUB menu created: $CFG::builddir/$grub_config\n"); exit; } if (defined $grub2_config && !defined $server) { generate_configs('', $generated, $filename); generate_grub2_config($grub2_config, $config_name, $CFG::builddir, $modules); print("GRUB2 configuration created: $CFG::builddir/$grub2_config\n"); exit; } my $pulsar_config; if (defined $pulsar) { $pulsar_config = "config-$CFG::pulsar_mac"; generate_configs('', $generated, $filename); generate_pulsar_config($pulsar_config, $modules); if (!defined $server) { print("Pulsar configuration created: $CFG::builddir/$pulsar_config\n"); exit; } } if (defined $scons) { my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules); # Filter-out generated files my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files); system_verbose($CFG::scons." ".join(" ", @to_build)); } if (defined $server) { ($server_grub_prefix = $CFG::server_grub_prefix) =~ s/\$NAME/$config_name/; ($server = $CFG::server) =~ s/\$NAME/$config_name/; my $bootloader_config; if ($grub2_config) { generate_configs('', $generated, $filename); $bootloader_config ||= "grub.cfg"; generate_grub2_config($grub2_config, $config_name, $server_grub_prefix, $modules); } elsif (defined $pulsar) { $bootloader_config = $pulsar_config; } else { generate_configs($server_grub_prefix, $generated, $filename); $bootloader_config ||= "menu.lst"; if (!grep { $_ eq $bootloader_config } @$modules) { generate_grub_config($bootloader_config, $config_name, $server_grub_prefix, $modules, $server_grub_prefix eq $CFG::server_grub_prefix ? "timeout 0" : undef); } } my ($hostname, $path) = split(":", $server, 2); if (! defined $path) { $path = $hostname; $hostname = ""; } my $files = "$bootloader_config " . join(" ", map({ ($file) = m/([^ ]*)/; $file; } @$modules)); my $combined_menu_lst = ($CFG::server =~ m|/\$NAME$|); map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules); my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb"; my $progress = $istty ? "--progress" : ""; system_verbose("rsync $progress -RLp $rsync_flags $files $server"); my $cmd = "cd $path/.. && cat */menu.lst > menu.lst"; if ($combined_menu_lst) { system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd); } } if (defined $iso_image) { generate_configs("(cd)", $generated, $filename); my $menu; generate_grub_config(\$menu, $config_name, "(cd)", $modules); $menu_iso .= "$menu\n"; map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules; } } if (defined $iso_image) { open(my $fh, ">menu-iso.lst"); print $fh "timeout 5\n\n$menu_iso"; close($fh); my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso))); $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"); print("ISO image created: $CFG::builddir/$iso_image\n"); } ###################################################################### # Boot NOVA using various methods and send serial output to stdout ###################################################################### if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) { die "You cannot do this with multiple scripts simultaneously"; } if ($variables->{WVTEST_TIMEOUT}) { print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n"; } if (!(defined $dhcp_tftp || defined $serial || defined $iprelay || defined $server || defined $iso_image)) { # Qemu @qemu_flags = split(/ /, $variables->{QEMU_FLAGS}) if exists $variables->{QEMU_FLAGS}; @qemu_flags = split(/ /, $qemu_flags_cmd) if $qemu_flags_cmd; push(@qemu_flags, split(/ /, $qemu_append)); if (defined $iso_image) { # Boot NOVA with grub (and test the iso image) push(@qemu_flags, ('-cdrom', "$config_name.iso")); } else { # Boot NOVA without GRUB # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans foreach (@$modules) {s/,/+/g;} generate_configs("", $generated, $filename); my ($kbin, $kcmd) = split(' ', shift(@$modules), 2); $kcmd = '' if !defined $kcmd; my $initrd = join ",", @$modules; push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd)); push(@qemu_flags, ('-initrd', $initrd)) if $initrd; } push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls) exec_verbose(($CFG::qemu, '-name', $config_name, @qemu_flags)); } my ($dhcpd_pid, $tftpd_pid); if (defined $dhcp_tftp) { generate_configs("(nd)", $generated, $filename); 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`; 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; } 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; sudo touch dhcpd.leases"); $dhcpd_pid = fork(); if ($dhcpd_pid == 0) { # This way, the spawned server are killed when this script is killed. exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid"); } $tftpd_pid = fork(); if ($tftpd_pid == 0) { exec_verbose("sudo in.tftpd --foreground --secure -v -v -v $CFG::builddir"); } $SIG{TERM} = sub { print "CHILDS KILLED\n"; kill 15, $dhcpd_pid, $tftpd_pid; }; } if ($serial || defined $iprelay) { my $CONN; if (defined $iprelay) { print "novaboot: Reseting the test box... "; relay(2, 1, 1); # Reset the machine usleep(100000); relay(2, 0); print "done\n"; $CONN = $IPRELAY; } elsif ($serial) { system("stty -F $serial raw -crtscts -onlcr 115200"); open($CONN, "+<", $serial) || die "open $serial: $!"; $CONN->autoflush(1); } if (!defined $dhcp_tftp && $CFG::grub_keys) { # Control grub via serial line print "Waiting for GRUB's serial output... "; while (<$CONN>) { if (/Press any key to continue/) { print $CONN "\n"; last; } } $CFG::grub_keys =~ s/\$NAME/$config_name;/; my @characters = split(//, $CFG::grub_keys); foreach (@characters) { print $CONN $_; usleep($_ eq "\n" ? 100000 : 10000); } print $CONN "\n"; print "done\n"; } # Pass the NOVA output to stdout. while (<$CONN>) { print; } kill 15, $dhcpd_pid, $tftpd_pid if ($dhcp_tftp); exit; } if (defined $dhcp_tftp) { my $pid = wait(); if ($pid == $dhcpd_pid) { print "dhcpd exited!\n"; } elsif ($pid == $tftpd_pid) { print "tftpd exited!\n"; } else { print "wait returned: $pid\n"; } kill(15, 0); # Kill current process group i.e. all remaining children } =head1 NAME novaboot - NOVA boot script interpreter =head1 SYNOPSIS B [ options ] [--] script... B<./script> [ options ] B --help =head1 DESCRIPTION This program makes it easier to boot NOVA or other operating system (OS) in different environments. It reads a so called novaboot script and uses it either to boot the OS in an emulator (e.g. in qemu) or to generate the configuration for a specific bootloader and optionally to copy the necessary binaries and other needed files to proper locations, perhaps on a remote server. In case the system is actually booted, its serial output is redirected to standard output if that is possible. A 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. With C you can: =over 3 =item 1. Run NOVA in Qemu. This is the default action when no other action is specified by command line switches. Thus running C (or C<./script> as described above) will run Qemu with configuration specified in the I