3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 use warnings (exists $ENV{NOVABOOT_TEST} ? (FATAL => 'all') : ());
19 use Getopt::Long qw(GetOptionsFromString);
24 use Time::HiRes("usleep");
28 use POSIX qw(:errno_h);
29 use Cwd qw(getcwd abs_path);
35 my $invocation_dir = $ENV{PWD} || getcwd();
37 ## Configuration file handling
39 # Default configuration
40 $CFG::hypervisor = "";
41 $CFG::hypervisor_params = "serial";
42 $CFG::genisoimage = "genisoimage";
43 $CFG::qemu = 'qemu -cpu coreduo -smp 2';
44 $CFG::default_target = 'qemu';
47 "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',
48 "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',
49 "localhost" => '--scriptmod=s/console=tty[A-Z0-9,]+// --server=/boot/novaboot/$NAME --grub2 --grub-prefix=/boot/novaboot/$NAME --grub2-prolog=" set root=\'(hd0,msdos1)\'"',
50 "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\""',
51 "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"',
54 chomp(my $nproc = `nproc`);
55 $CFG::scons = "scons -j$nproc";
56 $CFG::make = "make -j$nproc";
63 package CFG; # Put config data into a separate namespace
68 die("ERROR: Failure compiling '$cfg' - $@");
69 } elsif (! defined($rc)) {
70 die("ERROR: Failure reading '$cfg' - $!");
72 die("ERROR: Failure processing '$cfg'");
75 $builddir = File::Spec->rel2abs($CFG::builddir, dirname($cfg)) if defined $CFG::builddir;
76 print STDERR "novaboot: Read $cfg\n";
81 # We don't use $0 here, because it points to the novaboot itself and
82 # not to the novaboot script. The problem with this approach is that
83 # when a script is run as "novaboot <options> <script>" then $ARGV[0]
84 # contains the first option. Hence the -f check.
85 my $dir = File::Spec->rel2abs($ARGV[0] && -f $ARGV[0] ? dirname($ARGV[0]) : '', $invocation_dir);
86 while ((-d $dir || -l $dir ) && $dir ne "/") {
87 push @cfgs, "$dir/.novaboot" if -r "$dir/.novaboot";
88 my @dirs = File::Spec->splitdir($dir);
89 $dir = File::Spec->catdir(@dirs[0..$#dirs-1]);
92 my $cfg = $ENV{'NOVABOOT_CONFIG'};
93 Getopt::Long::Configure(qw/no_ignore_case pass_through/);
94 GetOptions ("config|c=s" => \$cfg);
95 read_config($_) foreach $cfg or reverse @cfgs;
97 ## Command line handling
100 GetOptions ("target|t=s" => \$explicit_target);
102 my ($append, $bender, @chainloaders, $concat, $config_name_opt, $dhcp_tftp, $dump_opt, $dump_config, @exiton, $gen_only, $grub_config, $grub_prefix, $grub_preamble, $grub2_prolog, $grub2_config, $help, $iprelay, $iso_image, $interactive, $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);
105 $rom_prefix = 'rom://';
106 $stty = 'raw -crtscts -onlcr 115200';
108 Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
111 "append|a=s" => \$append,
112 "bender|b" => \$bender,
113 "build-dir=s" => sub { my ($n, $v) = @_; $builddir = File::Spec->rel2abs($v); },
114 "concat" => \$concat,
115 "chainloader=s" => \@chainloaders,
116 "dhcp-tftp|d" => \$dhcp_tftp,
117 "dump" => \$dump_opt,
118 "dump-config" => \$dump_config,
119 "exiton=s" => \@exiton,
120 "gen-only" => \$gen_only,
121 "grub|g:s" => \$grub_config,
122 "grub-preamble=s"=> \$grub_preamble,
123 "grub-prefix=s" => \$grub_prefix,
124 "grub2:s" => \$grub2_config,
125 "grub2-prolog=s" => \$grub2_prolog,
126 "iprelay=s" => \$iprelay,
127 "iso:s" => \$iso_image,
128 "interactive|i" => \$interactive,
129 "name=s" => \$config_name_opt,
130 "make|m:s" => \$make,
131 "no-file-gen" => \$no_file_gen,
134 "pulsar|p:s" => \$pulsar,
135 "pulsar-root=s" => \$pulsar_root,
136 "qemu|Q:s" => \$qemu,
137 "qemu-append=s" => \$qemu_append,
138 "qemu-flags|q=s" => \$qemu_flags_cmd,
139 "remote-cmd=s" => \$remote_cmd,
140 "remote-expect=s"=> \$remote_expect,
141 "reset-cmd=s" => \$reset_cmd,
142 "rsync-flags=s" => \$rsync_flags,
143 "scons:s" => \$scons,
144 "scriptmod=s" => \@scriptmod,
145 "serial|s:s" => \$serial,
146 "server:s" => \$server,
147 "strip-rom" => sub { $rom_prefix = ''; },
150 "uboot-init=s" => \$uboot_init,
155 # First process target options
157 my $t = $explicit_target || $CFG::default_target;
158 exists $CFG::targets{$t} or die("Unknown target '$t' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
159 GetOptionsFromString($CFG::targets{$t}, %opt_spec);
162 # Then process other command line options - some of them may override
163 # what was specified by the target
164 GetOptions %opt_spec or die("Error in command line arguments");
165 pod2usage(1) if $help;
166 pod2usage(-exitstatus => 0, -verbose => 2) if $man;
168 ### Dump sanitized configuration (if requested)
172 $Data::Dumper::Indent=1;
173 print "# This file is in perl syntax.\n";
174 foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1)
175 if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); }
176 if ( @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); }
177 if ( %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); }
183 ### Sanitize configuration
185 if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
188 if (defined $serial) {
189 $serial ||= "/dev/ttyUSB0";
190 $ENV{NB_SERIAL} = $serial;
192 if (defined $grub_config) { $grub_config ||= "menu.lst"; }
193 if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
195 ## Parse the novaboot script(s)
201 my ($modules, $variables, $generated, $continuation);
203 if ($ARGV ne $last_fn) { # New script
204 die "Missing EOF in $last_fn" if $file;
205 die "Unfinished line in $last_fn" if $line;
207 push @scripts, { 'filename' => $ARGV,
208 'modules' => $modules = [],
209 'variables' => $variables = {},
210 'generated' => $generated = []};
214 next if /^#/ || /^\s*$/; # Skip comments and empty lines
216 foreach my $mod(@scriptmod) { eval $mod; }
218 print "$_\n" if $dump_opt;
220 if (/^([A-Z_]+)=(.*)$/) { # Internal variable
221 $$variables{$1} = $2;
222 push(@exiton, $2) if ($1 eq "EXITON");
225 if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
226 push @$modules, "$1$2";
228 push @$generated, {filename => $1, content => $file};
232 if ($file && $_ eq $EOF) { # Heredoc end
236 if ($file) { # Heredoc content
237 push @{$file}, "$_\n";
240 $_ =~ s/^[[:space:]]*// if ($continuation);
241 if (/\\$/) { # Line continuation
242 $line .= substr($_, 0, length($_)-1);
248 $line .= " $append" if ($append && scalar(@$modules) == 0);
250 if ($line =~ /^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
251 push @$modules, "$1$2";
252 push @$generated, {filename => $1, command => $3};
256 push @$modules, $line;
260 #print Dumper(\@scripts);
266 sub generate_configs($$$) {
267 my ($base, $generated, $filename) = @_;
268 if ($base) { $base = "$base/"; };
269 foreach my $g(@$generated) {
270 if (exists $$g{content}) {
271 my $config = $$g{content};
272 my $fn = $$g{filename};
273 open(my $f, '>', $fn) || die("$fn: $!");
274 map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
276 print "novaboot: Created $fn\n";
277 } elsif (exists $$g{command} && ! $no_file_gen) {
278 $ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
279 system_verbose("( $$g{command} ) > $$g{filename}");
284 sub generate_grub_config($$$$;$)
286 my ($filename, $title, $base, $modules_ref, $preamble) = @_;
287 if ($base) { $base = "$base/"; };
288 open(my $fg, '>', $filename) or die "$filename: $!";
289 print $fg "$preamble\n" if $preamble;
290 print $fg "title $title\n" if $title;
291 #print $fg "root $base\n"; # root doesn't really work for (nd)
293 foreach (@$modules_ref) {
296 my ($kbin, $kcmd) = split(' ', $_, 2);
297 $kcmd = '' if !defined $kcmd;
298 print $fg "kernel ${base}$kbin $kcmd\n";
300 s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
301 print $fg "module $base$_\n";
305 print("novaboot: Created $builddir/$filename\n");
309 sub generate_grub2_config($$$$;$$)
311 my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
312 if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
313 open(my $fg, '>', $filename) or die "$filename: $!";
314 print $fg "$preamble\n" if $preamble;
315 $title ||= 'novaboot';
316 print $fg "menuentry $title {\n";
317 print $fg "$prolog\n" if $prolog;
319 foreach (@$modules_ref) {
322 my ($kbin, $kcmd) = split(' ', $_, 2);
323 $kcmd = '' if !defined $kcmd;
324 print $fg " multiboot ${base}$kbin $kcmd\n";
327 # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
328 $_ = join(' ', ($args[0], @args));
329 s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
330 print $fg " module $base$_\n";
335 print("novaboot: Created $builddir/$filename\n");
339 sub generate_pulsar_config($$)
341 my ($filename, $modules_ref) = @_;
342 open(my $fg, '>', $filename) or die "$filename: $!";
343 print $fg "root $pulsar_root\n" if defined $pulsar_root;
346 foreach (@$modules_ref) {
349 ($kbin, $kcmd) = split(' ', $_, 2);
350 $kcmd = '' if !defined $kcmd;
353 s|\brom://|$rom_prefix|g;
354 print $fg "load $_\n";
357 # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
358 print $fg "exec $kbin $kcmd\n";
360 print("novaboot: Created $builddir/$filename\n");
364 sub shell_cmd_string(@)
366 return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
371 print "novaboot: Running: ".shell_cmd_string(@_)."\n";
375 sub system_verbose($)
378 print "novaboot: Running: $cmd\n";
379 my $ret = system($cmd);
380 if ($ret & 0x007f) { die("Command terminated by a signal"); }
381 if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
382 if ($ret) { die("Command failure $ret"); }
387 if (exists $variables->{WVDESC}) {
388 print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
389 } elsif ($last_fn =~ /\.wv$/) {
390 print "Testing \"all\" in $last_fn:\n";
393 ## Connect to the target and check whether is not occupied
395 # We have to do this before file generation phase, because file
396 # generation is intermixed with file deployment phase and we want to
397 # check whether the target is not used by somebody else before
398 # deploying files. Otherwise, we may rewrite other user's files on a
401 my $exp; # Expect object to communicate with the target over serial line
403 my ($target_reset, $target_power_on, $target_power_off);
405 if (defined $iprelay) {
407 $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
410 my $paddr = sockaddr_in($port, inet_aton($addr));
411 my $proto = getprotobyname('tcp');
412 socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
413 print "novaboot: Connecting to IP relay... ";
414 connect($IPRELAY, $paddr) || die "connect: $!";
416 $exp = Expect->init(\*$IPRELAY);
420 print $exp "\xFF\xF6"; # AYT
421 my $connected = $exp->expect(20, # Timeout in seconds
422 '<iprelayd: connected>',
423 '-re', '<WEB51 HW[^>]*>');
428 my ($relay, $onoff) = @_;
429 die unless ($relay == 1 || $relay == 2);
431 my $cmd = ($relay == 1 ? 0x5 : 0x6) | ($onoff ? 0x20 : 0x10);
432 return "\xFF\xFA\x2C\x32".chr($cmd)."\xFF\xF0";
436 my ($relay, $onoff) = @_;
437 die unless ($relay == 1 || $relay == 2);
438 my $cmd = ($relay == 1 ? 0xdf : 0xbf) | ($onoff ? 0x00 : 0xff);
439 return "\xFF\xFA\x2C\x97".chr($cmd)."\xFF\xF0";
443 my ($relay, $onoff, $can_giveup) = @_;
444 my $confirmation = '';
446 print $exp relaycmd($relay, $onoff);
447 my $confirmed = $exp->expect(20, # Timeout in seconds
448 relayconf($relay, $onoff));
451 print("Relay confirmation timeout - ignoring\n");
453 die "Relay confirmation timeout";
459 $target_reset = sub {
460 relay(2, 1, 1); # Reset the machine
465 $target_power_off = sub {
466 relay(1, 1); # Press power button
467 usleep(6000000); # Long press to switch off
471 $target_power_on = sub {
472 relay(1, 1); # Press power button
473 usleep(100000); # Short press
479 system_verbose("stty -F $serial $stty");
480 open($CONN, "+<", $serial) || die "open $serial: $!";
481 $exp = Expect->init(\*$CONN);
482 } elsif ($remote_cmd) {
483 print "novaboot: Running: $remote_cmd\n";
484 $exp = Expect->spawn($remote_cmd);
487 if ($remote_expect) {
488 $exp->expect(10, $remote_expect) || die "Expect for '$remote_expect' timed out";
491 if (defined $reset_cmd) {
492 $target_reset = sub {
493 system_verbose($reset_cmd);
497 if (defined $on_opt && defined $target_power_on) {
501 if (defined $off_opt && defined $target_power_off) {
502 print "novaboot: Switching the target off...\n";
503 &$target_power_off();
507 $builddir ||= dirname(File::Spec->rel2abs( ${$scripts[0]}{filename})) if scalar @scripts;
508 if (defined $builddir) {
509 chdir($builddir) or die "Can't change directory to $builddir: $!";
510 print "novaboot: Entering directory `$builddir'\n";
513 ## File generation phase
514 my (%files_iso, $menu_iso, $filename);
515 my $config_name = '';
517 foreach my $script (@scripts) {
518 $filename = $$script{filename};
519 $modules = $$script{modules};
520 $generated = $$script{generated};
521 $variables = $$script{variables};
523 ($config_name = $filename) =~ s#.*/##;
524 $config_name = $config_name_opt if (defined $config_name_opt);
526 if (exists $variables->{BUILDDIR}) {
527 $builddir = File::Spec->rel2abs($variables->{BUILDDIR});
528 chdir($builddir) or die "Can't change directory to $builddir: $!";
529 print "novaboot: Entering directory `$builddir'\n";
533 if (exists $variables->{KERNEL}) {
534 $kernel = $variables->{KERNEL};
536 if ($CFG::hypervisor) {
537 $kernel = $CFG::hypervisor . " ";
538 if (exists $variables->{HYPERVISOR_PARAMS}) {
539 $kernel .= $variables->{HYPERVISOR_PARAMS};
541 $kernel .= $CFG::hypervisor_params;
545 @$modules = ($kernel, @$modules) if $kernel;
546 @$modules = (@chainloaders, @$modules);
547 @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
550 ($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
551 $prefix ||= $builddir;
552 # TODO: use $grub_prefix as first parameter if some switch is given
553 generate_configs('', $generated, $filename);
555 ### Generate bootloader configuration files
556 my @bootloader_configs;
557 push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
558 push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
559 push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
561 ### Run scons or make
563 my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
564 # Filter-out generated files
565 my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
567 system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
568 system_verbose($make || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
571 ### Copy files (using rsync)
572 if (defined $server && !defined($gen_only)) {
573 (my $real_server = $server) =~ s/\$NAME/$config_name/;
575 my ($hostname, $path) = split(":", $real_server, 2);
576 if (! defined $path) {
580 my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
581 map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
582 my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
583 my $progress = $istty ? "--progress" : "";
584 system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
585 if ($server =~ m|/\$NAME$| && $concat) {
586 my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
587 system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
591 ### Prepare ISO image generation
592 if (defined $iso_image) {
593 generate_configs("(cd)", $generated, $filename);
595 generate_grub_config(\$menu, $config_name, "(cd)", $modules);
596 $menu_iso .= "$menu\n";
597 map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
601 ## Generate ISO image
602 if (defined $iso_image) {
603 open(my $fh, ">menu-iso.lst");
604 print $fh "timeout 5\n\n$menu_iso";
606 my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
607 $iso_image ||= "$config_name.iso";
608 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");
609 print("ISO image created: $builddir/$iso_image\n");
612 exit(0) if defined $gen_only;
614 ## Boot the system using various methods and send serial output to stdout
616 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
617 die "You cannot do this with multiple scripts simultaneously";
620 if ($variables->{WVTEST_TIMEOUT}) {
621 print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
626 $str =~ s/^\s+|\s+$//g;
634 $qemu ||= $variables->{QEMU} || $CFG::qemu;
635 my @qemu_flags = split(" ", $qemu);
636 $qemu = shift(@qemu_flags);
638 @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
639 @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
640 push(@qemu_flags, split(/ +/, trim($qemu_append || '')));
642 if (defined $iso_image) {
643 # Boot NOVA with grub (and test the iso image)
644 push(@qemu_flags, ('-cdrom', "$config_name.iso"));
646 # Boot NOVA without GRUB
648 # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
649 foreach (@$modules) {s/,/+/g;}
650 generate_configs("", $generated, $filename);
652 if (scalar @$modules) {
653 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
654 $kcmd = '' if !defined $kcmd;
656 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
657 my $initrd = join ",", @$modules;
659 push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
660 push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
661 push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
664 push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
665 unshift(@qemu_flags, ('-name', $config_name));
666 print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
667 $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
670 ### Local DHCPD and TFTPD
672 my ($dhcpd_pid, $tftpd_pid);
674 if (defined $dhcp_tftp)
676 generate_configs("(nd)", $generated, $filename);
677 system_verbose('mkdir -p tftpboot');
678 generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
679 open(my $fh, '>', 'dhcpd.conf');
680 my $mac = `cat /sys/class/net/eth0/address`;
682 print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
683 range 10.23.23.10 10.23.23.100;
684 filename \"bin/boot/grub/pxegrub.pxe\";
685 next-server 10.23.23.1;
688 hardware ethernet $mac;
689 fixed-address 10.23.23.1;
692 system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
693 sudo ip l set dev eth0 up;
694 sudo touch dhcpd.leases");
696 # We run servers by forking ourselves, because the servers end up
697 # in our process group and get killed by signals sent to the
698 # process group (e.g. Ctrl-C on terminal).
700 exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
702 exec_verbose("sudo in.tftpd --foreground --secure -v -v -v --pidfile tftpd.pid $builddir") if ($tftpd_pid == 0);
704 # Kill server when we die
705 $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid');
706 system_verbose('sudo pkill --pidfile=tftpd.pid'); };
709 ### Serial line or IP relay
711 if (defined $target_reset) {
712 print "novaboot: Reseting the test box... ";
717 if (defined $uboot) {
718 print "novaboot: Waiting for uBoot prompt...\n";
721 [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
722 '=> ') || die "No uBoot prompt deteceted";
723 $exp->send("$uboot_init\n") if $uboot_init;
724 $exp->expect(10, '=> ') || die "uBoot prompt timeout";
726 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
728 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
729 my $initrd = shift @$modules;
731 my $kern_addr = '800000';
732 my $initrd_addr = '-';
735 $exp->send("tftp $kern_addr $kbin\n");
737 [qr/#/, sub { exp_continue; }],
738 '=> ') || die "Kernel load failed";
740 $dtb_addr = '7f0000';
741 $exp->send("tftp $dtb_addr $dtb\n");
743 [qr/#/, sub { exp_continue; }],
744 '=> ') || die "Device tree load failed";
746 if (defined $initrd) {
747 $initrd_addr = 'b00000';
748 $exp->send("tftp $initrd_addr $initrd\n");
750 [qr/#/, sub { exp_continue; }],
751 '=> ') || die "Initrd load failed";
753 $exp->send("set bootargs $kcmd\n");
754 $exp->expect(1, '=> ') || die "uBoot prompt timeout";
755 $exp->send("bootm $kern_addr $initrd_addr $dtb_addr\n");
756 $exp->expect(1, "\n") || die "uBoot command timeout";
760 # Serial line of the target is available
761 print "novaboot: Serial line interaction (press Ctrl-C to interrupt)...\n";
764 $exp->expect(undef, @exiton);
767 if (-t STDIN) { # Set up bi-directional communication if we run on terminal
768 my $infile = new IO::File;
769 $infile->IO::File::fdopen(*STDIN,'r');
770 my $in_object = Expect->exp_init($infile);
771 $in_object->set_group($exp);
774 $in_object->debug(1);
775 $in_object->set_seq('~~\.', sub { print "novaboot: Escape sequence detected\r\n"; undef; });
776 $in_object->manual_stty(0); # Use raw terminal mode
778 $in_object->manual_stty(1); # Do not modify terminal settings
780 push(@inputs, $in_object);
782 Expect::interconnect(@inputs);
786 ## Kill dhcpc or tftpd
787 if (defined $dhcp_tftp) {
788 die("novaboot: This should kill servers on background\n");
795 novaboot - A tool for booting various operating systems on various hardware or in qemu
799 B<novaboot> [ options ] [--] script...
801 B<./script> [ options ]
805 This program makes it easier to boot NOVA or other operating system
806 (OS) on different targets (machines or emulators). It reads a so
807 called novaboot script, that specifies the boot configuration, and
808 setups the target to boot that configuration. Setting up the target
809 means to generate the bootloader configuration files, deploy the
810 binaries and other needed files to proper locations, perhaps on a
811 remote boot server and reset the target. Then, target's serial output
812 is redirected to standard output if that is possible.
814 A typical way of using novaboot is to make the novaboot script
815 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
816 booting a particular OS configuration becomes the same as executing a
817 local program - the novaboot script.
819 For example, with C<novaboot> you can:
825 Run an OS in Qemu. This is the default action when no other action is
826 specified by command line switches. Thus running C<novaboot ./script>
827 (or C<./script> as described above) will run Qemu and make it boot the
828 configuration specified in the I<script>.
832 Create a bootloader configuration file (currently supported
833 bootloaders are GRUB, GRUB2, Pulsar and uBoot) and copy it with all
834 other files needed for booting to another, perhaps remote, location.
836 ./script --server=192.168.1.1:/tftp --iprelay=192.168.1.2
838 This command copies files to the TFTP server and uses
839 TCP/IP-controlled relay to reset the test box and receive its serial
844 Run DHCP and TFTP server on developer's machine to PXE-boot the OS
849 When a PXE-bootable machine is connected via Ethernet to developer's
850 machine, it will boot the configuration described in I<script>.
854 Create bootable ISO images. E.g.
856 novaboot --iso -- script1 script2
858 The created ISO image will have GRUB bootloader installed on it and
859 the boot menu will allow selecting between I<script1> and I<script2>
864 Note that the options needed for a specific target can be stored in a
865 L</"CONFIGURATION FILE"> and then it is sufficient to use only the
866 B<-t> option to specify the name of the target.
868 =head1 PHASES AND OPTIONS
870 Novaboot performs its work in several phases. Each phase can be
871 influenced by several options, certain phases can be skipped. The list
872 of phases (in the execution order) and the corresponding options
875 =head2 Configuration reading phase
877 After starting, novaboot reads configuration files. By default, it
878 searches for files named F<.novaboot> starting from the directory of
879 the novaboot script (or working directory, see bellow) and continuing
880 upwards up to the root directory. The configuration files are read in
881 order from the root directory downwards with latter files overriding
882 settings from the former ones.
884 In certain cases, the location of the novaboot script cannot be
885 determined in this early phase. This happens either when the script is
886 read from the standard input or when novaboot is invoked explicitly
887 and options precede the script name, as in the example L</"4."> above.
888 In this case the current working directory is used as a starting point
889 for configuration file search.
893 =item -c, --config=I<filename>
895 Use the specified configuration file instead of the default one(s).
899 =head2 Command line processing phase
905 Dump the current configuration to stdout end exits. Useful as an
906 initial template for a configuration file.
910 Print short (B<-h>) or long (B<--help>) help.
912 =item -t, --target=I<target>
914 This option serves as a user configurable shortcut for other novaboot
915 options. The effect of this option is the same as the options stored
916 in the C<%targets> configuration variable under key I<target>. See
917 also L</"CONFIGURATION FILE">.
921 =head2 Script preprocessing phase
923 This phases allows to modify the parsed novaboot script before it is
924 used in the later phases.
928 =item -a, --append=I<parameters>
930 Appends a string to the first "filename" line in the novaboot script.
931 This can be used to append parameters to the kernel's or root task's
936 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
937 ports and stores the information about them in the BIOS data area for
940 =item --chainloader=I<chainloader>
942 Chainloader that is loaded before the kernel and other files specified
943 in the novaboot script. E.g. 'bin/boot/bender promisc'.
947 Prints the content of the novaboot script after removing comments and
948 evaluating all I<--scriptmod> expressions. Exit after reading (and
951 =item --scriptmod=I<perl expression>
953 When novaboot script is read, I<perl expression> is executed for every
954 line (in $_ variable). For example, C<novaboot
955 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
956 in the script with I<omega6>.
958 When this option is present, it overrides I<$script_modifier> variable
959 from the configuration file, which has the same effect. If this option
960 is given multiple times all expressions are evaluated in the command
965 Strip I<rom://> prefix from command lines and generated config files.
966 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
970 =head2 File generation phase
972 In this phase, files needed for booting are generated in a so called
973 I<build directory> (see TODO). In most cases configuration for a
974 bootloader is generated automatically by novaboot. It is also possible
975 to generate other files using I<heredoc> or I<"<"> syntax in novaboot
976 scripts. Finally, binaries can be generated in this phases by running
981 =item --build-dir=I<directory>
983 Overrides the default build directory location.
985 The default build directory location is determined as follows: If the
986 configuration file defines the C<$builddir> variable, its value is
987 used. Otherwise, it is the directory that contains the first processed
990 =item -g, --grub[=I<filename>]
992 Generates grub bootloader menu file. If the I<filename> is not
993 specified, F<menu.lst> is used. The I<filename> is relative to the
994 build directory (see B<--build-dir>).
996 =item --grub-preamble=I<prefix>
998 Specifies the I<preable> that is at the beginning of the generated
999 GRUB or GRUB2 config files. This is useful for specifying GRUB's
1002 =item --grub-prefix=I<prefix>
1004 Specifies I<prefix> that is put in front of every file name in GRUB's
1005 F<menu.lst>. The default value is the absolute path to the build directory.
1007 If the I<prefix> contains string $NAME, it will be replaced with the
1008 name of the novaboot script (see also B<--name>).
1010 =item --grub2[=I<filename>]
1012 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
1013 specified F<grub.cfg> is used. The content of the menuentry can be
1014 customized with B<--grub-preable>, B<--grub2-prolog> or
1015 B<--grub_prefix> options.
1017 In order to use the the generated menuentry on your development
1018 machine that uses GRUB2, append the following snippet to
1019 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
1020 i.e. run update-grub on Debian/Ubuntu.
1022 if [ -f /path/to/nul/build/grub.cfg ]; then
1023 source /path/to/nul/build/grub.cfg
1026 =item --grub2-prolog=I<prolog>
1028 Specifies text I<preable> that is put at the beginning of the entry
1031 =item -m, --make[=make command]
1033 Runs C<make> to build files that are not generated by novaboot itself.
1035 =item --name=I<string>
1037 Use the name I<string> instead of the name of the novaboot script.
1038 This name is used for things like a title of grub menu or for the
1039 server directory where the boot files are copied to.
1043 Do not generate files on the fly (i.e. "<" syntax) except for the
1044 files generated via "<<WORD" syntax.
1046 =item -p, --pulsar[=mac]
1048 Generates pulsar bootloader configuration file named F<config-I<mac>>
1049 The I<mac> string is typically a MAC address and defaults to
1052 =item --scons[=scons command]
1054 Runs C<scons> to build files that are not generated by novaboot
1059 Exit novaboot after file generation phase.
1063 =head2 Target connection check
1065 If supported by the target, the connection to it is made and it is
1066 checked whether the target is not occupied by another novaboot
1071 =item --iprelay=I<addr[:port]>
1073 Use TCP/IP relay and serial port to access the target's serial port
1074 and powercycle it. The IP address of the relay is given by I<addr>
1075 parameter. If I<port> is not specified, it default to 23.
1077 Note: This option is supposed to work with HWG-ER02a IP relays.
1079 =item -s, --serial[=device]
1081 Target's serial line is connected to host's serial line (device). The
1082 default value for device is F</dev/ttyUSB0>.
1084 The value of this option is exported in NB_NOVABOOT environment
1085 variable to all subprocesses run by C<novaboot>.
1087 =item --stty=I<settings>
1089 Specifies settings passed to C<stty> invoked on the serial line
1090 specified with B<--serial> option. If this option is not given,
1091 C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
1093 =item --remote-cmd=I<cmd>
1095 Command that mediates connection to the target's serial line. For
1096 example C<ssh server 'cu -l /dev/ttyS0'>.
1098 =item --remote-expect=I<string>
1100 Wait for reception of I<string> on the remote connection before
1105 =head2 File deployment phase
1107 In some setups, it is necessary to copy the files needed for booting
1108 to a particular location, e.g. to a TFTP boot server or to the
1113 =item -d, --dhcp-tftp
1115 Turns your workstation into a DHCP and TFTP server so that the OS can
1116 be booted via PXE BIOS (or similar mechanism) on the test machine
1117 directly connected by a plain Ethernet cable to your workstation.
1119 The DHCP and TFTP servers require root privileges and C<novaboot>
1120 uses C<sudo> command to obtain those. You can put the following to
1121 I</etc/sudoers> to allow running the necessary commands without
1122 asking for password.
1124 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
1125 your_login ALL=NOPASSWD: NOVABOOT
1127 =item --iso[=filename]
1129 Generates the ISO image that boots NOVA system via GRUB. If no filename
1130 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
1131 of the novaboot script (see also B<--name>).
1133 =item --server[=[[user@]server:]path]
1135 Copy all files needed for booting to another location (implies B<-g>
1136 unless B<--grub2> is given). The files will be copied (by B<rsync>
1137 tool) to the directory I<path>. If the I<path> contains string $NAME,
1138 it will be replaced with the name of the novaboot script (see also
1143 If B<--server> is used and its value ends with $NAME, then after
1144 copying the files, a new bootloader configuration file (e.g. menu.lst)
1145 is created at I<path-wo-name>, i.e. the path specified by B<--server>
1146 with $NAME part removed. The content of the file is created by
1147 concatenating all files of the same name from all subdirectories of
1148 I<path-wo-name> found on the "server".
1150 =item --rsync-flags=I<flags>
1152 Specifies which I<flags> are appended to F<rsync> command line when
1153 copying files as a result of I<--server> option.
1157 =head2 Target power-on and reset phase
1163 Switch on/off the target machine. Currently works only with
1166 =item -Q, --qemu[=I<qemu-binary>]
1168 Boot the configuration in qemu. Optionally, the name of qemu binary
1169 can be specified as a parameter.
1171 =item --qemu-append=I<flags>
1173 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1174 C<-cpu coreduo -smp 2>).
1176 =item -q, --qemu-flags=I<flags>
1178 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1179 -smp 2>) with I<flags> specified here.
1181 =item --reset-cmd=I<cmd>
1183 Command that resets the target.
1187 =head2 Interaction with the bootloader on the target
1193 Interact with uBoot bootloader to boot the thing described in the
1194 novaboot script. Implementation of this option is currently tied to a
1195 particular board that we use. It may be subject to changes in the
1200 Command(s) to send the uBoot bootloader before loading the images and
1205 =head2 Target interaction phase
1207 In this phase, target's serial output is passed to C<novaboot> stdout.
1208 If C<novaboot>'s stdin is on TTY, the stdin is passed to the target
1209 allowing interactive work with the target.
1211 This phase end when the target hangs up or when Ctrl-C is pressed.
1215 =item --exiton=I<string>
1217 When I<string> is sent by the target, novaboot exits. This option can
1218 be specified multiple times.
1220 If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
1221 treated as regular expression. For example:
1223 --exiton -re --exiton 'error:.*failed'
1225 =item -i, --interactive
1227 Setup things for interactive use of target. Your terminal will be
1228 switched to raw mode. In raw mode, your system does not process input
1229 in any way (no echoing of entered characters, no interpretation
1230 special characters). This, among others, means that Ctrl-C is passed
1231 to the target and does no longer interrupt novaboot. Use "~~."
1232 sequence to exit novaboot.
1236 =head1 NOVABOOT SCRIPT SYNTAX
1238 The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
1240 Lines starting with "#" are ignored.
1242 Lines that end with "\" are concatenated with the following line after
1243 removal of the final "\" and leading whitespace of the following line.
1245 Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1246 expression) assign values to internal variables. See VARIABLES
1249 Otherwise, the first word on the line represents the filename
1250 (relative to the build directory (see B<--build-dir>) of the module to
1251 load and the remaining words are passed as the command line
1254 When the line ends with "<<WORD" then the subsequent lines until the
1255 line containing only WORD are copied literally to the file named on
1258 When the line ends with "< CMD" the command CMD is executed with
1259 C</bin/sh> and its standard output is stored in the file named on that
1260 line. The SRCDIR variable in CMD's environment is set to the absolute
1261 path of the directory containing the interpreted novaboot script.
1264 #!/usr/bin/env novaboot
1265 WVDESC=Example program
1266 bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1267 verbose hostkeyb:0,0x60,1,12,2
1269 hello.nulconfig <<EOF
1270 sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1271 rom://bin/apps/hello.nul
1274 This example will load three modules: sigma0.nul, hello.nul and
1275 hello.nulconfig. sigma0 gets some command line parameters and
1276 hello.nulconfig file is generated on the fly from the lines between
1281 The following variables are interpreted in the novaboot script:
1287 Novaboot chdir()s to this directory before file generation phase. The
1288 directory name specified here is relative to the build directory
1289 specified by other means (see L</--build-dir>).
1293 Assigning this variable has the same effect as specifying L</--exiton>
1296 =item HYPERVISOR_PARAMS
1298 Parameters passed to hypervisor. The default value is "serial", unless
1299 overriden in configuration file.
1303 The kernel to use instead of NOVA hypervisor specified in the
1304 configuration file. The value should contain the name of the kernel
1305 image as well as its command line parameters. If this variable is
1306 defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
1310 Use a specific qemu binary (can be overriden with B<-Q>) and flags
1311 when booting this script under qemu. If QEMU_FLAGS variable is also
1312 specified flags specified in QEMU variable are replaced by those in
1317 Use specific qemu flags (can be overriden with B<-q>).
1321 Description of the wvtest-compliant program.
1323 =item WVTEST_TIMEOUT
1325 The timeout in seconds for WvTest harness. If no complete line appears
1326 in the test output within the time specified here, the test fails. It
1327 is necessary to specify this for long running tests that produce no
1328 intermediate output.
1332 =head1 CONFIGURATION FILE
1334 Novaboot can read its configuration from one or more files. By
1335 default, novaboot looks for files named F<.novaboot> as described in
1336 L</Configuration reading phase>. Alternatively, its location can be
1337 specified with the B<-c> switch or with the NOVABOOT_CONFIG
1338 environment variable. The configuration file has perl syntax and
1339 should set values of certain Perl variables. The current configuration
1340 can be dumped with the B<--dump-config> switch. Some configuration
1341 variables can be overriden by environment variables (see below) or by
1342 command line switches.
1344 Supported configuration variables include:
1350 Build directory location relative to the location of the configuration
1353 =item $default_target
1355 Default target (see below) to use when no target is explicitly
1356 specified on command line with the B<--target> option.
1360 Hash of shortcuts to be used with the B<--target> option. If the hash
1361 contains, for instance, the following pair of values
1363 'mybox' => '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1365 then the following two commands are equivalent:
1367 ./script --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1372 =head1 ENVIRONMENT VARIABLES
1374 Some options can be specified not only via config file or command line
1375 but also through environment variables. Environment variables override
1376 the values from configuration file and command line parameters
1377 override the environment variables.
1381 =item NOVABOOT_CONFIG
1383 Name of the novaboot configuration file to use instead of the default
1386 =item NOVABOOT_BENDER
1388 Defining this variable has the same meaning as B<--bender> option.
1394 Michal Sojka <sojka@os.inf.tu-dresden.de>