From 35b919185e4f60a7fc4ef750e526e376b2ed6225 Mon Sep 17 00:00:00 2001 From: Bruce Potter Date: Sat, 15 Feb 2014 16:31:35 -0500 Subject: [PATCH] add cmds to push the network boot info to softlayer nodes --- xCAT-SoftLayer/bin/modifygrub | 249 ++++++++++++++++++++++++++++++++++ xCAT-SoftLayer/bin/pushinitrd | 147 ++++++++++++++++++++ 2 files changed, 396 insertions(+) create mode 100755 xCAT-SoftLayer/bin/modifygrub create mode 100755 xCAT-SoftLayer/bin/pushinitrd diff --git a/xCAT-SoftLayer/bin/modifygrub b/xCAT-SoftLayer/bin/modifygrub new file mode 100755 index 000000000..6457516ce --- /dev/null +++ b/xCAT-SoftLayer/bin/modifygrub @@ -0,0 +1,249 @@ +#!/usr/bin/perl + +# Modify the grub config file on the node to boot the specified kernel and initrd. +# This script is meant to be run on the node via xdsh -e. +# Currently requires that dns on the mn be configured and working to resolve the short node names. + +use strict; +use Getopt::Long; +use Data::Dumper; +use Socket; + +# Globals - these are set once and then only read. +my $HELP; +my $VERBOSE; +my $WAITTIME; +my $XCATNETBOOTTITLE = 'xCAT network boot kernel and initrd'; + +my $usage = sub { + my $exitcode = shift @_; + print "Usage: modifygrub [-?|-h|--help] [-v|--verbose] [-w waittime] \n\n"; + if (!$exitcode) { + print "Modify the grub config file on the node to boot the specified kernel and initrd.\n"; + } + exit $exitcode; +}; + +if (-f '/etc/os-release') { die "This script doesn't support ubuntu yet.\n"; } + +# Process the cmd line args +Getopt::Long::Configure("bundling"); +#Getopt::Long::Configure("pass_through"); +Getopt::Long::Configure("no_pass_through"); +if (!GetOptions('h|?|help' => \$HELP, 'v|verbose' => \$VERBOSE, 'w|waittime' => \$WAITTIME)) { $usage->(1); } + +if ($HELP) { $usage->(0); } +if (scalar(@ARGV) != 4) { $usage->(1); } +if (!defined($WAITTIME)) { $WAITTIME = 90; } # seconds to wait after configuring the nic (to let the switch handle the state change) +my %args; +$args{kernelpath} = $ARGV[0]; +$args{initrdpath} = $ARGV[1]; +$args{kernelparms} = $ARGV[2]; +$args{mnip} = $ARGV[3]; + +addKernelParms(\%args); # replace and add some parms to args{kernelparms} +updateGrub(\%args); # update the grub config with an entry filled with the info in args + +exit(0); + + +# Add ip and net info to the kernel parms. Modifies the kernelparms value of the args hash passed in. +sub addKernelParms { + my $args = shift @_; + + # replace '!myipfn!' with the mn ip + my $mnip = $args->{mnip}; + $args->{kernelparms} =~ s/!myipfn!/$mnip/g; + + # replace with the nodename + my $nodename = $ENV{NODE}; # this env var is set by xdsh + $args->{kernelparms} =~ s//$nodename/g; + + # get node ip and add it to the kernel parms + my ($nic, $ip, $netmask, $gateway) = getNodeIpInfo($args); + if (!$ip) { die "Error: could not find the NIC that would connect to the xCAT mgmt node's IP (".$args->{mnip}.").\n"; } + $args->{kernelparms} .= " hostip=$ip netmask=$netmask gateway=$gateway netdevice=$nic netwait=$WAITTIME"; +} + + +# get this nodes nic, ip, netmask, and gateway. Returns them in a 4 element array. +sub getNodeIpInfo { + my $args = shift @_; + my ($ipprefix) = $args->{mnip}=~m/^(\d+\.\d+)\./; #todo: this is a hack, just using the 1st 2 octets of the mn ip addr + verbose("using IP prefix $ipprefix"); + + # parse ip addr show output, looking for ipprefix, to determine nic and ip + my @output = runcmd("ip addr show"); + my ($nic, $ipandmask); + foreach my $line (@output) { + my ($nictmp, $iptmp); + if (($nictmp) = $line=~m/^\d+:\s+(\S+): /) { $nic = $nictmp; } # new stanza, remember it + if (($iptmp) = $line=~m/^\s+inet\s+($ipprefix\S+) /) { $ipandmask = $iptmp; last; } # got ip, we are done + } + my ($ip, $netmask) = convertIpAndMask($ipandmask); + + # if the nic is a bonded nic (common on sl), then find the 1st real nic that is part of it + my $realnic = $nic; + if ($nic =~ /^bond/) { + my @nics = grep(m/\s+master\s+$nic\s+/, @output); + if (!scalar(@nics)) { die "Error: can't find the NICs that are part of $nic.\n"; } + ($realnic) = $nics[0]=~m/^\d+:\s+(\S+): /; + } + + # finally, find the gateway + my $gateway; + my @output = runcmd("ip route"); + # we are looking for a line like: 10.0.0.0/8 via 10.54.51.1 dev bond0 + my @networks = grep(m/ via .* $nic\s*$/, @output); + if (scalar(@networks)) { ($gateway) = $networks[0]=~m/ via\s+(\S+)/; } + else { + # use the mn ip as a fall back + $gateway = $args->{mnip}; + verbose("using xCAT mgmt node IP as the fall back gateway."); + } + + verbose("IP info: realnic=$realnic, ip=$ip, netmask=$netmask, gateway=$gateway"); + return ($realnic, $ip, $netmask, $gateway); +} + + +# Convert an ip/mask in slash notation (like 10.0.0.1/26) to separate ip and netmask like 10.0.0.1 and 255.255.255.192 +sub convertIpAndMask { + my $ipandmask = shift @_; + my ($ip, $masknum) = split('/', $ipandmask); + my $netbin = oct("0b" . '1' x $masknum . '0' x (32-$masknum)); # create a str like '1111100', then convert to binary + my @netarr=unpack('C4',pack('N',$netbin)); # separate into the 4 octets + my $netmask=join('.',@netarr); # put them together into the normal looking netmask + return ($ip, $netmask); +} + + +# not used - resolve the hostname to an ip addr +sub getipaddr { + my $hostname = shift @_; + my $packed_ip; + $packed_ip = inet_aton($hostname); + if (!$packed_ip) { return undef; } + return inet_ntoa($packed_ip); +} + + +# Update the grub config file with a new stanza for booting our kernel and initrd +sub updateGrub { + my $args = shift @_; + + # this is the entry we want in the grub file + my @entry = ( + "title $XCATNETBOOTTITLE\n", + "\troot (hd0,0)\n", + "\tkernel " . $args->{kernelpath} . ' ' . $args->{kernelparms} . "\n", + "\tinitrd " . $args->{initrdpath} . "\n", + ); + + # open the grub file and see if it is in there or if we have to add it + my $grubfile = findGrubPath(); + verbose("reading $grubfile"); + open(FILE, $grubfile) || die "Error: can not open config file $grubfile for reading: $!\n"; + my @lines = ; + close FILE; + + my $needtowritefile = 1; + if (grep(/^title\s+$XCATNETBOOTTITLE/, @lines)) { $needtowritefile = updateGrubEntry(\@lines, \@entry); } # there is already an entry in there + else { addGrubEntry (\@lines, \@entry); } + + # write the file with the new/updated xcat entry + if ($needtowritefile) { + verbose("updating $grubfile"); + open(FILE, '>', $grubfile) || die "Error: can not open config file $grubfile for writinging: $!\n"; + print FILE @lines; + close FILE; + } + else { print "Info: $grubfile did not need modifying. It was already up to date.\n"; } +} + + +# add our entry as the 1st one in the grub file +sub addGrubEntry { + my ($lines, $entry) = @_; + # find the index of the 1st stanza (it starts with 'title') + my $i; + for ($i=0; $i[$i] =~ m/^title\s+/) { verbose('adding xcat entry before:'.$lines->[$i]); last; } # found it + } + + # splice the entry right before the i-th line (which may also be 1 past the end) + splice(@$lines, $i, 0, @$entry); +} + + +# check the xcat entry in the grub file and see if it needs to be updated. Return 1 if it does. +sub updateGrubEntry { + my ($lines, $entry) = @_; + # find the index of the xcat stanza + my $i; + for ($i=0; $i++; $i[$i] =~ m/^title\s+$XCATNETBOOTTITLE/) { last; } # found it + } + + # compare the next few lines with the corresponding line in @$entries and replace if different + my $replaced = 0; + for (my $j=0; $j++; $j[$i+$j] ne $entry->[$j]) { # this line was different + $lines->[$i+$j] = $entry->[$j]; + $replaced = 1; + } + } + + return $replaced; +} + + +# depending on the distro, find the correct grub file and return its path +sub findGrubPath { + # for rhel/centos it is /boot/grub/grub.conf, for sles it is /boot/grub/menu.lst + my @paths = qw(/boot/grub/grub.conf /boot/grub/menu.lst); + foreach my $p (@paths) { + if (-f $p) { return $p; } + } + + die "Error: Can't find grub config file.\n"; + + #todo: support ubuntu: you add an executable file in /etc/grub.d named 06_xcatnetboot that prints out the + # entry to add. Then run grub-mkconfig. +} + + +# Pring msg only if -v was specified +sub verbose { if ($VERBOSE) { print shift, "\n"; } } + + + +# Run a command. If called in the context of return an array, it will capture the output +# of the cmd and return it. Otherwise, it will display the output to stdout. +# If the cmd has a non-zero rc, this function will die with a msg. +sub runcmd +{ + my ($cmd) = @_; + my $rc; + + $cmd .= ' 2>&1' ; + verbose($cmd); + + my @output; + if (wantarray) { + @output = `$cmd`; + $rc = $?; + } + else { + system($cmd); + $rc = $?; + } + + if ($rc) { + $rc = $rc >> 8; + if ($rc > 0) { die "Error: rc $rc return from cmd: $cmd\n"; } + else { die "Error: system error returned from cmd: $cmd\n"; } + } + elsif (wantarray) { return @output; } +} + diff --git a/xCAT-SoftLayer/bin/pushinitrd b/xCAT-SoftLayer/bin/pushinitrd new file mode 100755 index 000000000..270431af3 --- /dev/null +++ b/xCAT-SoftLayer/bin/pushinitrd @@ -0,0 +1,147 @@ +#!/usr/bin/perl + +# Copy the initrd, kernel, params, and static IP info to nodes, so they can net install +# even across vlans (w/o setting up pxe/dhcp broadcast relay). This assumes a working +# OS is on the node. This script is primarily meant to be used in the softlayer environment. + +#todo: site attr for using static ip? + +use strict; +use Getopt::Long; +use Data::Dumper; + +# Globals - these are set once and then only read. +my $HELP; +my $VERBOSE; + +my $usage = sub { + my $exitcode = shift @_; + print "Usage: pushinitrd [-?|-h|--help] [-v|--verbose] \n\n"; + if (!$exitcode) { + print "Copy the initrd, kernel, params, and static IP info to nodes, so they can net install\n"; + print "even across vlans (w/o setting up pxe/dhcp broadcast relay). This assumes a working\n"; + print "OS is on the node, that you've run nodeset for these nodes, and that all of the nodes\n"; + print "are using the same osimage.\n"; + } + exit $exitcode; +}; + +# Process the cmd line args +Getopt::Long::Configure("bundling"); +#Getopt::Long::Configure("pass_through"); +Getopt::Long::Configure("no_pass_through"); +if (!GetOptions('h|?|help' => \$HELP, 'v|verbose' => \$VERBOSE)) { $usage->(1); } + +if ($HELP) { $usage->(0); } +if (scalar(@ARGV) != 1) { $usage->(1); } +my $noderange = $ARGV[0]; + +my %bootparms = getBootParms($noderange); + +copyFilesToNodes($noderange, \%bootparms); + +updateGrubOnNodes($noderange, \%bootparms); + +exit(0); + + +# Query the db for the kernel, initrd, and kcmdline attributes of the 1st node in the noderange +sub getBootParms { + my $nr = shift @_; + my %bootparms; + my @output = runcmd("nodels $nr bootparams.kernel bootparams.initrd bootparams.kcmdline"); + + # the attributes can be displayed in a different order than requested, so need to grep for them + my @gresults; + foreach my $a (qw(kernel initrd kcmdline)) { + my $attr = "bootparams.$a"; + @gresults = grep(/^\S+:\s+$attr:/, @output); + if (!scalar(@gresults)) { die "Error: attribute $attr not defined for the noderange. Did you run 'nodeset osimage=' ?\n"; } + # for now just pick the 1st one. They should all be the same, except for the node name kcmdline + chomp($gresults[0]); + $gresults[0] =~ s/^\S+:\s+$attr:\s*//; + $bootparms{$a} = $gresults[0]; + if ($a eq 'kcmdline') { $bootparms{$a} =~ s|/install/autoinst/\S+|/install/autoinst/|; } + } + + # get the mgmt node cluster-facing ip addr + @output = runcmd('lsdef -t site -i master -c'); + my ($junk, $ip) = split(/=/, $output[0]); + $bootparms{mnip} = $ip; + + verbose(Dumper(\%bootparms)); + return %bootparms; +} + + +# Copy the kernel and initrd to the nodes +# Args: noderange, reference to the bootparms hash +sub copyFilesToNodes { + my $nr = shift @_; + my $bootparms = shift @_; + foreach my $a (qw(kernel initrd)) { + my $file = $bootparms->{$a}; + my $localfile = "/tftpboot/$file"; + # for the remote file name, use the last 2 parts of the path, separated by "-" + my $remotefile = $file; + $remotefile =~ s|^.*/([^/]+)/([^/]+)$|$1-$2|; + $remotefile = "/boot/$remotefile"; + print "Copying $localfile to $nr:$remotefile\n"; + runcmd("xdcp $nr -p $localfile $remotefile"); + } +} + + +# Run the modifygrub script on the nodes to update the grub config file +# Args: noderange, reference to the bootparms hash +sub updateGrubOnNodes { + my $nr = shift @_; + my $bootparms = shift @_; + my $vtxt = ($VERBOSE ? '-v' : ''); + my @output = runcmd('which modifygrub'); + my $modifygrub = $output[0]; + chomp($modifygrub); + my $cmd = "xdsh $nr -e $modifygrub $vtxt " . $bootparms->{kernel} . ' ' . $bootparms->{initrd} . ' '; + # we need to quote the kernel parms, both here when passing it to xdsh, and on the node + # when xdsh is passing it to modifygrub. The way to get single quotes inside single quotes + # is to quote each of the outer single quotes with double quotes. + $cmd .= q("'"') . $bootparms->{kcmdline} . q('"'"); + $cmd .= ' ' . $bootparms->{mnip}; + print "Running modifygrub on $nr to update the grub configuration.\n"; + runcmd($cmd); +} + + +# Pring msg only if -v was specified +sub verbose { if ($VERBOSE) { print shift, "\n"; } } + + + +# Run a command. If called in the context of return an array, it will capture the output +# of the cmd and return it. Otherwise, it will display the output to stdout. +# If the cmd has a non-zero rc, this function will die with a msg. +sub runcmd +{ + my ($cmd) = @_; + my $rc; + + $cmd .= ' 2>&1' ; + verbose($cmd); + + my @output; + if (wantarray) { + @output = `$cmd`; + $rc = $?; + } + else { + system($cmd); + $rc = $?; + } + + if ($rc) { + $rc = $rc >> 8; + if ($rc > 0) { die "Error: rc $rc return from cmd: $cmd\n"; } + else { die "Error: system error returned from cmd: $cmd\n"; } + } + elsif (wantarray) { return @output; } +}