#!/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{\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; } # ------------------------------------------------------------------------