Skip to content

Instantly share code, notes, and snippets.

@aleks-mariusz
Last active August 29, 2015 14:00
Show Gist options
  • Save aleks-mariusz/953f24d1593fed54cea9 to your computer and use it in GitHub Desktop.
Save aleks-mariusz/953f24d1593fed54cea9 to your computer and use it in GitHub Desktop.
Jul 28 2005 - It's a bit fun when one can use ordinary systems such as a laptop for an import task one has to do in the lab commonly. Case in point, I was able to use my powerbook at work and at the same time needed to install a decent amount of solaris systems. So i wrote a script to turn it into and manage it as a jumpstart server for netbooti…
#!/usr/bin/perl
$| = 1;
#
# jumpstartManager.pl
#
# This script (along with the support files) will turn any powerbook/ibook
# running Tiger into a portable jumpstart server.
#
# v1.0 - July 24th, 2005 -
# written by: Alex Koralewski (akoralewski@doitt.nyc.gov)
#
# Note: The script was loosely based on the fine instructions written by
# Chris Marget # available at http://logsoft.com/chris/osx_jumpstart/
#
use Cwd;
use File::Copy;
use Data::Dumper;
#use Term::ReadKey;
use strict; no strict "refs";
require 5.006;
our $jsSystem; # constants and state of system-wide jumpstart facility
our $jsMenu; # menu content, structure, and flow control
our $jsCmdLocation; # commands used to set up jumpstart
our $jsOS; # operating systems used for jumpstart
our $jsFlash; # flash archives available for jumpstart
our $jsClient; # jumpstart client configurations
our $jsClusterOptions; # system clusters available for install
&scriptInit(); # read in and set variables
&sanityCheck(); # ensure certain pre-requisuites are met
&jsSystemStatusUpdate();# populate info about the system-wide jumpstart facility
&jsOSRescan(); # populate info about available operating systems
&jsClientReload(); # populate info about configured jumpstart clients
&menuDraw(["main","q"]); # start the ball rolling
############################### GENERAL FUNCTIONS ##############################################
sub scriptInit {
require 'jsManagerConfig.pl'; # read in config variables
srand(time()^($$+($$<<15))); # seed RND based on current time and pid
my $i = 0;
$jsSystem->{'clearScreen'} = `clear`; # better than forking clear each time we need it
# $jsSystem->{'windowWidth'} = (&GetTerminalSize())[0];
}
sub sanityCheck {
print "Now performing sanity checks.. please wait.. ";
# check the euid of the person running the script and make a decision if it's 0 (superuser)
if ($< != 0) { &menuFormat("Sorry! This software needs to be run as ROOT!"); exit; }
# check what type of system we're running on
open UNAME, $jsCmdLocation->{'uname'}." -mr|";
my @unameOutput = <UNAME>;
close UNAME;
my ($rev,$mach) = split (/\s+/, $unameOutput[0], 2);
if ($mach !~ /Macintosh/ || $rev !~ /^8\./) {
print ( "\nWARNING: This software is intended for Mac OS X Tiger\n",
"It may not function properly on this non-Tiger system!\n\n",
"Press ENTER to continue.. "
);
my $wait = <STDIN>; # a rudimentary way to make the user confirm his blasphemy
}
# will need the location of the script to know where the other goodies like profiles are located
$jsSystem->{'scriptHome'} = ($0 =~ /^(.+)\/[^\/]+$/)[0];
$jsSystem->{'scriptHome'} = &getcwd() if $jsSystem->{'scriptHome'} =~ /^\.$/;
if ($jsSystem->{'scriptHome'} !~ /^\//) {
print <<_EOS;
ERROR: Improper invocation of script..
Please either:
1.) run it with the full path to the script, or
2.) use only the ./ notation when running it
This will ensure that the script can figure out where it's running
from (in order to properly handle NFS exportation of config files).
_EOS
exit;
}
# certain directory structure expected
die "directory 'profiles' not found in script's home!\n" unless -d $jsSystem->{'scriptHome'}."/profiles";
die "directory 'sysidcfg' not found in script's home!\n" unless -d $jsSystem->{'scriptHome'}."/sysidcfg";
if (open TEST, ">".$jsSystem->{'scriptHome'}."testFile") {
unlink($jsSystem->{'scriptHome'}."testFile");
} else {
print "\nERROR: This script cannot run from a read-only media. Please copy the disk image containing\n";
print "this script to any read/write media such as your hard drive, mount it there and try again.. ";
exit;
}
close TEST;
# TODO: check if the script mount point is R/W, needed to write profiles..
# go through each command in our command hash
map { die "Cannot execute command '$_': $!\n" unless -x $jsCmdLocation->{$_} } (keys %{$jsCmdLocation});
# this script depends on a dedicated location on the system set up for jumpstarting, here we verify it exists
my $jumpstartLocationFound = 0;
open SCSELECT, $jsCmdLocation->{'scselect'}." 2>&1 |";
while (<SCSELECT>) { chomp;
my ($active,$locIdentifier,$locName) = (/^\s+(\S*)\s+([0-9A-Z-]+)\s+\(([^)]+)\)$/);
next unless $locName =~ /jump\s*start/i;
++$jumpstartLocationFound;
$jsSystem->{'jsLocationIdentifier'} = $locIdentifier;
}
if (!$jumpstartLocationFound) {
print <<_EOS;
ERROR: There was no Location on this system defined to be used for jumpstarting.
This location setting is required by this software in order to properly set up
the network interface. Please define a jumpstart location as follows:
1.) Click on the Apple icon, Choose "Locations", choose "Network Preferences.."
2.) Click on the "Network" icon.
3.) In the "Location" drop-down menu, select "New Location", name it "Jumpstart"
4.) In the "Show" drop-down menu, select "Network Port Configuration".
5.) Uncheck everything except "Built-in Ethernet".
6.) In the "Show" drop-down menu, select "Built-in Ethernet".
7.) In the "TCP/IP" tab, in the "Configure IPv4" drop-down, select "Manually".
8.) In the "IP Addresss" field, enter: 192.168.100.1
9.) In the "Subnet Mask" field, enter: 255.255.255.0
10.) In the "Router" field, and enter: 192.168.100.254
Note also you can use your own IP range other than 192.168.100.x but this script
requires a 255.255.255.0 netmask (as well as a valid router entry as well).
11.) Click the "Apply Now" button.
You can now quit System Preferences and re-run this script. The script will
detect the presence of the new location, but it cannot verify the accuracy of
the information entered when defining the location, so you are responsible for
performing this step correctly yourself.
_EOS
exit;
}
print "done, everything looks good..\n";
}
sub promptUser { my ($question,$defaultAnswer) = @_;
# print the question, and if we have a default, include that at the end of the question
print "\n$question ".( $defaultAnswer ? "[$defaultAnswer]" : "" ).": ";
my $input = <STDIN>; chomp $input;
# if the user only pressed return (nothing entered) then we'll use the default as the answer
$input = $defaultAnswer if $input eq "";
return $input;
}
sub parseTimeStamp { my $ts = shift;
# take the long string of digits and format them nicely in YYYY/MM/DD-HH:MM:SS format
return sprintf "%04d/%02d/%02d-%02d:%02d:%02d", ($ts =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
}
sub randBetween { my ($min,$max) = @_;
return $min + rand($max-$min);
}
sub niclCmd { my ($root,$command,$path,$property,$value) = @_;
open NICL, $jsCmdLocation->{'nicl'}." $root $command $path $property $value 2>/dev/null |";
my @niclOutput = map { chomp; $_ } <NICL>;
close NICL;
return \@niclOutput;
}
sub getPidOf { my $processName = shift;
my $pidFound;
open PS, $jsCmdLocation->{'ps'}." -ax |";
while (<PS>) { chomp;
$pidFound = $1 if /^\s*(\d+)\s+\S+\s+\S+\s+\S+\s$processName/;
last if $pidFound;
}
close PS;
return $pidFound;
}
sub pKill { my ($processName,$signal) = (@_);
while (my $pid = &getPidOf($processName)) {
kill(($signal ? $signal : 15),$pid);
# perl has a kill() function
# system($jsCmdLocation->{'kill'},( $signal ? "-$signal" : ""),$pid);
}
}
sub isServiceDisabled { my $serviceName = shift;
return system($jsCmdLocation->{'service'}." --test-if-configured-on $serviceName");
}
# normally return() is only defined in the CORE:: namespace, not in the main:: namespace, this is a proxy
sub return {
return;
}
sub quitProgram {
&menuFormat("Thanks for using Jumpstart Manager(tm) for Mac OS X Tiger!");
exit;
}
######################################### MENU FUNCTIONS ################################################
sub menuFormat { my @textLines = @_;
# print out the text but with some nice whitespace around each item in the text array
map { print "\n$_\n" } (@textLines);
}
sub menuDraw { my ($menuInfo) = shift;
my ($sectionName,$exitSelection) = @{$menuInfo};
# we pass a anonymous array/list, now we must break it down
my $currSelection;
do { print $jsSystem->{'clearScreen'}; # will clear the screen for us more efficiently w/o forking clear each time
# this is a ref to a code stub to introduce the menu we're about to draw
&{$jsMenu->{$sectionName}->{'intro'}}();
# draw the menu and grab the selection the user picked
$currSelection = &menuDispatch($jsMenu->{$sectionName}->{'structure'});
} while ($currSelection !~ /^$exitSelection/i);
}
sub menuDispatch { my $menuInfo = shift;
my $selection;
# create a mapping from selection to item (we're passed this backwards for ease of declaration)
my %selectionToMenuItem;
map {
$selectionToMenuItem{lc($_->{'choice'})} = $_
} (@{$menuInfo});
# draw each selection in the order it was declared
map { print "-", lc($_->{'choice'}), "-\t", $_->{'label'}, "\n"; } (@{$menuInfo});
# keep repeating this until we get a valid selection
while (!exists($selectionToMenuItem{lc($selection)})) {
$selection = &promptUser(
"Please make a ". ( $selection ne "" ? "VALID " : "" ). "selection"
);
}
# call the function of the parameter passed with any args available
&{$selectionToMenuItem{lc($selection)}->{'func'}}($selectionToMenuItem{lc($selection)}->{'arg'});
# the caller might be interested in what was chosen
return (lc($selection));
}
sub menuBuildFromArray { my ($arrayRef,$selectionFunc) = @_;
my @menu;
my @menuSelection;
my $menuCounter = 0;
foreach my $currItem (@{$arrayRef}) {
my %menuItem;
# we only care about the first key
my $key = (keys(%{$currItem}))[0];
my $value = $currItem->{$key};
$menuItem{'choice'} = $menuCounter;
$menuItem{'label'} = $value;
$menuItem{'func'} = $selectionFunc;
$menuItem{'arg'} = $key;
push @menu, \%menuItem;
++$menuCounter;
}
return \@menu;
}
######################################## JUMPSTART SYSTEM #############################################
sub jsSystemPrint {
my ($colorBad,$colorGood,$colorNormal) = ( "\033[1;31m", "\033[1;32m", "\033[0m" );
printf ("\n%-4s%-35s%-16s%s\n", "Num", "Facility", "State", "Notes");
my $i = 1;
foreach my $currFacility (map { lc($_) } @{$jsSystem->{'facilityNames'}}) {
printf ("%-4s%-35s%-16s%s\n",
$i,
$jsSystem->{'facility'}->{$currFacility}->{'name'},
(
$jsSystem->{'facility'}->{$currFacility}->{'ready'} ? $colorGood : $colorBad
).$jsSystem->{'facility'}->{$currFacility}->{'states'}->[$jsSystem->{'facility'}->{$currFacility}->{'ready'}].
$colorNormal.
" "x(16-length( # don't ask me why but colors screw up text widths within printf, so we pad with spaces
$jsSystem->{'facility'}->{$currFacility}->{'states'}->[
$jsSystem->{'facility'}->{$currFacility}->{'ready'}
]
)
),
$jsSystem->{'facility'}->{$currFacility}->{'info'}
);
++$i;
}
print "\n";
}
sub jsSystemEnable {
print <<_EOS;
NOTE: Enabling jumpstart will make system-level changes as the super-user on
the system. These changes include network settings, which may cause you to be
disconnected from any networks you may be currently connected to. It is advised
that you save your work on any network shares before proceeding..
_EOS
print "Press ENTER to continue.. ";
my $wait = <STDIN>;
print "\n\nAttempting to enable Jumpstart facilities.. \n";
foreach my $currFacility (@{$jsSystem->{'facilityNames'}}) {
printf "\t- %-36s ", $jsSystem->{'facility'}->{lc($currFacility)}->{'name'}.".. ";
if (!$jsSystem->{'facility'}->{lc($currFacility)}->{'ready'}) {
&{'jsSystemEnable'.$currFacility}();
print "done!\n";
} else {
print "skipping, already up!\n";
}
}
print "\n";
print "Press ENTER to continue.. ";
$wait = <STDIN>;
&jsSystemStatusUpdate();
}
sub jsSystemDisable {
print "\n\nAttempting to disable/revert Jumpstart facilities.. \n";
foreach my $currFacility (reverse @{$jsSystem->{'facilityNames'}}) {
printf "\t- %-36s ", $jsSystem->{'facility'}->{lc($currFacility)}->{'name'}.".. ";
if ($jsSystem->{'facility'}->{lc($currFacility)}->{'ready'}) {
&{'jsSystemDisable'.$currFacility}();
print "done!\n";
} else {
print "skipping, already down!\n";
}
}
print "\n";
print "Press ENTER to continue.. ";
my $wait = <STDIN>;
&jsSystemStatusUpdate();
}
sub jsSystemEnableLocation {
open SCSELECT, $jsCmdLocation->{'scselect'}." 2>&1 |";
while (<SCSELECT>) { chomp;
my ($active,$locIdentifier,$locName) = (/^\s+(\S*)\s+([0-9A-Z-]+)\s+\(([^)]+)\)$/);
next unless $active;
$jsSystem->{'previousLocationIdentifier'} = $locIdentifier;
}
close SCSELECT;
open SCSELECT, $jsCmdLocation->{'scselect'}." ".$jsSystem->{'jsLocationIdentifier'}."|";
my @x = <SCSELECT>;
close SCSELECT;
sleep 2;
}
sub jsSystemDisableLocation {
if (!$jsSystem->{'previousLocationIdentifier'}) {
print "no previous settings to restore.. ";
return;
}
open SCSELECT, $jsCmdLocation->{'scselect'}." ".$jsSystem->{'previousLocationIdentifier'}."|";
my @x = <SCSELECT>;
close SCSELECT;
}
sub jsSystemEnableMedia {
print "waiting for cable to be connected.. ";
do { &jsSystemUpdateMedia();
sleep 1;
} while (!$jsSystem->{'facility'}->{'media'}->{'ready'});
return;
}
sub jsSystemDisableMedia {
# how exactly do you want me to unplug the ethernet cord from your system within this script?
return;
}
sub jsSystemEnablePortmap {
system($jsCmdLocation->{'service'},'com.apple.portmap','start');
}
sub jsSystemDisablePortmap {
system($jsCmdLocation->{'service'},'com.apple.portmap','stop');
}
sub jsSystemEnableBlackhole {
&niclCmd('/','create','/machines/blackhole','ip_address','0.0.0.0');
}
sub jsSystemDisableBlackhole {
&niclCmd('/','delete','/machines/blackhole');
}
sub jsSystemEnableBootparamd {
system($jsCmdLocation->{'bootparamd'});
}
sub jsSystemDisableBootparamd {
&pKill($jsCmdLocation->{'bootparamd'});
}
sub jsSystemEnableTFTPd {
mkdir("/private/tftpboot") unless -d "/private/tftpboot";
symlink("/private/tftpboot","/tftpboot") unless -l "/tftpboot";
system($jsCmdLocation->{'service'},'tftp','start');
}
sub jsSystemDisableTFTPd {
system($jsCmdLocation->{'service'},'tftp','stop');
unlink("/tftpboot");
unlink("/private/tftpboot");
}
sub jsSystemEnableRarpd {
system($jsCmdLocation->{'rarpd'},$jsSystem->{'ethernetInterface'});
# i just can't seem to use system() properly with this guy..
# open RARPD, $jsCmdLocation->{'rarpd'}." ".$jsSystem->{'ethernetInterface'}."|";
# my @x = <RARPD>;
# close RARPD;
}
sub jsSystemDisableRarpd {
&pKill($jsCmdLocation->{'rarpd'});
}
sub jsSystemEnableICMP {
# sure i could have used system() but then the stderr would screw up my progress indicator
open SYSCTL, $jsCmdLocation->{'sysctl'}." -w net.inet.icmp.maskrepl=1 2>/dev/null |";
my @x = <SYSCTL>;
close SYSCTL;
}
sub jsSystemDisableICMP {
# sure i could have used system() but then the stderr would screw up my progress indicator
open SYSCTL, $jsCmdLocation->{'sysctl'}." -w net.inet.icmp.maskrepl=0 2>/dev/null |";
my @x = <SYSCTL>;
close SYSCTL;
}
sub jsSystemEnableNFSd {
open NFSD, $jsCmdLocation->{'nfsd'}." -t -u -n 6|";
my @x = <NFSD>;
close NFSD;
}
sub jsSystemDisableNFSd {
&pKill($jsCmdLocation->{'nfsd'});
&pKill('nfsd-master',"9"); # these guys just don't want to die, gotta -9 them
&pKill('nfsd-server',"9"); # these guys just don't want to die, gotta -9 them
}
sub jsSystemEnableMountd {
system($jsCmdLocation->{'mountd'},"-r");
}
sub jsSystemDisableMountd {
# make sure we try to kill both types of mountd invocations
&pKill($jsCmdLocation->{'mountd'}); &pKill('mountd');
}
sub jsSystemStatusUpdate {
# let's assume all facilities are unavailable initially, will be verified later
map { $jsSystem->{'facility'}->{lc($_)}->{'ready'} = 0;
delete($jsSystem->{'facility'}->{lc($_)}->{'info'})
} @{$jsSystem->{'facilityNames'}};
foreach my $currFacility (@{$jsSystem->{'facilityNames'}}) {
&{'jsSystemUpdate'.$currFacility}();
}
}
sub jsSystemUpdateLocation {
#-check system location (via scselect)
open SCSELECT, $jsCmdLocation->{'scselect'}." 2>&1 |";
while (<SCSELECT>) { chomp;
my ($active,$locIdentifier,$locName) = (/^\s+(\S*)\s+([0-9A-Z-]+)\s+\(([^)]+)\)$/);
next unless $active;
$jsSystem->{'facility'}->{'location'}->{'info'} = "Set for $locName";
}
close SCSELECT;
++$jsSystem->{'facility'}->{'location'}->{'ready'} if $jsSystem->{'facility'}->{'location'}->{'info'} =~ /jump\s*start/i;
}
sub jsSystemUpdateMedia {
open IFCONFIG, $jsCmdLocation->{'ifconfig'}." ".$jsSystem->{'ethernetInterface'}."|";
while (<IFCONFIG>) { chomp;
($jsSystem->{'ipAddress'},$jsSystem->{'networkPrefix'}) = ($1,$2) if /inet ((\d+\.\d+\.\d+)\.\d+) netmask/;
$jsSystem->{'facility'}->{'media'}->{'info'} = "On $1" if /media: \S+ \(([^)]+)\) status:/;
++$jsSystem->{'facility'}->{'media'}->{'ready'} if /status: active/;
}
close IFCONFIG;
$jsSystem->{'facility'}->{'media'}->{'info'} = "Unplugged" unless $jsSystem->{'facility'}->{'media'}->{'ready'};
}
sub jsSystemUpdatePortmap {
$jsSystem->{'facility'}->{'portmap'}->{'info'} = "Will be started by launchd" unless &isServiceDisabled("com.apple.portmap");
$jsSystem->{'facility'}->{'portmap'}->{'info'} = "Running with Process ID $1" if &getPidOf($jsCmdLocation->{'portmap'}) =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'portmap'}->{'ready'} if $jsSystem->{'facility'}->{'portmap'}->{'info'};
}
sub jsSystemUpdateBlackhole {
foreach my $niclLine (@{&niclCmd("/","cat","/machines/blackhole")}) {
$jsSystem->{'facility'}->{'blackhole'}->{'info'} .= (split /\s+/, $niclLine)[1]. " ";
}
return unless $jsSystem->{'facility'}->{'blackhole'}->{'info'};
$jsSystem->{'facility'}->{'blackhole'}->{'info'} = 'Aliased to "blackhole"';
++$jsSystem->{'facility'}->{'blackhole'}->{'ready'};
}
sub jsSystemUpdateBootparamd {
$jsSystem->{'facility'}->{'bootparamd'}->{'info'} = "Running with Process ID $1" if &getPidOf($jsCmdLocation->{'bootparamd'}) =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'bootparamd'}->{'ready'} if $jsSystem->{'facility'}->{'bootparamd'}->{'info'};
}
sub jsSystemUpdateTFTPd {
# when we disable this, we'll always see this message needlessly
# $jsSystem->{'facility'}->{'tftpd'}->{'info'} = "Missing /tftpboot symlink!" unless -l "/tftpboot";
$jsSystem->{'facility'}->{'tftpd'}->{'info'} = "Missing /private/tftpboot directory!" unless -d "/private/tftpboot";
return $jsSystem->{'facility'}->{'tftpd'}->{'ready'} = 0 if $jsSystem->{'facility'}->{'tftpd'}->{'info'};
my $isDisabledTFTP = &isServiceDisabled("tftp");
$jsSystem->{'facility'}->{'tftpd'}->{'info'} = "Will be started by xinetd" unless $isDisabledTFTP;
# the chances of actually catching the tftpd process running are slim to none.. this guy is only ran when needed from xinetd for brief times it's used
# $jsSystem->{'facility'}->{'tftpd'}->{'info'} = "Running with Process ID $1" if &getPidOf($jsCmdLocation->{'tftpd'}) =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'tftpd'}->{'ready'} if $jsSystem->{'facility'}->{'tftpd'}->{'info'};
}
sub jsSystemUpdateRarpd {
$jsSystem->{'facility'}->{'rarpd'}->{'info'} = "Running with Process ID $1" if &getPidOf($jsCmdLocation->{'rarpd'}) =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'rarpd'}->{'ready'} if $jsSystem->{'facility'}->{'rarpd'}->{'info'};
}
sub jsSystemUpdateICMP {
open SYSCTL, $jsCmdLocation->{'sysctl'}." net.inet.icmp.maskrepl |";
while (<SYSCTL>) { chomp;
$jsSystem->{'facility'}->{'icmp'}->{'ready'} = $1 if /^net\.inet\.icmp\.maskrepl: (\d)$/;
}
close SYSCTL;
}
sub jsSystemUpdateNFSd {
$jsSystem->{'facility'}->{'nfsd'}->{'info'} = "Running with Process ID $1" if &getPidOf('nfsd') =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'nfsd'}->{'ready'} if $jsSystem->{'facility'}->{'nfsd'}->{'info'};
}
sub jsSystemUpdateMountd {
$jsSystem->{'facility'}->{'mountd'}->{'info'} = "Running with Process ID $1" if &getPidOf('mountd') =~ /^(\d+)$/;
$jsSystem->{'facility'}->{'mountd'}->{'info'} = "Running with Process ID $1" if &getPidOf($jsCmdLocation->{'mountd'}) =~ /^(\d+)$/;
++$jsSystem->{'facility'}->{'mountd'}->{'ready'} if $jsSystem->{'facility'}->{'mountd'}->{'info'};
if ($jsSystem->{'facility'}->{'mountd'}->{'ready'}) {
kill 1, $1 if $jsSystem->{'facility'}->{'mountd'}->{'info'} =~ /Process ID (\d+)/;
}
}
sub jsSystemUpdateRulesOK {
my $rulesFile = $jsSystem->{'scriptHome'}."/rules";
&copy($jsSystem->{'scriptHome'}."/rules",$jsSystem->{'scriptHome'}."/rules.ok");
my $checksum = `$jsCmdLocation->{'cksum'} -o 2 $rulesFile`;
open RULESOK, ">>".$jsSystem->{'scriptHome'}."/rules.ok" || die "Cannot append to rules.ok: $!\n";
print RULESOK "# version=2 checksum=$1\n" if $checksum =~ /^(\d+)\s/;
close RULESOK;
}
################################### JUMPSTART O/S SUBSYSTEM #######################################
sub jsOSPrint {
# check if any o/s are available at all
if (scalar(keys %{$jsOS->{'byOS'}}) == 0) {
&menuFormat("No Jumpstartable Operating Systems Detected. Please mount images first!\n");
return;
}
# print out the header
printf "\n%12s%3s%6s%6s %s\n", "OS ", "Ver", "Rev", "Arch", "Location";
# go through each level, collecting information
foreach my $currOS (keys %{$jsOS->{'byOS'}}) {
foreach my $currVer (keys %{$jsOS->{'byOS'}->{$currOS}}) {
foreach my $currRev (keys %{$jsOS->{'byOS'}->{$currOS}->{$currVer}}) {
foreach my $currArch (keys %{$jsOS->{'byOS'}->{$currOS}->{$currVer}->{$currRev}}) {
# then print out what we have so far
printf ("%12s%3d%6s%6s located in %s\n",
$currOS,
$currVer,
$currRev,
$currArch,
$jsOS->{'byOS'}->{$currOS}->{$currVer}->{$currRev}->{$currArch}
);
}
}
}
}
print "\n";
}
sub jsOSBuildMenu {
my @osMenu;
my $osCounter = 0;
foreach my $currOS (keys %{$jsOS->{'byOS'}}) {
foreach my $currVer (keys %{$jsOS->{'byOS'}->{$currOS}}) {
foreach my $currRev (keys %{$jsOS->{'byOS'}->{$currOS}->{$currVer}}) {
foreach my $currArch (keys %{$jsOS->{'byOS'}->{$currOS}->{$currVer}->{$currRev}}) {
my %menuItem; ++$osCounter;
$menuItem{'choice'} = $osCounter;
$menuItem{'label'} = sprintf("%s%3d%6s%6s", $currOS, $currVer, $currRev, $currArch);
$menuItem{'func'} = 'jsClientSetOS';
$menuItem{'arg'} = $jsOS->{'byOS'}->{$currOS}->{$currVer}->{$currRev}->{$currArch};
push @osMenu, \%menuItem;
}
}
}
}
return \@osMenu;
}
sub jsOSRescan {
# delete any o/s we previously seen
map { delete $jsOS->{$_} } (keys %{$jsOS});
# search any mounted volumes for .inf files
my @volumeINFList = glob("/Volumes/*/.volume.inf");
# go through each inf file found
foreach my $currVolumeINF (@volumeINFList) {
# save contents of current inf
open VOLUMEINF, $currVolumeINF || die "Cannot open volume.info located at $currVolumeINF : $!\n";
my $volumeINFContent = <VOLUMEINF>;
close VOLUMEINF;
# skip if we don't have a properly formatted volume.info
next unless $volumeINFContent =~ /^VI"([^"]+)"$/;
my ($osName,$osVer,$osRev,$osArch) = split /_/, $1;
# save the info in a nested tree structure ordered by o/s, including the volume's root directory
$jsOS->{'byOS'}->{$osName}->{$osVer}->{join("/",($osRev =~ /^(\d+)(\d{2})$/))}->{$osArch} = ($currVolumeINF =~ /^(.+)\/[^\/]+$/)[0];
# save the info using the volume as the categorization key
$jsOS->{'byVol'}->{($currVolumeINF =~ /^(.+)\/[^\/]+$/)[0]} = {
'osRev' => join("/",($osRev =~ /^(\d+)(\d{2})$/)),
'osVer' => $osVer,
'osName' => $osName,
'osArch' => $osArch,
};
# save the info using the versions as the categorization key, in an anonymous array, in case there's multiple version revisions
push (@{$jsOS->{'byVer'}->{$osVer}},
{ 'osName' => $osName,
'osArch' => $osArch,
'volume' => ($currVolumeINF =~ /^(.+)\/[^\/]+$/)[0],
'osRev' => join("/",($osRev =~ /^(\d+)(\d{2})$/)),
}
);
}
}
################################# JUMPSTART FLASH SUBSYSTEM #######################################
sub jsFlashPrint {
# check if we even have any flash archives
if (&jsFlashNumArchives() == 0) {
# check if we are running this for the first time, in which case we don't know where to look for flash archives
if ($jsFlash->{'repository'} eq "") {
&menuFormat("No Flash Archives repository specified yet. Please specify a directory first!\n");
} else {
&menuFormat("No Flash Archives located in ".$jsFlash->{'repository'}.".\n\nPlease try another directory!\n");
}
return;
}
# draw header
&menuFormat(
"Found ".&jsFlashNumArchives()." Flash Archive".(
&jsFlashNumArchives() > 1 ? "s" : ""
)." in ".$jsFlash->{'repository'}.":"
);
printf "\n%-4s%-14s%-15s%-21s%-17s%-6s%-20s%s\n", "Num", "Filename", "Flashname", "Source", "Platform", "Size", "Date", "OS Detail";
# draw each flash archive's information
my $flashCounter = 0;
foreach my $currFA (@{$jsFlash->{'archive'}}) {
++$flashCounter;
printf ("%-4d%-14s%-15s%-21s%-17s%-6s%-20s%s\n",
$flashCounter,
substr(($currFA->{'filename'} =~ /\/([^\/.]+)(\.[^\/.]+)?$/)[0],0,13),
substr($currFA->{'content_name'},0,20),
$currFA->{'creation_node'},
($currFA->{'creation_platform'} =~ /([^,]+)$/)[0],
&jsFlashSizeFormat($currFA->{'files_archived_size'}),
&parseTimeStamp($currFA->{'creation_date'}),
$currFA->{'creation_os_name'}." ".$currFA->{'creation_release'}." ".$currFA->{'creation_processor'}." ".$currFA->{'creation_os_version'}
);
}
print "\n";
}
sub jsFlashNumArchives {
return scalar(@{$jsFlash->{'archive'}});
}
sub jsFlashSizeFormat { my $size = shift;
my $i = 0; my @sizeQuantifier = qw/Bytes KB MB GB/;
while ($size > 1024) { ++$i; $size /= 1024; }
return sprintf("%.1f%s",$size,$sizeQuantifier[$i]);
}
sub jsFlashBuildMenu {
my @flashMenu;
my $flashCounter = 0;
foreach my $currFA (@{$jsFlash->{'archive'}}) {
my %menuItem; ++$flashCounter;
$menuItem{'choice'} = $flashCounter;
$menuItem{'label'} = $currFA->{'content_name'};
$menuItem{'func'} = 'jsClientSetFlash';
$menuItem{'arg'} = $flashCounter-1;
push @flashMenu, \%menuItem;
}
return \@flashMenu;
}
sub jsFlashQuery {
# see what we currently have set, if anything
my $flashLocation = $jsFlash->{'repository'};
# initially we don't have a concept of valid, so we'll set this to -1
my $validDir = -1;
do { my $newFlashLocation = &promptUser(
"Please specify a ".(
$validDir == 0 ? "VALID " : ""
)."directory where you'd like to look for Flash Archives in\nDirectory [".$jsFlash->{'repository'}."]"
);
$validDir = 0;
$flashLocation = $newFlashLocation unless $newFlashLocation eq "";
$validDir = 1 if -d $flashLocation;
} while (!$validDir);
# save this location
$jsFlash->{'repository'} = $flashLocation;
# and scan any files recursively within the directory for flash archives
&jsFlashScanDir($jsFlash->{'repository'});
}
sub jsFlashScanDir { my $dirToScan = shift;
my (@dirsToProcess);
# open the directory listing for reading
opendir(DIR,$dirToScan) || return "Cannot look at directory $dirToScan : $!\n";
while (my $dirEntry = readdir(DIR)) {
# treat directories specially
if (-d $dirToScan."/".$dirEntry) {
# we won't bother with parent and self directories
next if $dirEntry =~ /^\.\.?$/;
# save the directory to be recusively scanned later
push @dirsToProcess, $dirToScan."/".$dirEntry;
} else { # otherwise we're dealing with a file
# grab info from the file if it's a true flash archive
&jsFlashArchiveParse($dirToScan."/".$dirEntry) if &jsFlashIsValid($dirToScan."/".$dirEntry);
# print "ENTRY: ".$dirToScan."/".$dirEntry."\n";
}
}
close DIR;
# go through each directory we saved
foreach my $newDir (@dirsToProcess) { &jsFlashScanDir($newDir); }
}
sub jsFlashIsValid { my $filename = shift;
my $flarHeader = "FlAsH-aRcHiVe"; # known first line
my $flarFirstLine; # actual line we'll read from the file
# open the file and read the first line
open FLASH, $filename || die "Cannot validate flash archive $filename : $!\n";
$flarFirstLine = <FLASH>;
close FLASH;
return scalar($flarFirstLine =~ /^$flarHeader/);
}
sub jsFlashArchiveParse { my $filename = shift;
my %flashInfo; # where we'll hold the temporary data read from the current flash archive
$flashInfo{'filename'} = $filename;
# open the file
open FLASH, $filename || die "Cannot parse flash archive $filename : $!\n";
while (<FLASH>) { chomp;
my ($key,$value) = split /=/, $_, 2;
$flashInfo{$key} = $value;
# and read it until we get to the size parameter
last if /^files_archived_size/;
}
close FLASH;
# save the info we have so far, in an array ref (so that order is preserved)
push (@{$jsFlash->{'archive'}}, \%flashInfo);
}
################################ JUMPSTART CLIENT FUNCTIONS #######################################
sub jsClientReload {
delete($jsClient->{'client'});
open RULES, $jsSystem->{'scriptHome'}."/rules" || die "Cannot read rules: $!\n";
while (<RULES>) { chomp;
next unless /^hostname\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/;
$jsClient->{'client'}->{$1}->{'preScript'} = $2;
$jsClient->{'client'}->{$1}->{'profile'} = $3;
$jsClient->{'client'}->{$1}->{'postScript'} = $4;
}
close RULES;
foreach my $currClient (keys %{$jsClient->{'client'}}) {
foreach my $niclOutput (@{&niclCmd("/","cat","/machines/$currClient")}) {
my ($key,$value) = split /:\s/, $niclOutput;
$jsClient->{'client'}->{$currClient}->{$key} = $value;
}
my $flashArchiveUsed;
next unless -f $jsSystem->{'scriptHome'}."/".$jsClient->{'client'}->{$currClient}->{'profile'};
open PROFILE, $jsSystem->{'scriptHome'}."/".$jsClient->{'client'}->{$currClient}->{'profile'};
while (<PROFILE>) { chomp;
$jsClient->{'client'}->{$currClient}->{'flashFile'} = $1 if /^archive_location\s+nfs\s+[^:]+:(\S+)/;
}
close PROFILE;
}
}
sub jsClientList {
if (scalar(keys %{$jsClient->{'client'}}) < 1) {
&menuFormat("No clients configured yet!\n");
return;
}
printf "\n%-20s%-17s%-17s%-5s%-10s%-24s\n", "Client Name", "Ethernet", "IP Address", "Ver", "Type", "Profile";
foreach my $currClient (sort keys %{$jsClient->{'client'}}) {
printf("%-20s%-17s%-17s%-5s%-10s%-24s\n",
$currClient,
$jsClient->{'client'}->{$currClient}->{'en_address'},
$jsClient->{'client'}->{$currClient}->{'ip_address'},
( $jsOS->{'byVol'}->{($jsClient->{'client'}->{$currClient}->{'bootparams'} =~ /install=[^:]+:(\S+)\s/)[0]}->{'osVer'} ? $jsOS->{'byVol'}->{($jsClient->{'client'}->{$currClient}->{'bootparams'} =~ /install=[^:]+:(\S+)\s/)[0]}->{'osVer'} : "N/A" ),
( exists($jsClient->{'client'}->{$currClient}->{'flashFile'}) ? "flash" : "initial" ),
$jsClient->{'client'}->{$currClient}->{'profile'}
);
}
print "\n";
}
sub jsClientAdd {
if (!$jsSystem->{'facility'}->{'location'}->{'ready'}) {
print "\nERROR: Cannot add a new client unless the proper network settings are in place first.\n";
print "Please go to the availability section in the main menu and enable jumpstart then try again\n";
print "\n";
print "Press ENTER to abort.. ";
my $wait = <STDIN>;
return;
}
# find out the basic information
my ($clientName,$clientEther,$clientType) = &jsClientInfoQuery();
return if $clientName eq ""; # no client name specified, something must have happened, bailing
# save the information temporarily
$jsClient->{'tempClient'} = {
'name' => $clientName,
'type' => $clientType,
'en_address' => $clientEther
};
my $configStatus; # allows us to prematurely bail out if needed
$configStatus = &jsClientConfigQuery(); # find out more detailed information on how to configure client
return if $configStatus == 0;
&jsClientProfileCommit();
&jsClientSetupRules();
&niclCmd("/","create","/machines/".$jsClient->{'tempClient'}->{'name'},"ip_address",$jsClient->{'tempClient'}->{'ip_address'});
&niclCmd("/","create","/machines/".$jsClient->{'tempClient'}->{'name'},"en_address",$jsClient->{'tempClient'}->{'en_address'});
&jsClientSetupTFTP();
&jsClientAddBootparams();
# read the boot params back in
# foreach my $niclOutput (@{&niclCmd("/","cat","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams")}) {
# my ($key,$value) = split /:\s/, $niclOutput;
# $jsClient->{'tempClient'}->{$jsClient->{'tempClient'}->{'name'}}->{$key} = $value;
# }
&jsClientEnsureExports();
&jsClientReload();
# $jsClient->{'client'}->{$jsClient->{'tempClient'}->{'name'}} = $jsClient->{'tempClient'};
delete($jsClient->{'tempClient'});
}
sub jsClientSetupTFTP {
my $hexFilename = sprintf "%02X%02X%02X%02X", split /\./, $jsClient->{'tempClient'}->{'ip_address'};
my $bootFilename = "inetboot.".uc($jsClient->{'tempClient'}->{'platform'}).".Solaris_".$jsClient->{'tempClient'}->{'ver'}."-1";
if (! -f "/private/tftpboot/$bootFilename") {
&copy(
$jsOS->{'byVer'}->{$jsClient->{'tempClient'}->{'ver'}}->[0]->{'volume'}."/Solaris_".$jsClient->{'tempClient'}->{'ver'}."/Tools/Boot/usr/platform/".$jsClient->{'tempClient'}->{'platform'}."/lib/fs/nfs/inetboot",
"/private/tftpboot/$bootFilename"
);
chmod 0755, "/private/tftpboot/$bootFilename";
}
# considering the tftp daemon may be chrooted, the symlink probably needs to be created with a relative path to the same directory
chdir("/private/tftpboot");
symlink($bootFilename, $hexFilename);
symlink($bootFilename, $hexFilename.".".uc($jsClient->{'tempClient'}->{'platform'}));
}
sub jsClientAddBootparams {
&niclCmd("/","create","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams","boottype=:in");
&niclCmd("/","append","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams","rootopts=:rsize=32768");
&niclCmd(
"/","append","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams",
"root=".$jsSystem->{'ipAddress'}.":".$jsOS->{'byVer'}->{$jsClient->{'tempClient'}->{'ver'}}->[0]->{'volume'}."/Solaris_".$jsClient->{'tempClient'}->{'ver'}."/Tools/Boot"
);
&niclCmd(
"/","append","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams",
"install=".$jsSystem->{'ipAddress'}.":".$jsOS->{'byVer'}->{$jsClient->{'tempClient'}->{'ver'}}->[0]->{'volume'}
);
&niclCmd(
"/","append","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams",
"sysid_config=".$jsSystem->{'ipAddress'}.":".$jsSystem->{'scriptHome'}."/sysidcfg/".$jsClient->{'tempClient'}->{'ver'}
);
&niclCmd(
"/","append","/machines/".$jsClient->{'tempClient'}->{'name'},"bootparams",
"install_config=".$jsSystem->{'ipAddress'}.":".$jsSystem->{'scriptHome'}
);
}
sub jsClientEnsureExports {
my @exportList = (
$jsSystem->{'scriptHome'},
$jsOS->{'byVer'}->{$jsClient->{'tempClient'}->{'ver'}}->[0]->{'volume'},
( $jsFlash->{'archive'}->[$jsClient->{'tempClient'}->{'flashIndex'}]->{'filename'} =~ /^(.*)\/[^\/]+$/)[0]
);
foreach my $currExport (@exportList) {
next unless $currExport;
$currExport =~ s#/#\\\\/#g;
&niclCmd("/","create","/exports/".$currExport,"opts","maproot=root:wheel,ro,alldirs");
&niclCmd("/","append","/exports/".$currExport,"clients",$jsClient->{'tempClient'}->{'name'});
}
&jsSystemUpdateMountd();
}
sub jsClientSetupRules {
open RULES, ">>".$jsSystem->{'scriptHome'}."/rules" || die "Cannot append to rules: $!\n";
printf RULES "hostname %-24s- %-32s -\n", $jsClient->{'tempClient'}->{'name'}, $jsClient->{'tempClient'}->{'profile'};
close RULES;
# print RULESNEW
# close RULES;
&jsSystemUpdateRulesOK();
}
sub jsClientInfoQuery {
my ($clientName,$clientEther,$clientType);
# get the name of this client, making sure one doesn't already exist with this name
my $clientNameTaken = -1;
do { $clientName = &promptUser(
"Enter ".(
$clientNameTaken == 1 ? "an unused" : "a"
)." client name (ENTER to abort)"
);
# returning an empty client name is a sure sign of a problem, which will be detected by the caller
return "" if $clientName eq "";
# client name sanity checks
if ($clientName =~ /\./) {
print "\nNOTE: Only the hostname is needed, domain information truncated!\n";
$clientName =~ s/^([^.]+)\..+$/$1/;
}
if ($clientName =~ /[A-Z]/) {
print "\nNOTE: for the sake of simplicity, $clientName will be renamed all lowercase!\n";
$clientName = lc($clientName);
#$clientName =~ tr/[A-Z]/[a-z]/; # what was i thinking?
}
# now see if this name is already used or not
$clientNameTaken = ( exists($jsClient->{'client'}->{$clientName}) ? 1 : 0 );
} while ($clientNameTaken);
# get the name of the mac address, making sure it's valid
my $validEther = -1;
do { $clientEther = &promptUser(
"What is $clientName\'s ".(
$validEther == 0 ? "VALID " : ""
)."MAC/ethernet address"
);
$validEther = ( $clientEther =~ /^([0-9a-f]{1,2}:){5}([0-9a-f]{1,2})$/i ? 1 : 0 );
} while (!$validEther);
$clientEther = lc($clientEther); # make sure it's all in lower case
# find out what type of installation the person is going to perform
do { $clientType = &promptUser("What type of installation would you like to perform on $clientName, (I)nitial, (F)lash, or (A)bort?","i");
return "" if $clientType =~ /^a/i;
} while ($clientType !~ /^(f|i)/i);
return ($clientName,$clientEther,$clientType);
}
sub jsClientConfigQuery {
return unless exists($jsClient->{'tempClient'});
my $networkPrefix = $jsSystem->{'networkPrefix'};
my $validIP = -1;
do { # find a random, but unused, ip address for this client
$jsClient->{'tempClient'}->{'ip_address'} = &promptUser("If you have an IP Address you'd like to use it, enter here, or press ENTER for a random one");
if ($jsClient->{'tempClient'}->{'ip_address'} eq "") {
my $ipAddressOctet;
do { $ipAddressOctet = int(&randBetween(1,255));
} while (&jsClientUsedIP("$networkPrefix.$ipAddressOctet"));
$jsClient->{'tempClient'}->{'ip_address'} = $jsSystem->{'networkPrefix'}.".".$ipAddressOctet;
$validIP = 1;
} elsif ($jsClient->{'tempClient'}->{'ip_address'} !~ /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) {
print "\nERROR: The ip address entered, ".$jsClient->{'tempClient'}->{'ip_address'}.", is not a valid IP address";
$validIP = 0;
} else {
if ($jsClient->{'tempClient'}->{'ip_address'} !~ /^$networkPrefix/) { $validIP = 0;
print "\nERROR: The ip address entered isn't on the same network $networkPrefix/24. Please try again.. ";
} else { $validIP = 1; }
if (&jsClientUsedIP($jsClient->{'tempClient'}->{'ip_address'})) {
print "\nERROR: The ip address entered is already used. Please select an unused address.";
$validIP = 0;
}
}
} while (!$validIP);
# follow the handling based on the type of installation we're performing
if ($jsClient->{'tempClient'}->{'type'} =~ /^i/i) {
# this section is for initial install
if (scalar(keys %{$jsOS->{'byOS'}}) < 1) {
print "\nERROR: There are no operating systems available for jumpstarting.\n";
print "Please mount a volume with one first and try again.\n";
print "\nNOTE: You may need to rescan for newly mounted volumes in the availability menu.";
print "\n\nPress ENTER to return to the previous menu.. ";
my $wait = <STDIN>;
return;
}
# here's we'll create a menu based on available o/s and ask user to pick one
&menuFormat("Please select an operating system to jumpstart with:");
&menuDispatch(&jsOSBuildMenu());
# fetch what version we're using, for profile naming purposes, based on the o/s selection made
$jsClient->{'tempClient'}->{'arch'} = $jsOS->{'byVol'}->{$jsClient->{'tempClient'}->{'osVolume'}}->{'osArch'};
$jsClient->{'tempClient'}->{'ver'} = $jsOS->{'byVol'}->{$jsClient->{'tempClient'}->{'osVolume'}}->{'osVer'};
} else { # this section is for flash installs
if (&jsFlashNumArchives() < 1) {
print "\nERROR: No flash archives currently available!\n\n";
if ($jsFlash->{'repository'} eq "") {
print "This is because there has been no location specified yet!\n";
print "Please select one in the Flash section located in the Availability section.";
} else {
print "This is because there were no valid flash archives found in ".$jsFlash->{'repository'}, ".\n";
print "Please specify a different location.";
}
print "\n\nPress ENTER to return to the previous menu..";
my $wait = <STDIN>;
return "";
}
&menuFormat("Please select a flash image to use for installation:");
&menuDispatch(&jsFlashBuildMenu());
$jsClient->{'tempClient'}->{'arch'} = $jsFlash->{'archive'}->[$jsClient->{'tempClient'}->{'flashIndex'}]->{'creation_processor'};
$jsClient->{'tempClient'}->{'ver'} = $1
if $jsFlash->{'archive'}->[$jsClient->{'tempClient'}->{'flashIndex'}]->{'creation_release'} =~ /\d+\.(\d+)/;
if (!exists($jsOS->{'byVer'}->{$jsClient->{'tempClient'}->{'ver'}}->[0]->{'volume'})) {
print "ERROR: The O/S version (".$jsClient->{'tempClient'}->{'ver'}.") is not available for booting. Please\n";
print "mount the disk image with the version needed and then visit the o/s menu in the availability section\n";
print "from the main menu.";
print "\n";
print "Press ENTER to abort.. ";
my $wait = <STDIN>;
return "";
}
}
# find out what kind of machine this is.. for profile naming purposes
$jsClient->{'tempClient'}->{'model'} = &promptUser("What type of machine is this? Please use the short model name, e.g. v440, e3500","unknown");
$jsClient->{'tempClient'}->{'model'} =~ s/\s+//g;
$jsClient->{'tempClient'}->{'platform'} = &promptUser("What kind of CPU architecture is it? e.g. sun4u, sun4m, etc","sun4u");
# we'll give the user a choice of specifying their profile file, unless they want us to build it, by default
my $buildProfile;
do { $buildProfile = &promptUser("Would you like to (B)uild a custom profile or use an (E)xisting file?","b");
} while ($buildProfile !~ /^(b|e)/i);
# create what the profile name will be so we can use this filename for future uses
my $profileFile = "profiles/"."S".
$jsClient->{'tempClient'}->{'ver'}."-".
$jsClient->{'tempClient'}->{'model'}.($jsClient->{'tempClient'}->{'type'} =~ /^f/i ? "-FLASH" : "" ).".profile";
# determine what our profile strategy will be
if ($buildProfile =~ /^e/i) {
# here we'll let the user specify their own file name, but give them a disclaimer
print "\nNOTE: Specifying your own profile assumes that you've verified it\n";
print "to work properly. This program will not do this for you!\n";
my $validFile = -1;
my $newProfile;
do { $newProfile = &promptUser(
"Enter ".(
$validFile == 0 ? "VALID " : ""
)."path/file containing profile to use for ".$jsClient->{'tempClient'}->{'name'}
);
$validFile = ( -f $newProfile ? 1 : 0 );
if (!$validFile) { # let's help the user out by guessing some possible locations for the profile
if (-f $jsSystem->{'scriptHome'}."/$newProfile") {
$newProfile = $jsSystem->{'scriptHome'}."/$newProfile";
$validFile = 1;
} elsif (-f $jsSystem->{'scriptHome'}."/profiles/$newProfile") {
$newProfile = $jsSystem->{'scriptHome'}."/profiles/$newProfile";
$validFile = 1;
}
}
} while (!$validFile);
if ($newProfile !~ /^$jsSystem->{'scriptHome'}/) {
print "\nWARNING: Profile specified \'$newProfile\' is outside of script's path (which is NFS exported).. \n";
print "It will ".(
-f $jsSystem->{'scriptHome'}."/$profileFile" ? "overwrite the file " : "be copied as "
).$jsSystem->{'scriptHome'}."/$profileFile\n";
&copy($newProfile, $jsSystem->{'scriptHome'}."/$profileFile");
} else {
$newProfile =~ s/^$jsSystem->{'scriptHome'}\///;
$profileFile = $newProfile;
}
} else { # we're generating the profile for them
&jsClientProfileQuery();
}
$jsClient->{'tempClient'}->{'profile'} = $profileFile;
print "\nIs the following client-specific information correct, and do you want proceed adding the user?\n";
map {
printf "%-16s%s\n", $_, $jsClient->{'tempClient'}->{$_} unless /^(ver|type|profileParams|cluster|arch)$/
} sort keys %{$jsClient->{'tempClient'}};
return "" unless &promptUser("Do you want to continue adding the client?","y") =~ /^y/i;
return 1;
}
sub jsClientProfileQuery {
my @profileParam;
my $settingsConfirmed;
do {
if ($jsClient->{'tempClient'}->{'type'} =~ /^i/i) {
push @profileParam, { 'install_type' => 'initial_install' };
push @profileParam, { 'system_type' => 'server' };
push @profileParam, { 'geo' => &promptUser("In which geographical location will this system be located?","N_America") };
&menuFormat("Please choose a cluster to install");
&menuDispatch(&menuBuildFromArray($jsClusterOptions,sub { $jsClient->{'tempClient'}->{'cluster'} = shift; }));
push @profileParam, { 'cluster' => $jsClient->{'tempClient'}->{'cluster'} };
} else {
push @profileParam, { 'install_type' => 'flash_install' };
push @profileParam, {
'archive_location' =>
'nfs '.$jsSystem->{'ipAddress'}.':'.$jsFlash->{'archive'}->[ $jsClient->{'tempClient'}->{'flashIndex'} ]->{'filename'}
};
}
if (&promptUser("Do you want to use the default disk layout?","n") =~ /^n/i) {
push @profileParam, { 'partitioning' => 'explicit' };
push @profileParam, @{&jsClientFSQuery()};
} else {
push @profileParam, { 'partitioning' => 'default' };
}
&menuFormat("The following profile parameters have been gathered:\n");
map {
printf "%-20s%s\n", (keys %{$_})[0], $_->{((keys %{$_})[0])};
} @profileParam;
my $answer = &promptUser("Are these profile settings correct?","y");
$settingsConfirmed = ( $answer =~ /^y/i ? 1 : 0 );
} while (!$settingsConfirmed);
$jsClient->{'tempClient'}->{'profileParams'} = \@profileParam;
}
sub jsClientProfileCommit {
open PROFILE, ">".$jsClient->{'tempClient'}->{'profile'} || die "Cannot write to ".$jsClient->{'tempClient'}->{'profile'}.": $!\n";
map { printf PROFILE "%-18s%-s\n", (keys %{$_})[0], $_->{((keys %{$_})[0])}; } @{$jsClient->{'tempClient'}->{'profileParams'}};
close PROFILE;
}
sub jsClientFSQuery {
my @fsParam;
print "\nNOTE: Here you will enter file system information. When you are done specifying file systems,\n";
print "you will have a chance to review the info gathered, and re-do it if you're not happy with it.\n";
my ($diskDevice,$diskSize,$diskMount,$metaDevice,@diskDevices);
my $diskConfirmed = -1;
do { # we're doing another set of disks. maybe it's our first time, but maybe
# the first set wasn't correct so this is a new set)
my %sliceUsed;
@fsParam = ( ); # starting over on a new set, erase anything we have so far
print "\nNOTE: WE ARE STARTING OVER!! ALL PREVIOUSLY ENTERED INFO WAS DISCARDED!" if $diskConfirmed == 0;
my $doneWithDisks; # will be set for each disk
my $startedOver = 1; # controls whether we're verbose, if this is the first disk in the set or not
do { $doneWithDisks = 0; # we're doing another filesystem in this set, maybe it's
# the first filesystem we are doing, maybe not
if ($startedOver) { # this is a lot of noise, so we'll only show this if it's the first fs in this set
print "\n";
print "\nNOTE: File system devices can be specified using the cXtYdZsN format for explicit\n";
print "controller/target/disk/slice combinations, or using the sX notation for the\n";
print "specific slice on whatever the root disk is. ".(
$jsClient->{'tempClient'}->{'ver'} > 8 ? "You can also specify if you\nwould like to use mirroring by using the mirror keyword or using the dN notation" : ""
)."\n";
}
my $validDiskDevice = -1;
do { &menuFormat("Device named '$diskDevice' is not valid!") if $validDiskDevice == 0;
$diskDevice = &promptUser("\nWhere should we put the ".( $startedOver ? "first" : "next" )." file system on?");
$validDiskDevice = ( $diskDevice =~ /^((c\dt\dd\d)?s\d)$/i ? 1 : 0 );
# if we're using 9 or above, we can actually do mirroring too
$validDiskDevice = ( $diskDevice =~ /^((c\dt\dd\d)?s\d|mirror(:d(12[0-7]|1[01][0-9]|[1-9][0-9]|[0-9]))?|d(12[0-7]|1[01][0-9]|[1-9][0-9]|[0-9]))$/i ? 1 : 0 ) if $jsClient->{'tempClient'}->{'ver'} > 8;
if ($validDiskDevice) {
if ($sliceUsed{$diskDevice}) {
print "\nERROR: That slice is in use elsewhere!\n";
$validDiskDevice = 0;
} else {
++$sliceUsed{$diskDevice} if $diskDevice =~ /^c\dt\dd\ds\d$/i;
}
}
if ($diskDevice eq "") { $doneWithDisks = 1; $validDiskDevice = 1; } # if it's blank, it's really a valid device since
# it's recognize.. we also know that we're done with file systems in this set
} while (!$validDiskDevice);
if (!$doneWithDisks) { # only proceed with the other disk related questions if we have a non-blank
# device name, cuz if we do, then we're done with file systems and don't need to ask this stuff.
if ($diskDevice =~ /^mirror(:(d\d+))?/i || $diskDevice =~ /^((d\d+))$/) {
if ($2 ne "") {
$metaDevice = $2;
} else {
if ($startedOver) {
print "\nNOTE: Metadevices are pseudo-devices that are mounted and are made up of a set of slices.\n";
print "One such arrangement is a mirror, for increased data protection. Metadevices range from d0-d127\n";
}
my $validMirror = -1;
do {
$metaDevice = &promptUser("What metadevice would you like to define? eg. d0, d10, etc");
$validMirror = ( $metaDevice =~ /^d(12[0-7]|1[01][0-9]|[1-9][0-9]|[0-9])$/i ? 1 : 0 );
} while (!$validMirror);
}
my $startedOver2 = 1;
my $doneWithDiskDevices = -1;
@diskDevices = ();
do {
if ($startedOver2) {
print "\nNOTE: Here you can specify what disk slices (in cXtYdZsN format) will make up the mirror\n";
}
my $validDiskDevice;
do {
$diskDevice = &promptUser("What ".( $doneWithDiskDevices == 0 ? "other " : "" )."slice would you like to use for the mirror metadevice $metaDevice (ENTER when done)?");
$validDiskDevice = ( $diskDevice =~ /^c\dt\dd\ds\d$/ ? 1 : 0 );
if ($validDiskDevice && $sliceUsed{$diskDevice}) {
print "\nERROR: That slice is in use elsewhere!\n";
$validDiskDevice = 0;
} else {
++$sliceUsed{$diskDevice} if $diskDevice =~ /^c\dt\dd\ds\d$/i;
}
$validDiskDevice = 1 if $diskDevice eq "";
} while (!$validDiskDevice);
push @diskDevices, $diskDevice unless $diskDevice eq "";
$doneWithDiskDevices = ( $diskDevice eq "" ? 1 : 0 );
if ($doneWithDiskDevices && scalar(@diskDevices) < 2) {
print "\nERROR: You will need to specify at least two slices!\n";
$doneWithDiskDevices = 0;
}
$startedOver2 = 0;
} while (!$doneWithDiskDevices);
$diskDevice = $metaDevice;
}
$diskDevice = "slice $1 on the root disk" if $diskDevice =~ /^s(\d)$/;
$diskDevice = "mirror d$1" if $diskDevice =~ /^d(\d+)$/;
if ($startedOver) {
print "\nNOTE: Disk size can be specified in number of megabytes, or specify 'free' to use whatever is remaining.\n";
print "you can also specify 'auto' if you want jumpstart to figure it out based on size of software.\n";
}
my $validDiskSize = -1;
do { &menuFormat("ERROR: $diskSize is not a valid size!") if $validDiskSize == 0;
$diskSize = &promptUser("How big should we make ".$diskDevice."?");
$validDiskSize = ( $diskSize =~ /^([\d.]+|free|auto)\s*([MmGg][bB]?)?$/ ? 1 : 0 );
} while (!$validDiskSize);
$diskSize = $1 if $diskSize =~ /(\d+)\s*[mM]/;
$diskSize = $1 * 1024 if $diskSize =~ /(\d+)\s*[gG]/;
if ($startedOver) {
print "\nNOTE: How ".$diskDevice.", which is ".(
$diskSize =~ /^\d+/ ? "$diskSize megs" : (
$diskSize =~ /^free/ ? "any remaining space left" : "automatically sized"
)
).", is used can be specified by either giving a\n";
print "mount point such as /usr, or saying swap if it's used as such. You can also leave it blank to\n";
print "not format/mount it anywhere.\n";
}
my $validDiskMount = -1;
do { &menuFormat("ERROR: Can't use ".$diskDevice." as $diskMount because it is invalid!") if $validDiskMount == 0;
$diskMount = &promptUser("How would you like to use ".$diskDevice."?");
$validDiskMount = ( $diskMount =~ /^\/([\w\/]+)?$/ ? 1 : 0 );
$validDiskMount = 1 if $diskMount =~ /^swap$/;
$validDiskMount = 1 if $diskMount eq "";
} while (!$validDiskMount);
print "\n\nDone configuring ".$diskDevice.", which is ".(
$diskSize =~ /^\d+$/ ? "$diskSize megs" : (
$diskSize =~ /^free/ ? "any remaining space left" : "automatically sized"
)
).( $diskMount ? ", to be used as $diskMount" : "" ),".\n";
$diskDevice = "rootdisk.s$1" if $diskDevice =~ /^slice (\d+)/;
$diskDevice = "mirror:$1 ".join(" ",@diskDevices) if $diskDevice =~ /^mirror (d\d+)$/;
push @fsParam, { 'filesys' => $diskDevice." ".$diskSize.( $diskMount ? " ".$diskMount : "") };
print "\nNOTE: Here is what we have so far.. \n";
foreach my $currParam (@fsParam) {
printf "%-14s%-10s%-10s%-10s%-10s%-10s\n", split /\s+/, $currParam->{'filesys'};
}
print "...Moving onto the next file system.\n";
print "(Press ENTER instead to end entering file systems and confirm the ones entered so far).";
} # done handling the latest disk device given (that wasn't blank)
$startedOver = 0;
} while (!$doneWithDisks);
my $metadbNeeded = 0;
map { ++$metadbNeeded if $_->{'filesys'} =~ /^mirror/ } @fsParam;
if ($metadbNeeded) {
my @metaDB;
my $doneWithMetadb = -1;
do { if ($doneWithMetadb != 0) {
print "\nNOTE: When the system handles meta-devices, it needs a place to put state information\n";
print "in meta-stat databases. You will need to specify a set of slices (in cXtYdZsN format) to\n";
print "put these databases on. There will need to be at least two.\n";
}
my $metadb;
my $validMetadb = -1;
do { print "\nERROR: $metadb is not a valid location for the meta-state database!\n" if $validMetadb == 0;
$metadb = &promptUser("Where ".(
$doneWithMetadb == 0 ? "else " : ""
)."would you like to put any meta-state databases? (ENTER when done)"
);
$validMetadb = ( $metadb =~ /^c\dt\dd\ds\d$/i ? 1 : 0 );
if ($validMetadb && $sliceUsed{$metadb}) {
print "\nERROR: That slice is already used elsewhere!\n";
$validMetadb = 0;
} else {
++$sliceUsed{$metadb} if $metadb =~ /^c\dt\dd\ds\d$/i;
}
$validMetadb = 1 if $metadb eq "";
} while (!$validMetadb);
push @metaDB, $metadb unless $metadb eq "";
$doneWithMetadb = ( $metadb eq "" ? 1 : 0 );
if (scalar(@metaDB) < 2 && $metadb eq "") {
print "\nERROR: You need at least two meta-state databases!\n";
$doneWithMetadb = 0;
}
} while (!$doneWithMetadb);
map { push @fsParam, { 'metadb' => $_ }; } (@metaDB);
}
print "\nNOTE: Done entering all file systems. Here is what we have so far:\n\n";
foreach my $currParam (@fsParam) {
printf "%-14s%-10s%-10s%-10s%-10s%-10s\n", (keys %{$currParam})[0], split(/\s+/, $currParam->{(keys %{$currParam})[0]});
}
$diskConfirmed = ( &promptUser("Is this file system information accurate?","y") =~ /^y/i ? 1 : 0 );
} while (!$diskConfirmed);
return \@fsParam;
}
sub jsClientUsedIP { my $ipToSearchFor = shift;
return 1 if $ipToSearchFor eq $jsClient->{'ipAddress'};
return 0 unless scalar(keys %{$jsClient->{'client'}}) > 0;
map { return 1 if $jsClient->{'client'}->{$_}->{'ip_address'} eq $ipToSearchFor } (keys %{$jsClient->{'client'}});
return 0;
}
sub jsClientSetOS { my $osPath = shift;
$jsClient->{'tempClient'}->{'osVolume'} = $osPath;
}
sub jsClientSetFlash { my $flashIndex = shift;
$jsClient->{'tempClient'}->{'flashIndex'} = $flashIndex;
}
# NO NO NO!!
#
# if just adding a client is this complicated, there's NO WAY i am figuring out to let someone edit a client!!!
#
#sub jsClientEdit {
# # do something
#
# print "\n\n";
# print "Editing Jumpstart Client.. \n";
# &jsClientReload();
#}
#
# if you want to edit a client so badly, erase him then re-add him the right way biatch!
sub jsClientDelete {
my $validClient = -1;
my $clientName;
do { $clientName = lc(&promptUser(
"Enter a ".(
$validClient == 0 ? "VALID " : ""
)."client name to delete (ENTER to abort)"
));
# returning an empty client name is a sure sign of a problem, which will be detected by the caller
return "" if $clientName eq "";
$validClient = ( exists($jsClient->{'client'}->{$clientName}) ? 1 : 0 );
} while (!$validClient);
my $deletionConfirmation = &promptUser("Are you sure you want to delete $clientName?","n");
return "" unless $deletionConfirmation =~ /^y/i;
unlink(glob("/private/tftpboot/".sprintf("%02X%02X%02X%02X",split /\./,$jsClient->{'client'}->{$clientName}->{'ip_address'})."*"));
&jsClientDeleteExport($jsSystem->{'scriptHome'},$clientName);
&jsClientDeleteExport(($jsClient->{'client'}->{$clientName}->{'flashFile'} =~ /^(.+)\/[^\/]+$/)[0],$clientName);
&jsClientDeleteExport((&niclCmd("/","cat","/machines/$clientName","bootparams")->[0] =~ /install=[^:]+:(\S+)\s/)[0],$clientName);
&jsClientDeleteRules($clientName);
&jsSystemUpdateRulesOK();
&niclCmd("/","delete","/machines/$clientName");
delete($jsClient->{'client'}->{$clientName});
&jsClientReload();
}
sub jsClientDeleteExport { my ($exportName,$clientName) = @_;
return unless $clientName;
$exportName =~ s#/#\\\\/#g;
&niclCmd("/","delete","/exports/$exportName","clients",$clientName);
&niclCmd("/","delete","/exports/$exportName") if scalar (split /\s+/, &niclCmd("/","cat","/exports/$exportName","clients")->[0]) == 1;
&jsSystemUpdateMountd();
}
sub jsClientDeleteRules { my $clientName = shift;
&copy($jsSystem->{'scriptHome'}."/rules",$jsSystem->{'scriptHome'}."/rules.tmp");
open RULESOLD, $jsSystem->{'scriptHome'}."/rules.tmp" || die "Cannot read from rules.tmp: $!\n";
open RULESNEW, ">".$jsSystem->{'scriptHome'}."/rules" || die "Cannot write to rules: $!\n";
while (<RULESOLD>) {
print RULESNEW $_ unless /hostname\s+$clientName/;
}
close RULESNEW;
close RULESOLD;
unlink($jsSystem->{'scriptHome'}."/rules.tmp");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment