2328 lines
67 KiB
Plaintext
2328 lines
67 KiB
Plaintext
|
#!/usr/bin/perl -w
|
||
|
# avoid shell starter method here - otherwise we cannot kill the daemon
|
||
|
use 5.008; ## minimum perl version
|
||
|
use strict;
|
||
|
use threads; ## REMOVE FOR UNTHREADED PERL
|
||
|
use Getopt::Std qw( getopts );
|
||
|
use POSIX qw( );
|
||
|
import License;
|
||
|
import GridEngine;
|
||
|
import Qconf;
|
||
|
|
||
|
my ($releaseDate) = qw( 2010-01-20 );
|
||
|
my ( $Path, $Script ) = map { m{^(.+)/([^/]+)$} } $0; # instead of fileparse
|
||
|
|
||
|
################################################################################
|
||
|
################################################################################
|
||
|
# CUSTOMIZE THESE SETTINGS TO MATCH YOUR REQUIREMENTS:
|
||
|
#
|
||
|
my $config = {
|
||
|
## file locations: can only be overwritten by command-line parameters
|
||
|
-config => "$Path/qlicserver.config",
|
||
|
-limits => "$Path/qlicserver.limits",
|
||
|
|
||
|
## fallback configuration - can be removed or left empty as desired
|
||
|
-defaultConfig => qq{
|
||
|
<?xml version="1.0"?>
|
||
|
<qlicserverConfig>
|
||
|
<!--
|
||
|
<parameters type="overwrite">
|
||
|
<param name="delay">30</param>
|
||
|
<param name="timeout">10</param>
|
||
|
<param name="ppid">ppid</param>
|
||
|
</parameters>
|
||
|
-->
|
||
|
<!--
|
||
|
| Map resource names (complexes) to values (features)
|
||
|
| served by the license manager(s).
|
||
|
| Resources without a "served" attribute are considered internal
|
||
|
| Resources with type "track" are reported but not managed
|
||
|
| The urgency is how much extra weight to give to particular resources.
|
||
|
| If a 'slot' is weighted with 1000, we can give an extra 10% to
|
||
|
| a few resources. [Format: INTEGER]
|
||
|
|
|
||
|
| The resource 'limit' specifies an upper limit to prevent applications
|
||
|
| from flooding the cluster and/or prevent jobs from consuming all the
|
||
|
| available licenses. A negative limit is subtracted from the total
|
||
|
| to obtain the limit. [Format: INTEGER]
|
||
|
|
|
||
|
| Derived resources are combined from sub-elements and inherit the
|
||
|
| limits from their sub-elements.
|
||
|
|
|
||
|
| Resources that are not served and not derived are internal resources
|
||
|
+ -->
|
||
|
<!--
|
||
|
<resources type="overwrite">
|
||
|
<!-- cfd applications
|
||
|
<resource name="foam"/>
|
||
|
<resource name="starjob" served="starpar" type="job" urgency="100" note="STAR-CD parallel starter"/>
|
||
|
<resource name="starp" served="hpcdomains" note="STAR-CD parallel"/>
|
||
|
<resource name="stars" served="starsuite" limit="2" note="STAR-CD serial"/>
|
||
|
<derived name="starcd">
|
||
|
<element>starp</element>
|
||
|
<element>stars</element>
|
||
|
</derived>
|
||
|
|
||
|
<!-- fea applications
|
||
|
<resource name="abaqus" served="abaqus" type="job"/>
|
||
|
<resource name="hyper" served="GridWorks"/>
|
||
|
<resource name="nastran1" served="NASTRAN" from="auglic1"/>
|
||
|
<resource name="nastran2" served="NASTRAN" from="auglic2"/>
|
||
|
<derived name="nastran">
|
||
|
<element>nastran1</element>
|
||
|
<element>nastran2</element>
|
||
|
</derived>
|
||
|
<resource name="thomat" note="abaqus high temp modelling" type="job"/>
|
||
|
|
||
|
<!-- other applications
|
||
|
<resource name="gtpower" served="GTpowerX" limit="4"/>
|
||
|
|
||
|
<!-- tracked applications
|
||
|
<resource name="gtise" served="GTise" type="track"/>
|
||
|
<resource name="hexa" served="aihexa" type="track"/>
|
||
|
<resource name="med" served="aimed" type="track"/>
|
||
|
<resource name="proam" served="proam" type="track"/>
|
||
|
<resource name="prostar" served="prostar" type="track"/>
|
||
|
|
||
|
</resources>
|
||
|
-->
|
||
|
</qlicserverConfig>
|
||
|
},
|
||
|
|
||
|
};
|
||
|
|
||
|
#
|
||
|
#
|
||
|
# END OF CUSTOMIZE SETTINGS
|
||
|
################################################################################
|
||
|
################################################################################
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
sub usage {
|
||
|
$! = 0; # clean exit
|
||
|
warn "@_\n" if @_;
|
||
|
die <<"USAGE";
|
||
|
usage: $Script [OPTION] [PARAM]
|
||
|
Query availability of floating licenses for the GridEngine.
|
||
|
|
||
|
help/debug options:
|
||
|
-h help
|
||
|
|
||
|
initialization options:
|
||
|
-c show complex definitions (format as per 'qconf -sc')
|
||
|
for possible inclusion via 'qconf -Mc ...'
|
||
|
|
||
|
-C provide initial values for
|
||
|
'qconf -mattr exechost complex_values ... global'
|
||
|
|
||
|
-i information about license features
|
||
|
(generates text for the config lookup table)
|
||
|
|
||
|
query options:
|
||
|
-l resource=value,...
|
||
|
|
||
|
similar to qsub(1), query the license server for the availability
|
||
|
of the requested resources. A missing value is treated as 1.
|
||
|
The resource 'slots' will be used to scale the resource requests
|
||
|
as required. Prints the resources available and exits with '99' if
|
||
|
the condition cannot be satisfied. Only externally served resources
|
||
|
are checked - resources internal to the GridEngine should never
|
||
|
need this check.
|
||
|
|
||
|
-n suppress adjustment of the managed licenses (useful for testing)
|
||
|
|
||
|
daemon options:
|
||
|
-d run query as a daemon
|
||
|
|
||
|
-k kill running daemon
|
||
|
|
||
|
-w wake-up daemon from sleep
|
||
|
|
||
|
params:
|
||
|
dir=DIR
|
||
|
base directory for output,qhost,qstat parameters
|
||
|
|
||
|
output=FILE
|
||
|
save query status to FILE
|
||
|
|
||
|
qhost=FILE
|
||
|
add extra qhost query and save status to FILE
|
||
|
|
||
|
qstat=FILE
|
||
|
save qstat query to FILE
|
||
|
|
||
|
timeout=N
|
||
|
command timeout in seconds (default: 10 seconds)
|
||
|
|
||
|
LM_LICENSE_FILE=STRING
|
||
|
override environment setting for server query
|
||
|
|
||
|
lmutil=STRING
|
||
|
fully qualified path to lmutil command
|
||
|
|
||
|
SGE_CLUSTER_NAME=STRING
|
||
|
provide cluster name
|
||
|
|
||
|
static params:
|
||
|
delay=N
|
||
|
waiting period in seconds between queries in daemon mode
|
||
|
(a delay of 0 is interpreted as 30 seconds)
|
||
|
|
||
|
ppid=(ppid | N | CMD)
|
||
|
which parent process id to watch in daemon mode.
|
||
|
This can be decisive for migration etc.
|
||
|
ppid = watch the lauching parent (default)
|
||
|
CMD = watch a particular process
|
||
|
N = watch a particular pid
|
||
|
|
||
|
command-line params:
|
||
|
debug emit debug information for the developer
|
||
|
|
||
|
config=FILE
|
||
|
specify alternative configuration file
|
||
|
(default: $config->{-config})
|
||
|
|
||
|
limits=FILE
|
||
|
specify alternative limits file/directory
|
||
|
(default: $config->{-limits})
|
||
|
|
||
|
This program has 2 major modes:
|
||
|
1. Adjust the number of managed licenses, based on license availability
|
||
|
and the number of granted resources (as determined by 'qstat') using the
|
||
|
'qconf -mattr exechost complex_values ... global' command
|
||
|
|
||
|
2. Query the license server for the availability of requested resources.
|
||
|
Exit with '99' (requeue) if the condition cannot be satisfied.
|
||
|
Prints the resources available.
|
||
|
|
||
|
FILES:
|
||
|
The configuration can be hardcoded into this program and/or controlled
|
||
|
via an XML configuration file:
|
||
|
$config->{-config}
|
||
|
|
||
|
The current limits for the resources are specified here:
|
||
|
$config->{-limits}
|
||
|
|
||
|
This can be either an XML file, or a directory.
|
||
|
When it is a directory, the limits are specified as a single digit
|
||
|
in each file that corresponds to a resource name.
|
||
|
|
||
|
NOTES:
|
||
|
Further information about the configuration can be found on the wiki
|
||
|
http://wiki.gridengine.info/wiki/index.php/Olesen-FLEXlm-Configuration
|
||
|
|
||
|
This code is provided as a courtesy to other users with absolutely no
|
||
|
guarantees! Post usage questions to the users\@gridengine.sunsource.net
|
||
|
mailing list - please do not email the author directly.
|
||
|
|
||
|
version ($releaseDate)
|
||
|
copyright (c) 2003-10 <Mark.Olesen\@faurecia.com>
|
||
|
|
||
|
Licensed and distributed under the Creative Commons
|
||
|
Attribution-NonCommercial-ShareAlike 3.0 License.
|
||
|
http://creativecommons.org/licenses/by-nc-sa/3.0
|
||
|
USAGE
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
my ( %opt, %cmdParam );
|
||
|
getopts( "hcCdikl:nw", \%opt ) or usage();
|
||
|
$opt{h} and usage();
|
||
|
my $Debugging;
|
||
|
|
||
|
# extract command-line parameters of the form param=value
|
||
|
for (@ARGV) {
|
||
|
if (/^([A-Za-z]\w*)=(.+?)$/) {
|
||
|
$cmdParam{$1} = $2;
|
||
|
}
|
||
|
elsif (/^([A-Za-z]\w*)$/) {
|
||
|
$cmdParam{$1} = undef;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# add debugging
|
||
|
if ( exists $cmdParam{debug} ) {
|
||
|
$Debugging++;
|
||
|
}
|
||
|
|
||
|
# override file locations: command-line parameters only
|
||
|
for (qw( config limits )) {
|
||
|
if ( exists $cmdParam{$_} ) {
|
||
|
$config->{"-$_"} = $cmdParam{$_};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
|
||
|
#
|
||
|
# change hash references to a comma-delimited string of key=value entries
|
||
|
#
|
||
|
sub hashrefToString {
|
||
|
join ',' => map {
|
||
|
my $r = $_;
|
||
|
ref $r ? join ',' => map { "$_=$r->{$_}" } sort keys %$r : '';
|
||
|
} @_;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# extract attrib="value" ... attrib="value"
|
||
|
#
|
||
|
sub parseXMLattrib {
|
||
|
my $str = shift || '';
|
||
|
my %attr;
|
||
|
|
||
|
while ($str =~ s{^\s*(\w+)=\"([^\"]*)\"}{}s
|
||
|
or $str =~ s{^\s*(\w+)=\'([^\']*)\'}{}s )
|
||
|
{
|
||
|
$attr{$1} = $2;
|
||
|
}
|
||
|
|
||
|
%attr;
|
||
|
}
|
||
|
|
||
|
|
||
|
#
|
||
|
# resolve output file name from the config->{-parameter}
|
||
|
# relative to output 'dir'
|
||
|
# stdout (-) and absolute names are left untouched,
|
||
|
# as are names in the current working directory (starting with "./")
|
||
|
#
|
||
|
sub resolveOutputFile {
|
||
|
my $name = shift;
|
||
|
|
||
|
my $file;
|
||
|
my $dir = $config->{-parameter}{dir};
|
||
|
if ( exists $config->{-parameter}{$name}
|
||
|
and defined $config->{-parameter}{$name} )
|
||
|
{
|
||
|
$file = $config->{-parameter}{$name};
|
||
|
|
||
|
if ( defined $dir
|
||
|
and length $dir
|
||
|
and $file !~ m{^\.?/}
|
||
|
and $file ne "-" )
|
||
|
{
|
||
|
-d $dir or mkdir $dir;
|
||
|
$file = "$dir/$file";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $file;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# update the configuration as required
|
||
|
#
|
||
|
sub updateConfig {
|
||
|
my $configFile = $config->{-config};
|
||
|
my $defaultConfig = $config->{-defaultConfig};
|
||
|
|
||
|
$config->{-configUpdate} ||= 0; # previous file update time
|
||
|
|
||
|
my $needUpdate;
|
||
|
keys %{ $config->{-resources} } or $needUpdate++; # first-time
|
||
|
|
||
|
my $fileString;
|
||
|
if ( defined $configFile and -f $configFile and -r _ ) {
|
||
|
my $mtime = ( stat $configFile )[9];
|
||
|
|
||
|
if ( $config->{-configUpdate} < $mtime ) {
|
||
|
$fileString = do {
|
||
|
local *FILE;
|
||
|
local $/;
|
||
|
if ( open FILE, $configFile ) {
|
||
|
$needUpdate++;
|
||
|
<FILE>;
|
||
|
}
|
||
|
else {
|
||
|
undef;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
$config->{-configUpdate} = $mtime;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return unless $needUpdate;
|
||
|
|
||
|
# clear old values
|
||
|
$config->{-parameter} = {}; # command-line and file '<param>' entries
|
||
|
$config->{-resources} = {}; # all the resources, original parameters
|
||
|
$config->{-derived} = {}; # derived resources only
|
||
|
$config->{-intern} = {}; # internal resources only
|
||
|
$config->{-managed} = {}; # managed internal/external/derived resources
|
||
|
$config->{-lookup} = {}; # reverse lookup (complex -> resource)
|
||
|
$config->{-mapFrom} = {}; # (optional) mapping based on server
|
||
|
|
||
|
# config precedence:
|
||
|
# -defaultConfig (hard-coded)
|
||
|
# -config (FILE)
|
||
|
|
||
|
# parameters precedence:
|
||
|
# -defaultConfig (hard-coded)
|
||
|
# -config (FILE)
|
||
|
# command-line
|
||
|
|
||
|
my ( %cfg, %param );
|
||
|
for ( $defaultConfig, $fileString ) {
|
||
|
defined or next;
|
||
|
|
||
|
# strip out all xml comments
|
||
|
s{<!--.*?-->\s*}{}sg;
|
||
|
|
||
|
## an overwrite mechanism for 'parameters' and 'resources'
|
||
|
if (s{<(parameters|resources) \s*([^<>]+) >}{}sx) {
|
||
|
my ( $tag, $attr ) = ( $1, $2 );
|
||
|
my %attr = parseXMLattrib($attr);
|
||
|
my $type = delete $attr{type};
|
||
|
if ( defined $type and $type eq "overwrite" ) {
|
||
|
if ( $tag eq "parameters" ) {
|
||
|
%param = ();
|
||
|
}
|
||
|
elsif ( $tag eq "resources" ) {
|
||
|
%cfg = ();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
## process <param ...> .. </param>
|
||
|
while (s{<param \s+([^<>]+) > (.+?) </param>}{}sx) {
|
||
|
my ( $attr, $value ) = ( $1, $2 );
|
||
|
my %attr = parseXMLattrib($attr);
|
||
|
my $name = delete $attr{name};
|
||
|
if ( defined $name ) {
|
||
|
$value =~ s{^\s+|\s+$}{}g;
|
||
|
$param{$name} = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
## process <resource .../> and <resource ...> .. </resource>
|
||
|
while (s{<resource \s+([^<>]+?) />}{}sx
|
||
|
or s{<resource \s+([^<>]+) > (.*?) </resource>}{}sx )
|
||
|
{
|
||
|
my ( $attr, undef ) = ( $1, $2 );
|
||
|
my %attr = parseXMLattrib($attr);
|
||
|
my $name = delete $attr{name};
|
||
|
## overwrite old value
|
||
|
if ( defined $name ) {
|
||
|
$cfg{$name} = {%attr};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
## process <derived ...> CONTENT </derived>
|
||
|
while (s{<derived \s+([^<>]+) > (.+?) </derived>}{}sx) {
|
||
|
my ( $attr, $content ) = ( $1, $2 );
|
||
|
my %attr = parseXMLattrib($attr);
|
||
|
my $name = delete $attr{name};
|
||
|
if ( defined $name ) {
|
||
|
delete $attr{served}; # derived are not served
|
||
|
delete $cfg{$name};
|
||
|
|
||
|
my @elem;
|
||
|
## process <element> ... </element>
|
||
|
while ( $content =~ s{<element> \s*(\w+)\s* </element>}{}sx ) {
|
||
|
push @elem, $1;
|
||
|
}
|
||
|
|
||
|
if (@elem) {
|
||
|
$cfg{$name} = {%attr};
|
||
|
$cfg{$name}{element} = [@elem];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for ( keys %cmdParam ) {
|
||
|
$param{$_} = $cmdParam{$_};
|
||
|
}
|
||
|
|
||
|
# assign the parameters
|
||
|
%{ $config->{-parameter} } = %param;
|
||
|
|
||
|
for my $name ( keys %cfg ) {
|
||
|
## All managed complexes are 'consumable' (mark as zero)
|
||
|
## unless otherwise noted
|
||
|
my $type;
|
||
|
if ( exists $cfg{$name}{type} ) {
|
||
|
$type = $cfg{$name}{type};
|
||
|
}
|
||
|
$type ||= 0;
|
||
|
|
||
|
if ( exists $cfg{$name}{element} ) {
|
||
|
## transfer derived information
|
||
|
# NB: probably can only have normal consumables
|
||
|
$config->{-derived}{$name} = delete $cfg{$name};
|
||
|
}
|
||
|
elsif ( exists $cfg{$name}{served} ) {
|
||
|
## create served -> resource lookup
|
||
|
my $served = $cfg{$name}{served};
|
||
|
|
||
|
if ( exists $cfg{$name}{from} ) {
|
||
|
## insert server-specific remapping, server name in lowercase
|
||
|
for ( map { split } lc $cfg{$name}{from} ) {
|
||
|
$config->{-mapFrom}{$_}{$served} = $name;
|
||
|
|
||
|
# since remapping occurs in the query,
|
||
|
# '-lookup' is an identity
|
||
|
$config->{-lookup}{$name} = [ $name, $type ];
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
$config->{-lookup}{$served} = [ $name, $type ];
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
## not served and not derived -> internal resource
|
||
|
## transfer information
|
||
|
$config->{-intern}{$name} = delete $cfg{$name};
|
||
|
}
|
||
|
|
||
|
## only tracked resources are unmanaged
|
||
|
$config->{-managed}{$name} = $type unless $type =~ /track/i;
|
||
|
}
|
||
|
|
||
|
# assign the rest
|
||
|
%{ $config->{-resources} } = %cfg;
|
||
|
|
||
|
## TODO:
|
||
|
## check that the derived type is consistently job/non-job
|
||
|
|
||
|
# update parameters:
|
||
|
# adjust timeout - the license server is the Achilles heel
|
||
|
if ( exists $config->{-parameter}{timeout} ) {
|
||
|
Shell->timeout( $config->{-parameter}{timeout} );
|
||
|
}
|
||
|
|
||
|
# adjust the license manager environment(s) and command(s)
|
||
|
for (@License::Manager) {
|
||
|
eval {
|
||
|
my $name = $_->envname();
|
||
|
if ( defined $name and exists $config->{-parameter}{$name} ) {
|
||
|
$_->setenv( $config->{-parameter}{$name} );
|
||
|
}
|
||
|
};
|
||
|
|
||
|
eval {
|
||
|
my $name = $_->cmdname();
|
||
|
if ( defined $name and exists $config->{-parameter}{$name} ) {
|
||
|
$_->setcmd( $config->{-parameter}{$name} );
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# extract limits from the specified file:
|
||
|
# <?xml version="1.0"?>
|
||
|
# <qlicserverLimits>
|
||
|
# <limits>
|
||
|
# <limit name="gtpower" limit="7"/>
|
||
|
# <limit name="stars" limit="2"/>
|
||
|
# <limit name="starp" limit="20"/>
|
||
|
# </limits>
|
||
|
# </qlicserverLimits>
|
||
|
#
|
||
|
# OR from files within the specified directory:
|
||
|
# The limits are specified as a single digit in each file that corresponds
|
||
|
# to a resource name. Negative limits are deducted from the total.
|
||
|
#
|
||
|
sub updateLimits {
|
||
|
my $diskValues = $config->{-limits};
|
||
|
my $limits;
|
||
|
|
||
|
# get defaults
|
||
|
for my $href (
|
||
|
$config->{-intern}, ##
|
||
|
$config->{-resources}, ##
|
||
|
$config->{-derived}, ##
|
||
|
)
|
||
|
{
|
||
|
for my $name ( keys %$href ) {
|
||
|
if ( exists $href->{$name}{limit} ) {
|
||
|
my $limit = $href->{$name}{limit};
|
||
|
if ( defined $limit ) {
|
||
|
$limits->{$name} = $limit;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
my $fileString;
|
||
|
if ( defined $diskValues and -f $diskValues ) {
|
||
|
## read from a single file (xml format)
|
||
|
$fileString = do {
|
||
|
local *FILE;
|
||
|
local $/;
|
||
|
if ( open FILE, $diskValues ) {
|
||
|
<FILE>;
|
||
|
}
|
||
|
else {
|
||
|
undef;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
elsif ( defined $diskValues and -d $diskValues ) {
|
||
|
## read from multiple files (text format)
|
||
|
local *DIR;
|
||
|
my $dir = $diskValues;
|
||
|
if ( opendir DIR, $dir ) {
|
||
|
my @files = grep { -f "$dir/$_" and -s _ } readdir DIR;
|
||
|
for my $name (@files) {
|
||
|
my $limit;
|
||
|
|
||
|
# use the last value
|
||
|
if ( open FILE, "$dir/$name" ) {
|
||
|
$limit = ( map { /^\s*(-?\d+)\s*$/ } <FILE> )[-1];
|
||
|
}
|
||
|
if ( defined $limit ) {
|
||
|
$limits->{$name} = $limit;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for ($fileString) {
|
||
|
defined or next;
|
||
|
|
||
|
# strip out all xml comments
|
||
|
s{<!--.*?-->\s*}{}sg;
|
||
|
|
||
|
## process <limit .../> and <limit ...></limit>
|
||
|
while (s{<limit \s+([^/<>]+) />}{}sx
|
||
|
or s{<limit \s+([^/<>]+) >\s*</limit>}{}sx )
|
||
|
{
|
||
|
my %attr = parseXMLattrib($1);
|
||
|
my $name = delete $attr{name};
|
||
|
my $limit = delete $attr{limit};
|
||
|
if ( defined $name and defined $limit ) {
|
||
|
$limits->{$name} = $limit;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# negative limits on internal resources are only possible
|
||
|
# when a total is known
|
||
|
for my $name ( keys %$limits ) {
|
||
|
if ( $limits->{$name} < 0
|
||
|
and exists $config->{-intern}{$name}
|
||
|
and not exists $config->{-intern}{$name}{total} )
|
||
|
{
|
||
|
delete $limits->{$name};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$limits;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# Prototype: mungeLicenses( HASHREF1 [, HASHREF2, [, HASHREF3]] )
|
||
|
#
|
||
|
# HASHREF1 => { # from the license manager
|
||
|
# feature => {
|
||
|
# total => NUM,
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# "*user@machine" => NUM, ## waiting licenses
|
||
|
# ...
|
||
|
# },
|
||
|
# }
|
||
|
#
|
||
|
# HASHREF2 => { # from qstat
|
||
|
# complex => {
|
||
|
# waiting => {
|
||
|
# "user" => NUM,
|
||
|
# },
|
||
|
# jobid => {
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# ...
|
||
|
# },
|
||
|
# total => NUM, # iff. an internal tracked value
|
||
|
# },
|
||
|
# }
|
||
|
#
|
||
|
# HASHREF3 => { # ulimit
|
||
|
# complex => NUM,
|
||
|
# }
|
||
|
#
|
||
|
# munge into
|
||
|
#
|
||
|
# HASHREF => {
|
||
|
# complex => {
|
||
|
# extern => NUM,
|
||
|
# intern => NUM,
|
||
|
# limit => NUM,
|
||
|
# total => NUM,
|
||
|
# waiting => NUM,
|
||
|
# served => STRING,
|
||
|
# users => {
|
||
|
# extern => { "user@machine" => NUM, },
|
||
|
# intern => { "user@machine" => NUM, },
|
||
|
# waiting => { "user" => NUM, },
|
||
|
# },
|
||
|
# },
|
||
|
# }
|
||
|
#
|
||
|
sub mungeLicenses {
|
||
|
my $served = shift;
|
||
|
my $consumed = shift || {};
|
||
|
my $limits = shift || {};
|
||
|
my $report = {};
|
||
|
|
||
|
#
|
||
|
# cast the interesting features into the desired format.
|
||
|
# include 'intern' usage, but do not adjust 'extern' yet.
|
||
|
#
|
||
|
for my $feature ( keys %$served ) {
|
||
|
my $externUsers = $served->{$feature} or next;
|
||
|
exists $config->{-lookup}{$feature} or next;
|
||
|
my ( $resource, $type ) = @{ $config->{-lookup}{$feature} };
|
||
|
|
||
|
# remove 'total' from hash
|
||
|
my $total = delete $externUsers->{total} || 0;
|
||
|
|
||
|
# internal job allocation, jobs waiting
|
||
|
my $internUsers = delete( $consumed->{$resource} ) || {};
|
||
|
my $waitingUsers = delete( $internUsers->{waiting} ) || {};
|
||
|
|
||
|
# potential management limits
|
||
|
# negative limit implies subtract from total
|
||
|
my $limit = $limits->{$resource};
|
||
|
if ( defined $limit ) {
|
||
|
$limit += $total if $limit < 0;
|
||
|
$limit = 0 if $limit < 0;
|
||
|
}
|
||
|
defined $limit and $limit < $total or $limit = $total;
|
||
|
|
||
|
$report->{$resource} = {
|
||
|
type => $type,
|
||
|
served => $feature,
|
||
|
total => $total,
|
||
|
limit => $limit,
|
||
|
users => {
|
||
|
extern => $externUsers,
|
||
|
intern => $internUsers,
|
||
|
waiting => $waitingUsers,
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# add in internal features
|
||
|
#
|
||
|
for my $resource ( keys %$consumed ) {
|
||
|
my $total = delete $consumed->{$resource}{total};
|
||
|
defined $total or next;
|
||
|
|
||
|
# internal job allocation, jobs waiting
|
||
|
my $internUser = delete( $consumed->{$resource} ) || {};
|
||
|
my $waitingUser = delete( $internUser->{waiting} ) || {};
|
||
|
|
||
|
# potential management limits
|
||
|
# negative limit implies subtract from total
|
||
|
my $limit = $limits->{$resource};
|
||
|
if ( defined $limit ) {
|
||
|
$limit += $total if $limit < 0;
|
||
|
$limit = 0 if $limit < 0;
|
||
|
}
|
||
|
defined $limit and $limit < $total or $limit = $total;
|
||
|
|
||
|
$report->{$resource} = {
|
||
|
type => "intern",
|
||
|
total => $total,
|
||
|
limit => $limit,
|
||
|
users => {
|
||
|
extern => {},
|
||
|
intern => $internUser,
|
||
|
waiting => $waitingUser,
|
||
|
},
|
||
|
};
|
||
|
}
|
||
|
|
||
|
# derived resources
|
||
|
# - external licenses are the external licenses of the components
|
||
|
# - the derived sub-resources may be reported/managed themselves
|
||
|
# or simply available directly from the server
|
||
|
for my $resource ( keys %{ $config->{-derived} } ) {
|
||
|
my $internUser = delete( $consumed->{$resource} ) || {};
|
||
|
my $waitingUser = delete( $internUser->{waiting} ) || {};
|
||
|
|
||
|
my $entry = $report->{$resource} = {
|
||
|
total => 0,
|
||
|
limit => 0,
|
||
|
users => {
|
||
|
extern => {},
|
||
|
intern => $internUser,
|
||
|
waiting => $waitingUser,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
for my $subResource ( @{ $config->{-derived}{$resource}{element} } ) {
|
||
|
my $part;
|
||
|
|
||
|
## reported sub-resource - already in the correct structure
|
||
|
if ( exists $report->{$subResource} ) {
|
||
|
$part = $report->{$subResource};
|
||
|
}
|
||
|
elsif ( exists $served->{$subResource} ) {
|
||
|
## served sub-resource - adjust into correct structure
|
||
|
$part = { -extern => { %{ $served->{$subResource} } } };
|
||
|
my $total = delete $part->{-extern}{total} || 0;
|
||
|
|
||
|
$part->{total} = $part->{limit} = $total;
|
||
|
}
|
||
|
|
||
|
defined $part or next; # not reported/managed and not served
|
||
|
|
||
|
# collect total/limit and extern
|
||
|
$entry->{total} += $part->{total} || 0;
|
||
|
$entry->{limit} += $part->{limit} || 0;
|
||
|
for ( keys %{ $part->{users}{extern} } ) {
|
||
|
$entry->{users}{extern}{$_} += $part->{users}{extern}{$_};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# the specified limit might be more stringent than that determined
|
||
|
# from the sub-resources
|
||
|
my $limit = $limits->{$resource};
|
||
|
if ( defined $limit ) {
|
||
|
if ( $entry->{limit} > $limit ) {
|
||
|
$limit += $entry->{total} if $limit < 0;
|
||
|
$limit = 0 if $limit < 0;
|
||
|
$entry->{limit} = $limit;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# - remove usage that is already accounted for
|
||
|
# - remove non-existent / implausible entry
|
||
|
# - prepend jobid.taskid with -ve to prevent it from being
|
||
|
# processed more than once
|
||
|
my $juggle = sub {
|
||
|
my ( $externUser, $internUser ) = @_;
|
||
|
|
||
|
for my $jobIdent ( grep { /^\d+[\.\d]*$/ } keys %$internUser ) {
|
||
|
for ( keys %{ $internUser->{$jobIdent} } ) {
|
||
|
if ( $externUser->{$_}
|
||
|
and $externUser->{$_} >= $internUser->{$jobIdent}{$_} )
|
||
|
{
|
||
|
$externUser->{$_} -= $internUser->{$jobIdent}{$_};
|
||
|
$internUser->{"-$jobIdent"}{$_} =
|
||
|
delete $internUser->{$jobIdent}{$_};
|
||
|
$externUser->{$_} > 0 or delete $externUser->{$_};
|
||
|
}
|
||
|
}
|
||
|
## remove empty hash references
|
||
|
keys %{ $internUser->{$jobIdent} }
|
||
|
or delete $internUser->{$jobIdent};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
for my $resource ( keys %$report ) {
|
||
|
my $entry = $report->{$resource};
|
||
|
my $externUsers = $entry->{users}{extern} or next; # cannot happen
|
||
|
my $internUsers = $entry->{users}{intern} or next;
|
||
|
my $waitingUsers = $entry->{users}{waiting} ||= {};
|
||
|
|
||
|
#
|
||
|
# juggle extern/intern consumption
|
||
|
#
|
||
|
$juggle->( $externUsers, $internUsers );
|
||
|
|
||
|
#
|
||
|
# reduce extern/intern user to canonical form
|
||
|
# "user@host" => count
|
||
|
#
|
||
|
for ( [ extern => $externUsers ], [ intern => $internUsers ] ) {
|
||
|
my ( $label, $ref ) = @$_;
|
||
|
for my $r ( $label =~ /intern/ ? values %$ref : $ref ) {
|
||
|
my %hash;
|
||
|
for ( keys %$r ) {
|
||
|
my ( $key, $value ) = split;
|
||
|
defined $value or $value = 1; # for pre-reduced format
|
||
|
my $count = $r->{$_};
|
||
|
$hash{$key} += $value * $count;
|
||
|
}
|
||
|
%$r = %hash;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# juggle again - licenses may be split across several groups or servers
|
||
|
#
|
||
|
$juggle->( $externUsers, $internUsers );
|
||
|
|
||
|
#
|
||
|
# collapse one level of indirection and drop job numbers
|
||
|
# user/intern => {
|
||
|
# jobid => {
|
||
|
# "user@machine" => count,
|
||
|
# },
|
||
|
# },
|
||
|
# -->
|
||
|
# user/intern => {
|
||
|
# "user@machine" => count,
|
||
|
# },
|
||
|
%$internUsers = do {
|
||
|
my %hash;
|
||
|
for my $ref ( values %$internUsers ) {
|
||
|
$hash{$_} += $ref->{$_} for keys %$ref;
|
||
|
}
|
||
|
%hash;
|
||
|
};
|
||
|
|
||
|
# add licenses reported as waiting by FlexLM
|
||
|
for ( grep { /^\*/ } keys %$externUsers ) {
|
||
|
$waitingUsers->{$_} += delete $externUsers->{$_};
|
||
|
}
|
||
|
|
||
|
# remove needless limiters
|
||
|
if ( $entry->{limit} >= $entry->{total} ) {
|
||
|
delete $entry->{limit};
|
||
|
}
|
||
|
|
||
|
# summarize the hashes to -> count
|
||
|
for (
|
||
|
[ extern => $externUsers ],
|
||
|
[ intern => $internUsers ],
|
||
|
[ waiting => $waitingUsers ],
|
||
|
)
|
||
|
{
|
||
|
my ( $label, $ref ) = @$_;
|
||
|
my $total;
|
||
|
$total += $_ for values %$ref;
|
||
|
$entry->{$label} = $total || 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $report;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# Prototype qlic_output(fileName, HASHREF1, HASHREF2)
|
||
|
#
|
||
|
# HASHREF1 => {
|
||
|
# feature => {
|
||
|
# extern => NUM,
|
||
|
# intern => NUM,
|
||
|
# limit => NUM,
|
||
|
# total => NUM,
|
||
|
# waiting => NUM,
|
||
|
# served => STRING,
|
||
|
# user => {
|
||
|
# extern => { "user@machine" => NUM, },
|
||
|
# intern => { "user@machine" => NUM, },
|
||
|
# waiting => { "user" => NUM, },
|
||
|
# },
|
||
|
# },
|
||
|
# }
|
||
|
#
|
||
|
#
|
||
|
# HASHREF2 => { # the changes
|
||
|
# feature => NUM,
|
||
|
# }
|
||
|
#
|
||
|
sub qlic_output {
|
||
|
my $cacheFile = shift;
|
||
|
my $report = shift;
|
||
|
my $mattr = hashrefToString(shift) || "NONE";
|
||
|
|
||
|
defined $cacheFile and length $cacheFile or return;
|
||
|
|
||
|
# use temp file with rename to avoid race conditions
|
||
|
my $tmpFile = $cacheFile;
|
||
|
if ( $cacheFile ne "-" ) { # catch "-" STDOUT alias
|
||
|
$tmpFile .= ".TMP";
|
||
|
unlink $tmpFile;
|
||
|
}
|
||
|
local *FILE;
|
||
|
open FILE, ">$tmpFile" or return;
|
||
|
|
||
|
# write dates, administration information, some environment variables
|
||
|
my $time = time;
|
||
|
my $date = POSIX::strftime( "%FT%T", localtime $time );
|
||
|
my $host = ( POSIX::uname() )[1];
|
||
|
my $user = getpwuid $<;
|
||
|
|
||
|
# cluster names/locations
|
||
|
my $sgeRoot = $ENV{SGE_ROOT} || "";
|
||
|
my $sgeCell = $ENV{SGE_CELL} || "default";
|
||
|
|
||
|
# cluster name is not standard - maybe from env or config file
|
||
|
my $clusterName = $ENV{SGE_CLUSTER_NAME} || "";
|
||
|
|
||
|
# cluster name might just be in the config information
|
||
|
if ( exists $config->{-parameter}{SGE_CLUSTER_NAME} ) {
|
||
|
my $value = $config->{-parameter}{SGE_CLUSTER_NAME};
|
||
|
if ( defined $value and length $value ) {
|
||
|
$clusterName = $value;
|
||
|
}
|
||
|
}
|
||
|
# $clusterName ||= "default"; ## fallback value
|
||
|
$clusterName = "default"; ## always use "default"
|
||
|
|
||
|
|
||
|
# header with comment about possible changes
|
||
|
print FILE << "XML_TEXT";
|
||
|
<?xml version="1.0"?>
|
||
|
<?qlicserver date="$date"?>
|
||
|
<qlicserver releaseDate="$releaseDate">
|
||
|
<!-- adjustment:
|
||
|
qconf -mattr exechost complex_values $mattr global
|
||
|
-->
|
||
|
<query>
|
||
|
<cluster name="$clusterName" root="$sgeRoot" cell="$sgeCell"/>
|
||
|
<host>$host</host>
|
||
|
<user>$user</user>
|
||
|
<time epoch="$time">$date</time>
|
||
|
</query>
|
||
|
<parameters>
|
||
|
XML_TEXT
|
||
|
|
||
|
# environment
|
||
|
for (qw( SGE_ROOT SGE_CELL SGE_ARCH SGE_BINARY_PATH SGE_qmaster )) {
|
||
|
if ( $ENV{$_} ) {
|
||
|
print FILE qq{ <env name="$_">$ENV{$_}</env>\n};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# show inherited license environment(s)
|
||
|
for (@License::Manager) {
|
||
|
my ( $name, $value ) = ( $_->envname(), $_->envvalue() );
|
||
|
if ( defined $name and not exists $config->{-parameter}{$name} ) {
|
||
|
print FILE qq{ <env name="$name">}
|
||
|
. ( $value || '' )
|
||
|
. qq{</env>\n};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# other parameters
|
||
|
for ( sort keys %{ $config->{-parameter} } ) {
|
||
|
my $value = $config->{-parameter}{$_};
|
||
|
if ( defined $value and length $value ) {
|
||
|
print FILE qq{ <param name="$_">$value</param>\n};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# finish parameters and start resources
|
||
|
print FILE ##
|
||
|
qq{ </parameters>\n}, ##
|
||
|
qq{ <resources>\n};
|
||
|
|
||
|
for my $name ( sort keys %{ $config->{-derived} } ) {
|
||
|
my @elem = @{ $config->{-derived}{$name}{element} };
|
||
|
if (@elem) {
|
||
|
print FILE +(
|
||
|
qq{ <derived name="$name">\n},
|
||
|
( map { qq{ <element>$_</element>\n} } @elem ),
|
||
|
qq{ </derived>\n},
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for my $resource ( sort keys %$report ) {
|
||
|
my $entry = $report->{$resource}
|
||
|
or warn "(WW) '$resource' not defined\n"
|
||
|
and next;
|
||
|
|
||
|
# hash some output values here:
|
||
|
my %output = (
|
||
|
name => $resource,
|
||
|
( map { $_ => $entry->{$_} } qw( served type waiting ) )
|
||
|
);
|
||
|
|
||
|
my ( $total, $limit, $extern, $intern ) =
|
||
|
@{$entry}{qw( total limit extern intern )};
|
||
|
|
||
|
my $managed = ( $total - $extern );
|
||
|
|
||
|
if ( defined $limit and $limit < $total ) {
|
||
|
if ( $managed > $limit ) {
|
||
|
$managed = $limit;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
undef $limit;
|
||
|
}
|
||
|
|
||
|
my $free = $managed - $intern;
|
||
|
|
||
|
$_ >= 0 or $_ = 0 for ( $free, $managed ); # should not be required
|
||
|
|
||
|
# transcribe directly from original data structure
|
||
|
if ( exists $config->{-resources}{$resource} ) {
|
||
|
my $rc = $config->{-resources}{$resource};
|
||
|
|
||
|
for (qw( served from note )) {
|
||
|
if ( exists $rc->{$_} ) {
|
||
|
$output{$_} = $rc->{$_};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
print FILE qq{ <resource};
|
||
|
for (
|
||
|
[ name => $output{name} ],
|
||
|
[ served => $output{served} ],
|
||
|
[ from => $output{from} ],
|
||
|
[ total => $total ],
|
||
|
[ limit => $limit ],
|
||
|
[ extern => $extern ],
|
||
|
[ intern => $intern ],
|
||
|
[ waiting => $output{waiting} ],
|
||
|
[ free => $free ],
|
||
|
[ type => $output{type} ],
|
||
|
[ note => $output{note} ],
|
||
|
)
|
||
|
{
|
||
|
my ( $k, $v ) = @$_;
|
||
|
if ( $k =~ /(total|limit)/ ) {
|
||
|
## unconditional output
|
||
|
print FILE qq{ $k="$v"} if defined $v;
|
||
|
}
|
||
|
else {
|
||
|
print FILE qq{ $k="$v"} if $v;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
my $output; # track if anything was written
|
||
|
my $users = $entry->{users} || {};
|
||
|
|
||
|
for ( ##
|
||
|
[ extern => $users->{extern} ], ##
|
||
|
[ intern => $users->{intern} ], ##
|
||
|
[ waiting => $users->{waiting} ], ##
|
||
|
)
|
||
|
{
|
||
|
my ( $label, $ref ) = @$_;
|
||
|
my %user;
|
||
|
$user{$_} += $ref->{$_} || 0 for keys %$ref;
|
||
|
|
||
|
# output users
|
||
|
for my $tag ( sort keys %user ) {
|
||
|
my $count = $user{$tag};
|
||
|
if ($count) {
|
||
|
my ( $name, $host ) = split /\@/, $tag;
|
||
|
|
||
|
if ( not $output++ ) {
|
||
|
print FILE qq{>\n};
|
||
|
}
|
||
|
print FILE qq{ <user name="$name"}
|
||
|
. ( $host ? qq{ host="$host"} : '' )
|
||
|
. qq{ type="$label"}
|
||
|
. qq{>$count</user>\n};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# finish contents or finish as an empty element
|
||
|
if ($output) {
|
||
|
print FILE qq{ </resource>\n};
|
||
|
}
|
||
|
else {
|
||
|
print FILE qq{/>\n};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# footer
|
||
|
print FILE ##
|
||
|
qq{ </resources>\n}, ##
|
||
|
qq{</qlicserver>\n};
|
||
|
|
||
|
close FILE; # explicitly close before rename
|
||
|
if ( $tmpFile ne $cacheFile ) {
|
||
|
chmod 0444 => $tmpFile; # output cache is readonly
|
||
|
rename $tmpFile => $cacheFile; # atomic
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# get the pid of a command
|
||
|
#
|
||
|
sub pidof {
|
||
|
my $cmd = shift;
|
||
|
map { /^\s*(\d+)\s*$/ } qx{/bin/ps -C $cmd -o pid= 2>/dev/null};
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# kill programs with the same name as this program
|
||
|
#
|
||
|
sub kill_daemon {
|
||
|
my $signal = shift || 9;
|
||
|
my @list = grep { $_ != $$ } pidof($Script);
|
||
|
kill $signal => @list if @list;
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# '-k'
|
||
|
# terminate processes
|
||
|
# ------------------------------------------------------------------------------
|
||
|
if ( $opt{k} ) {
|
||
|
kill_daemon 15; # TERM
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# '-w'
|
||
|
# wakeup daemon
|
||
|
# ------------------------------------------------------------------------------
|
||
|
if ( $opt{w} ) {
|
||
|
kill_daemon 10; # USR1
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
# for rest of the options, we need an updated configuration
|
||
|
updateConfig();
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# '-c' / '-C'
|
||
|
# configuration
|
||
|
# ------------------------------------------------------------------------------
|
||
|
if ( $opt{C} or $opt{c} ) {
|
||
|
|
||
|
#
|
||
|
# show complexes (format as per 'qconf -sc');
|
||
|
#
|
||
|
if ( $opt{c} ) {
|
||
|
print <<'PRINT';
|
||
|
#
|
||
|
# complexes for re-importing via "qconf -mc",
|
||
|
# licenses mostly weighted with '0' urgency (slot count used instead)
|
||
|
#
|
||
|
# name shortcut type relop requestable consumable default urgency
|
||
|
# ------------------------------------------------------------------------------
|
||
|
PRINT
|
||
|
for my $name ( sort keys %{ $config->{-managed} } ) {
|
||
|
my $consumable =
|
||
|
$config->{-managed}{$name} =~ /job/i ? "JOB" : "YES";
|
||
|
my $urgency = 0;
|
||
|
|
||
|
# brute-force search for urgency
|
||
|
for my $href (
|
||
|
$config->{-resources}, #
|
||
|
$config->{-derived}, #
|
||
|
$config->{-intern}, #
|
||
|
)
|
||
|
{
|
||
|
if ( exists $href->{$name} ) {
|
||
|
if ( exists $href->{$name}{urgency} ) {
|
||
|
$urgency = $href->{$name}{urgency};
|
||
|
}
|
||
|
last;
|
||
|
}
|
||
|
}
|
||
|
print "$name\t$name\tINT\t<=\tYES\t$consumable\t0\t$urgency\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( $opt{C} ) {
|
||
|
my $qconf = Qconf->query();
|
||
|
|
||
|
## ignore complexes that are already known
|
||
|
delete @{ $config->{-managed} }{ keys %$qconf };
|
||
|
|
||
|
if ( %{ $config->{-managed} } ) {
|
||
|
## initialize all values with zero
|
||
|
for ( values %{ $config->{-managed} } ) {
|
||
|
$_ = 0;
|
||
|
}
|
||
|
|
||
|
print <<'PRINT';
|
||
|
# initialize remaining managed resources with the following command:
|
||
|
PRINT
|
||
|
print " qconf -mattr exechost complex_values ",
|
||
|
hashrefToString( $config->{-managed} ), " global\n\n";
|
||
|
}
|
||
|
else {
|
||
|
print "# nothing to do\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# '-i'
|
||
|
# query the license servers for available license features
|
||
|
# ------------------------------------------------------------------------------
|
||
|
if ( $opt{i} ) {
|
||
|
my $license = License->query();
|
||
|
|
||
|
# header
|
||
|
print << 'XML_TEXT';
|
||
|
<?xml version="1.0"?>
|
||
|
<qlicserverConfig>
|
||
|
<!-- NOTE:
|
||
|
| This is a configuration fragment of served resources.
|
||
|
| It is missing limits, urgency and derived, internal and remapped resources.
|
||
|
| DO NOT USE THIS FILE DIRECTLY AS YOUR CONFIGURATION FILE.
|
||
|
+ -->
|
||
|
<resources>
|
||
|
XML_TEXT
|
||
|
|
||
|
my @new;
|
||
|
for ( sort keys %$license ) {
|
||
|
my ( $type, $feature, $resource ) = ( "", $_, lc $_ );
|
||
|
|
||
|
if ( exists $config->{-lookup}{$feature} ) {
|
||
|
( $resource, $type ) = @{ $config->{-lookup}{$feature} };
|
||
|
print qq{ <resource name="$resource" served="$feature"};
|
||
|
if ($type) {
|
||
|
print qq{ type="$type"};
|
||
|
}
|
||
|
else {
|
||
|
my %h = %{ $config->{-resources}{$resource} };
|
||
|
delete $h{served};
|
||
|
for ( sort keys %h ) {
|
||
|
print qq{ $_="$h{$_}"};
|
||
|
}
|
||
|
}
|
||
|
print qq{/>\n};
|
||
|
}
|
||
|
else {
|
||
|
push @new, $feature;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# footer
|
||
|
print << 'XML_TEXT';
|
||
|
</resources>
|
||
|
</qlicserverConfig>
|
||
|
XML_TEXT
|
||
|
|
||
|
if (@new) {
|
||
|
print << 'XML_TEXT';
|
||
|
|
||
|
<!--
|
||
|
NEW SERVED FEATURES DISCOVERED
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
XML_TEXT
|
||
|
|
||
|
for (@new) {
|
||
|
print qq{ <resource name="\L$_\E" served="$_"/>\n};
|
||
|
}
|
||
|
print qq{-->\n};
|
||
|
}
|
||
|
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# '-l resource=value,...'
|
||
|
# query the license server for the availability
|
||
|
# ------------------------------------------------------------------------------
|
||
|
if ( $opt{l} ) {
|
||
|
|
||
|
# only check served/derived resources, to avoid extra qconf -se
|
||
|
# and since this check should be unnecessary for internal resources anyhow
|
||
|
delete @{ $config->{-managed} }{ keys %{ $config->{-intern} } };
|
||
|
|
||
|
# comma -> space-delimited, extracting 'slots' along the way
|
||
|
my $slots;
|
||
|
my @list =
|
||
|
map {
|
||
|
my ( $rc, $request ) = split /=+/;
|
||
|
defined $request and $request =~ /^\d+\.?\d*$/ or $request ||= 1;
|
||
|
|
||
|
if ( exists $config->{-managed}{$rc} ) {
|
||
|
[ $rc => $request ];
|
||
|
}
|
||
|
else {
|
||
|
## number of slots ('slots=' or 's=')
|
||
|
$slots = $request if $rc =~ /^(?:s|slots)$/;
|
||
|
();
|
||
|
}
|
||
|
}
|
||
|
map { s{,}{ }g; split; } $opt{l};
|
||
|
|
||
|
@list or exit 0;
|
||
|
|
||
|
# qstat query
|
||
|
my $qstat = GridEngine->qstat(
|
||
|
undef, ## without file caching
|
||
|
$config->{-managed} ## distinguish complex types
|
||
|
);
|
||
|
|
||
|
# get my own job identifier from the environment
|
||
|
# treat non-array job (task=undefined) as task=0
|
||
|
( my $jobIdent = ( $ENV{JOB_ID} || 0 ) . '.' . ( $ENV{SGE_TASK_ID} || 0 ) )
|
||
|
=~ s/[a-z]+$/0/i;
|
||
|
|
||
|
# never count myself in the overal balance, otherwise we block our own way!
|
||
|
for ( values %$qstat ) {
|
||
|
delete $_->{$jobIdent};
|
||
|
}
|
||
|
|
||
|
# get the projected resource availability:
|
||
|
my $licenses = mungeLicenses(
|
||
|
License->query( $config->{-mapFrom} ), ## license availability
|
||
|
$qstat, ## qstat query
|
||
|
updateLimits() ## limits are interesting
|
||
|
);
|
||
|
|
||
|
my $failed;
|
||
|
$slots ||= 1; # safety
|
||
|
for (@list) {
|
||
|
my ( $rc, $request ) = @$_;
|
||
|
if ( exists $licenses->{$rc} ) { # safety
|
||
|
my ( $total, $limit, $extern, $intern ) =
|
||
|
@{ $licenses->{$rc} }{qw( total limit extern intern )};
|
||
|
|
||
|
my $managed = ( $total - $extern );
|
||
|
|
||
|
if ( defined $limit and $limit < $total ) {
|
||
|
if ( $managed > $limit ) {
|
||
|
$managed = $limit;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
undef $limit;
|
||
|
}
|
||
|
|
||
|
my $free = $managed - $intern;
|
||
|
|
||
|
if ( $free < 0 ) {
|
||
|
$free = 0;
|
||
|
}
|
||
|
|
||
|
## scale non-'job' consumables
|
||
|
$request *= $slots unless $config->{-managed}{$rc} =~ /job/i;
|
||
|
$request = sprintf "%.0f", $request;
|
||
|
|
||
|
if ( $request > $free ) {
|
||
|
$request = $free;
|
||
|
$failed++;
|
||
|
}
|
||
|
}
|
||
|
$_ = "$rc=$request";
|
||
|
}
|
||
|
print join( ',' => @list ), "\n";
|
||
|
|
||
|
exit( $failed ? 99 : 0 );
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# standard query, with optional '-d' (daemonize)
|
||
|
# ------------------------------------------------------------------------------
|
||
|
my $daemon = $opt{d};
|
||
|
|
||
|
if ($daemon) { # daemonize
|
||
|
|
||
|
# the delay between loops
|
||
|
my $delay = $config->{-parameter}{delay};
|
||
|
$daemon = ( $delay and $delay =~ /^\d+$/ ) ? $delay : 30;
|
||
|
|
||
|
# terminate old processes
|
||
|
kill_daemon 15; # TERM
|
||
|
|
||
|
# option 1 (default):
|
||
|
# - watch the pid of the original parent process
|
||
|
# option 2:
|
||
|
# - watch the pid of a particular process (eg, sge_qmaster)
|
||
|
# option 3:
|
||
|
# - watch a particular pid (a pid <= 1 implies a true daemon)
|
||
|
|
||
|
my $ppid = getppid(); # get ppid before forking
|
||
|
|
||
|
# we can can check this process quite simply
|
||
|
*check_ppid = sub { kill 0 => $ppid };
|
||
|
|
||
|
if ( exists $config->{-parameter}{ppid} ) {
|
||
|
my $value = $config->{-parameter}{ppid};
|
||
|
if ( $value ne "ppid" ) {
|
||
|
if ( $value =~ /^\d+$/ ) {
|
||
|
$ppid = $value;
|
||
|
}
|
||
|
else {
|
||
|
($ppid) = pidof($value);
|
||
|
defined $ppid
|
||
|
or die "no pid for command '$value' ... exiting\n";
|
||
|
}
|
||
|
|
||
|
no warnings 'redefine';
|
||
|
if ( $ppid <= 1 ) {
|
||
|
## a true daemon - ignore the parent
|
||
|
*check_ppid = sub { 1; };
|
||
|
}
|
||
|
else {
|
||
|
## kill 0 doesn't always work if we don't own the process
|
||
|
## use the /proc system if it seems to exist
|
||
|
if ( -d "/proc/$$" and -d "/proc/$ppid" ) {
|
||
|
*check_ppid = sub { -d "/proc/$ppid"; };
|
||
|
}
|
||
|
else {
|
||
|
## or revert to a more expensive system call
|
||
|
*check_ppid = sub {
|
||
|
system "/bin/ps -p $ppid -o pid= >/dev/null 2>&1";
|
||
|
($?) ? 0 : 1;
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# test if we can watch this pid before attempting to fork
|
||
|
check_ppid()
|
||
|
or die "cannot watch ppid=$ppid '$value' ... exiting\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# this makes the code quasi-independent of the parent process
|
||
|
# but should allow it to detect when the launching load-sensor
|
||
|
# has restarted
|
||
|
#
|
||
|
*processing = sub {
|
||
|
if ( $daemon > 0 and check_ppid() ) {
|
||
|
## daemon still running and ppid still alive
|
||
|
sleep( $daemon || 0 );
|
||
|
}
|
||
|
else {
|
||
|
## ppid looks dead - let's die too
|
||
|
$daemon = 0;
|
||
|
}
|
||
|
return $daemon;
|
||
|
};
|
||
|
|
||
|
my $pid = fork;
|
||
|
exit if $pid; # let parent exit
|
||
|
defined $pid or die "Couldn't fork: $!";
|
||
|
|
||
|
# a new process group for the child
|
||
|
POSIX::setsid() or die "Can't start a new session: $!";
|
||
|
}
|
||
|
else {
|
||
|
$daemon = 0;
|
||
|
*processing = sub { $daemon = 0; };
|
||
|
}
|
||
|
|
||
|
if ($daemon) {
|
||
|
## Trap fatal signals, setting flag to exit gracefully
|
||
|
$SIG{INT} = $SIG{TERM} = sub { $daemon = 0; };
|
||
|
$SIG{PIPE} = "IGNORE";
|
||
|
$SIG{USR1} = sub { sleep 0; }; # allow wake-up on demand
|
||
|
$SIG{USR2} = sub {
|
||
|
sleep 0; # wake-up
|
||
|
$daemon = -1; # signal end
|
||
|
};
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# the main license query and 'qconf -mattr' code
|
||
|
# standard - execute once
|
||
|
# daemon - loop until killed
|
||
|
#
|
||
|
do {
|
||
|
updateConfig();
|
||
|
|
||
|
my $limits = updateLimits();
|
||
|
my $served = License->query( $config->{-mapFrom} );
|
||
|
my $qconf = Qconf->query();
|
||
|
|
||
|
# qstat query and cache to a file
|
||
|
my $qstat = GridEngine->qstat(
|
||
|
resolveOutputFile("qstat"), ## optional cache
|
||
|
$config->{-managed} ## distinguish complex types
|
||
|
);
|
||
|
|
||
|
# cache qhost query to a file
|
||
|
GridEngine->qhost( resolveOutputFile("qhost") );
|
||
|
|
||
|
# merge in the intern tracked resources
|
||
|
# take total from config, for the limits or from qconf
|
||
|
for ( keys %{ $config->{-intern} } ) {
|
||
|
if ( exists $qconf->{$_} ) {
|
||
|
if ( exists $config->{-intern}{$_}{total} ) {
|
||
|
$qstat->{$_}{total} = $config->{-intern}{$_}{total};
|
||
|
}
|
||
|
elsif ( exists $limits->{$_} and $limits->{$_} >= 0 ) {
|
||
|
$qstat->{$_}{total} = $limits->{$_};
|
||
|
}
|
||
|
else {
|
||
|
$qstat->{$_}{total} = $qconf->{$_};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# assign 'total => 0' for managed licenses that were not
|
||
|
# reported from the server (eg, server down)
|
||
|
for ( keys %{ $config->{-lookup} } ) {
|
||
|
$served->{$_} ||= { total => 0 };
|
||
|
}
|
||
|
|
||
|
my $licenses = mungeLicenses( $served, $qstat, $limits );
|
||
|
my $change = Qconf->diff( $qconf, $licenses );
|
||
|
|
||
|
# cache output to a file
|
||
|
qlic_output( resolveOutputFile("output"), $licenses, $change );
|
||
|
|
||
|
if ($Debugging) {
|
||
|
$opt{n}++;
|
||
|
eval {
|
||
|
use Data::Dumper;
|
||
|
warn Data::Dumper->Dump( [ $licenses, $change ],
|
||
|
[qw(License Change)] ), "\n";
|
||
|
};
|
||
|
|
||
|
exit;
|
||
|
}
|
||
|
|
||
|
Qconf->mattr( hashrefToString($change) ) unless $opt{n};
|
||
|
|
||
|
} while processing();
|
||
|
|
||
|
exit 0;
|
||
|
|
||
|
# ------------------------------------------------------------------ end-of-main
|
||
|
# somewhat like the qx// command with a timeout mechanism,
|
||
|
# but for safety it only handles a list form (no shell escapes)
|
||
|
#
|
||
|
|
||
|
package Shell;
|
||
|
our ( $timeout, $report );
|
||
|
|
||
|
BEGIN {
|
||
|
$timeout = 10;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# assign new value for reporting the timeout
|
||
|
#
|
||
|
sub report {
|
||
|
my ( $caller, $value ) = @_;
|
||
|
$report = $value;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# assign new timeout
|
||
|
#
|
||
|
sub timeout {
|
||
|
my ( $caller, $value ) = @_;
|
||
|
$timeout = ( $value and $value =~ /^\d+$/ ) ? $value : 10;
|
||
|
}
|
||
|
|
||
|
sub cmd {
|
||
|
my ( $caller, @command ) = @_;
|
||
|
my ( @lines, $pid, $redirected );
|
||
|
local ( *OLDERR, *PIPE );
|
||
|
|
||
|
# kill off truant child: this works well for unthreaded processes,
|
||
|
# but threaded processes are still an issue
|
||
|
local $SIG{__DIE__} = sub { kill TERM => $pid if $pid; };
|
||
|
|
||
|
eval {
|
||
|
local $SIG{ALRM} = sub { die "TIMEOUT\n" }; # NB: '\n' required
|
||
|
alarm $timeout if $timeout;
|
||
|
@command or die "$caller: Shell->cmd with an undefined query\n";
|
||
|
|
||
|
if ( open OLDERR, ">&", \*STDERR ) {
|
||
|
$redirected++;
|
||
|
open STDERR, ">/dev/null";
|
||
|
}
|
||
|
|
||
|
$pid = open PIPE, '-|', @command; # open without shell (forked)
|
||
|
if ($pid) {
|
||
|
@lines = <PIPE>;
|
||
|
}
|
||
|
|
||
|
die "(EE) ", @lines if $?;
|
||
|
alarm 0;
|
||
|
};
|
||
|
|
||
|
# restore stderr
|
||
|
open STDERR, ">&OLDERR" if $redirected;
|
||
|
|
||
|
if ($@) {
|
||
|
if ( $@ =~ /^TIMEOUT/ ) {
|
||
|
warn "(WW) TIMEOUT after $timeout seconds on '@command'\n" if $report;
|
||
|
return undef;
|
||
|
}
|
||
|
else {
|
||
|
die $@; # propagate unexpected errors
|
||
|
}
|
||
|
}
|
||
|
|
||
|
wantarray ? @lines : join '' => @lines;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
# --------------------------------------------------------------- end-of-package
|
||
|
# FlexLM queries
|
||
|
#
|
||
|
# The env variable 'LM_LICENSE_FILE' contains a colon-delimited list
|
||
|
# with "port@server:port@server".
|
||
|
# The queries for the same server (but different ports) are grouped together
|
||
|
# and run in a common thread.
|
||
|
# eg,
|
||
|
# port1@server1:port1@server2:port2@server1
|
||
|
# -> port1@server1:port2@server1 + port1@server2
|
||
|
# running in two threads.
|
||
|
#
|
||
|
# NOTE: for grouping to work, the servers must be named consistently
|
||
|
# eg,
|
||
|
# port1@server1.domain:port2@server1:port3@server1.ip.addr
|
||
|
# -> port1@server1.domain + port2@server1 + port3@server1.ip.addr
|
||
|
#
|
||
|
# To suppress grouping by server, entries can be surrounded by brace brackets.
|
||
|
# eg,
|
||
|
# port1@server1:port1@server2:{port2@server1}
|
||
|
# -> port2@server1 + port1@server1 + port1@server2
|
||
|
#
|
||
|
# or,
|
||
|
# {port1@server1:port1@server2:port2@server1}
|
||
|
# -> port1@server1:port1@server2:port2@server1
|
||
|
#
|
||
|
#
|
||
|
# This behaviour can be useful when license server triads are in use.
|
||
|
# When a triad is in place, the single query to all three servers returns the
|
||
|
# correct information, whereas three separate queries would incorrectly return
|
||
|
# a triple count!
|
||
|
# eg,
|
||
|
# port1@server1:{port@triad1:port@triad2:port@triad3}
|
||
|
# -> port@triad1:port@triad2:port@triad3 + port1@server1
|
||
|
#
|
||
|
# As a side-effect, entries enclosed in brace brackets will be queried first.
|
||
|
#
|
||
|
package Flexlm;
|
||
|
our ( $env, $cmd, @args, @servers );
|
||
|
|
||
|
BEGIN {
|
||
|
$env = $ENV{LM_LICENSE_FILE};
|
||
|
$cmd = "lmutil"; # query
|
||
|
@args = qw( lmstat -a -c ); # cmd (query) arguments
|
||
|
push @License::Manager, __PACKAGE__;
|
||
|
|
||
|
sub _assign_servers {
|
||
|
my $value = shift;
|
||
|
@servers = ();
|
||
|
|
||
|
if ($value) {
|
||
|
my %index;
|
||
|
my $index = 0;
|
||
|
|
||
|
# get grouped server queries
|
||
|
while ( $value =~ s/\{(.*?)\}// ) {
|
||
|
if ($1) {
|
||
|
push @servers, $1;
|
||
|
$index++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for ( map { s{[:;]+}{ }g; split } $value ) {
|
||
|
( my $name = $_ ) =~ s/^\d*\@//; ## port@server or @server
|
||
|
if ( defined $index{$name} ) {
|
||
|
$servers[ $index{$name} ] .= ":$_";
|
||
|
}
|
||
|
else {
|
||
|
$index{$name} = $index++;
|
||
|
push @servers, $_;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_assign_servers($env);
|
||
|
}
|
||
|
|
||
|
sub cmdname {
|
||
|
return "lmutil";
|
||
|
}
|
||
|
|
||
|
sub envname {
|
||
|
return "LM_LICENSE_FILE";
|
||
|
}
|
||
|
|
||
|
sub envvalue {
|
||
|
return $env;
|
||
|
}
|
||
|
|
||
|
sub setcmd {
|
||
|
my ( $caller, $value ) = @_;
|
||
|
|
||
|
if ( defined $value ) {
|
||
|
$cmd = $value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# setenv does not actually need to set the environment since we use
|
||
|
# the '-c' option directly
|
||
|
sub setenv {
|
||
|
my ( $caller, $value ) = @_;
|
||
|
|
||
|
if ( defined $value and ( not defined $env or $env ne $value ) ) {
|
||
|
$env = $value;
|
||
|
_assign_servers($value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# PARSE Flexlm output that looks like this
|
||
|
#
|
||
|
# License server status: port@server
|
||
|
# License file(s) on server: ...
|
||
|
#
|
||
|
# Users of PATRAN: (Total of 7 licenses available)
|
||
|
#
|
||
|
# "PATRAN" v2003.1130, vendor: MSC
|
||
|
# floating license
|
||
|
#
|
||
|
# user1 host1 host1 (v2002.0120) (server.domain/port 861), start Fri 1/31 11:00
|
||
|
# user2 host2 host2 (v2001.0523) (server.domain/port 1007), start Fri 1/31 12:24
|
||
|
# user3 host3 /dev/pts/0 (v1999.1020) (license.server.domain/port 352), start Fri 1/31 13:11
|
||
|
#
|
||
|
# ------------------------------------------------------------------------------
|
||
|
#
|
||
|
# Note that 'lmstat' also seems to use entries from the ~/.flexlmrc file and/or
|
||
|
# daemon-specific environment variables such as '*_LICENSE_FILE'.
|
||
|
#
|
||
|
# We thus limit the query to the entries explicitly found in LM_LICENSE_FILE
|
||
|
#
|
||
|
# return:
|
||
|
# HASHREF => {
|
||
|
# feature => {
|
||
|
# total => number,
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# },
|
||
|
# }
|
||
|
sub query_server {
|
||
|
my ( $caller, $server ) = @_;
|
||
|
my $license = {};
|
||
|
|
||
|
$server ||= join( ":" => @servers );
|
||
|
|
||
|
my @lines = Shell->cmd( $cmd, @args, $server );
|
||
|
|
||
|
defined $lines[0] or return $license;
|
||
|
|
||
|
# warn "parse <@lines>\n";
|
||
|
my ( $serverInfo, $feature );
|
||
|
|
||
|
for (@lines) {
|
||
|
defined or next;
|
||
|
|
||
|
## We don't currently do anything with this information
|
||
|
## capture server port/name
|
||
|
# if (/^License \s+ server \s+ status: \s+ (\d+\@\S+?)\s*$/mgcx)
|
||
|
# {
|
||
|
# $serverInfo = lc $1;
|
||
|
# next;
|
||
|
# }
|
||
|
|
||
|
## capture error status
|
||
|
## e.g. Users of DesignWare-Regression: (Error: 10 licenses, unsupported by licensed server)
|
||
|
if ( my ( $what, $total ) =
|
||
|
/^Users \s+ of \s+ (\S+?): .+? [Ee]rror:\s+ (\d+) \s+ licen[cs]e/mgcx
|
||
|
)
|
||
|
{
|
||
|
$feature = $what;
|
||
|
$license->{$feature} ||= { total => 0 };
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
## extract total licenses available, record the 'feature' name
|
||
|
if ( my ( $what, $total ) =
|
||
|
/^Users \s+ of \s+ (\S+?): .+? \s+ (\d+) \s+ licen[cs]e/mgcx )
|
||
|
{
|
||
|
$feature = $what;
|
||
|
$license->{$feature}{total} += $total;
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
$feature and exists $license->{$feature} or next;
|
||
|
|
||
|
# lines with ", start" indicate a license is in use
|
||
|
#
|
||
|
# 'user' and 'machine' are the first 2 entries
|
||
|
#
|
||
|
if (/, \s+ start \s+/x) {
|
||
|
my ($count) = /(\d+) \s+ licen[cs]e/x;
|
||
|
$count ||= 1;
|
||
|
|
||
|
my ( $user, $host ) = map { lc } split;
|
||
|
$host =~ s/\..*$//; # unqualified hostname
|
||
|
|
||
|
$license->{$feature}{"$user\@$host $count"}++;
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
# add in queued licenses - identify with '*' prefix
|
||
|
if ( my ($count) = /\s+ queued \s+ for \s+ (\d+) \s+ licen[cs]es/x ) {
|
||
|
my ( $user, $host ) = map { lc } split;
|
||
|
$host =~ s/\..*$//; # unqualified hostname
|
||
|
|
||
|
$license->{$feature}{"*$user\@$host"} += $count || 1;
|
||
|
next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $license;
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# spawn threads and merge results from multiple 'query_server' calls
|
||
|
#
|
||
|
# The optional remapping field can be used to rename features on a
|
||
|
# server-by-server basis before returning the hash. This only works when
|
||
|
# threading works correctly - ie, each query corresponds to exactly a
|
||
|
# single server
|
||
|
#
|
||
|
sub query {
|
||
|
my $caller = shift;
|
||
|
my $mapFrom = shift || {};
|
||
|
my $license = {};
|
||
|
|
||
|
@servers or return $license;
|
||
|
|
||
|
if ( @servers <= 1 and keys %$mapFrom ) {
|
||
|
return $caller->query_server();
|
||
|
}
|
||
|
|
||
|
## REMOVE REMAINDER FOR UNTHREADED PERL
|
||
|
|
||
|
my @threads; # record the server names / thread ids here
|
||
|
for my $server (@servers) {
|
||
|
my $thread = threads->create( sub { $caller->query_server($server) } );
|
||
|
if ( defined $thread ) {
|
||
|
my ( $lookup, %server );
|
||
|
|
||
|
# group the servers, avoid touching the alias
|
||
|
for ( map { s{[:;]+}{ }g; split } ( my $srv = $server ) ) {
|
||
|
( $lookup = $_ ) =~ s/^\d*\@//; ## port@server or @server
|
||
|
$lookup = lc $lookup;
|
||
|
$server{$lookup}++;
|
||
|
}
|
||
|
|
||
|
keys %server == 1 or undef $lookup;
|
||
|
push @threads, [ $lookup, $thread ];
|
||
|
}
|
||
|
else {
|
||
|
warn "could not start thread for server $server\n;";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# collect data, waiting for all threads to finish
|
||
|
# each thread returns a hash-of-hashes
|
||
|
for (@threads) {
|
||
|
my ( $lookup, $thread ) = @$_;
|
||
|
my ($hash) = $thread->join();
|
||
|
|
||
|
# establish possible server-specific remapping
|
||
|
my $remap = {};
|
||
|
if ( defined $lookup and exists $mapFrom->{$lookup} ) {
|
||
|
$remap = $mapFrom->{$lookup};
|
||
|
}
|
||
|
|
||
|
for ( keys %$hash ) {
|
||
|
my $subhash = $hash->{$_};
|
||
|
## allow server-specific remapping
|
||
|
my $feature = exists $remap->{$_} ? $remap->{$_} : $_;
|
||
|
|
||
|
for my $k ( keys %$subhash ) {
|
||
|
my $v = $subhash->{$k};
|
||
|
$license->{$feature}{$k} += $v;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $license;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
# --------------------------------------------------------------- end-of-package
|
||
|
# A class for combining several types of license managers.
|
||
|
# Assumes that the same license feature cannot be managed by more than a
|
||
|
# single license manager type
|
||
|
|
||
|
package License;
|
||
|
|
||
|
sub query {
|
||
|
my $caller = shift;
|
||
|
return +{ map { %{ $_->query(@_) } } @License::Manager };
|
||
|
}
|
||
|
|
||
|
sub envnames {
|
||
|
my $caller = shift;
|
||
|
return map { $_->envname() } @License::Manager;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
# --------------------------------------------------------------- end-of-package
|
||
|
# provide paths to GridEngine bin/ and utilbin/
|
||
|
# and wrappers to the Shell->cmd()
|
||
|
|
||
|
package GridEngine;
|
||
|
our ( $bin, $utilbin );
|
||
|
|
||
|
BEGIN {
|
||
|
$ENV{SGE_SINGLE_LINE} = 1; # do not break up long lines with backslashes
|
||
|
|
||
|
$bin = $ENV{SGE_BINARY_PATH} || '';
|
||
|
$utilbin = $ENV{SGE_utilbin} || '';
|
||
|
|
||
|
if ( -d ( $ENV{SGE_ROOT} || '' ) ) {
|
||
|
my $arch = $ENV{SGE_ARCH}
|
||
|
|| qx{$ENV{SGE_ROOT}/util/arch}
|
||
|
|| 'NONE';
|
||
|
|
||
|
chomp $arch;
|
||
|
|
||
|
-d $bin or $bin = "$ENV{SGE_ROOT}/bin/$arch";
|
||
|
-d $utilbin or $utilbin = "$ENV{SGE_ROOT}/utilbin/$arch";
|
||
|
}
|
||
|
|
||
|
for ( $bin, $utilbin ) {
|
||
|
if ( -d $_ ) {
|
||
|
s{/*$}{/};
|
||
|
}
|
||
|
else {
|
||
|
$_ = '';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# relay command to Shell
|
||
|
sub bin {
|
||
|
my $caller = shift;
|
||
|
my $cmd = $bin . (shift);
|
||
|
|
||
|
return Shell->cmd( $cmd, @_ );
|
||
|
}
|
||
|
|
||
|
# relay command to Shell
|
||
|
sub utilbin {
|
||
|
my $caller = shift;
|
||
|
my $cmd = $utilbin . (shift);
|
||
|
|
||
|
return Shell->cmd( $cmd, @_ );
|
||
|
}
|
||
|
|
||
|
# write readonly cache file,
|
||
|
# using temp file with rename to avoid race conditions
|
||
|
sub writeCache {
|
||
|
my $caller = shift;
|
||
|
my $cacheFile = shift;
|
||
|
|
||
|
defined $cacheFile and length $cacheFile and @_ or return;
|
||
|
|
||
|
my $tmpFile = $cacheFile;
|
||
|
if ( $cacheFile ne "-" ) { # catch "-" STDOUT alias
|
||
|
$tmpFile .= ".TMP";
|
||
|
unlink $tmpFile;
|
||
|
}
|
||
|
local *FILE;
|
||
|
open FILE, ">$tmpFile" or return;
|
||
|
|
||
|
for (@_) {
|
||
|
print FILE $_;
|
||
|
}
|
||
|
|
||
|
close FILE; # explicitly close before rename
|
||
|
if ( $tmpFile ne $cacheFile ) {
|
||
|
chmod 0444 => $tmpFile; # output cache is readonly
|
||
|
rename $tmpFile => $cacheFile; # atomic
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# qhost query
|
||
|
#
|
||
|
# PARSE qhost xml output that looks like this:
|
||
|
#
|
||
|
# <?xml version='1.0'?>
|
||
|
# <qhost xmlns="http://gridengine.sunsource.net/61/qhost">
|
||
|
# <host name='host.domain'>
|
||
|
# <hostvalue name='arch_string'>lx26-amd64</hostvalue>
|
||
|
# <hostvalue name='num_proc'>2</hostvalue>
|
||
|
# <hostvalue name='load_avg'>0.09</hostvalue>
|
||
|
# <hostvalue name='mem_total'>3.9G</hostvalue>
|
||
|
# <hostvalue name='mem_used'>663.7M</hostvalue>
|
||
|
# <hostvalue name='swap_total'>4.0G</hostvalue>
|
||
|
# <hostvalue name='swap_used'>679.3M</hostvalue>
|
||
|
# <queue name='desk'>
|
||
|
# <queuevalue qname='desk' name='qtype_string'>BIP</queuevalue>
|
||
|
# <queuevalue qname='desk' name='slots_used'>0</queuevalue>
|
||
|
# <queuevalue qname='desk' name='slots'>1</queuevalue>
|
||
|
# <queuevalue qname='desk' name='state_string'></queuevalue>
|
||
|
# </queue>
|
||
|
# <job name='52713'>
|
||
|
# <jobvalue jobid='52713' name='priority'>'0.630035'</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='qinstance_name'>queue@host</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='job_name'>NAME</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='job_owner'>OWNER</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='job_state'>r</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='start_time'>1198055059</jobvalue>
|
||
|
# <jobvalue jobid='52713' name='pe_master'>MASTER</jobvalue>
|
||
|
# </job>
|
||
|
# </host>
|
||
|
# </qhost>
|
||
|
#
|
||
|
# fix xmlns=... with xmlns:xsd=...
|
||
|
# issue:
|
||
|
# http://gridengine.sunsource.net/issues/show_bug.cgi?id=2515
|
||
|
#
|
||
|
sub qhost {
|
||
|
my $caller = shift;
|
||
|
my $cacheFile = shift;
|
||
|
|
||
|
# record qhost xml output to a file
|
||
|
defined $cacheFile and length $cacheFile or return;
|
||
|
|
||
|
my @args = qw( -q -j -xml );
|
||
|
my $lines = GridEngine->bin( qhost => @args ) or return;
|
||
|
|
||
|
# replace xmlns= with xmlns:xsd=
|
||
|
# only needed for older GridEngine versions
|
||
|
$lines =~ s{\s+xmlns=}{ xmlns:xsd=}s;
|
||
|
|
||
|
# document the request without affecting the xml structure:
|
||
|
# inject the query date and arguments as processing instructions
|
||
|
# newer perl can use \K for a variable-length look behind
|
||
|
my $date = POSIX::strftime( "%FT%T", localtime );
|
||
|
$lines =~ s{^(<\?xml[^\?]+\?>)}{$1\n<?qhost date="$date"?>\n<?qhost command="@args"?>};
|
||
|
|
||
|
GridEngine->writeCache( $cacheFile, $lines );
|
||
|
}
|
||
|
|
||
|
# ------------------------------------------------------------------------------
|
||
|
# PARSE qstat xml output that looks like this:
|
||
|
#
|
||
|
# <?xml version='1.0'?>
|
||
|
# <job_info xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||
|
# <queue_info>
|
||
|
# <job_list state="running">
|
||
|
# <JB_job_number>934</JB_job_number>
|
||
|
# <JAT_prio>0.56000</JAT_prio>
|
||
|
# <JB_name>my_job_name</JB_name>
|
||
|
# <JB_owner>user_name</JB_owner>
|
||
|
# <state>r</state>
|
||
|
# <JAT_start_time>11/30/2004 10:38:23</JAT_start_time>
|
||
|
# <queue_name>cfd@host.domain</queue_name>
|
||
|
# <slots>1</slots>
|
||
|
# <hard_request name="license">1</hard_request>
|
||
|
# <hard_req_queue>cfd</hard_req_queue>
|
||
|
# </job_list>
|
||
|
# </queue_info>
|
||
|
# <job_info>
|
||
|
# </job_info>
|
||
|
# </job_info>
|
||
|
# ------------------------------------------------------------------------------
|
||
|
|
||
|
# extract
|
||
|
# * <JB_job_number> <JB_owner> <slots> <hard_request> <queue_name>
|
||
|
# return:
|
||
|
# HASHREF => {
|
||
|
# complex => {
|
||
|
# waiting => {
|
||
|
# "*user" => count,
|
||
|
# },
|
||
|
# jobid => {
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# "user@machine nlicense" => occurances,
|
||
|
# },
|
||
|
# },
|
||
|
# }
|
||
|
#
|
||
|
sub qstat {
|
||
|
my $caller = shift;
|
||
|
my $cacheFile = shift;
|
||
|
my $managedType = shift || {};
|
||
|
my $status = {};
|
||
|
|
||
|
my @args = qw( -u * -xml -r -s prs );
|
||
|
|
||
|
my $lines = GridEngine->bin( qstat => @args )
|
||
|
or return $status;
|
||
|
|
||
|
# optionally record qstat xml output to a file
|
||
|
if ($cacheFile)
|
||
|
{
|
||
|
# document the request without affecting the xml structure:
|
||
|
# inject the query date and arguments as processing instructions
|
||
|
# newer perl can use \K for a variable-length look behind
|
||
|
my $date = POSIX::strftime( "%FT%T", localtime );
|
||
|
$lines =~ s{^(<\?xml[^\?]+\?>)}{$1\n<?qstat date="$date"?>\n<?qstat args="@args"?>};
|
||
|
|
||
|
GridEngine->writeCache( $cacheFile, $lines );
|
||
|
}
|
||
|
|
||
|
my %re = (
|
||
|
state => qr{<state>([A-Za-z]+)</state>},
|
||
|
slots => qr{<slots>(\d+)</slots>},
|
||
|
tasks => qr{<tasks>(\d+.*?)</tasks>},
|
||
|
job => qr{<JB_job_number>(.+?)</JB_job_number>},
|
||
|
user => qr{<JB_owner>(.+?)</JB_owner>},
|
||
|
host => qr{<queue_name>.+?\@(.+?)</queue_name>},
|
||
|
);
|
||
|
|
||
|
for ( grep { $_ } split m{</job_list>}, $lines ) {
|
||
|
my ($state) = /$re{state}/;
|
||
|
my ($slots) = /$re{slots}/ or last;
|
||
|
my ($user) = /$re{user}/ or last;
|
||
|
my ($jobIdent) = /$re{job}/ or last;
|
||
|
my ($host) = /$re{host}/;
|
||
|
my ($tasks) = /$re{tasks}/;
|
||
|
|
||
|
$tasks ||= 0;
|
||
|
$jobIdent .= ".$tasks";
|
||
|
|
||
|
## waiting jobs/tasks
|
||
|
if ( $state and $state =~ /[qw]/ ) {
|
||
|
my $ntasks;
|
||
|
if ($tasks) {
|
||
|
my ( $min, $max, $step );
|
||
|
|
||
|
# parse n[-m[:s]] and n,m
|
||
|
# these should be the only possibilities
|
||
|
if ( ( $min, $max, $step ) =
|
||
|
$tasks =~ /^(\d+)(?:-(\d+)(?::(\d+))?)?$/
|
||
|
or ( $min, $max ) = $tasks =~ /^(\d+),(\d+)?$/ )
|
||
|
{
|
||
|
$max ||= $min;
|
||
|
$step ||= 1;
|
||
|
for ( ; $min <= $max ; $min += $step ) {
|
||
|
$ntasks++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
$ntasks ||= 1;
|
||
|
|
||
|
while (
|
||
|
s{<(\S*hard_request).*?\s+name=\"(\S+)\".*?>(\d[\.\d]*)</\1>}{})
|
||
|
{
|
||
|
my ( $name, $request ) = ( $2, $3 );
|
||
|
|
||
|
## scale non-'job' consumables
|
||
|
$request *= $slots
|
||
|
unless exists $managedType->{$name}
|
||
|
and $managedType->{$name} =~ /job/i;
|
||
|
|
||
|
my $count = sprintf "%.0f", ( $request * $ntasks );
|
||
|
$status->{$name}{waiting}{$user} += $count;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
$host or next; # safety
|
||
|
$host =~ s{\..*$}{}; # strip domain - unqualified host name
|
||
|
my $consumer = "\L$user\@$host";
|
||
|
|
||
|
while (
|
||
|
s{<(\S*hard_request).*?\s+name=\"(\S+)\".*?>(\d[\.\d]*)</\1>}{})
|
||
|
{
|
||
|
my ( $name, $request ) = ( $2, $3 );
|
||
|
|
||
|
## scale non-'job' consumables
|
||
|
$request *= $slots
|
||
|
unless exists $managedType->{$name}
|
||
|
and $managedType->{$name} =~ /job/i;
|
||
|
|
||
|
my $count = sprintf "%.0f", $request;
|
||
|
$status->{$name}{$jobIdent}{"$consumer $count"}++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $status;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
# --------------------------------------------------------------- end-of-package
|
||
|
package Qconf;
|
||
|
|
||
|
BEGIN {
|
||
|
$ENV{SGE_SINGLE_LINE} = 1; # do not break up long lines with backslashes
|
||
|
}
|
||
|
|
||
|
# extract 'administrator_mail'
|
||
|
|
||
|
sub mail {
|
||
|
my $caller = shift;
|
||
|
|
||
|
my @lines = GridEngine->bin( qconf => qw( -sconf ) );
|
||
|
defined $lines[0] or return undef;
|
||
|
|
||
|
@lines = grep { s{^\s*administrator_mail\s+}{} } @lines;
|
||
|
chomp @lines;
|
||
|
|
||
|
return $lines[0];
|
||
|
}
|
||
|
|
||
|
# query 'complex_values' from the global host
|
||
|
# return hashref
|
||
|
sub query {
|
||
|
my $caller = shift;
|
||
|
|
||
|
my @lines = GridEngine->bin( qconf => qw( -se global ) );
|
||
|
defined $lines[0] or return +{};
|
||
|
|
||
|
return +{
|
||
|
map {
|
||
|
s/,/ /g;
|
||
|
map { /^(.+)=(.+)\s*$/ } split;
|
||
|
} grep { s/^\s*complex_values\s+// } @lines
|
||
|
};
|
||
|
}
|
||
|
|
||
|
#
|
||
|
# set 'complex_values' of the global host
|
||
|
#
|
||
|
sub mattr {
|
||
|
my $caller = shift;
|
||
|
my $val = shift;
|
||
|
|
||
|
GridEngine->bin(
|
||
|
qconf => ( qw( -mattr exechost complex_values ), $val, "global" ) )
|
||
|
if $val;
|
||
|
}
|
||
|
|
||
|
# determine what exists in the globals and in complex_values and has changed
|
||
|
#
|
||
|
# Prototype ->diff( HASHREF1, HASHREF2 );
|
||
|
#
|
||
|
#
|
||
|
# HASHREF1 => { # from the 'qconf -se global'
|
||
|
# feature => total,
|
||
|
# }
|
||
|
#
|
||
|
# HASHREF2 => { # from 'mungeLicenses'
|
||
|
# feature => {
|
||
|
# type => STRING or undef,
|
||
|
# total => INT,
|
||
|
# limit => INT,
|
||
|
# extern => INT,
|
||
|
# ...
|
||
|
# }
|
||
|
# }
|
||
|
#
|
||
|
# determine the number of resources that can be managed by the GridEngine:
|
||
|
# managed = total - external_count
|
||
|
#
|
||
|
sub diff {
|
||
|
my $caller = shift;
|
||
|
my ( $complex_values, $licenses ) = @_;
|
||
|
my $changes = {};
|
||
|
|
||
|
for my $resource ( keys %$complex_values ) {
|
||
|
my $entry = $licenses->{$resource} or next;
|
||
|
|
||
|
my ( $total, $limit, $extern ) = @{$entry}{qw( total limit extern )};
|
||
|
my $managed = $total - $extern;
|
||
|
if ( defined $limit and $limit < $managed ) {
|
||
|
$managed = $limit;
|
||
|
}
|
||
|
|
||
|
$managed >= 0 or $managed = 0; # should not be required
|
||
|
|
||
|
$complex_values->{$resource} == $managed
|
||
|
or $changes->{$resource} = $managed;
|
||
|
}
|
||
|
|
||
|
return $changes;
|
||
|
}
|
||
|
|
||
|
1;
|
||
|
|
||
|
# --------------------------------------------------------------- end-of-package
|
||
|
|
||
|
# ------------------------------------------------------------------ end-of-file
|