package xCAT_plugin::statelite; BEGIN { $::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat'; } use lib "$::XCATROOT/lib/perl"; use xCAT::Table; use Getopt::Long; use File::Basename; use File::Path; use File::Copy; use File::Find; use Cwd; use File::Temp; use xCAT::Utils qw(genpassword); use xCAT::SvrUtils; use Data::Dumper; Getopt::Long::Configure("bundling"); Getopt::Long::Configure("pass_through"); my $cmdname = "liteimg"; my $statedir = ".statelite"; my $verbose = "0"; sub handled_commands { return { $cmdname => "statelite" } } # function to handle request. Basically, get the information # about the image and then do the action on it. Is that vague enough? sub process_request { my $request = shift; my $callback = shift; my $doreq = shift; my $sitetab = xCAT::Table->new('site'); my $ent = $sitetab->getAttribs({key=>'installdir'},['value']); my $installroot = "/install"; # get /install directory if ($ent and $ent->{value}) { $installroot = $ent->{value}; } # if not defined, error out... or should we set it as default? unless ($installroot) { $callback->({error=>["No installdir defined in site table"],errorcode=>[1]}); return; } @ARGV = @{$request->{arg}}; my $argc = scalar @ARGV; if ($argc == 0) { $callback->({info=>["$cmdname -h # this help message\n$cmdname -v # version\n$cmdname -V # verbose\n$cmdname -p -a -o \n$cmdname imagename"]}); return; } my $osver; my $arch; my $profile; my $rootimg_dir; my $destdir; my $imagename; GetOptions( "profile|p=s" => \$profile, "arch|a=s" => \$arch, "osver|o=s" => \$osver, "help|h" => \$help, "version|v" => \$version, "verbose|V" => \$verbose ); if ($version) { my $version = xCAT::Utils->Version(); $callback->({info=>[$version]}); return; } if ($help) { $callback->({info=>["$cmdname -h # this help message\n$cmdname -v # version\n$cmdname -V # verbose\n$cmdname [-p profile] [-a architecture] [-o OS]\n$cmdname imagename"]}); return; } # if they only gave us the image name: if (@ARGV > 0) { $imagename=$ARGV[0]; if ($arch or $osver or $profile) { $callback->({error=>["-o, -p and -a options are not allowed when a image name is specified."],errorcode=>[1]}); return; } #load the module in memory eval {require("$::XCATROOT/lib/perl/xCAT/Table.pm")}; if ($@) { $callback->({error=>[$@],errorcode=>[1]}); return; } #get the info from the osimage and linux my $osimagetab=xCAT::Table->new('osimage', -create=>1); if (!$osimagetab) { # os image doesn't exist in table. $callback->({error=>["The osimage table cannot be opened."],errorcode=>[1]}); return; } # open the linux image table to get more attributes... for later. my $linuximagetab=xCAT::Table->new('linuximage', -create=>1); if (!$linuximagetab) { $callback->({error=>["The linuximage table cannot be opened."],errorcode=>[1]}); return; } # get the os, arch, and profile from the image name table. (my $ref) = $osimagetab->getAttribs({imagename => $imagename}, 'osvers', 'osarch', 'profile'); if (!$ref) { $callback->({error=>["Cannot find image \'$imagename\' from the osimage table."],errorcode=>[1]}); return; } $osver=$ref->{'osvers'}; $arch=$ref->{'osarch'}; $profile=$ref->{'profile'}; } # end of case where they give us osimage. unless ($osver and $arch and $profile ) { $callback->({error=>["osimage.osvers, osimage.osarch, and osimage.profile and must be specified for the image $imagename in the database, or you must specify -o os -p profile -a arch."],errorcode=>[1]}); return; } $destdir="$installroot/netboot/$osver/$arch/$profile"; $rootimg_dir="$destdir/rootimg"; my $oldpath=cwd(); # now we have all the info we need: # - rootimg_dir # - osver # - arch # - profile $callback->({info=>["going to modify $rootimg_dir"]}); #get the root password for the node my $passtab = xCAT::Table->new('passwd'); if ($passtab) { (my $pent) = $passtab->getAttribs({key=>'system',username=>'root'},'password'); if ($pent and defined ($pent->{password})) { my $pass = $pent->{password}; my $oldmask=umask(0077); my $shadow; open($shadow,"<","$rootimg_dir/etc/shadow"); my @shadents = <$shadow>; close($shadow); open($shadow,">","$rootimg_dir/etc/shadow"); unless ($pass =~ /^\$1\$/) { $pass = crypt($pass,'$1$'.genpassword(8)); } print $shadow "root:$pass:13880:0:99999:7:::\n"; foreach (@shadents) { unless (/^root:/) { print $shadow "$_"; } } close($shadow); umask($oldmask); } } # sync fils configured in the synclist to the rootimage #if (!$imagename) { # $syncfile = xCAT::SvrUtils->getsynclistfile(undef, $osver, $arch, $profile, "netboot"); # if (defined ($syncfile) && -f $syncfile # && -d $rootimg_dir) { # print "sync files from $syncfile to the $rootimg_dir\n"; # `$::XCATROOT/bin/xdcp -i $rootimg_dir -F $syncfile`; # } #} #store the image in the DB if (!$imagename) { my @ret=xCAT::SvrUtils->update_tables_with_diskless_image($osver, $arch, $profile, 'statelite'); if ($ret[0] != 0) { $callback->({error=>["Error when updating the osimage tables: " . $ret[1]]}); } $imagename="$osver-$arch-statelite-$profile" } # check if the file "litefile.save" exists or not # if it doesn't exist, then we get the synclist, and run liteMe # if it exists, it means "liteimg" has run more than one time, we need to compare with the synclist my @listSaved; if ( -e "$rootimg_dir/.statelite/litefile.save") { open SAVED, "$rootimg_dir/.statelite/litefile.save"; # store all its contents to @listSaved; while() { chomp $_; push @listSaved, $_; } close SAVED; } my %hashSaved = (); if ( parseLiteFiles(\@listSaved, \%hashSaved) ) { $callback->({error=>["parseLiteFiles failed for listSaved!"]}); return ; } # now get the files for the node my @synclist = xCAT::Utils->runcmd("ilitefile $imagename", 0, 1); unless (@synclist) { $callback->({error=>["There are no files to sync for $imagename. You have to have some files read/write filled out in the synclist table."],errorcode=>[1]}); return; } my $listNew = $synclist[0]; # the directory/file in litefile table must be the absolute path ("/***") foreach my $entry (@$listNew) { my @tmp = split (/\s+/, $entry); unless ($tmp[2] =~ m{^/}) { $callback->({error=>[qq{ $tmp[2] is not one absolute path. }], errorcode=>[1]}); return; } if ($tmp[1] =~ m{con} and $tmp[2] =~ m{/$}) { $callback->({error=>[qq{ $tmp[2] is directory, don't use "con" as its option }], errorcode=>[1]}); return; } } my %hashNew = (); if ( parseLiteFiles($listNew, \%hashNew) ) { $callback->({error=>["parseLiteFiles failed for listNew!"]}); return; } # validate the options for all litefile entries # if there is any scenario not supported, the command exits foreach my $entry (keys %hashNew) { my @tmp = split (/\s+/, $entry); my $f = $tmp[1]; if ($hashNew{$entry}) { if ( $tmp[0] =~ m/ro$/ or $tmp[0] =~ m/con$/) { $callback->({error=>[qq{the directory "$f" should not be with "ro" or "con" as its option}], errorcode=>[1]}); return; } foreach my $child ( @{$hashNew{$entry}} ) { my @tmpc = split (/\s+/, $child); my $fc = $tmpc[1]; if ($tmp[0] =~ m/link/) { if ($tmpc[0] eq "link,ro") { $callback->({error=>[qq{Based on the option of $f, $fc should not use "link,ro" as its option}], errorcode=> [1]}); return; } if ($tmpc[0] !~ m/link/) { $callback->({error=>[qq{Based on the option of $f, $fc can only use "link"-based options}], errorcode=> [1]}); return; } } else { if ($tmpc[0] =~ m/link/) { $callback->({error=>[qq{Based on the option of $f, $fc should not use "link"-based options}], errorcode=>[1]}); return; } } if ( ($tmp[0] =~ m{persistent}) and ($tmpc[0] !~ m{persistent}) ) { # TODO: if the parent is "persistent", the child can be ro/persistent/rw/con $callback->({error=>["$fc should have persistent option like $f "], errorcode=> [ 1]}); return; } } } } # backup the file/directory before recovering the files in litefile.save unless ( -d "$rootimg_dir/.statebackup") { if (-e "$rootimg_dir/.statebackup") { xCAT::Utils->runcmd("rm $rootimg_dir/.statebackup", 0, 1); } $verbose && $callback->({info=>["mkdir $rootimg_dir/.statebackup"]}); xCAT::Utils->runcmd("mkdir $rootimg_dir/.statebackup", 0, 1); } # recovery the files in litefile.save if necessary foreach my $line (keys %hashSaved) { my @oldentry = split(/\s+/, $line); my $f = $oldentry[1]; my @newentries = grep /\s+$f$/, @{$listNew}; my @entry; if(scalar @newentries == 1) { @entry = split(/\s+/, $newentries[0]); } # backup the children to .statebackup if ($hashSaved{$line}) { my $childrenRef = $hashSaved{$line}; unless ( -d "$rootimg_dir/.statebackup$f" ) { xCAT::Utils->runcmd("rm -rf $rootimg_dir/.statebackup$f", 0, 1) if (-e "$rootimg_dir/.statebackup$f"); $verbose && $callback->({info=>["mkdir $rootimg_dir/.statebackup$f"]}); xCAT::Utils->runcmd("mkdir -p $rootimg_dir/.statebackup$f"); } foreach my $child (@{$childrenRef}) { my @tmpc = split(/\s+/, $child); my $name = $rootimg_dir . $tmpc[1]; if (-e $name) { $verbose && $callback->({info=>["cp -r -a $name $rootimg_dir/.statebackup$f"]}); xCAT::Utils->runcmd("cp -r -a $name $rootimg_dir/.statebackup$f"); } } } # there's one parent directory, whose option is different from the old one unless ($entry[1] eq $oldentry[0]) { recoverFiles($rootimg_dir, \@oldentry, $callback); # if its children items exist, we need to copy the backup files from .statebackup to the rootfs, if ($hashSaved{$line}) { $verbose && $callback->({info=>["$f has child file/directory in the litefile table."]}); my $childrenRef = $hashSaved{$line}; foreach my $child (@{$childrenRef}) { # recover them from .statebackup to $rootimg_dir my @tmpc = split (/\s+/, $child); my $name = $tmpc[1]; my @newentries = grep /\s+$name$/, @{listNew}; my $destf = $rootimg_dir . $name; my $srcf = $rootimg_dir . "/.statebackup" . $name; if ( -e $destf ) { $verbose && $callback->({info => ["rm -rf $destf"]}); xCAT::Utils->runcmd("rm -rf $destf", 0, 1); } # maybe the dir of $destf doesn't exist, so we will create one my $dirDestf = dirname $destf; unless ( -d $dirDestf ) { $verbose && $callback->({info=>["mkdir -p $dirDestf"]}); xCAT::Utils->runcmd("mkdir -p $dirDestf", 0, 1); } if ( -e $srcf ) { $verbose && $callback->({info=>["recovering from $srcf to $destf"]}); xCAT::Utils->runcmd("cp -r -a $srcf $destf", 0, 1); } } } } # recover the children if ($hashSaved{$line}) { $verbose && $callback->({info=>["$f has child file/directory in the litefile table."]}); my $childrenRef = $hashSaved{$line}; foreach my $child (@{$childrenRef}) { my @tmpc = split (/\s+/, $child); my $name = $tmpc[1]; my @newentries = grep /\s+$name$/, @{listNew}; my @entry; if (scalar @newentries == 1) { @entry = split(/\s+/, $newentries[0]); } unless($tmpc[0] eq $entry[1]) { recoverFiles($rootimg_dir, \@tmpc, $callback); } } } } # remove .statebackup $verbose && $callback->({info=>["remove .statebackup"]}); xCAT::Utils->runcmd("rm -rf $rootimg_dir/.statebackup", 0, 1); # then store the @synclist to litefile.save #system("cp $rootimg_dir/.statelite/litefile.save $rootimg_dir/.statelite/litefile.save1"); open SAVED, ">$rootimg_dir/.statelite/litefile.save"; foreach my $line (@{$listNew}) { print SAVED "$line\n"; } close SAVED; liteMe($rootimg_dir, \%hashNew, $callback); # now stick the rc file in: # this is actually a pre-rc file because it gets run before the node boots up all the way. $verbose && $callback->({info => ["put the statelite rc file to $rootimg_dir/etc/init.d/"]}); if ($osver =~ m/^rh[a-zA-Z]*5/ and $arch eq "ppc64") { # special case for redhat5.x on PPC64 system("cp -a $::XCATROOT/share/xcat/netboot/add-on/statelite/rc.statelite.ppc.redhat $rootimg_dir/etc/init.d/statelite"); }else { system("cp -a $::XCATROOT/share/xcat/netboot/add-on/statelite/rc.statelite $rootimg_dir/etc/init.d/statelite"); } } sub liteMe { my $rootimg_dir = shift; my $hashNewRef = shift; my $callback = shift; unless (-d $rootimg_dir) { $callback->({error=>["no rootimage dir"],errorcode=>[1]}); return; } unless ( -d "$rootimg_dir/$statedir" ) { # snapshot directory for tmpfs and persistent data. if ( -e "$rootimg_dir/$statedir" ) { xCAT::Utils->runcmd("rm -rf $rootimg_dir/$statedir", 0, 1); } $callback->({info=>["creating $rootimg_dir/$statedir"]}); xCAT::Utils->runcmd("mkdir -p $rootimg_dir/$statedir", 0, 1); } unless ( -d "$rootimg_dir/$statedir/tmpfs" ) { xCAT::Utils->runcmd("mkdir -p $rootimg_dir/$statedir/tmpfs", 0, 1); } foreach my $line (keys %{$hashNewRef}) { liteItem($rootimg_dir, $line, 0, $callback); if($hashNewRef->{$line}) { # there're children my $childrenRef = $hashNewRef->{$line}; foreach my $child (@{$childrenRef}) { liteItem($rootimg_dir, $child, 1, $callback); } } } $callback->({info=>["done."]}); # end loop, synclist should now all be in place. } sub getRelDir { my $f = shift; $f = dirname($f); if($f eq "/"){ return "."; } my $l = ""; my @arr = split("/", $f); foreach (1 .. $#arr){ $l .= "../"; } chop($l); # get rid of last / return $l } =head3 parseLiteFiles In the liteentry table, one directory and its sub-items (including sub-directory and entries) can co-exist; In order to handle such a scenario, one hash is generated to show the hirarachy relationship For example, one array with entry names is used as the input: my @entries = ( "imagename persistent /var/", "imagename tempfs /var/tmp/", "imagename link /root/", "imagename link /root/.bashrc", "imagename link /root/test/", "imagename link /root/second/third", "imagename tempfs /etc/resolv.conf", "imagename tempfs /var/run/" ); Then, one hash will generated as: %hashentries = { 'persistent /var/' => [ 'tempfs /var/tmp/', 'tempfs /var/run/' ], 'tempfs /etc/resolv.conf' => undef, 'link /root/' => [ 'link /root/.bashrc', 'link /root/test/', 'link /root/second/third" ] }; Arguments: one array with entrynames, one hash to hold the entries parsed Returns: 0 if sucucess 1 if fail =cut sub parseLiteFiles { my ($flref, $dhref) = @_; my @entries = @{$flref}; foreach (@entries) { my $entry = $_; my @str = split /\s+/, $entry; shift @str; # remove the imgname in @entries $entry = join "\t", @str; my $file = $str[1]; chop $file if ($file =~ m{/$}); unless (exists $dhref->{"$entry"}) { my $parent = dirname $file; my @res; my $found = 0; while($parent ne "/") { # to see whether $parent exists in @entries or not $parent .= "/" unless ($parent =~ m/\/$/); @res = grep {$_ =~ m/\Q$parent\E$/} @entries; $found = scalar @res; last if ($found eq 1); $parent = dirname $parent; } if($found eq 1) { # $parent is found in @entries # handle $res[0]; my @tmpresentry=split /\s+/, $res[0]; shift @tmpresentry; # remove the imgname in @tmpresentry $res[0] = join "\t", @tmpresentry; chop $parent; my @keys = keys %{$dhref}; my $kfound = grep {$_ =~ m/\Q$res[0]\E$/} @keys; if($kfound eq 0) { $dhref->{$res[0]} = []; } push @{$dhref->{"$res[0]"}}, $entry; }else { $dhref->{"$entry"} = (); } } } return 0; } =head3 recoverFiles =cut sub recoverFiles { my ($rootimg_dir, $oldentry, $callback) = @_; $f = $oldentry->[1]; #$callback->({info => ["! updating $f ..."]}); if ($oldentry->[0] =~ m{^link}) { my $target = $rootimg_dir . $f; if (-l $target) { #not one directory my $location = readlink $target; # if it is not linked from tmpfs, it should have been modified by the .postinstall file if ($location =~ /\.statelite\/tmpfs/) { xCAT::Utils->runcmd("rm -rf $target", 0, 1); my $default = $rootimg_dir . "/.default" . $f; if( -e $default) { xCAT::Utils->runcmd("cp -r -a $default $target", 0, 1); }else { # maybe someone deletes the copy in .default directory xCAT::Utils->runcmd("touch $target", 0, 1); } } } else { chop $target; if( -l $target ) { my $location = readlink $target; if ($location =~ /\.statelite\/tmpfs/) { xCAT::Utils->runcmd("rm -rf $target", 0, 1); my $default = $rootimg_dir . "/.default" . $f; if( -e $default) { xCAT::Utils->runcmd("cp -r -a $default $target", 0, 1); } else { xCAT::Utils->runcmd("mkdir $target", 0, 1); } } } } $target = $rootimg_dir . "/.statelite/tmpfs" . $f; xCAT::Utils->runcmd("rm -rf $target", 0, 1); } else { # shouldn't copy back from /.default, maybe the user has replaced the file/directory in .postinstall file my $default = $rootimg_dir . $f; xCAT::Utils->runcmd("rm -rf $default", 0, 1); # TODO: not sure whether it's necessary right now } return 0; } =head3 liteItem =cut sub liteItem { my ($rootimg_dir, $item, $isChild, $callback) = @_; my @entry = split (/\s+/, $item); my $f = $entry[1]; # file name my $rif = $rootimg_dir . $f; # the file's location in rootimg_dir my $d = dirname($f); if ($entry[0] =~ m/link/) { # 1. copy original contents if they exist to .default directory # 2. remove file # 3. create symbolic link to .statelite if ($isChild == 0) { #check if the file has been moved to .default by its parent or by last liteimg, if yes, then do nothing my $ret=`readlink -m $rootimg_dir$f`; if ($? == 0) { if ($ret =~ /$rootimg_dir\/.default/) { $verbose && $callback->({info=>["do nothing for file $f"]}); next; } } # copy the file to /.defaults if( -f "$rif" or -d "$rif"){ # if its already a link then leave it alone. unless(-l $rif){ # mk the directory if it doesn't exist: unless ( -d "$rootimg_dir/.default$d" ) { $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$d"]}); system("mkdir -p $rootimg_dir/.default$d"); } # copy the file in place. $verbose && $callback->({info=>["cp -r -a $rif $rootimg_dir/.default$d"]}); system("cp -r -a $rif $rootimg_dir/.default$d"); # remove the real file $verbose && $callback->({info=>["rm -rf $rif"]}); system("rm -rf $rif"); } } else { # in this case the file doesn't exist in the image so we create something to it. # here we're modifying the read/only image unless (-d "$rootimg_dir$d") { $verbose && $callback->({info=>["mkdir -p $rootimg_dir$d"]}); system("mkdir -p $rootimg_dir$d"); } unless(-d "$rootimg_dir/.default$d"){ $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$d"]}); system("mkdir -p $rootimg_dir/.default$d"); } # now make touch the file: if($f =~ /\/$/){ # if its a directory, make the directory in .default $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$f"]}); system("mkdir -p $rootimg_dir/.default$f"); } else { # if its just a file then link it. $verbose && $callback->({info=>["touch $rootimg_dir/.default$f"]}); system("touch $rootimg_dir/.default$f"); } } # now create the spot in tmpfs $verbose && $callback->({info=>["mkdir -p $rootimg_dir/$statedir/tmpfs$d"]}); system("mkdir -p $rootimg_dir/$statedir/tmpfs$d"); # now for each file, create a symbollic link to /.statelite/tmpfs/ # strip the last / if its a directory for linking, but be careful! # doing ln -sf ../../tmp ../tmp when tmp is a directory creates 50 levels of links. # have to do: # ln -sf ../../tmp ../../tmp/ <- note the / on the end! if($f =~ /\/$/){ $f =~ s/\/$//g; } # now do the links. # link the .default file to the .statelite file and the real file to the .statelite file. # ../ for tmpfs # ../ for .statelite # the rest are for the paths in the file. my $l = getRelDir($f); $verbose && $callback->({info=>["ln -sf ../../$l/.default$f $rootimg_dir/$statedir/tmpfs$f"]}); system("ln -sfn ../../$l/.default$f $rootimg_dir/$statedir/tmpfs/$f"); $verbose && $callback->({info=>["ln -sf $l/$statedir/tmpfs$f $rootimg_dir$f"]}); system("ln -sfn $l/$statedir/tmpfs$f $rootimg_dir$f"); } else { # since its parent directory has been linked to .default and .statelite/tmpfs/, # what we need to do is only to check whether it exists in .default directory if($f =~ m{/$}) { # one directory unless ( -d "$rootimg_dir/.default$f" ) { if (-e "$rootimg_dir/.default$f") { xCAT::Utils->runcmd("rm -rf $rootimg_dir/.default$f", 0, 1); } $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$f"]}); xCAT::Utils->runcmd("mkdir -p $rootimg_dir/.default$f", 0, 1); } }else { # only one file my $fdir = dirname($f); unless ( -d "$rootimg_dir/.default$fdir") { $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$fdir"]}); xCAT::Utils->runcmd("mkdir -p $rootimg_dir/.default$fdir", 0, 1); } unless( -e "$rootimg_dir/.default$f") { $verbose && $callback->({info=>["touch $rootimg_dir/.default$f"]}); xCAT::Utils->runcmd("touch $rootimg_dir/.default$f", 0, 1); } } } } else { # if no such file like $rif, create one unless ( -e "$rif" ) { if ($f =~ m{/$}) { $verbose && $callback->({info=>["mkdir -p $rif"]}); system("mkdir -p $rif"); } else { # check whether its directory exists or not my $rifdir = $rootimg_dir . $d; unless (-e $rifdir) { $verbose && $callback->({info => ["mkdir $rifdir"]}); mkdir($rifdir); } $verbose && $callback->({info=>["touch $rif"]}); system("touch $rif"); } } unless ( -e "$rootimg_dir/.default$d" ) { $verbose && $callback->({info=>["mkdir -p $rootimg_dir/.default$d"]}); system("mkdir -p $rootimg_dir/.default$d"); } # copy the file to /.defaults $verbose && $callback->({info=>["cp -r -a $rif $rootimg_dir/.default$d"]}); system("cp -r -a $rif $rootimg_dir/.default$d"); } } 1;