xcat-core/perl-xCAT/xCAT/NodeRange.pm

704 lines
24 KiB
Perl

# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
package xCAT::NodeRange;
require xCAT::Table;
require Exporter;
use strict;
#Perl implementation of noderange
our @ISA = qw(Exporter);
our @EXPORT = qw(noderange nodesmissed);
our @EXPORT_OK = qw(extnoderange abbreviate_noderange);
my $missingnodes=[];
my $nodelist; #=xCAT::Table->new('nodelist',-create =>1);
my $grptab;
#TODO: MEMLEAK note
# I've moved grptab up here to avoid calling 'new' on it on every noderange
# Something is wrong in the Table object such that it leaks
# a few kilobytes of memory, even if nodelist member is not created
# To reproduce the mem leak, move 'my $grptab' to the place where it is used
# then call 'getAllNodesAttribs' a few thousand times on some table
# No one noticed before 2.3 because the lifetime of processes doing noderange
# expansion was short (seconds)
# In 2.3, the problem has been 'solved' for most contexts in that the DB worker
# reuses Table objects rather than ever destroying them
# The exception is when the DB worker process itself wants to expand
# a noderange, which only ever happens from getAllNodesAttribs
# in this case, we change NodeRange to reuse the same Table object
# even if not relying upon DB worker to figure it out for noderange
# This may be a good idea anyway, regardless of memory leak
# It remains a good way to induce the memleak to correctly fix it
# rather than hiding from the problem
#my $nodeprefix = "node";
my @allnodeset;
my %allnodehash;
my @grplist;
my $didgrouplist;
my $glstamp=0;
my $allnodesetstamp=0;
my $allgrphashstamp=0;
my %allgrphash;
my $retaincache=0;
my $recurselevel=0;
my @cachedcolumns;
#TODO: With a very large nodelist (i.e. 65k or so), deriving the members
# of a group is a little sluggish. We may want to put in a mechanism to
# maintain a two-way hash anytime nodelist or nodegroup changes, allowing
# nodegroup table and nodelist to contain the same information about
# group membership indexed in different ways to speed this up.
# At low scale, little to no difference/impact would be seen
# at high scale, changing nodelist or nodegroup would be perceptibly longer,
# but many other operations would probably benefit greatly.
sub subnodes (\@@) {
#Subtract set of nodes from the first list
my $nodes = shift;
my $node;
foreach $node (@_) {
@$nodes = (grep(!/^$node$/,@$nodes));
}
}
sub nodesmissed {
return @$missingnodes;
}
sub reset_db {
#workaround, something seems to be trying to use a corrupted reference to grptab
#this allows init_dbworker to reset the object
$grptab=0;
}
sub nodesbycriteria {
#TODO: this should be in a common place, shared by tabutils nodech/nodels and noderange
#there is a set of functions already, but the path is a little complicated and
#might be hooked into the objective usage style, which this function is not trying to match
#Return nodes by criteria. Can accept a list reference of criteria
#returns a hash reference of criteria expressions to nodes that meet
my $nodes = shift; #the set from which to match
my $critlist = shift; #list of criteria to match
my %tables;
my %shortnames = (
groups => [qw(nodelist groups)],
tags => [qw(nodelist groups)],
mgt => [qw(nodehm mgt)],
#switch => [qw(switch switch)],
);
unless (ref $critlist) {
$critlist = [ $critlist ];
}
my $criteria;
my %critnodes;
my $value;
my $tabcol;
my $matchtype;
foreach $criteria (@$critlist) {
my $table;
my $column;
$tabcol=$criteria;
if ($criteria =~ /^[^=]*\!=/) {
($criteria,$value) = split /!=/,$criteria,2;
$matchtype='natch';
} elsif ($criteria =~ /^[^=]*=~/) {
($criteria,$value) = split /=~/,$criteria,2;
$value =~ s/^\///;
$value =~ s/\/$//;
$matchtype='regex';
} elsif ($criteria =~ /[^=]*==/) {
($criteria,$value) = split /==/,$criteria,2;
$matchtype='match';
} elsif ($criteria =~ /[^=]*=/) {
($criteria,$value) = split /=/,$criteria,2;
$matchtype='match';
} elsif ($criteria =~ /[^=]*!~/) {
($criteria,$value) = split /!~/,$criteria,2;
$value =~ s/^\///;
$value =~ s/\/$//;
$matchtype='negex';
}
if ($shortnames{$criteria}) {
($table, $column) = @{$shortnames{$criteria}};
} elsif ($criteria =~ /\./) {
($table, $column) = split('\.', $criteria, 2);
} else {
return undef;
}
unless (grep /$column/,@{$xCAT::Schema::tabspec{$table}->{cols}}) {
return undef;
}
push @{$tables{$table}},[$column,$tabcol,$value,$matchtype]; #Mark this as something to get
}
my $tab;
foreach $tab (keys %tables) {
my $tabh = xCAT::Table->new($tab,-create=>0);
unless ($tabh) { next; }
my @cols;
foreach (@{$tables{$tab}}) {
push @cols, $_->[0];
}
if ($tab eq "nodelist") { #fun caching interaction
my $neednewcache=0;
my $nlcol;
foreach $nlcol (@cols) {
unless (grep /^$nlcol\z/,@cachedcolumns) {
$neednewcache=1;
push @cachedcolumns,$nlcol;
}
}
if ($neednewcache) {
if ($nodelist) {
$nodelist->_clear_cache();
$nodelist->_build_cache(\@cachedcolumns);
}
}
}
my $rechash = $tabh->getNodesAttribs($nodes,\@cols); #TODO: if not defined nodes, getAllNodesAttribs may be faster actually...
foreach my $node (@$nodes) {
my $recs = $rechash->{$node};
my $critline;
foreach $critline (@{$tables{$tab}}) {
foreach my $rec (@$recs) {
my $value="";
if (defined $rec->{$critline->[0]}) {
$value = $rec->{$critline->[0]};
}
my $compstring = $critline->[2];
if ($critline->[3] eq 'match' and $value eq $compstring) {
push @{$critnodes{$critline->[1]}},$node;
} elsif ($critline->[3] eq 'natch' and $value ne $compstring) {
push @{$critnodes{$critline->[1]}},$node;
} elsif ($critline->[3] eq 'regex' and $value =~ /$compstring/) {
push @{$critnodes{$critline->[1]}},$node;
} elsif ($critline->[3] eq 'negex' and $value !~ /$compstring/) {
push @{$critnodes{$critline->[1]}},$node;
}
}
}
}
}
return \%critnodes;
}
sub expandatom { #TODO: implement table selection as an atom (nodetype.os==rhels5.3)
my $atom = shift;
if ($recurselevel > 4096) { die "NodeRange seems to be hung on evaluating $atom, recursion limit hit"; }
unless (scalar(@allnodeset) and (($allnodesetstamp+5) > time())) { #Build a cache of all nodes, some corner cases will perform worse, but by and large it will do better. We could do tests to see where the breaking points are, and predict how many atoms we have to evaluate to mitigate, for now, implement the strategy that keeps performance from going completely off the rails
$allnodesetstamp=time();
$nodelist->_set_use_cache(1);
@allnodeset = $nodelist->getAllAttribs('node','groups');
%allnodehash = map { $_->{node} => 1 } @allnodeset;
}
my $verify = (scalar(@_) == 1 ? shift : 1);
#print "atom=$atom, verify=$verify\n";
my @nodes= ();
#TODO: these env vars need to get passed by the client to xcatd
my $nprefix=(defined ($ENV{'XCAT_NODE_PREFIX'}) ? $ENV{'XCAT_NODE_PREFIX'} : 'node');
my $nsuffix=(defined ($ENV{'XCAT_NODE_SUFFIX'}) ? $ENV{'XCAT_NODE_SUFFIX'} : '');
if ($verify && $allnodehash{$atom}) { #The atom is a plain old nodename
return ($atom);
}
if ($atom =~ /^\(.*\)$/) { # handle parentheses by recursively calling noderange()
$atom =~ s/^\((.*)\)$/$1/;
$recurselevel++;
return noderange($atom);
}
if ($atom =~ /@/) {
$recurselevel++;
return noderange($atom);
}
# Try to match groups?
if ($verify) {
unless ($grptab) {
$grptab = xCAT::Table->new('nodegroup');
}
if ($grptab and (($glstamp < (time()-5)) or (not $didgrouplist and not scalar @grplist))) {
$didgrouplist = 1;
$glstamp=time();
@grplist = @{$grptab->getAllEntries()};
}
my $isdynamicgrp = 0;
foreach my $grpdef_ref (@grplist) {
my %grpdef = %$grpdef_ref;
# Try to match a dynamic node group
# do not try to match the static node group from nodegroup table,
# the static node groups are stored in nodelist table.
if (($grpdef{'groupname'} eq $atom) && ($grpdef{'grouptype'} eq 'dynamic'))
{
$isdynamicgrp = 1;
my $grpname = $atom;
my %grphash;
$grphash{$grpname}{'objtype'} = 'group';
$grphash{$grpname}{'grouptype'} = 'dynamic';
$grphash{$grpname}{'wherevals'} = $grpdef{'wherevals'};
my $memberlist = xCAT::DBobjUtils->getGroupMembers($grpname, \%grphash);
foreach my $grpmember (split ",", $memberlist)
{
push @nodes, $grpmember;
}
last; #there should not be more than one group with the same name
}
}
# The atom is not a dynamic node group, is it a static node group???
if(!$isdynamicgrp)
{
unless (scalar %allgrphash and (time() < ($allgrphashstamp+5))) { #build a group membership cache
$allgrphashstamp=time();
%allgrphash=();
my $nlent;
foreach $nlent (@allnodeset) {
my @groups=split(/,/,$nlent->{groups});
my $grp;
foreach $grp (@groups) {
push @{$allgrphash{$grp}},$nlent->{node};
}
}
}
if ($allgrphash{$atom}) {
push @nodes,@{$allgrphash{$atom}};
}
}
# check to see if atom is a defined group name that didn't have any current members
if ( scalar @nodes == 0 ) {
if(scalar @grplist) { #Use previously constructed cache to avoid hitting DB worker so much
#my @grouplist = $grptab->getAllAttribs('groupname');
for my $row ( @grplist ) {
if ( $row->{groupname} eq $atom ) {
return ();
}
}
}
}
}
if ($atom =~ m/[=~]/) { #TODO: this is the clunky, slow code path to acheive the goal. It also is the easiest to write, strange coincidence. Aggregating multiples would be nice
my @nodes;
foreach (@allnodeset) {
push @nodes,$_->{node};
}
my $nbyc_ref = nodesbycriteria(\@nodes,[$atom]);
if ($nbyc_ref)
{
my $nbyc = $nbyc_ref->{$atom};
if (defined $nbyc) {
return @$nbyc;
}
}
return ();
}
if ($atom =~ m/^[0-9]+\z/) { # if only numbers, then add the prefix
my $nodename=$nprefix.$atom.$nsuffix;
return expandatom($nodename,$verify);
}
my $nodelen=@nodes;
if ($nodelen > 0) {
return @nodes;
}
if ($atom =~ m/^\//) { # A regular expression
unless ($verify) { # If not in verify mode, regex makes zero possible sense
return ($atom);
}
#TODO: check against all groups
$atom = substr($atom,1);
foreach (@allnodeset) { #$nodelist->getAllAttribs('node')) {
if ($_->{node} =~ m/^${atom}$/) {
push(@nodes,$_->{node});
}
}
return(@nodes);
}
if ($atom =~ m/(.+?)\[(.+?)\](.*)/) { # square bracket range
# if there are more than 1 [], we picked off just the 1st. if there is another, we will process it later
my @subelems = split(/([\,\-\:])/,$2);
my $subrange="";
my $subelem;
my $start = $1;
my $ending = $3;
my $morebrackets = $ending =~ /\[.+?\]/; # if there are more brackets, we have to expand just the 1st part, then add the 2nd part later
while (scalar @subelems) {
my $subelem = shift @subelems;
my $subop=shift @subelems;
$subrange=$subrange."$start$subelem" . ($morebrackets?'':$ending) . "$subop";
}
foreach (split /,/,$subrange) {
my @newnodes=expandatom($_, ($morebrackets?0:$verify));
if (!$morebrackets) { push @nodes,@newnodes; }
else {
# for each of the new nodes, add the 2nd brackets and then expand
foreach my $n (@newnodes) {
push @nodes, expandatom("$n$ending", $verify);
}
}
}
return @nodes;
}
if ($atom =~ m/\+/) { # process the + operator
$atom =~ m/^(.*)([0-9]+)([^0-9\+]*)\+([0-9]+)/;
my ($front, $increment) = split(/\+/, $atom, 2);
my ($pref, $startnum, $dom) = $front =~ /^(.*?)(\d+)(\..+)?$/;
my $suf=$3;
my $end=$startnum+$increment;
my $endnum = sprintf("%d",$end);
if (length ($startnum) > length ($endnum)) {
$endnum = sprintf("%0".length($startnum)."d",$end);
}
if (($pref eq "") && ($suf eq "")) {
$pref=$nprefix;
$suf=$nsuffix;
}
foreach ("$startnum".."$endnum") {
my @addnodes=expandatom($pref.$_.$suf,$verify);
@nodes=(@nodes,@addnodes);
}
return (@nodes);
}
if ($atom =~ m/[-:]/) { # process the minus range operator
my $left;
my $right;
if ($atom =~ m/:/) {
($left,$right)=split /:/,$atom;
} else {
my $count= ($atom =~ tr/-//);
if (($count % 2)==0) { #can't understand even numbers of - in range context
if ($verify) {
push @$missingnodes,$atom;
return ();
} else { #but we might not really be in range context, if noverify
return ($atom);
}
}
my $expr="([^-]+?".("-[^-]*"x($count/2)).")-(.*)";
$atom =~ m/$expr/;
$left=$1;
$right=$2;
}
if ($left eq $right) { #if they said node1-node1 for some strange reason
return expandatom($left,$verify);
}
my @leftarr=split(/(\d+)/,$left);
my @rightarr=split(/(\d+)/,$right);
if (scalar(@leftarr) != scalar(@rightarr)) { #Mismatch formatting..
if ($verify) {
push @$missingnodes,$atom;
return (); #mismatched range, bail.
} else { #Not in verify mode, just have to guess it's meant to be a nodename
return ($atom);
}
}
my $prefix = "";
my $suffix = "";
foreach (0..$#leftarr) {
my $idx = $_;
if ($leftarr[$idx] =~ /^\d+$/ and $rightarr[$idx] =~ /^\d+$/) { #pure numeric component
if ($leftarr[$idx] ne $rightarr[$idx]) { #We have found the iterator (only supporting one for now)
my $prefix = join('',@leftarr[0..($idx-1)]); #Make a prefix of the pre-validated parts
my $luffix; #However, the remainder must still be validated to be the same
my $ruffix;
if ($idx eq $#leftarr) {
$luffix="";
$ruffix="";
} else {
$ruffix = join('',@rightarr[($idx+1)..$#rightarr]);
$luffix = join('',@leftarr[($idx+1)..$#leftarr]);
}
if ($luffix ne $ruffix) { #the suffixes mismatched..
if ($verify) {
push @$missingnodes,$atom;
return ();
} else {
return ($atom);
}
}
foreach ($leftarr[$idx]..$rightarr[$idx]) {
my @addnodes=expandatom($prefix.$_.$luffix,$verify);
push @nodes,@addnodes;
}
return (@nodes); #the return has been built, return, exiting loop and all
}
} elsif ($leftarr[$idx] ne $rightarr[$idx]) {
if ($verify) {
push @$missingnodes,$atom;
return ();
} else {
return ($atom);
}
}
$prefix .= $leftarr[$idx]; #If here, it means that the pieces were the same, but more to come
}
#I cannot conceive how the code could possibly be here, but whatever it is, it must be questionable
if ($verify) {
push @$missingnodes,$atom;
return (); #mismatched range, bail.
} else { #Not in verify mode, just have to guess it's meant to be a nodename
return ($atom);
}
}
if ($verify) {
push @$missingnodes,$atom;
return ();
} else {
return ($atom);
}
}
sub retain_cache { #A semi private operation to be used *ONLY* in the interesting Table<->NodeRange module interactions.
$retaincache=shift;
unless ($retaincache) { #take a call to retain_cache(0) to also mean that any existing
#cache must be zapped
if ($nodelist) { $nodelist->_build_cache(1); }
$glstamp=0;
$allnodesetstamp=0;
$allgrphashstamp=0;
undef $nodelist;
@allnodeset=();
%allnodehash=();
@grplist=();
$didgrouplist = 0;
%allgrphash=();
}
}
sub extnoderange { #An extended noderange function. Needed as the more straightforward function return format too simple for this.
my $range = shift;
my $namedopts = shift;
my $verify=1;
if ($namedopts->{skipnodeverify}) {
$verify=0;
}
my $return;
$retaincache=1;
$return->{node}=[noderange($range,$verify)];
if ($namedopts->{intersectinggroups}) {
my %grouphash=();
my $nlent;
foreach (@{$return->{node}}) {
$nlent=$nodelist->getNodeAttribs($_,['groups']); #TODO: move to noderange side cache
if ($nlent and $nlent->{groups}) {
foreach (split /,/,$nlent->{groups}) {
$grouphash{$_}=1;
}
}
}
$return->{intersectinggroups}=[sort keys %grouphash];
}
return $return;
}
sub abbreviate_noderange {
#takes a list of nodes or a string and abbreviates
my $nodes=shift;
my %grouphash;
my %sizedgroups;
my %nodesleft;
my %targetelems;
unless (ref $nodes) {
$nodes = noderange($nodes);
}
%nodesleft = map { $_ => 1 } @{$nodes};
unless ($nodelist) {
$nodelist =xCAT::Table->new('nodelist',-create =>1);
}
my $group;
foreach($nodelist->getAllAttribs('node','groups')) {
my @groups=split(/,/,$_->{groups}); #The where clause doesn't guarantee the atom is a full group name, only that it could be
foreach $group (@groups) {
push @{$grouphash{$group}},$_->{node};
}
}
foreach $group (keys %grouphash) {
#skip single node sized groups, these outliers frequently pasted into non-noderange capable contexts
if (scalar @{$grouphash{$group}} < 2) { next; }
push @{$sizedgroups{scalar @{$grouphash{$group}}}},$group;
}
my $node;
#use Data::Dumper;
#print Dumper(\%sizedgroups);
foreach (reverse sort {$a <=> $b} keys %sizedgroups) {
GROUP: foreach $group (@{$sizedgroups{$_}}) {
foreach $node (@{$grouphash{$group}}) {
unless (grep $node eq $_,keys %nodesleft) {
#this group contains a node that isn't left, skip it
next GROUP;
}
}
foreach $node (@{$grouphash{$group}}){
delete $nodesleft{$node};
}
$targetelems{$group}=1;
}
}
return (join ',',keys %targetelems,keys %nodesleft);
}
sub noderange {
$missingnodes=[];
#We for now just do left to right operations
my $range=shift;
$range =~ s/['"]//g;
my $verify = (scalar(@_) >= 1 ? shift : 1);
#excludenodes attribute in site table,
#these nodes should be excluded for any xCAT commands
my $exsitenode = (scalar(@_) >= 1 ? shift : 1);
unless ($nodelist) {
$nodelist =xCAT::Table->new('nodelist',-create =>1);
$nodelist->_set_use_cache(0); #TODO: a more proper external solution
@cachedcolumns = ('node','groups');
$nodelist->_build_cache(\@cachedcolumns,noincrementref=>1);
$nodelist->_set_use_cache(1); #TODO: a more proper external solution
}
my %nodes = ();
my %delnodes = ();
my $op = ",";
my @elems = split(/(,(?![^[]*?])(?![^\(]*?\)))/,$range); # commas outside of [] or ()
if (scalar(@elems)==1) {
@elems = split(/(@(?![^\(]*?\)))/,$range); # only split on @ when no , are present (inner recursion)
}
while (my $atom = shift @elems) {
if ($atom eq ',') {
next;
}
if ($atom =~ /^-/) { # if this is an exclusion, strip off the minus, but remember it
$atom = substr($atom,1);
$op = $op."-";
}
if ($atom =~ /^\^(.*)$/) { # get a list of nodes from a file
open(NRF,$1);
while (<NRF>) {
my $line=$_;
unless ($line =~ m/^[\^#]/) {
$line =~ m/^([^: ]*)/;
my $newrange = $1;
chomp($newrange);
$recurselevel++;
my @filenodes = noderange($newrange);
foreach (@filenodes) {
$nodes{$_}=1;
}
}
}
close(NRF);
next;
}
my %newset = map { $_ =>1 } expandatom($atom,$verify); # expand the atom and make each entry in the resulting array a key in newset
if ($op =~ /@/) { # compute the intersection of the current atom and the node list we have received before this
foreach (keys %nodes) {
unless ($newset{$_}) {
delete $nodes{$_};
}
}
} elsif ($op =~ /,-/) { # add the nodes from this atom to the exclude list
foreach (keys %newset) {
$delnodes{$_}=1; #delay removal to end
}
} else { # add the nodes from this atom to the total node list
foreach (keys %newset) {
$nodes{$_}=1;
}
}
$op = shift @elems;
} # end of main while loop
# Exclude the nodes in site attribute excludenodes?
if ($exsitenode) {
my $badnoderange = 0;
my @badnodes = ();
if ($::XCATSITEVALS{excludenodes}) {
@badnodes = noderange($::XCATSITEVALS{excludenodes}, 1, 0);
foreach my $bnode (@badnodes) {
if (!$delnodes{$bnode}) {
$delnodes{$bnode} = 1;
}
}
}
}
# Now remove all the exclusion nodes
foreach (keys %nodes) {
if ($delnodes{$_}) {
delete $nodes{$_};
}
}
if ($recurselevel) {
$recurselevel--;
}
return sort (keys %nodes);
}
1;
=head1 NAME
xCAT::NodeRange - Perl module for xCAT noderange expansion
=head1 SYNOPSIS
use xCAT::NodeRange;
my @nodes=noderange("storage@rack1,node[1-200],^/tmp/nodelist,node300-node400,node401+10,500-550");
=head1 DESCRIPTION
noderange interprets xCAT noderange formatted strings and returns a list of xCAT nodelists. The following two operations are supported on elements, and interpreted left to right:
, union next element with everything to the left.
@ take intersection of element to the right with everything on the left (i.e. mask out anything to the left not belonging to what is described to the right)
Each element can be a number of things:
A node name, i.e.:
=item * node1
A hyphenated node range (only one group of numbers may differ between the left and right hand side, and those numbers will increment in a base 10 fashion):
node1-node200 node1-compute-node200-compute
node1:node200 node1-compute:node200-compute
A noderange denoted by brackets:
node[1-200] node[001-200]
A regular expression describing the noderange:
/d(1.?.?|200)
A node plus offset (this increments the first number found in nodename):
node1+199
And most of the above substituting groupnames.
3C
3C
NodeRange tries to be intelligent about detecting padding, so you can:
node001-node200
And it will increment according to the pattern.
=head1 AUTHOR
Jarrod Johnson (jbjohnso@us.ibm.com)
=head1 COPYRIGHT
Copyright 2007 IBM Corp. All rights reserved.
=cut