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 ($interactive && !-t STDIN) {
186 die("novaboot: Interactive mode not supported when not on terminal");
189 if (defined $config_name_opt && scalar(@ARGV) > 1) { die "You cannot use --name with multiple scripts"; }
192 if (defined $serial) {
193 $serial ||= "/dev/ttyUSB0";
194 $ENV{NB_SERIAL} = $serial;
196 if (defined $grub_config) { $grub_config ||= "menu.lst"; }
197 if (defined $grub2_config) { $grub2_config ||= "grub.cfg"; }
199 ## Parse the novaboot script(s)
205 my ($modules, $variables, $generated, $continuation);
207 if ($ARGV ne $last_fn) { # New script
208 die "Missing EOF in $last_fn" if $file;
209 die "Unfinished line in $last_fn" if $line;
211 push @scripts, { 'filename' => $ARGV,
212 'modules' => $modules = [],
213 'variables' => $variables = {},
214 'generated' => $generated = []};
218 next if /^#/ || /^\s*$/; # Skip comments and empty lines
220 foreach my $mod(@scriptmod) { eval $mod; }
222 print "$_\n" if $dump_opt;
224 if (/^([A-Z_]+)=(.*)$/) { # Internal variable
225 $$variables{$1} = $2;
226 push(@exiton, $2) if ($1 eq "EXITON");
229 if (/^([^ ]*)(.*?)[[:space:]]*<<([^ ]*)$/) { # Heredoc start
230 push @$modules, "$1$2";
232 push @$generated, {filename => $1, content => $file};
236 if ($file && $_ eq $EOF) { # Heredoc end
240 if ($file) { # Heredoc content
241 push @{$file}, "$_\n";
244 $_ =~ s/^[[:space:]]*// if ($continuation);
245 if (/\\$/) { # Line continuation
246 $line .= substr($_, 0, length($_)-1);
252 $line .= " $append" if ($append && scalar(@$modules) == 0);
254 if ($line =~ /^([^ ]*)(.*?)[[:space:]]*< ?(.*)$/) { # Command substitution
255 push @$modules, "$1$2";
256 push @$generated, {filename => $1, command => $3};
260 push @$modules, $line;
264 #print Dumper(\@scripts);
270 sub generate_configs($$$) {
271 my ($base, $generated, $filename) = @_;
272 if ($base) { $base = "$base/"; };
273 foreach my $g(@$generated) {
274 if (exists $$g{content}) {
275 my $config = $$g{content};
276 my $fn = $$g{filename};
277 open(my $f, '>', $fn) || die("$fn: $!");
278 map { s|\brom://([^ ]*)|$rom_prefix$base$1|g; print $f "$_"; } @{$config};
280 print "novaboot: Created $fn\n";
281 } elsif (exists $$g{command} && ! $no_file_gen) {
282 $ENV{SRCDIR} = dirname(File::Spec->rel2abs( $filename, $invocation_dir ));
283 system_verbose("( $$g{command} ) > $$g{filename}");
288 sub generate_grub_config($$$$;$)
290 my ($filename, $title, $base, $modules_ref, $preamble) = @_;
291 if ($base) { $base = "$base/"; };
292 open(my $fg, '>', $filename) or die "$filename: $!";
293 print $fg "$preamble\n" if $preamble;
294 print $fg "title $title\n" if $title;
295 #print $fg "root $base\n"; # root doesn't really work for (nd)
297 foreach (@$modules_ref) {
300 my ($kbin, $kcmd) = split(' ', $_, 2);
301 $kcmd = '' if !defined $kcmd;
302 print $fg "kernel ${base}$kbin $kcmd\n";
304 s|\brom://([^ ]*)|$rom_prefix$base$1|g; # Translate rom:// files - needed for vdisk parameter of sigma0
305 print $fg "module $base$_\n";
309 print("novaboot: Created $builddir/$filename\n");
313 sub generate_grub2_config($$$$;$$)
315 my ($filename, $title, $base, $modules_ref, $preamble, $prolog) = @_;
316 if ($base && substr($base,-1,1) ne '/') { $base = "$base/"; };
317 open(my $fg, '>', $filename) or die "$filename: $!";
318 print $fg "$preamble\n" if $preamble;
319 $title ||= 'novaboot';
320 print $fg "menuentry $title {\n";
321 print $fg "$prolog\n" if $prolog;
323 foreach (@$modules_ref) {
326 my ($kbin, $kcmd) = split(' ', $_, 2);
327 $kcmd = '' if !defined $kcmd;
328 print $fg " multiboot ${base}$kbin $kcmd\n";
331 # GRUB2 doesn't pass filename in multiboot info so we have to duplicate it here
332 $_ = join(' ', ($args[0], @args));
333 s|\brom://|$rom_prefix|g; # We do not need to translate path for GRUB2
334 print $fg " module $base$_\n";
339 print("novaboot: Created $builddir/$filename\n");
343 sub generate_pulsar_config($$)
345 my ($filename, $modules_ref) = @_;
346 open(my $fg, '>', $filename) or die "$filename: $!";
347 print $fg "root $pulsar_root\n" if defined $pulsar_root;
350 foreach (@$modules_ref) {
353 ($kbin, $kcmd) = split(' ', $_, 2);
354 $kcmd = '' if !defined $kcmd;
357 s|\brom://|$rom_prefix|g;
358 print $fg "load $_\n";
361 # Put kernel as last - this is needed for booting Linux and has no influence on non-Linux OSes
362 print $fg "exec $kbin $kcmd\n";
364 print("novaboot: Created $builddir/$filename\n");
368 sub shell_cmd_string(@)
370 return join(' ', map((/^[-_=a-zA-Z0-9\/\.\+]+$/ ? "$_" : "'$_'"), @_));
375 print "novaboot: Running: ".shell_cmd_string(@_)."\n";
379 sub system_verbose($)
382 print "novaboot: Running: $cmd\n";
383 my $ret = system($cmd);
384 if ($ret & 0x007f) { die("Command terminated by a signal"); }
385 if ($ret & 0xff00) {die("Command exit with non-zero exit code"); }
386 if ($ret) { die("Command failure $ret"); }
391 if (exists $variables->{WVDESC}) {
392 print "Testing \"$variables->{WVDESC}\" in $last_fn:\n";
393 } elsif ($last_fn =~ /\.wv$/) {
394 print "Testing \"all\" in $last_fn:\n";
397 ## Connect to the target and check whether is not occupied
399 # We have to do this before file generation phase, because file
400 # generation is intermixed with file deployment phase and we want to
401 # check whether the target is not used by somebody else before
402 # deploying files. Otherwise, we may rewrite other user's files on a
405 my $exp; # Expect object to communicate with the target over serial line
407 my ($target_reset, $target_power_on, $target_power_off);
409 if (defined $iprelay) {
411 $iprelay =~ /([.0-9]+)(:([0-9]+))?/;
414 my $paddr = sockaddr_in($port, inet_aton($addr));
415 my $proto = getprotobyname('tcp');
416 socket($IPRELAY, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
417 print "novaboot: Connecting to IP relay... ";
418 connect($IPRELAY, $paddr) || die "connect: $!";
420 $exp = Expect->init(\*$IPRELAY);
424 print $exp "\xFF\xF6"; # AYT
425 my $connected = $exp->expect(20, # Timeout in seconds
426 '<iprelayd: connected>',
427 '-re', '<WEB51 HW[^>]*>');
432 my ($relay, $onoff) = @_;
433 die unless ($relay == 1 || $relay == 2);
435 my $cmd = ($relay == 1 ? 0x5 : 0x6) | ($onoff ? 0x20 : 0x10);
436 return "\xFF\xFA\x2C\x32".chr($cmd)."\xFF\xF0";
440 my ($relay, $onoff) = @_;
441 die unless ($relay == 1 || $relay == 2);
442 my $cmd = ($relay == 1 ? 0xdf : 0xbf) | ($onoff ? 0x00 : 0xff);
443 return "\xFF\xFA\x2C\x97".chr($cmd)."\xFF\xF0";
447 my ($relay, $onoff, $can_giveup) = @_;
448 my $confirmation = '';
450 print $exp relaycmd($relay, $onoff);
451 my $confirmed = $exp->expect(20, # Timeout in seconds
452 relayconf($relay, $onoff));
455 print("Relay confirmation timeout - ignoring\n");
457 die "Relay confirmation timeout";
463 $target_reset = sub {
464 relay(2, 1, 1); # Reset the machine
469 $target_power_off = sub {
470 relay(1, 1); # Press power button
471 usleep(6000000); # Long press to switch off
475 $target_power_on = sub {
476 relay(1, 1); # Press power button
477 usleep(100000); # Short press
483 system_verbose("stty -F $serial $stty");
484 open($CONN, "+<", $serial) || die "open $serial: $!";
485 $exp = Expect->init(\*$CONN);
486 } elsif ($remote_cmd) {
487 print "novaboot: Running: $remote_cmd\n";
488 $exp = Expect->spawn($remote_cmd);
491 if ($remote_expect) {
492 $exp->expect(10, $remote_expect) || die "Expect for '$remote_expect' timed out";
495 if (defined $reset_cmd) {
496 $target_reset = sub {
497 system_verbose($reset_cmd);
501 if (defined $on_opt && defined $target_power_on) {
505 if (defined $off_opt && defined $target_power_off) {
506 print "novaboot: Switching the target off...\n";
507 &$target_power_off();
511 $builddir ||= dirname(File::Spec->rel2abs( ${$scripts[0]}{filename})) if scalar @scripts;
512 if (defined $builddir) {
513 chdir($builddir) or die "Can't change directory to $builddir: $!";
514 print "novaboot: Entering directory `$builddir'\n";
517 ## File generation phase
518 my (%files_iso, $menu_iso, $filename);
519 my $config_name = '';
521 foreach my $script (@scripts) {
522 $filename = $$script{filename};
523 $modules = $$script{modules};
524 $generated = $$script{generated};
525 $variables = $$script{variables};
527 ($config_name = $filename) =~ s#.*/##;
528 $config_name = $config_name_opt if (defined $config_name_opt);
530 if (exists $variables->{BUILDDIR}) {
531 $builddir = File::Spec->rel2abs($variables->{BUILDDIR});
532 chdir($builddir) or die "Can't change directory to $builddir: $!";
533 print "novaboot: Entering directory `$builddir'\n";
537 if (exists $variables->{KERNEL}) {
538 $kernel = $variables->{KERNEL};
540 if ($CFG::hypervisor) {
541 $kernel = $CFG::hypervisor . " ";
542 if (exists $variables->{HYPERVISOR_PARAMS}) {
543 $kernel .= $variables->{HYPERVISOR_PARAMS};
545 $kernel .= $CFG::hypervisor_params;
549 @$modules = ($kernel, @$modules) if $kernel;
550 @$modules = (@chainloaders, @$modules);
551 @$modules = ("bin/boot/bender", @$modules) if ($bender || defined $ENV{'NOVABOOT_BENDER'});
554 ($prefix = $grub_prefix) =~ s/\$NAME/$config_name/ if defined $grub_prefix;
555 $prefix ||= $builddir;
556 # TODO: use $grub_prefix as first parameter if some switch is given
557 generate_configs('', $generated, $filename);
559 ### Generate bootloader configuration files
560 my @bootloader_configs;
561 push @bootloader_configs, generate_grub_config($grub_config, $config_name, $prefix, $modules, $grub_preamble) if (defined $grub_config);
562 push @bootloader_configs, generate_grub2_config($grub2_config, $config_name, $prefix, $modules, $grub_preamble, $grub2_prolog) if (defined $grub2_config);
563 push @bootloader_configs, generate_pulsar_config('config-'.($pulsar||'novaboot'), $modules) if (defined $pulsar);
565 ### Run scons or make
567 my @files = map({ ($file) = m/([^ ]*)/; $file; } @$modules);
568 # Filter-out generated files
569 my @to_build = grep({ my $file = $_; !scalar(grep($file eq $$_{filename}, @$generated)) } @files);
571 system_verbose($scons || $CFG::scons." ".join(" ", @to_build)) if (defined $scons);
572 system_verbose($make || $CFG::make ." ".join(" ", @to_build)) if (defined $make);
575 ### Copy files (using rsync)
576 if (defined $server && !defined($gen_only)) {
577 (my $real_server = $server) =~ s/\$NAME/$config_name/;
579 my ($hostname, $path) = split(":", $real_server, 2);
580 if (! defined $path) {
584 my $files = join(" ", map({ ($file) = m/([^ ]*)/; $file; } ( @$modules, @bootloader_configs)));
585 map({ my $file = (split)[0]; die "$file: $!" if ! -f $file; } @$modules);
586 my $istty = -t STDOUT && ($ENV{'TERM'} || 'dumb') ne 'dumb';
587 my $progress = $istty ? "--progress" : "";
588 system_verbose("rsync $progress -RLp $rsync_flags $files $real_server");
589 if ($server =~ m|/\$NAME$| && $concat) {
590 my $cmd = join("; ", map { "( cd $path/.. && cat */$_ > $_ )" } @bootloader_configs);
591 system_verbose($hostname ? "ssh $hostname '$cmd'" : $cmd);
595 ### Prepare ISO image generation
596 if (defined $iso_image) {
597 generate_configs("(cd)", $generated, $filename);
599 generate_grub_config(\$menu, $config_name, "(cd)", $modules);
600 $menu_iso .= "$menu\n";
601 map { ($file,undef) = split; $files_iso{$file} = 1; } @$modules;
605 ## Generate ISO image
606 if (defined $iso_image) {
607 open(my $fh, ">menu-iso.lst");
608 print $fh "timeout 5\n\n$menu_iso";
610 my $files = "boot/grub/menu.lst=menu-iso.lst " . join(" ", map("$_=$_", keys(%files_iso)));
611 $iso_image ||= "$config_name.iso";
612 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");
613 print("ISO image created: $builddir/$iso_image\n");
616 exit(0) if defined $gen_only;
618 ## Boot the system using various methods and send serial output to stdout
620 if (scalar(@scripts) > 1 && ( defined $dhcp_tftp || defined $serial || defined $iprelay)) {
621 die "You cannot do this with multiple scripts simultaneously";
624 if ($variables->{WVTEST_TIMEOUT}) {
625 print "wvtest: timeout ", $variables->{WVTEST_TIMEOUT}, "\n";
630 $str =~ s/^\s+|\s+$//g;
638 $qemu ||= $variables->{QEMU} || $CFG::qemu;
639 my @qemu_flags = split(" ", $qemu);
640 $qemu = shift(@qemu_flags);
642 @qemu_flags = split(/ +/, trim($variables->{QEMU_FLAGS})) if exists $variables->{QEMU_FLAGS};
643 @qemu_flags = split(/ +/, trim($qemu_flags_cmd)) if $qemu_flags_cmd;
644 push(@qemu_flags, split(/ +/, trim($qemu_append || '')));
646 if (defined $iso_image) {
647 # Boot NOVA with grub (and test the iso image)
648 push(@qemu_flags, ('-cdrom', "$config_name.iso"));
650 # Boot NOVA without GRUB
652 # Non-patched qemu doesn't like commas, but NUL can live with pluses instead of commans
653 foreach (@$modules) {s/,/+/g;}
654 generate_configs("", $generated, $filename);
656 if (scalar @$modules) {
657 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
658 $kcmd = '' if !defined $kcmd;
660 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
661 my $initrd = join ",", @$modules;
663 push(@qemu_flags, ('-kernel', $kbin, '-append', $kcmd));
664 push(@qemu_flags, ('-initrd', $initrd)) if $initrd;
665 push(@qemu_flags, ('-dtb', $dtb)) if $dtb;
668 push(@qemu_flags, qw(-serial stdio)); # Redirect serial output (for collecting test restuls)
669 unshift(@qemu_flags, ('-name', $config_name));
670 print "novaboot: Running: ".shell_cmd_string($qemu, @qemu_flags)."\n";
671 $exp = Expect->spawn(($qemu, @qemu_flags)) || die("exec() failed: $!");
674 ### Local DHCPD and TFTPD
676 my ($dhcpd_pid, $tftpd_pid);
678 if (defined $dhcp_tftp)
680 generate_configs("(nd)", $generated, $filename);
681 system_verbose('mkdir -p tftpboot');
682 generate_grub_config("tftpboot/os-menu.lst", $config_name, "(nd)", \@$modules, "timeout 0");
683 open(my $fh, '>', 'dhcpd.conf');
684 my $mac = `cat /sys/class/net/eth0/address`;
686 print $fh "subnet 10.23.23.0 netmask 255.255.255.0 {
687 range 10.23.23.10 10.23.23.100;
688 filename \"bin/boot/grub/pxegrub.pxe\";
689 next-server 10.23.23.1;
692 hardware ethernet $mac;
693 fixed-address 10.23.23.1;
696 system_verbose("sudo ip a add 10.23.23.1/24 dev eth0;
697 sudo ip l set dev eth0 up;
698 sudo touch dhcpd.leases");
700 # We run servers by forking ourselves, because the servers end up
701 # in our process group and get killed by signals sent to the
702 # process group (e.g. Ctrl-C on terminal).
704 exec_verbose("sudo dhcpd -d -cf dhcpd.conf -lf dhcpd.leases -pf dhcpd.pid") if ($dhcpd_pid == 0);
706 exec_verbose("sudo in.tftpd --foreground --secure -v -v -v --pidfile tftpd.pid $builddir") if ($tftpd_pid == 0);
708 # Kill server when we die
709 $SIG{__DIE__} = sub { system_verbose('sudo pkill --pidfile=dhcpd.pid');
710 system_verbose('sudo pkill --pidfile=tftpd.pid'); };
713 ### Serial line or IP relay
715 if (defined $target_reset) {
716 print "novaboot: Reseting the test box... ";
721 if (defined $uboot) {
722 print "novaboot: Waiting for uBoot prompt...\n";
725 [qr/Hit any key to stop autoboot:/, sub { $exp->send("\n"); exp_continue; }],
726 '=> ') || die "No uBoot prompt deteceted";
727 $exp->send("$uboot_init\n") if $uboot_init;
728 $exp->expect(10, '=> ') || die "uBoot prompt timeout";
730 my ($kbin, $kcmd) = split(' ', shift(@$modules), 2);
732 @$modules = map { if (/\.dtb$/) { $dtb=$_; (); } else { $_ } } @$modules;
733 my $initrd = shift @$modules;
735 my $kern_addr = '800000';
736 my $initrd_addr = '-';
739 $exp->send("tftp $kern_addr $kbin\n");
741 [qr/#/, sub { exp_continue; }],
742 '=> ') || die "Kernel load failed";
744 $dtb_addr = '7f0000';
745 $exp->send("tftp $dtb_addr $dtb\n");
747 [qr/#/, sub { exp_continue; }],
748 '=> ') || die "Device tree load failed";
750 if (defined $initrd) {
751 $initrd_addr = 'b00000';
752 $exp->send("tftp $initrd_addr $initrd\n");
754 [qr/#/, sub { exp_continue; }],
755 '=> ') || die "Initrd load failed";
757 $exp->send("set bootargs $kcmd\n");
758 $exp->expect(1, '=> ') || die "uBoot prompt timeout";
759 $exp->send("bootm $kern_addr $initrd_addr $dtb_addr\n");
760 $exp->expect(1, "\n") || die "uBoot command timeout";
764 # Serial line of the target is available
765 my $interrupt = 'Ctrl-C';
766 if ($interactive && !@exiton) {
767 $interrupt = '"~~."';
769 print "novaboot: Serial line interaction (press $interrupt to interrupt)...\n";
772 $exp->expect(undef, @exiton);
775 if (-t STDIN) { # Set up bi-directional communication if we run on terminal
776 my $infile = new IO::File;
777 $infile->IO::File::fdopen(*STDIN,'r');
778 my $in_object = Expect->exp_init($infile);
779 $in_object->set_group($exp);
782 $in_object->debug(1);
783 $in_object->set_seq('~~\.', sub { print "novaboot: Escape sequence detected\r\n"; undef; });
784 $in_object->manual_stty(0); # Use raw terminal mode
786 $in_object->manual_stty(1); # Do not modify terminal settings
788 push(@inputs, $in_object);
790 Expect::interconnect(@inputs);
794 ## Kill dhcpc or tftpd
795 if (defined $dhcp_tftp) {
796 die("novaboot: This should kill servers on background\n");
803 novaboot - A tool for booting various operating systems on various hardware or in qemu
807 B<novaboot> [ options ] [--] script...
809 B<./script> [ options ]
813 This program makes it easier to boot NOVA or other operating system
814 (OS) on different targets (machines or emulators). It reads a so
815 called novaboot script, that specifies the boot configuration, and
816 setups the target to boot that configuration. Setting up the target
817 means to generate the bootloader configuration files, deploy the
818 binaries and other needed files to proper locations, perhaps on a
819 remote boot server and reset the target. Then, target's serial output
820 is redirected to standard output if that is possible.
822 A typical way of using novaboot is to make the novaboot script
823 executable and set its first line to I<#!/usr/bin/env novaboot>. Then,
824 booting a particular OS configuration becomes the same as executing a
825 local program - the novaboot script.
827 For example, with C<novaboot> you can:
833 Run an OS in Qemu. This is the default action when no other action is
834 specified by command line switches. Thus running C<novaboot ./script>
835 (or C<./script> as described above) will run Qemu and make it boot the
836 configuration specified in the I<script>.
840 Create a bootloader configuration file (currently supported
841 bootloaders are GRUB, GRUB2, Pulsar and uBoot) and copy it with all
842 other files needed for booting to another, perhaps remote, location.
844 ./script --server=192.168.1.1:/tftp --iprelay=192.168.1.2
846 This command copies files to the TFTP server and uses
847 TCP/IP-controlled relay to reset the test box and receive its serial
852 Run DHCP and TFTP server on developer's machine to PXE-boot the OS
857 When a PXE-bootable machine is connected via Ethernet to developer's
858 machine, it will boot the configuration described in I<script>.
862 Create bootable ISO images. E.g.
864 novaboot --iso -- script1 script2
866 The created ISO image will have GRUB bootloader installed on it and
867 the boot menu will allow selecting between I<script1> and I<script2>
872 Note that the options needed for a specific target can be stored in a
873 L</"CONFIGURATION FILE"> and then it is sufficient to use only the
874 B<-t> option to specify the name of the target.
876 =head1 PHASES AND OPTIONS
878 Novaboot performs its work in several phases. Each phase can be
879 influenced by several options, certain phases can be skipped. The list
880 of phases (in the execution order) and the corresponding options
883 =head2 Configuration reading phase
885 After starting, novaboot reads configuration files. By default, it
886 searches for files named F<.novaboot> starting from the directory of
887 the novaboot script (or working directory, see bellow) and continuing
888 upwards up to the root directory. The configuration files are read in
889 order from the root directory downwards with latter files overriding
890 settings from the former ones.
892 In certain cases, the location of the novaboot script cannot be
893 determined in this early phase. This happens either when the script is
894 read from the standard input or when novaboot is invoked explicitly
895 and options precede the script name, as in the example L</"4."> above.
896 In this case the current working directory is used as a starting point
897 for configuration file search.
901 =item -c, --config=I<filename>
903 Use the specified configuration file instead of the default one(s).
907 =head2 Command line processing phase
913 Dump the current configuration to stdout end exits. Useful as an
914 initial template for a configuration file.
918 Print short (B<-h>) or long (B<--help>) help.
920 =item -t, --target=I<target>
922 This option serves as a user configurable shortcut for other novaboot
923 options. The effect of this option is the same as the options stored
924 in the C<%targets> configuration variable under key I<target>. See
925 also L</"CONFIGURATION FILE">.
929 =head2 Script preprocessing phase
931 This phases allows to modify the parsed novaboot script before it is
932 used in the later phases.
936 =item -a, --append=I<parameters>
938 Appends a string to the first "filename" line in the novaboot script.
939 This can be used to append parameters to the kernel's or root task's
944 Use F<bender> chainloader. Bender scans the PCI bus for PCI serial
945 ports and stores the information about them in the BIOS data area for
948 =item --chainloader=I<chainloader>
950 Chainloader that is loaded before the kernel and other files specified
951 in the novaboot script. E.g. 'bin/boot/bender promisc'.
955 Prints the content of the novaboot script after removing comments and
956 evaluating all I<--scriptmod> expressions. Exit after reading (and
959 =item --scriptmod=I<perl expression>
961 When novaboot script is read, I<perl expression> is executed for every
962 line (in $_ variable). For example, C<novaboot
963 --scriptmod=s/sigma0/omega6/g> replaces every occurrence of I<sigma0>
964 in the script with I<omega6>.
966 When this option is present, it overrides I<$script_modifier> variable
967 from the configuration file, which has the same effect. If this option
968 is given multiple times all expressions are evaluated in the command
973 Strip I<rom://> prefix from command lines and generated config files.
974 The I<rom://> prefix is used by NUL. For NRE, it has to be stripped.
978 =head2 File generation phase
980 In this phase, files needed for booting are generated in a so called
981 I<build directory> (see TODO). In most cases configuration for a
982 bootloader is generated automatically by novaboot. It is also possible
983 to generate other files using I<heredoc> or I<"<"> syntax in novaboot
984 scripts. Finally, binaries can be generated in this phases by running
989 =item --build-dir=I<directory>
991 Overrides the default build directory location.
993 The default build directory location is determined as follows: If the
994 configuration file defines the C<$builddir> variable, its value is
995 used. Otherwise, it is the directory that contains the first processed
998 =item -g, --grub[=I<filename>]
1000 Generates grub bootloader menu file. If the I<filename> is not
1001 specified, F<menu.lst> is used. The I<filename> is relative to the
1002 build directory (see B<--build-dir>).
1004 =item --grub-preamble=I<prefix>
1006 Specifies the I<preable> that is at the beginning of the generated
1007 GRUB or GRUB2 config files. This is useful for specifying GRUB's
1010 =item --grub-prefix=I<prefix>
1012 Specifies I<prefix> that is put in front of every file name in GRUB's
1013 F<menu.lst>. The default value is the absolute path to the build directory.
1015 If the I<prefix> contains string $NAME, it will be replaced with the
1016 name of the novaboot script (see also B<--name>).
1018 =item --grub2[=I<filename>]
1020 Generate GRUB2 menuentry in I<filename>. If I<filename> is not
1021 specified F<grub.cfg> is used. The content of the menuentry can be
1022 customized with B<--grub-preable>, B<--grub2-prolog> or
1023 B<--grub_prefix> options.
1025 In order to use the the generated menuentry on your development
1026 machine that uses GRUB2, append the following snippet to
1027 F</etc/grub.d/40_custom> file and regenerate your grub configuration,
1028 i.e. run update-grub on Debian/Ubuntu.
1030 if [ -f /path/to/nul/build/grub.cfg ]; then
1031 source /path/to/nul/build/grub.cfg
1034 =item --grub2-prolog=I<prolog>
1036 Specifies text I<preable> that is put at the beginning of the entry
1039 =item -m, --make[=make command]
1041 Runs C<make> to build files that are not generated by novaboot itself.
1043 =item --name=I<string>
1045 Use the name I<string> instead of the name of the novaboot script.
1046 This name is used for things like a title of grub menu or for the
1047 server directory where the boot files are copied to.
1051 Do not generate files on the fly (i.e. "<" syntax) except for the
1052 files generated via "<<WORD" syntax.
1054 =item -p, --pulsar[=mac]
1056 Generates pulsar bootloader configuration file named F<config-I<mac>>
1057 The I<mac> string is typically a MAC address and defaults to
1060 =item --scons[=scons command]
1062 Runs C<scons> to build files that are not generated by novaboot
1067 Exit novaboot after file generation phase.
1071 =head2 Target connection check
1073 If supported by the target, the connection to it is made and it is
1074 checked whether the target is not occupied by another novaboot
1079 =item --iprelay=I<addr[:port]>
1081 Use TCP/IP relay and serial port to access the target's serial port
1082 and powercycle it. The IP address of the relay is given by I<addr>
1083 parameter. If I<port> is not specified, it default to 23.
1085 Note: This option is supposed to work with HWG-ER02a IP relays.
1087 =item -s, --serial[=device]
1089 Target's serial line is connected to host's serial line (device). The
1090 default value for device is F</dev/ttyUSB0>.
1092 The value of this option is exported in NB_NOVABOOT environment
1093 variable to all subprocesses run by C<novaboot>.
1095 =item --stty=I<settings>
1097 Specifies settings passed to C<stty> invoked on the serial line
1098 specified with B<--serial> option. If this option is not given,
1099 C<stty> is called with C<raw -crtscts -onlcr 115200> settings.
1101 =item --remote-cmd=I<cmd>
1103 Command that mediates connection to the target's serial line. For
1104 example C<ssh server 'cu -l /dev/ttyS0'>.
1106 =item --remote-expect=I<string>
1108 Wait for reception of I<string> on the remote connection before
1113 =head2 File deployment phase
1115 In some setups, it is necessary to copy the files needed for booting
1116 to a particular location, e.g. to a TFTP boot server or to the
1121 =item -d, --dhcp-tftp
1123 Turns your workstation into a DHCP and TFTP server so that the OS can
1124 be booted via PXE BIOS (or similar mechanism) on the test machine
1125 directly connected by a plain Ethernet cable to your workstation.
1127 The DHCP and TFTP servers require root privileges and C<novaboot>
1128 uses C<sudo> command to obtain those. You can put the following to
1129 I</etc/sudoers> to allow running the necessary commands without
1130 asking for password.
1132 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
1133 your_login ALL=NOPASSWD: NOVABOOT
1135 =item --iso[=filename]
1137 Generates the ISO image that boots NOVA system via GRUB. If no filename
1138 is given, the image is stored under I<NAME>.iso, where I<NAME> is the name
1139 of the novaboot script (see also B<--name>).
1141 =item --server[=[[user@]server:]path]
1143 Copy all files needed for booting to another location (implies B<-g>
1144 unless B<--grub2> is given). The files will be copied (by B<rsync>
1145 tool) to the directory I<path>. If the I<path> contains string $NAME,
1146 it will be replaced with the name of the novaboot script (see also
1151 If B<--server> is used and its value ends with $NAME, then after
1152 copying the files, a new bootloader configuration file (e.g. menu.lst)
1153 is created at I<path-wo-name>, i.e. the path specified by B<--server>
1154 with $NAME part removed. The content of the file is created by
1155 concatenating all files of the same name from all subdirectories of
1156 I<path-wo-name> found on the "server".
1158 =item --rsync-flags=I<flags>
1160 Specifies which I<flags> are appended to F<rsync> command line when
1161 copying files as a result of I<--server> option.
1165 =head2 Target power-on and reset phase
1171 Switch on/off the target machine. Currently works only with
1174 =item -Q, --qemu[=I<qemu-binary>]
1176 Boot the configuration in qemu. Optionally, the name of qemu binary
1177 can be specified as a parameter.
1179 =item --qemu-append=I<flags>
1181 Append I<flags> to the default qemu flags (QEMU_FLAGS variable or
1182 C<-cpu coreduo -smp 2>).
1184 =item -q, --qemu-flags=I<flags>
1186 Replace the default qemu flags (QEMU_FLAGS variable or C<-cpu coreduo
1187 -smp 2>) with I<flags> specified here.
1189 =item --reset-cmd=I<cmd>
1191 Command that resets the target.
1195 =head2 Interaction with the bootloader on the target
1201 Interact with uBoot bootloader to boot the thing described in the
1202 novaboot script. Implementation of this option is currently tied to a
1203 particular board that we use. It may be subject to changes in the
1208 Command(s) to send the uBoot bootloader before loading the images and
1213 =head2 Target interaction phase
1215 In this phase, target's serial output is passed to C<novaboot> stdout.
1216 If C<novaboot>'s stdin is on TTY, the stdin is passed to the target
1217 allowing interactive work with the target.
1219 This phase end when the target hangs up or when Ctrl-C is pressed.
1223 =item --exiton=I<string>
1225 When I<string> is sent by the target, novaboot exits. This option can
1226 be specified multiple times.
1228 If I<string> is C<-re>, then the next B<--exiton>'s I<string> is
1229 treated as regular expression. For example:
1231 --exiton -re --exiton 'error:.*failed'
1233 =item -i, --interactive
1235 Setup things for interactive use of target. Your terminal will be
1236 switched to raw mode. In raw mode, your system does not process input
1237 in any way (no echoing of entered characters, no interpretation
1238 special characters). This, among others, means that Ctrl-C is passed
1239 to the target and does no longer interrupt novaboot. Use "~~."
1240 sequence to exit novaboot.
1244 =head1 NOVABOOT SCRIPT SYNTAX
1246 The syntax tries to mimic POSIX shell syntax. The syntax is defined with the following rules.
1248 Lines starting with "#" are ignored.
1250 Lines that end with "\" are concatenated with the following line after
1251 removal of the final "\" and leading whitespace of the following line.
1253 Lines in the form I<VARIABLE=...> (i.e. matching '^[A-Z_]+=' regular
1254 expression) assign values to internal variables. See VARIABLES
1257 Otherwise, the first word on the line represents the filename
1258 (relative to the build directory (see B<--build-dir>) of the module to
1259 load and the remaining words are passed as the command line
1262 When the line ends with "<<WORD" then the subsequent lines until the
1263 line containing only WORD are copied literally to the file named on
1266 When the line ends with "< CMD" the command CMD is executed with
1267 C</bin/sh> and its standard output is stored in the file named on that
1268 line. The SRCDIR variable in CMD's environment is set to the absolute
1269 path of the directory containing the interpreted novaboot script.
1272 #!/usr/bin/env novaboot
1273 WVDESC=Example program
1274 bin/apps/sigma0.nul S0_DEFAULT script_start:1,1 \
1275 verbose hostkeyb:0,0x60,1,12,2
1277 hello.nulconfig <<EOF
1278 sigma0::mem:16 name::/s0/log name::/s0/timer name::/s0/fs/rom ||
1279 rom://bin/apps/hello.nul
1282 This example will load three modules: sigma0.nul, hello.nul and
1283 hello.nulconfig. sigma0 gets some command line parameters and
1284 hello.nulconfig file is generated on the fly from the lines between
1289 The following variables are interpreted in the novaboot script:
1295 Novaboot chdir()s to this directory before file generation phase. The
1296 directory name specified here is relative to the build directory
1297 specified by other means (see L</--build-dir>).
1301 Assigning this variable has the same effect as specifying L</--exiton>
1304 =item HYPERVISOR_PARAMS
1306 Parameters passed to hypervisor. The default value is "serial", unless
1307 overriden in configuration file.
1311 The kernel to use instead of NOVA hypervisor specified in the
1312 configuration file. The value should contain the name of the kernel
1313 image as well as its command line parameters. If this variable is
1314 defined and non-empty, the variable HYPERVISOR_PARAMS is not used.
1318 Use a specific qemu binary (can be overriden with B<-Q>) and flags
1319 when booting this script under qemu. If QEMU_FLAGS variable is also
1320 specified flags specified in QEMU variable are replaced by those in
1325 Use specific qemu flags (can be overriden with B<-q>).
1329 Description of the wvtest-compliant program.
1331 =item WVTEST_TIMEOUT
1333 The timeout in seconds for WvTest harness. If no complete line appears
1334 in the test output within the time specified here, the test fails. It
1335 is necessary to specify this for long running tests that produce no
1336 intermediate output.
1340 =head1 CONFIGURATION FILE
1342 Novaboot can read its configuration from one or more files. By
1343 default, novaboot looks for files named F<.novaboot> as described in
1344 L</Configuration reading phase>. Alternatively, its location can be
1345 specified with the B<-c> switch or with the NOVABOOT_CONFIG
1346 environment variable. The configuration file has perl syntax and
1347 should set values of certain Perl variables. The current configuration
1348 can be dumped with the B<--dump-config> switch. Some configuration
1349 variables can be overriden by environment variables (see below) or by
1350 command line switches.
1352 Supported configuration variables include:
1358 Build directory location relative to the location of the configuration
1361 =item $default_target
1363 Default target (see below) to use when no target is explicitly
1364 specified on command line with the B<--target> option.
1368 Hash of shortcuts to be used with the B<--target> option. If the hash
1369 contains, for instance, the following pair of values
1371 'mybox' => '--server=boot:/tftproot --serial=/dev/ttyUSB0 --grub',
1373 then the following two commands are equivalent:
1375 ./script --server=boot:/tftproot --serial=/dev/ttyUSB0 --grub
1380 =head1 ENVIRONMENT VARIABLES
1382 Some options can be specified not only via config file or command line
1383 but also through environment variables. Environment variables override
1384 the values from configuration file and command line parameters
1385 override the environment variables.
1389 =item NOVABOOT_CONFIG
1391 Name of the novaboot configuration file to use instead of the default
1394 =item NOVABOOT_BENDER
1396 Defining this variable has the same meaning as B<--bender> option.
1402 Michal Sojka <sojka@os.inf.tu-dresden.de>