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, @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);
105 $rom_prefix = 'rom://';
106 $stty = 'raw -crtscts -onlcr 115200';
108 my @expect_seen = ();
112 push(@expect_seen, '-re') if $n eq "expect-re";
113 push(@expect_seen, $v);
119 unless (@expect_seen) { die("No --expect before --send"); }
120 my $ret = ($n eq "sendcont") ? exp_continue : 0;
121 unshift(@expect_raw, sub { shift->send(eval("\"$v\"")); $ret; });
122 unshift(@expect_raw, @expect_seen);
126 Getopt::Long::Configure(qw/no_ignore_case no_pass_through/);
129 "append|a=s" => \@append,
130 "bender|b" => \$bender,
131 "build-dir=s" => sub { my ($n, $v) = @_; $builddir = File::Spec->rel2abs($v); },
132 "concat" => \$concat,
133 "chainloader=s" => \@chainloaders,
134 "dhcp-tftp|d" => \$dhcp_tftp,
135 "dump" => \$dump_opt,
136 "dump-config" => \$dump_config,
137 "exiton=s" => \@exiton,
138 "expect=s" => \&handle_expect,
139 "expect-re=s" => \&handle_expect,
140 "expect-raw=s" => sub { my ($n, $v) = @_; unshift(@expect_raw, eval($v)); },
141 "gen-only" => \$gen_only,
142 "grub|g:s" => \$grub_config,
143 "grub-preamble=s"=> \$grub_preamble,
144 "grub-prefix=s" => \$grub_prefix,
145 "grub2:s" => \$grub2_config,
146 "grub2-prolog=s" => \$grub2_prolog,
147 "iprelay=s" => \$iprelay,
148 "iso:s" => \$iso_image,
149 "kernel|k=s" => \$kernel_opt,
150 "interactive|i" => \$interactive,
151 "name=s" => \$config_name_opt,
152 "make|m:s" => \$make,
153 "no-file-gen" => \$no_file_gen,
156 "pulsar|p:s" => \$pulsar,
157 "pulsar-root=s" => \$pulsar_root,
158 "qemu|Q:s" => \$qemu,
159 "qemu-append=s" => \$qemu_append,
160 "qemu-flags|q=s" => \$qemu_flags_cmd,
161 "remote-cmd=s" => \$remote_cmd,
162 "remote-expect=s"=> \$remote_expect,
163 "reset-cmd=s" => \$reset_cmd,
164 "rsync-flags=s" => \$rsync_flags,
165 "scons:s" => \$scons,
166 "scriptmod=s" => \@scriptmod,
167 "send=s" => \&handle_send,
168 "sendcont=s" => \&handle_send,
169 "serial|s:s" => \$serial,
170 "server:s" => \$server,
171 "strip-rom" => sub { $rom_prefix = ''; },
174 "uboot-init=s" => \$uboot_init,
179 # First process target options
181 my $t = defined($explicit_target) ? $explicit_target : $CFG::default_target;
183 exists $CFG::targets{$t} or die("Unknown target '$t' (valid targets are: ".join(", ", sort keys(%CFG::targets)).")");
184 GetOptionsFromString($CFG::targets{$t}, %opt_spec);
188 # Then process other command line options - some of them may override
189 # what was specified by the target
190 GetOptions %opt_spec or die("Error in command line arguments");
191 pod2usage(1) if $help;
192 pod2usage(-exitstatus => 0, -verbose => 2) if $man;
194 ### Dump sanitized configuration (if requested)
198 $Data::Dumper::Indent=1;
199 print "# This file is in perl syntax.\n";
200 foreach my $key(sort(keys(%CFG::))) { # See "Symbol Tables" in perlmod(1)
201 if (defined ${$CFG::{$key}}) { print Data::Dumper->Dump([${$CFG::{$key}}], ["*$key"]); }
202 if ( @{$CFG::{$key}}) { print Data::Dumper->Dump([\@{$CFG::{$key}}], ["*$key"]); }
203 if ( %{$CFG::{$key}}) { print Data::Dumper->Dump([\%{$CFG::{$key}}], ["*$key"]); }
209 ### Sanitize configuration
211 if ($interactive && !-t STDIN) {
212 die("novaboot: Interactive mode not supported when not on terminal");
215 if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
218 if (defined $serial) {
219 $serial ||= "/dev/ttyUSB0";
220 $ENV{NB_SERIAL} = $serial;
222 if (defined $grub_config) { $grub_config ||= "menu.lst"; }
223 if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
225 ## Parse the novaboot script(s)
230 my ($modules, $variables, $generated, $continuation);
232 if ($ARGV ne $last_fn) { # New script
233 die "Missing EOF in $last_fn" if $file;
234 die "Unfinished line in $last_fn" if $continuation;
236 push @scripts, { 'filename' => $ARGV,
237 'modules' => $modules = [],
238 'variables' => $variables = {},
239 'generated' => $generated = []};
243 next if /^#/ || /^\s*$/; # Skip comments and empty lines
245 $_ =~ s/^[[:space:]]*// if ($continuation);
247 if (/\\$/) { # Line continuation
248 $continuation .= substr($_, 0, length($_)-1);
252 if ($continuation) { # Last continuation line
253 $_ = $continuation . $_;
257 foreach my $mod(@scriptmod) { eval $mod; }
259 if ($file && $_ eq $EOF) { # Heredoc end
263 if ($file) { # Heredoc content
264 push @{$file}, "$_\n";
267 if (/^([A-Z_]+)=(.*)$/) { # Internal variable
268 $$variables{$1} = $2;
269 push(@exiton, $2) if ($1 eq "EXITON");
272 if (s/^load *//) { # Load line
273 die("novaboot: '$last_fn' line $.: Missing file name\n") unless /^[^ <]+/;
274 if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
275 push @$modules, "$1$2";
277 push @$generated, {filename => $1, content => $file};
281 if (/^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
282 push @$modules, "$1$2";
283 push @$generated, {filename => $1, command => $3};
290 die("novaboot: Cannot parse script '$last_fn' line $.. Didn't you forget 'load' keyword?\n");
293 # print Dumper(\@scripts);
295 foreach my $script (@scripts) {
296 $modules = $$script{modules};
297 @$modules[0] =~ s/^[^ ]*/$kernel_opt/ if $kernel_opt;
298 @$modules[0] .= ' ' . join(' ', @append) if @append;
301 if (exists $variables->{KERNEL}) {
302 $kernel = $variables->{KERNEL};
304 if ($CFG::hypervisor) {
305 $kernel = $CFG::hypervisor . " ";
306 if (exists $variables->{HYPERVISOR_PARAMS}) {
307 $kernel .= $variables->{HYPERVISOR_PARAMS};
309 $kernel .= $CFG::hypervisor_params;
313 @$modules = ($kernel, @$modules) if $kernel;
314 @$modules = (@chainloaders, @$modules);
315 @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
319 foreach my $script (@scripts) {
320 print join("\n", @{$$script{modules}})."\n";
327 sub generate_configs($$$) {
328 my ($base, $generated, $filename) = @_;
329 if ($base) { $base = "$base/"; };
330 foreach my $g(@$generated) {
331 if (exists $$g{content}) {
332 my $config = $$g{content};
333 my $fn = $$g{filename};
334 open(my $f, '>', $fn) || die("$fn: $!");
335 map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
337 print "novaboot: Created $fn\n";
338 } elsif (exists $$g{command} && ! $no_file_gen) {
339 $ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
340 system_verbose("( $$g{command} ) > $$g{filename}");
345 sub generate_grub_config($$$$;$)
347 my ($filename, $title, $base, $modules_ref, $preamble) = @_;
348 if ($base) { $base = "$base/"; };
349 open(my $fg, '>', $filename) or die "$filename: $!";
350 print $fg "$preamble\n" if $preamble;
351 print $fg "title $title\n" if $title;
352 #print $fg "root $base\n"; # root doesn't really work for (nd)
354 foreach (@$modules_ref) {
357 my ($kbin, $kcmd) = split(' ', $_, 2);
358 $kcmd = '' if !defined $kcmd;
359 print $fg "kernel ${base}$kbin $kcmd\n";
361 s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
362 print $fg "module $base$_\n";
366 print("novaboot: Created $builddir/$filename\n");
370 sub generate_grub2_config($$$$;$$)
372 my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
373 if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
374 open(my $fg, '>', $filename) or die "$filename: $!";
375 print $fg "$preamble\n" if $preamble;
376 $title ||= 'novaboot';
377 print $fg "menuentry $title {\n";
378 print $fg "$prolog\n" if $prolog;
380 foreach (@$modules_ref) {
383 my ($kbin, $kcmd) = split(' ', $_, 2);
384 $kcmd = '' if !defined $kcmd;
385 print $fg " multiboot ${base}$kbin $kcmd\n";
388 # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
389 $_ = join(' ', ($args[0], @args));
390 s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
391 print $fg " module $base$_\n";
396 print("novaboot: Created $builddir/$filename\n");
400 sub generate_pulsar_config($$)
402 my ($filename, $modules_ref) = @_;
403 open(my $fg, '>', $filename) or die "$filename: $!";
404 print $fg "root $pulsar_root\n" if defined $pulsar_root;
407 foreach (@$modules_ref) {
410 ($kbin, $kcmd) = split(' ', $_, 2);
411 $kcmd = '' if !defined $kcmd;
414 s|\brom://|$rom_prefix|g;
415 print $fg "load $_\n";
418 # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
419 print $fg "exec $kbin $kcmd\n";
421 print("novaboot: Created $builddir/$filename\n");
425 sub shell_cmd_string(@)
427 return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
432 print "novaboot: Running: ".shell_cmd_string(@_)."\n";
436 sub system_verbose($)
439 print "novaboot: Running: $cmd\n";
440 my $ret = system($cmd);
441 if ($ret & 0x007f) { die("Command terminated by a signal"); }
442 if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
443 if ($ret) { die("Command failure $ret"); }
448 if (exists $variables->{WVDESC}) {
449 print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
450 } elsif ($last_fn =~ /\.wv$/) {
451 print "Testing \"all\" in $last_fn:\n";
454 ## Connect to the target and check whether is not occupied
456 # We have to do this before file generation phase, because file
457 # generation is intermixed with file deployment phase and we want to
458 # check whether the target is not used by somebody else before
459 # deploying files. Otherwise, we may rewrite other user's files on a
462 my $exp; # Expect object to communicate with the target over serial line
464 my ($target_reset, $target_power_on, $target_power_off);
466 if (defined $iprelay) {
468 $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
471 my $paddr = sockaddr_in($port, inet_aton($addr));
472 my $proto = getprotobyname('tcp');
473 socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
474 print "novaboot: Connecting to IP relay... ";
475 connect($IPRELAY, $paddr) || die "connect: $!";
477 $exp = Expect->init(\*$IPRELAY);
481 print $exp "\xFF\xF6"; # AYT
482 my $connected = $exp->expect(20, # Timeout in seconds
483 '<iprelayd: connected>',
484 '-re', '<WEB51 HW[^>]*>');
489 my ($relay, $onoff) = @_;
490 die unless ($relay == 1 || $relay == 2);
492 my $cmd = ($relay == 1 ? 0x5 : 0x6) | ($onoff ? 0x20 : 0x10);
493 return "\xFF\xFA\x2C\x32".chr($cmd)."\xFF\xF0";
497 my ($relay, $onoff) = @_;
498 die unless ($relay == 1 || $relay == 2);
499 my $cmd = ($relay == 1 ? 0xdf : 0xbf) | ($onoff ? 0x00 : 0xff);
500 return "\xFF\xFA\x2C\x97".chr($cmd)."\xFF\xF0";
504 my ($relay, $onoff, $can_giveup) = @_;
505 my $confirmation = '';
507 print $exp relaycmd($relay, $onoff);
508 my $confirmed = $exp->expect(20, # Timeout in seconds
509 relayconf($relay, $onoff));
512 print("Relay confirmation timeout - ignoring\n");
514 die "Relay confirmation timeout";
520 $target_reset = sub {
521 relay(2, 1, 1); # Reset the machine
526 $target_power_off = sub {
527 relay(1, 1); # Press power button
528 usleep(6000000); # Long press to switch off
532 $target_power_on = sub {
533 relay(1, 1); # Press power button
534 usleep(100000); # Short press
540 system_verbose("stty -F $serial $stty");
541 open($CONN, "+<", $serial) || die "open $serial: $!";
542 $exp = Expect->init(\*$CONN);
543 } elsif ($remote_cmd) {
544 print "novaboot: Running: $remote_cmd\n";
545 $exp = Expect->spawn($remote_cmd);
548 if ($remote_expect) {
549 $exp->expect(10, $remote_expect) || die "Expect for '$remote_expect' timed out";
552 if (defined $reset_cmd) {
553 $target_reset = sub {
554 system_verbose($reset_cmd);
558 if (defined $on_opt && defined $target_power_on) {
562 if (defined $off_opt && defined $target_power_off) {
563 print "novaboot: Switching the target off...\n";
564 &$target_power_off();
568 $builddir ||= dirname(File::Spec->rel2abs( ${$scripts[0]}{filename})) if scalar @scripts;
569 if (defined $builddir) {
570 chdir($builddir) or die "Can't change directory to $builddir: $!";
571 print "novaboot: Entering directory `$builddir'\n";
574 ## File generation phase
575 my (%files_iso, $menu_iso, $filename);
576 my $config_name = '';
578 foreach my $script (@scripts) {
579 $filename = $$script{filename};
580 $modules = $$script{modules};
581 $generated = $$script{generated};
582 $variables = $$script{variables};
584 ($config_name = $filename) =~ s#.*/##;
585 $config_name = $config_name_opt if (defined $config_name_opt);
587 if (exists $variables->{BUILDDIR}) {
588 $builddir = File::Spec->rel2abs($variables->{BUILDDIR});
589 chdir($builddir) or die "Can't change directory to $builddir: $!";
590 print "novaboot: Entering directory `$builddir'\n";
594 ($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
595 $prefix ||= $builddir;
596 # TODO: use $grub_prefix as first parameter if some switch is given
597 generate_configs('', $generated, $filename);
599 ### Generate bootloader configuration files
600 my @bootloader_configs;
601 push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
602 push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
603 push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
605 ### Run scons or make
607 my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
608 # Filter-out generated files
609 my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
611 system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
612 system_verbose($make || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
615 ### Copy files (using rsync)
616 if (defined $server && !defined($gen_only)) {
617 (my $real_server = $server) =~ s/\$NAME/$config_name/;
619 my ($hostname, $path) = split(":", $real_server, 2);
620 if (! defined $path) {
624 my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
625 map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
626 my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
627 my $progress = $istty ? "--progress" : "";
628 system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
629 if ($server =~ m|/\$NAME$| && $concat) {
630 my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
631 system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
635 ### Prepare ISO image generation
636 if (defined $iso_image) {
637 generate_configs("(cd)", $generated, $filename);
639 generate_grub_config(\$menu, $config_name, "(cd)", $modules);
640 $menu_iso .= "$menu\n";
641 map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
645 ## Generate ISO image
646 if (defined $iso_image) {
647 open(my $fh, ">menu-iso.lst");
648 print $fh "timeout 5\n\n$menu_iso";
650 my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
651 $iso_image ||= "$config_name.iso";
652 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");
653 print("ISO image created: $builddir/$iso_image\n");
656 exit(0) if defined $gen_only;
658 ## Boot the system using various methods and send serial output to stdout
660 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
661 die "You cannot do this with multiple scripts simultaneously";
664 if ($variables->{WVTEST_TIMEOUT}) {
665 print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
670 $str =~ s/^\s+|\s+$//g;
678 $qemu ||= $variables->{QEMU} || $CFG::qemu;
679 my @qemu_flags = split(" ", $qemu);
680 $qemu = shift(@qemu_flags);
682 @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
683 @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
684 push(@qemu_flags, split(/ +/, trim($qemu_append || '')));
686 if (defined $iso_image) {
687 # Boot NOVA with grub (and test the iso image)
688 push(@qemu_flags, ('-cdrom', "$config_name.iso"));
690 # Boot NOVA without GRUB
692 # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
693 foreach (@$modules) {s/,/+/g;}
694 generate_configs("", $generated, $filename);
696 if (scalar @$modules) {
697 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
698 $kcmd = '' if !defined $kcmd;
700 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
701 my $initrd = join ",", @$modules;
703 push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
704 push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
705 push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
708 push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
709 unshift(@qemu_flags, ('-name', $config_name));
710 print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
711 $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
714 ### Local DHCPD and TFTPD
716 my ($dhcpd_pid, $tftpd_pid);
718 if (defined $dhcp_tftp)
720 generate_configs("(nd)", $generated, $filename);
721 system_verbose('mkdir -p tftpboot');
722 generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
723 open(my $fh, '>', 'dhcpd.conf');
724 my $mac = `cat /sys/class/net/eth0/address`;
726 print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
727 range 10.23.23.10 10.23.23.100;
728 filename \"bin/boot/grub/pxegrub.pxe\";
729 next-server 10.23.23.1;
732 hardware ethernet $mac;
733 fixed-address 10.23.23.1;
736 system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
737 sudo ip l set dev eth0 up;
738 sudo touch dhcpd.leases");
740 # We run servers by forking ourselves, because the servers end up
741 # in our process group and get killed by signals sent to the
742 # process group (e.g. Ctrl-C on terminal).
744 exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
746 exec_verbose("sudo in.tftpd --foreground --secure -v -v -v --pidfile tftpd.pid $builddir") if ($tftpd_pid == 0);
748 # Kill server when we die
749 $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid');
750 system_verbose('sudo pkill --pidfile=tftpd.pid'); };
753 ### Serial line or IP relay
755 if (defined $target_reset) {
756 print "novaboot: Reseting the test box... ";
761 if (defined $uboot) {
762 print "novaboot: Waiting for uBoot prompt...\n";
764 #$exp->exp_internal(1);
766 [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
767 '=> ') || die "No uBoot prompt deteceted";
768 $exp->send("$uboot_init\n") if $uboot_init;
769 $exp->expect(10, '=> ') || die "uBoot prompt timeout";
771 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
773 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
774 my $initrd = shift @$modules;
776 my $kern_addr = '800000';
777 my $initrd_addr = '-';
780 $exp->send("tftp $kern_addr $kbin\n");
782 [qr/#/, sub { exp_continue; }],
783 '=> ') || die "Kernel load failed";
785 $dtb_addr = '7f0000';
786 $exp->send("tftp $dtb_addr $dtb\n");
788 [qr/#/, sub { exp_continue; }],
789 '=> ') || die "Device tree load failed";
791 if (defined $initrd) {
792 $initrd_addr = 'b00000';
793 $exp->send("tftp $initrd_addr $initrd\n");
795 [qr/#/, sub { exp_continue; }],
796 '=> ') || die "Initrd load failed";
798 $exp->send("set bootargs '$kcmd'\n");
799 $exp->expect(5, '=> ') || die "uBoot prompt timeout";
800 $exp->send("bootm $kern_addr $initrd_addr $dtb_addr\n");
801 $exp->expect(5, "\n") || die "uBoot command timeout";
805 # Serial line of the target is available
806 my $interrupt = 'Ctrl-C';
807 if ($interactive && !@exiton) {
808 $interrupt = '"~~."';
810 print "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
813 $exp->expect(undef, @expect_raw, @exiton);
816 if (-t STDIN) { # Set up bi-directional communication if we run on terminal
817 my $infile = new IO::File;
818 $infile->IO::File::fdopen(*STDIN,'r');
819 my $in_object = Expect->exp_init($infile);
820 $in_object->set_group($exp);
823 $in_object->set_seq('~~\.', sub { print "novaboot: Escape sequence detected\r\n"; undef; });
824 $in_object->manual_stty(0); # Use raw terminal mode
826 $in_object->manual_stty(1); # Do not modify terminal settings
828 push(@inputs, $in_object);
831 #print Dumper(\@expect_raw);
832 $exp->expect(undef, @expect_raw) if @expect_raw;
833 Expect::interconnect(@inputs) unless defined($exp->exitstatus);
837 ## Kill dhcpc or tftpd
838 if (defined $dhcp_tftp) {
839 die("novaboot: This should kill servers on background\n");
846 novaboot - A tool for booting various operating systems on various hardware or in qemu
852 B<novaboot> [option]... [--] script...
854 B<./script> [option]...
858 This program makes booting of an operating system (e.g. NOVA or Linux)
859 as simple as running a local program. It facilitates booting on local
860 or remote hosts or in emulators such as qemu. Novaboot operation is
861 controlled by command line options and by a so called novaboot script,
862 which can be thought as a generalization of bootloader configuration
863 files. Based on this input, novaboot setups everything for the target
864 host to boot the desired configuration, i.e. it generates the
865 bootloader configuration file in the proper format, deploy the
866 binaries and other needed files to required locations, perhaps on a
867 remote boot server and reset the target host. Finally, target host's
868 serial output is redirected to standard output if that is possible.
870 Typical way of using novaboot is to make the novaboot script
871 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
872 booting a particular OS configuration becomes the same as executing a
873 local program - the novaboot script.
875 For example, with C<novaboot> you can:
881 Run an OS in Qemu. This is the default action when no other action is
882 specified by command line switches. Thus running C<novaboot ./script>
883 (or C<./script> as described above) will run Qemu and make it boot the
884 configuration specified in the F<script>.
888 Create a bootloader configuration file (currently supported
889 bootloaders are GRUB, GRUB2, Pulsar and U-Boot) and copy it with all
890 other files needed for booting to a remote boot server.
892 ./script --server=192.168.1.1:/tftp --iprelay=192.168.1.2
894 This command copies files to the TFTP server and uses
895 TCP/IP-controlled relay to reset the target host and receive its
900 Run DHCP and TFTP server on developer's machine to PXE-boot the target
905 When a PXE-bootable machine is connected via Ethernet to developer's
906 machine, it will boot the configuration described in I<script>.
910 Create bootable ISO images. E.g.
912 novaboot --iso -- script1 script2
914 The created ISO image will have GRUB bootloader installed on it and
915 the boot menu will allow selecting between I<script1> and I<script2>
920 Note that the options needed for a specific target can be stored in a
921 L</"CONFIGURATION FILE">. Then it is sufficient to use only the B<-t>
922 option to specify the name of the target.
924 =head1 PHASES AND OPTIONS
926 Novaboot performs its work in several phases. Each phase can be
927 influenced by several options, certain phases can be skipped. The list
928 of phases (in the execution order) and the corresponding options
931 =head2 Configuration reading phase
933 After starting, novaboot reads configuration files. By default, it
934 searches for files named F<.novaboot> starting from the directory of
935 the novaboot script (or working directory, see bellow) and continuing
936 upwards up to the root directory. The configuration files are read in
937 order from the root directory downwards with latter files overriding
938 settings from the former ones.
940 In certain cases, the location of the novaboot script cannot be
941 determined in this early phase. This happens either when the script is
942 read from the standard input or when novaboot is invoked explicitly
943 and options precede the script name, as in the example L</"4."> above.
944 In this case the current working directory is used as a starting point
945 for configuration file search.
949 =item -c, --config=I<filename>
951 Use the specified configuration file instead of the default one(s).
955 =head2 Command line processing phase
961 Dump the current configuration to stdout end exits. Useful as an
962 initial template for a configuration file.
966 Print short (B<-h>) or long (B<--help>) help.
968 =item -t, --target=I<target>
970 This option serves as a user configurable shortcut for other novaboot
971 options. The effect of this option is the same as the options stored
972 in the C<%targets> configuration variable under key I<target>. See
973 also L</"CONFIGURATION FILE">.
977 =head2 Script preprocessing phase
979 This phases allows to modify the parsed novaboot script before it is
980 used in the later phases.
984 =item -a, --append=I<parameters>
986 Append a string to the first C<load> line in the novaboot script. This
987 can be used to append parameters to the kernel's or root task's
988 command line. Can appear multiple times.
992 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
993 ports and stores the information about them in the BIOS data area for
996 =item --chainloader=I<chainloader>
998 Chainloader that is loaded before the kernel and other files specified
999 in the novaboot script. E.g. 'bin/boot/bender promisc'.
1003 Print the modules to boot and their parameters after this phase
1004 finishes. Then exit. This is useful for seeing the effect of other
1005 options in this section.
1007 =item -k, --kernel=F<file>
1009 Replace the first word on the first C<load> line in the novaboot
1010 script with F<file>.
1012 =item --scriptmod=I<perl expression>
1014 When novaboot script is read, I<perl expression> is executed for every
1015 line (in $_ variable). For example, C<novaboot
1016 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
1017 in the script with I<omega6>.
1019 When this option is present, it overrides I<$script_modifier> variable
1020 from the configuration file, which has the same effect. If this option
1021 is given multiple times all expressions are evaluated in the command
1026 =head2 File generation phase
1028 In this phase, files needed for booting are generated in a so called
1029 I<build directory> (see L</--build-dir>). In most cases configuration
1030 for a bootloader is generated automatically by novaboot. It is also
1031 possible to generate other files using I<heredoc> or I<"<"> syntax in
1032 novaboot scripts. Finally, binaries can be generated in this phases by
1033 running C<scons> or C<make>.
1037 =item --build-dir=I<directory>
1039 Overrides the default build directory location.
1041 The default build directory location is determined as follows: If the
1042 configuration file defines the C<$builddir> variable, its value is
1043 used. Otherwise, it is the directory that contains the first processed
1046 See also L</BUILDDIR> variable.
1048 =item -g, --grub[=I<filename>]
1050 Generates grub bootloader menu file. If the I<filename> is not
1051 specified, F<menu.lst> is used. The I<filename> is relative to the
1052 build directory (see B<--build-dir>).
1054 =item --grub-preamble=I<prefix>
1056 Specifies the I<preable> that is at the beginning of the generated
1057 GRUB or GRUB2 config files. This is useful for specifying GRUB's
1060 =item --grub-prefix=I<prefix>
1062 Specifies I<prefix> that is put in front of every file name in GRUB's
1063 F<menu.lst>. The default value is the absolute path to the build directory.
1065 If the I<prefix> contains string $NAME, it will be replaced with the
1066 name of the novaboot script (see also B<--name>).
1068 =item --grub2[=I<filename>]
1070 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
1071 specified F<grub.cfg> is used. The content of the menuentry can be
1072 customized with B<--grub-preable>, B<--grub2-prolog> or
1073 B<--grub_prefix> options.
1075 In order to use the the generated menuentry on your development
1076 machine that uses GRUB2, append the following snippet to
1077 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
1078 i.e. run update-grub on Debian/Ubuntu.
1080 if [ -f /path/to/nul/build/grub.cfg ]; then
1081 source /path/to/nul/build/grub.cfg
1084 =item --grub2-prolog=I<prolog>
1086 Specifies text I<preable> that is put at the beginning of the entry
1089 =item -m, --make[=make command]
1091 Runs C<make> to build files that are not generated by novaboot itself.
1093 =item --name=I<string>
1095 Use the name I<string> instead of the name of the novaboot script.
1096 This name is used for things like a title of grub menu or for the
1097 server directory where the boot files are copied to.
1101 Do not generate files on the fly (i.e. "<" syntax) except for the
1102 files generated via "<<WORD" syntax.
1104 =item -p, --pulsar[=mac]
1106 Generates pulsar bootloader configuration file named F<config-I<mac>>
1107 The I<mac> string is typically a MAC address and defaults to
1110 =item --scons[=scons command]
1112 Runs C<scons> to build files that are not generated by novaboot
1117 Strip I<rom://> prefix from command lines and generated config files.
1118 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
1122 Exit novaboot after file generation phase.
1126 =head2 Target connection check
1128 If supported by the target, the connection to it is made and it is
1129 checked whether the target is not occupied by another novaboot
1134 =item --iprelay=I<addr[:port]>
1136 Use TCP/IP relay and serial port to access the target's serial port
1137 and powercycle it. The IP address of the relay is given by I<addr>
1138 parameter. If I<port> is not specified, it default to 23.
1140 Note: This option is supposed to work with HWG-ER02a IP relays.
1142 =item -s, --serial[=device]
1144 Target's serial line is connected to host's serial line (device). The
1145 default value for device is F</dev/ttyUSB0>.
1147 The value of this option is exported in NB_NOVABOOT environment
1148 variable to all subprocesses run by C<novaboot>.
1150 =item --stty=I<settings>
1152 Specifies settings passed to C<stty> invoked on the serial line
1153 specified with B<--serial> option. If this option is not given,
1154 C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
1156 =item --remote-cmd=I<cmd>
1158 Command that mediates connection to the target's serial line. For
1159 example C<ssh server 'cu -l /dev/ttyS0'>.
1161 =item --remote-expect=I<string>
1163 Wait for reception of I<string> after establishing the the remote
1164 connection before continuing.
1168 =head2 File deployment phase
1170 In some setups, it is necessary to copy the files needed for booting
1171 to a particular location, e.g. to a TFTP boot server or to the
1176 =item -d, --dhcp-tftp
1178 Turns your workstation into a DHCP and TFTP server so that the OS can
1179 be booted via PXE BIOS (or similar mechanism) on the test machine
1180 directly connected by a plain Ethernet cable to your workstation.
1182 The DHCP and TFTP servers require root privileges and C<novaboot>
1183 uses C<sudo> command to obtain those. You can put the following to
1184 I</etc/sudoers> to allow running the necessary commands without
1185 asking for password.
1187 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
1188 your_login ALL=NOPASSWD: NOVABOOT
1190 =item --iso[=filename]
1192 Generates the ISO image that boots NOVA system via GRUB. If no filename
1193 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
1194 of the novaboot script (see also B<--name>).
1196 =item --server[=[[user@]server:]path]
1198 Copy all files needed for booting to another location (implies B<-g>
1199 unless B<--grub2> is given). The files will be copied (by B<rsync>
1200 tool) to the directory I<path>. If the I<path> contains string $NAME,
1201 it will be replaced with the name of the novaboot script (see also
1206 If B<--server> is used and its value ends with $NAME, then after
1207 copying the files, a new bootloader configuration file (e.g. menu.lst)
1208 is created at I<path-wo-name>, i.e. the path specified by B<--server>
1209 with $NAME part removed. The content of the file is created by
1210 concatenating all files of the same name from all subdirectories of
1211 I<path-wo-name> found on the "server".
1213 =item --rsync-flags=I<flags>
1215 Specifies which I<flags> are appended to F<rsync> command line when
1216 copying files as a result of I<--server> option.
1220 =head2 Target power-on and reset phase
1226 Switch on/off the target machine. Currently works only with
1229 =item -Q, --qemu[=I<qemu-binary>]
1231 Boot the configuration in qemu. Optionally, the name of qemu binary
1232 can be specified as a parameter.
1234 =item --qemu-append=I<flags>
1236 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1237 C<-cpu coreduo -smp 2>).
1239 =item -q, --qemu-flags=I<flags>
1241 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1242 -smp 2>) with I<flags> specified here.
1244 =item --reset-cmd=I<cmd>
1246 Command that resets the target.
1250 =head2 Interaction with the bootloader on the target
1256 Interact with uBoot bootloader to boot the thing described in the
1257 novaboot script. Implementation of this option is currently tied to a
1258 particular board that we use. It may be subject to changes in the
1263 Command(s) to send the U-Boot bootloader before loading the images and
1268 =head2 Target interaction phase
1270 In this phase, target's serial output is redirected to stdout and if
1271 stdin is a TTY, it is redirected to the target's serial input allowing
1272 interactive work with the target.
1276 =item --exiton=I<string>
1278 When I<string> is sent by the target, novaboot exits. This option can
1279 be specified multiple times.
1281 If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
1282 treated as regular expression. For example:
1284 --exiton -re --exiton 'error:.*failed'
1286 =item -i, --interactive
1288 Setup things for interactive use of target. Your terminal will be
1289 switched to raw mode. In raw mode, your system does not process input
1290 in any way (no echoing of entered characters, no interpretation
1291 special characters). This, among others, means that Ctrl-C is passed
1292 to the target and does no longer interrupt novaboot. Use "~~."
1293 sequence to exit novaboot.
1295 =item --expect=I<string>
1297 When I<string> is received from the target, send the string specified
1298 with the subsequent B<--send*> option to the target.
1300 =item --expect-re=I<regex>
1302 When target's output matches regular expression I<regex>, send the
1303 string specified with the subsequent B<--send*> option to the target.
1305 =item --expect-raw=I<perl-code>
1307 Provides direct control over Perl's Expect module.
1309 =item --send=I<string>
1311 Send I<string> to the target after the previously specified
1312 B<--expect*> was matched in the target's output. The I<string> may
1313 contain escape sequences such as "\n".
1315 Note that I<string> is actually interpreted by Perl, so it can contain
1316 much more that escape sequences. This behavior may change in the
1319 Example: C<--expect='login: ' --send='root\n'>
1321 =item --sendcont=I<string>
1323 Similar to B<--send> but continue expecting more input.
1325 Example: C<--expect='Continue?' --sendcont='yes\n'>
1329 =head1 NOVABOOT SCRIPT SYNTAX
1331 The syntax tries to mimic POSIX shell syntax. The syntax is defined
1332 with the following rules.
1334 Lines starting with "#" and empty lines are ignored.
1336 Lines that end with "\" are concatenated with the following line after
1337 removal of the final "\" and leading whitespace of the following line.
1339 Lines of the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1340 expression) assign values to internal variables. See L</VARIABLES>
1343 Lines starting with C<load> keyword represent modules to boot. The
1344 word after C<load> is a file name (relative to the build directory
1345 (see B<--build-dir>) of the module to load and the remaining words are
1346 passed to it as the command line parameters.
1348 When the C<load> line ends with "<<WORD" then the subsequent lines
1349 until the line containing solely WORD are copied literally to the file
1350 named on that line. This is similar to shell's heredoc feature.
1352 When the C<load> line ends with "< CMD" then command CMD is executed
1353 with C</bin/sh> and its standard output is stored in the file named on
1354 that line. The SRCDIR variable in CMD's environment is set to the
1355 absolute path of the directory containing the interpreted novaboot
1359 #!/usr/bin/env novaboot
1360 WVDESC=Example program
1361 load bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1362 verbose hostkeyb:0,0x60,1,12,2
1363 load bin/apps/hello.nul
1364 load hello.nulconfig <<EOF
1365 sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1366 rom://bin/apps/hello.nul
1369 This example will load three modules: F<sigma0.nul>, F<hello.nul> and
1370 F<hello.nulconfig>. sigma0 receives some command line parameters and
1371 F<hello.nulconfig> file is generated on the fly from the lines between
1372 C<<<EOF> and C<EOF>.
1376 The following variables are interpreted in the novaboot script:
1382 Novaboot chdir()s to this directory before file generation phase. The
1383 directory name specified here is relative to the build directory
1384 specified by other means (see L</--build-dir>).
1388 Assigning this variable has the same effect as specifying L</--exiton>
1391 =item HYPERVISOR_PARAMS
1393 Parameters passed to hypervisor. The default value is "serial", unless
1394 overridden in configuration file.
1398 The kernel to use instead of the hypervisor specified in the
1399 configuration file with the C<$hypervisor> variable. The value should
1400 contain the name of the kernel image as well as its command line
1401 parameters. If this variable is defined and non-empty, the variable
1402 HYPERVISOR_PARAMS is not used.
1406 Use a specific qemu binary (can be overridden with B<-Q>) and flags
1407 when booting this script under qemu. If QEMU_FLAGS variable is also
1408 specified flags specified in QEMU variable are replaced by those in
1413 Use specific qemu flags (can be overridden with B<-q>).
1417 Description of the wvtest-compliant program.
1419 =item WVTEST_TIMEOUT
1421 The timeout in seconds for WvTest harness. If no complete line appears
1422 in the test output within the time specified here, the test fails. It
1423 is necessary to specify this for long running tests that produce no
1424 intermediate output.
1428 =head1 CONFIGURATION FILE
1430 Novaboot can read its configuration from one or more files. By
1431 default, novaboot looks for files named F<.novaboot> as described in
1432 L</Configuration reading phase>. Alternatively, its location can be
1433 specified with the B<-c> switch or with the NOVABOOT_CONFIG
1434 environment variable. The configuration file has perl syntax and
1435 should set values of certain Perl variables. The current configuration
1436 can be dumped with the B<--dump-config> switch. Some configuration
1437 variables can be overridden by environment variables (see below) or by
1438 command line switches.
1440 Supported configuration variables include:
1446 Build directory location relative to the location of the configuration
1449 =item $default_target
1451 Default target (see below) to use when no target is explicitly
1452 specified on command line with the B<--target> option.
1456 Hash of shortcuts to be used with the B<--target> option. If the hash
1457 contains, for instance, the following pair of values
1459 'mybox' => '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1461 then the following two commands are equivalent:
1463 ./script --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1468 =head1 ENVIRONMENT VARIABLES
1470 Some options can be specified not only via config file or command line
1471 but also through environment variables. Environment variables override
1472 the values from configuration file and command line parameters
1473 override the environment variables.
1477 =item NOVABOOT_CONFIG
1479 Name of the novaboot configuration file to use instead of the default
1482 =item NOVABOOT_BENDER
1484 Defining this variable has the same meaning as B<--bender> option.
1490 Michal Sojka <sojka@os.inf.tu-dresden.de>