#!/usr/bin/perl -w
use strict;
use Getopt::Std qw( getopts );
use POSIX qw( );
( my $Script = $0 ) =~ s{^.*/}{};
my $time = time;
my $date = POSIX::strftime( "%F %T", localtime $time );
$ENV{SGE_ROOT} or die "Error $Script: \$SGE_ROOT is not set\n";
$ENV{SGE_CELL} ||= "default"; # sge_cell
my ($siteLocation) = join "/" => ( $ENV{SGE_ROOT}, $ENV{SGE_CELL}, "site" );
my $config = {
-site => ( $siteLocation || '' ),
-cache => ( $siteLocation || '' ) . "/cache/" . "qlicserver.xml",
-limits => ( $siteLocation || '' ) . "/" . "qlicserver.limits",
};
# ------------------------------------------------------------------------
# utils
#
# calculate age from an epoch value
#
sub age {
my ( $a, $b ) = @_;
my $diff = ( $a - $b );
my $sign = '';
if ( $diff < 0 ) { # handle negatives
$sign = '-';
$diff = abs($diff);
}
sprintf "$sign%d:%02d:%02d", # format into hh:mm:ss
( int $diff / 3_600 ), # hh
( ( int $diff / 60 ) % 60 ), # mm
( $diff % 60 ); # ss
}
#
# change hash references to a comma-delimited string of key=value entries
#
sub hashRefToString {
map {
my $ref = $_;
ref $ref ? map { "$_=$ref->{$_}" } sort keys %$ref : ()
} @_;
}
#
# 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;
}
# ------------------------------------------------------------------------
# processing of qlicserver.xml output
#
package qlicCache;
sub new {
my $caller = shift;
my $file = shift;
my $self = bless {
( map { $_ => {} } qw( -rc ) ),
( map { $_ => '' } qw( -date -host -age ) )
}, $caller;
-f $file or return $self;
local $_ = do { local ( @ARGV, $/ ) = $file; <>; };
# strip out all xml comments
s{\s*}{}sg;
# get the header section
s{^(.+?)}{}s or return;
my $header = $1;
for ($header) {
if (m{]*))? > \s*(.+?)\s* }sx) {
my ( $attr, $content ) = ( $1, $2 );
$self->{-host} = $content;
}
if (m{}sx) {
my ( $attr, $content ) = ( $1, $2 );
my %attr = main::parseXMLattrib($attr);
$self->{-age} = main::age( $time, $attr{epoch} ) if $attr{epoch};
$self->{-date} = $content;
}
}
# only retain the contents
s{^.*?}{}s and s{.*$}{}s or return;
## process
while (s{]*))? />}{}sx) {
my ($attr) = ($1);
my %attr = main::parseXMLattrib($attr);
my $name = delete $attr{name};
if ( defined $name ) {
for ( keys %attr ) {
$self->{-rc}{$name}{$_} = $attr{$_} || 0;
}
}
}
## process CONTENT
while (s{]*))? > \s* (.*?) \s* }{}sx) {
my ( $attr, $content ) = ( $1, $2 );
my %attr = main::parseXMLattrib($attr);
my $name = delete $attr{name};
if ( defined $name ) {
for ( keys %attr ) {
$self->{-rc}{$name}{$_} = $attr{$_} || 0;
}
}
## process COUNT
while ( $content =~ s{]*))? >\s*(\d+)\s*}{}sx )
{
my ( $attr, $count ) = ( $1, $2 );
my %attr = main::parseXMLattrib($attr);
my $user = delete $attr{name};
my $host = delete $attr{host};
if ( defined $user ) {
if ( defined $host ) {
$self->{-rc}{$name}{-where}{"$user\@$host"} = $count;
}
else {
## tag waiting with '[]'
$self->{-rc}{$name}{-where}{$user} = "[$count]";
}
}
}
}
return $self;
}
sub query {
my $self = shift;
%{ $self->{-rc} };
}
sub dump {
use Data::Dumper;
print Dumper( $_[0] );
}
sub available {
my $self = shift;
sort keys %{ $self->{-rc} };
}
# ------------------------------------------------------------------------
# processing of qlicserver.limits FILE or DIRECTORY
#
package qlicLimits;
sub new {
my $caller = shift;
my $diskValues = shift;
my $self = bless {}, $caller;
my $fileString;
if ( defined $diskValues and -f $diskValues ) {
$fileString = do {
local ( *FILE, $/ );
if ( open FILE, $diskValues ) {
;
}
else {
undef;
}
};
}
elsif ( defined $diskValues and -d $diskValues ) {
local *DIR;
my $dir = $diskValues;
if ( opendir DIR, $dir ) {
my @files = grep { -f "$dir/$_" and -r _ } readdir DIR;
for my $name (@files) {
my $limit;
# use the last value
if ( open FILE, "$dir/$name" ) {
$limit = ( map { /^\s*(-?\d+)\s*$/ } )[-1];
}
defined $limit or $limit = "NONE";
$self->{$name} = $limit;
}
}
}
for ($fileString) {
defined or next;
# strip out all comments
s{\s*}{}sg;
## process and
while (s{]*))? />}{}sx
or s{]*))? >\s*}{}sx )
{
my %attr = main::parseXMLattrib($1);
my $name = delete $attr{name};
my $limit = delete $attr{limit};
if ( defined $name and defined $limit ) {
$self->{$name} = $limit;
}
}
}
return $self;
}
sub change {
my $caller = shift;
my $diskValues = shift;
my @args = @_;
@args or return;
my ( %pending, %adjusted );
for (@args) {
s{,}{ }g; # comma -> space-delimited
my %h = map { /^(.+?)=(.*)$/ } split;
for ( keys %h ) {
defined $h{$_} and length $h{$_} or $h{$_} = "NONE";
$pending{$_} = $h{$_};
}
}
keys %pending or return;
my $user = getpwuid $<;
if ( defined $diskValues and -d $diskValues ) {
local *DIR;
my $dir = $diskValues;
if ( opendir DIR, $dir ) {
my @files = grep { -f "$dir/$_" and -w _ } readdir DIR;
for my $name (@files) {
if ( exists $pending{$name} ) {
local *FILE;
if ( open FILE, ">", "$dir/$name" ) {
print FILE "# adjusted by $user $date\n",
"$pending{$name}\n";
$adjusted{$name} = delete $pending{$name};
}
}
}
}
}
for ( [ "adjusted" => \%adjusted ], [ "not adjusted" => \%pending ], ) {
my ( $label, $href ) = @$_;
keys %$href or next;
print "$label: ",
join( "," => map { qq{$_=$href->{$_}} } sort keys %$href ), "\n";
}
}
sub output {
my $self = shift;
my @list = map { qq{$_=$self->{$_}} } sort keys %$self;
print "limits: ";
if (@list) {
print join( "," => @list ), "\n";
}
else {
print "NONE\n";
}
}
# ------------------------------------------------------------------------
# main
#
package main;
import qlicCache;
# ------------------------------------------------------------------------
sub usage {
$! = 0; # clean exit
warn "@_\n" if @_;
die <<"USAGE";
usage:
$Script [OPTION]
$Script [OPTION] resource=limit .. resource=limit
with options:
-c FILE alternative location for the license cache
-C FILE alternative location for the license limit
-d dump cache file as raw xml
-D dump license cache in perl format
-f display free licenses only
-l list license limit
-q display free licenses via qhost query
-u license usage via 'lacct'
-U license usage per user via 'lacct -u'
-w show who/where ('[]' indicates waiting jobs)
-h this help
* extract / display information for the GridEngine license cache
$config->{-cache}
* adjust / display information for the license limits
$config->{-limits}
copyright (c) 2003-10
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;
getopts( 'hc:C:DdflqUuw', \%opt ) or usage();
$opt{h} and usage();
if ( $opt{U} ) {
## user-based usage from accounting
my @query = ( "lacct", "-u" );
exec @query;
exit 2;
}
elsif ( $opt{u} ) {
## usage from accounting
my @query = ( "lacct", $opt{w} ? "-w" : () );
exec @query;
exit 2;
}
if ( $opt{d} ) {
my $file = $opt{c} || $config->{-cache};
if ( not -f $file and $file !~ m{/} ) {
$file = "$config->{-site}/$file";
}
-f $file or exit 1;
local $_ = do { local ( @ARGV, $/ ) = $file; <>; };
# do a basic check for well-formed xml
# this might not be the case if there is a race condition
# and the file has not been fully written
# check for '' at the end
my ($tag) = m{^<\?xml (?:\s+[^<>]*?)? \?>\s*\<(\w+)}sx;
unless ( $tag and m{\Q$tag\E>\s*$} ) {
sleep 2; ## wait and try again
$_ = do { local ( @ARGV, $/ ) = $file; <>; };
}
$_ ||= ''; ## avoid uninitialized
print;
exit;
}
if ( $opt{q} ) {
my %free =
map { /^\s+gc:(\S+?)=(\d\S*)\s*$/ }
grep { /^global/ ... /^\S/ } qx{qhost -F};
$_ += 0 for values %free;
for ( sort keys %free ) {
my $intval = $free{$_} + 0; # normalize as integers
print "$_=$intval\n";
}
exit;
}
if ( $opt{l} ) {
qlicLimits->new( $opt{C} || $config->{-limits} )->output();
exit;
}
if (@ARGV) {
qlicLimits->change( $opt{C} || $config->{-limits}, @ARGV );
exit;
}
my $info = qlicCache->new( $opt{c} || $config->{-cache} );
$info->dump() if $opt{D};
my %resources = $info->query();
#
# display free licenses
#
if ( $opt{f} ) {
for my $resource ( sort keys %resources ) {
my $count = $resources{$resource} || {};
my $free = $count->{free} || 0;
print "$resource=$free\n" if $free;
}
exit;
}
#
# display who/where
#
if ( $opt{w} ) {
my ($len) = sort { $b <=> $a } map { length } keys %resources;
$len += 2;
my $fmt = "%-${len}s";
my $indent = sprintf "\n$fmt", '';
for my $resource ( sort keys %resources ) {
my $count = $resources{$resource} || {};
my @list = hashRefToString( $count->{-where} );
if (@list) {
printf $fmt, $resource;
print join( $indent => @list ), "\n";
}
}
exit;
}
#
# define table output format
#
my @outputList;
format =
# name total limit extern intern wait free
@<<<<<<<<<<< @>>>>> @>>>>> @>>>>> @>>>>> @>>>> @>>>>
@outputList
.
#
# display table header
# --------------------
{
my @info = qw( host age );
my ($len) = sort { $b <=> $a } map { length } @info;
print map {
my $k = sprintf "%-${len}s", $_;
my $v = $info->{"-$_"};
$v ? "$k = $v\n" : ();
} @info;
print "\n";
@outputList = qw( resource total limit extern intern wait free );
write;
s/./-/g for @outputList;
write;
}
#
# display table body
# ------------------
for my $resource ( sort keys %resources ) {
my $count = $resources{$resource} || {};
@outputList =
( $resource, @{$count}{qw( total limit extern intern waiting free )} );
my $type = $resources{$resource}{type} || 0;
# no total?
$_ ||= "?" for $outputList[1];
if ( $type eq "intern" ) {
$_ = "*" for $outputList[3];
}
elsif ( $type eq "track" ) {
$_ = "*" for $outputList[4];
}
# easy to read representation for zero
for (@outputList) {
defined $_ or $_ = '.';
}
write;
}
# ------------------------------------------------------------------------