diff --git a/bin/hpc_lsusers b/bin/hpc_lsusers new file mode 100755 index 0000000..b3fd3df --- /dev/null +++ b/bin/hpc_lsusers @@ -0,0 +1,110 @@ +#!/usr/bin/perl -w + +# lshpcusers - list hpc users in format: +# username, login group, sciper, email, slurm accountsi, scratch dirs +# +# Author : EPFL-SCITAS +# Date : January 2016 +# + +use strict; +use FindBin; +use lib "$FindBin::RealBin/../lib/blib"; +use HPCUsers; +use Scratch; + +# ----------------------------------------------------------------------------- +# Command line options +# +my %opt; + +my $print_scratch = 0; # print scratch usage, or not + +sub print_usage() { + print STDERR << "EOF"; +List users on Scitas hpc clusters. + + usage: $0 [-s] [-h] + + OPTIONS + -s : show scratch usage + -h : this (help) message + + example: $0 +EOF + exit; +} + +sub init() +{ + use Getopt::Std; + my $opt_string = 'hs'; + getopts("$opt_string", \%opt) or print_usage(); + print_usage() if $opt{h}; + $print_scratch = 1 if $opt{s}; + print_usage() if (scalar @ARGV > 1); +} + +sub print_users() +{ + if ($print_scratch == 1) { + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s %s\n", + "User","Group:gid","Sciper","Employee","Title", + "Email","Slurm Accounts","Scratch dirs"); + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s %s\n", + "----","---------","------","--------","-----", + "-----","--------------","------------"); + } else { + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s\n", + "User","Group:gid","Sciper","Employee","Title", + "Email","Slurm Accounts"); + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s\n", + "----","---------","------","--------","-----", + "-----","--------------"); + } + foreach my $username (HPCUsers::all_users()) + { + my $user = $username; + if ($HPCUsers::users{$username}{"in_hpc_group"} == 0) { + $user = "*" . $username; + } + my $group = $HPCUsers::users{$username}{"group"}; + my $gid = $HPCUsers::users{$username}{"gid"}; + if ($gid == -1) { + $group = "-1"; + } else { + $group .= ":" . $gid; + } + my $accounts = HPCUsers::accounts_to_string($username); + $accounts = "NOASSOC" if ($accounts eq ""); + + # All scratch directories belonging to user + my $scratch_dirs = Scratch::scratch_dirs_to_string(HPCUsers::uid($username)); + + if ($print_scratch == 1) { + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s %s\n", + $user, $group, + $HPCUsers::users{$username}{"sciper"}, + $HPCUsers::users{$username}{"type"}, + substr($HPCUsers::users{$username}{"title"},0,24), + substr($HPCUsers::users{$username}{"email"},0,28), + $accounts, + $scratch_dirs); + } else { + printf("%-12s%-20s%-8s%-12s%-26s%-30s%-20s\n", + $user, $group, + $HPCUsers::users{$username}{"sciper"}, + $HPCUsers::users{$username}{"type"}, + substr($HPCUsers::users{$username}{"title"},0,24), + substr($HPCUsers::users{$username}{"email"},0,28), + $accounts); + } + } +} + +# ----------------------------------------------------------------------------- + +&init(); +&HPCUsers::init(); +&Scratch::init() if ($print_scratch == 1); +&print_users(); diff --git a/etc/accounts.yaml b/etc/accounts.yaml new file mode 100644 index 0000000..b557b68 --- /dev/null +++ b/etc/accounts.yaml @@ -0,0 +1,103 @@ +# Definition of all slurm accounts for SCITAS clusters +# File format is yaml +# +# Each account is defined by: +# - : a unique account name, which matches the slurm account name +# Global definitions +# - groups: the list of user's goups having access to the account +# - parent: the parent account in the slurn hierarchy +# - share: the number of slurm Share(s) for the account +# - maxwall: maxwalltime for the account +# - maxnodes: max number os nodes per job for the account +# Optional cluster specific definitions +# - : +# - parent: the parent account in the slurm hierarchy for the cluster +# - share: the number of slurm Share(s) for the account for the cluster +# - maxwall: maxwalltime for the account for the cluster +# - maxnodes: max number os nodes per job for the account for the cluster +# +# (c) SCITAS,EPFL 2016 + +# Slurm account definitions +--- +#################### +# fremium accounts # +#################### +free: + parent: root + share: 1 + maxwall: "06:00:00" + maxnodes: 1 + deneb: + share: 64 + fidis: + share: 84 + groups: + - hpc-free + +#################### +# share accounts # +#################### +# By default: +# for accounts on cluster where share have been purchased +# parent : root +# maxwall : "3-00:00:00" +# on the other clusters, where no share have been purchased (share : 1) +# parent : free +toto: + deneb: + share: 1036 + fidis: + share: 1652 + groups: + - hpc-toto + +coucou: + deneb: + share: 89 + groups: + - hpc-coucou + +#################### +# premium accounts # +#################### +premium: + parent: "root" + share: 1 + maxnodes: 1 + deneb: + share: 113 + maxwall: "3-00:00:00" + maxnodes: 8 + fidis: + share: 1354 + maxwall: "3-00:00:00" + maxnodes: 8 + groups: [] + +premium_example: + parent: premium + groups: + - hpc-lab1 + +######################## +# Accounts for courses # +######################## +courses: + parent: "root" + share: 1 + maxnodes: 1 + deneb: + share: 32 + maxwall: "1-00:00:00" + maxnodes: 8 + fidis: + share: 56 + maxwall: "1-00:00:00" + maxnodes: 8 + groups: [] + +hpc_lecture: + parent: courses + groups: + - hpc2018 diff --git a/lib/blib/Cmd.pm b/lib/blib/Cmd.pm new file mode 100644 index 0000000..1c2c746 --- /dev/null +++ b/lib/blib/Cmd.pm @@ -0,0 +1,49 @@ +# Cmd.pm - run commands and get the exit status and ouput +# +# ---- Perl script for running linux commands +# +# Author : EPFL-SCITAS +# Date : August 2018 +# +package Cmd; + +use strict; + +# ----------------------------------------------------------------------------- +our $dry_run = 1; # don't run the commands if true + +# result of last command performed +# result => cmd ; command line +# result => exitcode ; command exit status +# result => output ; stdout output +# result => status ; could be "dryrun", "reinitialized" +our %result; + +# for debugging +sub print_result() { + print "--> cmd=", $result{cmd}, " exitcode=", $result{exitcode}, " output=", $result{output}, " status=", $result{status}, "\n"; +} + +# Run the given command and set global variable %result; return 1 +# If $dryrun is true, don't run the command and return 2 +sub run_command($) +{ + my ($cmd) = @_; + + $result{cmd} = $cmd; + $result{output} = ""; + $result{exitcode} = 0; + if ($dry_run) { + $result{status} = "dryrun"; + return 2; + } + $result{status} = ""; + + # run the command + $result{output} = `$cmd`; + $result{exitcode} = $? >> 8; + + return 1; +} + +1; diff --git a/lib/blib/HPCUsers.pm b/lib/blib/HPCUsers.pm new file mode 100644 index 0000000..fec65b8 --- /dev/null +++ b/lib/blib/HPCUsers.pm @@ -0,0 +1,391 @@ +# HPCUsers.pm - handling scitas cluster users +# +# ---- Perl script accessing ldap directory +# +# Author : EPFL-SCITAS +# Date : January 2016 +# + +package HPCUsers; + +use Net::LDAP; +use strict; +use Log; +use Slurm; + +# ----------------------------------------------------------------------------- + +my $use_only_ldap = 1; + +# users associative array, indexed by username +# users{$username} => uid ; +# users{$username} => gid ; user's login group +# users{$username} => sciper ; +# users{$username} => email ; +# users{$username} => memberOf ; list of groups the user is member of in the scoldap form, +# (S13052,S13055,S13267,U12923,...) +# users{$username} => type ; employee type, such as Edudiant,Personnel,Manuelle,Enseignant +# users{$username} => title ; description of user role at EPFL +# users{$username} => home ; pathname of home directory +# users{$username} => in_hpc_group ; is in hpc_cluster_users group, or not +our %users; + +# associative array for groups, indexed by group's uniqueIdentifier +# groups{$id} => gid; +our %groups; + +# associative array for gids, indexed by gid +# gids{$gid} => scoldap_name; +# gids{$gid} => name; +our %gids; + + + +# ---- +# return a list of all usernames +# +sub all_users () +{ + return (sort keys(%users)); +} + +sub user_exists($) +{ + my ($username) = @_; + return defined($users{$username}); +} + +sub gid_exists($) +{ + my ($gid) = @_; + return defined($gids{$gid}); +} + +sub is_in_hpc_group($) +{ + my ($username) = @_; + return 0 if (!user_exists($username)); + return $users{$username}{in_hpc_group}; +} + +# return true is the user is accredited, i.e. in ldap.epfl.ch "o=epfl,c=ch" +sub is_accredited($) +{ + my ($username) = @_; + return 0 if (!user_exists($username)); + return ($users{$username}{gid} != -1); +} + +sub is_student($) +{ + my ($username) = @_; + if (user_exists($username)) { + return 1 if ($users{$username}{group} =~ /-(ma.|ba.|pmh|h|e|hc)$/); + } + return 0; +} + +sub uid($) +{ + my ($username) = @_; + return -1 if (!user_exists($username)); + return $users{$username}{uid}; +} + +sub gid($) +{ + my ($username) = @_; + return -1 if (!user_exists($username)); + return $users{$username}{gid}; +} + +sub group($) +{ + my ($username) = @_; + return "" if (!user_exists($username)); + return $users{$username}{group}; +} + +sub home_directory($) +{ + my ($username) = @_; + return "" if (!user_exists($username)); + if ($users{$username}{home} ne "") { + return $users{$username}{home}; + } + return "/home/$username" if (-d "/home/$username"); +} + +sub user_to_log($) +{ + my ($username) = @_; + + my $user = $username; + if ($users{$username}{"in_hpc_group"} == 0) { + $user = "*" . $username; + } + my $group = $users{$username}{"group"}; + my $gid = $HPCUsers::users{$username}{"gid"}; + $group = "?" if ($gid == -1); + return sprintf("user=%s group=%s sciper=%s email=%s", + $user, $group, + $users{$username}{"sciper"}, + $users{$username}{"email"}); +} + +sub user_to_string($) +{ + my ($username) = @_; + + my $user = $username; + if ($users{$username}{"in_hpc_group"} == 0) { + $user = "*" . $username; + } + + my $group = $HPCUsers::users{$username}{"group"}; + my $gid = $HPCUsers::users{$username}{"gid"}; + $group = "?" if ($gid == -1); + + my $accounts = accounts_to_string($username); + + return sprintf("user=%s group=%s sciper=%s email=%s accounts=%s", + $user, $group, + $users{$username}{"sciper"}, + $users{$username}{"email"}, + $accounts); +} + +# ---- +# return a list of all group ids +# +sub all_gids () +{ + return (keys(%gids)); +} + +#sub gid_to_name ($) +#{ +# my ($gid) = @_; +# my($gname,$gpasswd,$ggid,$all_users) = getgrgid($gid); +# return $gname; +#} + +sub accounts_to_string($) { + my ($username) = @_; + return Log::array_to_string(Slurm::user_accounts($username)); +} + +sub member_of($) { + my ($username) = @_; + my @grps = (); + foreach my $group_id (@{$users{$username}{memberOf}}) { + if (defined($groups{$group_id})) { + my $gid = $groups{$group_id}{gid}; + push(@grps,$gids{$gid}{name}); + } + } + return @grps; +} + +# return a comma separated goupnames +sub groups_to_string($) { + my ($username) = @_; + return Log::array_to_string(member_of($username)); +} + +sub print_users() +{ + foreach my $username (sort keys(%users)) { + print "$username\n"; + print " => uid : ", $users{$username}{uid}, "\n"; + print " => gid : ", $users{$username}{gid}, "\n"; + print " => sciper : ", $users{$username}{sciper}, "\n"; + print " => email : ", $users{$username}{email}, "\n"; + print " => groups : ", groups_to_string($username), "\n"; + } +} + +sub add_user($) +{ + my ($username) = @_; + if (length($username) > 0 && !defined($users{$username})) { + my %new=(uid => -1, gid => -1, group => "", sciper => "", email => "", + type => "", title => "", home => "", in_hpc_group => 0); + $users{$username} = {%new}; + @{$users{$username}{memberOf}} = (); # list of scoldap group_ids the user is member of + } +} + + +############################################################################ +# Search ldap and scoldap for users and groups information +############################################################################ + +# get users login_group, homeDir, type and title from ldap database +sub search_ldap_for_users() +{ + sub gid_to_group ($$$) + { + my ($ldap,$mesg,$gid) = @_; + if (!defined($gids{$gid})) { + $mesg = $ldap->search(base => "c=ch", filter => "(&(objectClass=posixGroup)(gidNumber=$gid))"); + foreach my $entry ($mesg->entries) { + # $entry->dump; + my $name = $entry->get_value("cn"); + $gids{$gid}{"scoldap_name"} = ""; + $gids{$gid}{"name"} = $name; + last; + } + } + return $gids{$gid}{"name"}; + } + + my $ldap = Net::LDAP->new('ldap.epfl.ch') or die "$@"; + my $mesg = $ldap->bind; + foreach my $username (keys(%users)) { + my $sciper = $users{$username}{sciper}; + my $homeDir = ""; + my $type = ""; + my $title = ""; + # use base c=ch to also get former users in epfl-old (not anymore accredited) + $mesg = $ldap->search(base => "c=ch", filter => "(uniqueIdentifier=$sciper)"); + # look for all entries so that order of accreditation doesn't matter + foreach my $entry ($mesg->entries) { + # $entry->dump; + if ($users{$username}{uid} == -1) { + # is the case for users not accredited in epfl-old + my $uid = $entry->get_value("uidNumber"); + $users{$username}{uid} = $uid if defined($uid); + } + my $gid = $entry->get_value("gidNumber"); + # scoldap and ldap are not always synchronized; take ldap as the reference + if ($gid != $users{$username}{gid}) { + $users{$username}{gid} = $gid; + } + if ($gid != -1) { + $users{$username}{group} = gid_to_group($ldap,$mesg,$gid); + } + $type = $entry->get_value("employeeType"); + $type = "?" if !defined $type; + $title = $entry->get_value("title;lang-en"); + $title = "" if !defined $title; + $homeDir = $entry->get_value("homeDirectory"); + last if ($type eq "Etudiant" && is_student($username)); + last if ($type eq "Personnel"); + } + # print ">>> ", $username, " sciper=", $users{$username}{sciper}, " type:", $type, " title:", $title, "\n"; + $users{$username}{"type"} = $type if (defined $type); + $users{$username}{"title"} = $title if (defined $title); + $users{$username}{"home"} = $homeDir if (defined $homeDir); + } + $mesg = $ldap->unbind; +} + +# Search scoldap for all of the groups the users are member of: +# - get the groupname and gid of each group, and +# - populate the %group and %gids associative arrays +sub search_scoldap_for_groups() +{ + my $scoldap = Net::LDAP->new('scoldap.epfl.ch') or die "$@"; + my $mesg = $scoldap->bind; + + foreach my $username (keys(%users)) { + foreach my $group_id (@{$users{$username}{memberOf}}) { + if (!defined($groups{$group_id})) { + $mesg = $scoldap->search(base => "ou=groups,o=epfl,c=ch", filter => "(uniqueIdentifier=$group_id)"); + foreach my $entry ($mesg->entries) { + # $entry->dump; + my $scoldap_name = $entry->get_value("displayName"); + # new 24-apr-2017: convert to lower-case + $scoldap_name = lc$scoldap_name; + my $name = $scoldap_name; + my $gid = $entry->get_value("gidNumber"); + if ($name =~ s/(.*)-unit/$1/) { + $name = lc$name; + } + $groups{$group_id}{gid} = $gid; + $gids{$gid}{name} = $scoldap_name; + $gids{$gid}{name} = $name; + last; + } + } + } + } + $mesg = $scoldap->unbind; +} + +# get users sciper, uid, login_gid and email from scoldap database +sub search_scoldap_for_users() +{ + my $scoldap = Net::LDAP->new('scoldap.epfl.ch') or die "$@"; + my $mesg = $scoldap->bind; + + foreach my $username (sort keys(%users)) { + # search regular accredited users as well as former users not any more accredited (in epfl-old). + # - accredited users have a positive uid and gid + # - epfl-old users are characterized by no "uidNumber" and "gidNumber" fields + $mesg = $scoldap->search(base => "o=epfl,c=ch", filter => "uid=$username"); + foreach my $entry ($mesg->entries) { + # $entry->dump; + my $uid = $entry->get_value("uidNumber"); + $users{$username}{uid} = $uid if defined($uid); + my $gid = $entry->get_value("gidNumber"); + $users{$username}{gid} = $gid if defined($gid); + $users{$username}{sciper} = $entry->get_value("uniqueIdentifier"); + my $email = $entry->get_value("mail"); + $users{$username}{email} = $email if (defined $email); + $users{$username}{memberOf} = (); + foreach my $group_id ($entry->get_value("memberOf")) { + push(@{$users{$username}{memberOf}},$group_id); + } + last; # in principle, there is only one, unique entry per user in scoldap anyway + } + } + $mesg = $scoldap->unbind; +} + +# get members of group 'hpc-cluster-users' with id S14274 in scoldap.epfl.ch +sub get_hpc_cluster_users() +{ + my $scoldap = Net::LDAP->new('scoldap.epfl.ch') or die "$@"; + my $mesg = $scoldap->bind; + + $mesg = $scoldap->search(base => "ou=groups,o=epfl,c=ch", filter => "uniqueIdentifier=S14274"); + foreach my $entry ($mesg->entries) { + my @all_users = $entry->get_value("memberUid"); + # create a %user entry for each user + foreach my $username (@all_users) { + # print $username, "\n"; + add_user($username); + if (user_exists($username)) { + $users{$username}{"in_hpc_group"} = 1; + } + } + } + $mesg = $scoldap->unbind; +} + +# initialize %users associative array by searching ldap directory +sub init () +{ + # get the members of group 'hpc-cluster-users' + &get_hpc_cluster_users(); + + # get slurm users and accounts, and their associations + &Slurm::init(); + # add slurm users which are not yet in 'hpc-cluster-users' + foreach my $username (Slurm::all_users()) { + add_user($username) if !defined($users{$username}); + } + + # search scoldap for user's uid, login group, sciper, email and list of groups + &search_scoldap_for_users(); + # search ldap for user's login_group, homeDir, type and title + &search_ldap_for_users(); + + # get group name and gids of all of the groups users are member of: + # &search_scoldap_for_groups(); + # &print_users(); +} + +1; diff --git a/lib/blib/Log.pm b/lib/blib/Log.pm new file mode 100644 index 0000000..55cebbe --- /dev/null +++ b/lib/blib/Log.pm @@ -0,0 +1,66 @@ +# Log.pm - handling /scratch users' data +# +# ---- Perl script querying scratch file system usage +# +# Author : EPFL-SCITAS +# Date : January 2016 +# +package Log; + +use strict; +use POSIX qw(strftime); + +our $log_str = ""; +our $logfile = "hpcusers.log"; + +sub log_date() +{ + return strftime "%F@%H:%M:%S ", localtime; +} + +sub log_message($) +{ + my ($msg) = @_; + + $log_str = log_date() . $msg; + return $log_str; +} + +# append message to the logfile +sub append_msg($) +{ + my ($msg) = @_; + if (open(LOG_OUT,">> $logfile")) { + print LOG_OUT $msg,"\n"; + close(LOG_OUT); + } +} + +# convert an array to a comma-separated string +sub array_to_string(@) { + my (@elems) = @_; + my $str = ""; + my $i = 0; + foreach my $elem (@elems) { + $str = $str . "," if $i > 0; + $str = $str . $elem; + $i++; + } + return $str; +} + +# print command, results to stdout, and log command if not dry run +sub output_last_cmd_result() +{ + my $cmd = $Cmd::result{cmd}; + my $str = log_message($cmd); + print $str, "\n" if ($cmd ne ""); + if (! $Cmd::dry_run) { + print $Cmd::result{output}, "\n"; + append_msg($str); + } +} + +1; + + diff --git a/lib/blib/ParseAccounts.pm b/lib/blib/ParseAccounts.pm new file mode 100644 index 0000000..1b46831 --- /dev/null +++ b/lib/blib/ParseAccounts.pm @@ -0,0 +1,224 @@ +# Accounts.pm - handling account-groups definitions +# +# Reads accounts.yaml definition file +# - Gets generic settings +# - Gets specific settings for the current slurm cluster +# +# Author : EPFL-SCITAS +# Date : March 2016 +# +# Revisions +# January 2017: add fidis +# August 2018: get the settings for the current cluster only, passed to init() + +package ParseAccounts; + +use strict; +no strict "refs"; +use YAML; +use Data::Dumper; +use Log; + +# --- account generic settings --- +# associative array, indexed by accountname +# generic{$accountname} => parent ; slurm parent account +# generic{$accountname} => share ; slurm share +# generic{$accountname} => maxwall ; slurm maxwall +# generic{$accountname} => maxnodes ; slurm max nodes +my %generic; + +# --- account specific settings for the target cluster passed to init() --- +# associative array, indexed by accountname +# cluster{$accountname} => parent ; slurm parent account +# cluster{$accountname} => share ; slurm share +# cluster{$accountname} => maxwall ; slurm maxwall +# cluster{$accountname} => maxnodes ; slurm max nodes +# +my %cluster; + +# associative array for groups, indexed by group's uniqueIdentifier +# groups{$name} => [accounts]; list of slurm accounts associated to group +our %groups; + +# the target cluster name +my $clustername; + +# the YAML account definition file +my $filename; + +# list of all acounts ordered from root to leaves of the account dependence tree +my @ordered_list_of_accounts; + +sub all_accounts () +{ + return (sort keys(%generic)); +} + +# list of accounts in the account tree descendant order (root to leaves) +sub all_accounts_tree () +{ + return @ordered_list_of_accounts; +} + +# get the specified account-key's value +sub get_value { + my ($accountname,$key) = @_; + + my $value = $cluster{$accountname}{$key}; + return $value if (defined($value) && $value ne ""); + $value = $generic{$accountname}{$key}; + return $value if (defined($value) && $value ne ""); + + # implicit rules, if "no" value specified: + if ($key eq "parent") { + # - parent=root for accounts with share > 1 + # - parent=free for accounts with share == 1 + return "free" if (get_value($accountname,"share") == 1); + return "root"; + } elsif ($key eq "maxwall") { + # get slurm inherited value from parent account, if any + my $parent = get_value($accountname,"parent"); + return "" if $parent eq ""; + return "3-00:00:00" if $parent eq "root"; + return get_value($parent,$key); + } elsif ($key eq "maxnodes") { + # get slurm inherited value from parent account, if any + my $parent = get_value($accountname,"parent"); + return -1 if ($parent eq "" || $parent eq "root"); # -1 means "no" value for maxnodes + return get_value($parent,$key); + } elsif ($key eq "share") { + return 1; + } + + # otherwise return "" witch means not "no" value set + return ""; +} +sub account_parent($) {return get_value($_[0],"parent");} +sub account_share($) {return get_value($_[0],"share");} +sub account_maxwall($) {return get_value($_[0],"maxwall");} +sub account_maxnodes($) {return get_value($_[0],"maxnodes");} + +sub print_groups () +{ + foreach my $groupname (sort keys(%groups)) { + print $groupname, ":", Log::array_to_string(@{$groups{$groupname}}), "\n"; + } +} + +sub print_accounts() +{ + foreach my $accountname (sort keys(%generic)) { + print "generic(", $accountname, ")\n"; + print " => parent : ", $generic{$accountname}{parent}, "\n"; + print " => share : ", $generic{$accountname}{share}, "\n"; + print " => maxwall : ", $generic{$accountname}{maxwall}, "\n"; + print " => maxnodes : ", $generic{$accountname}{maxnodes}, "\n"; + } + # print cluster specific values + foreach my $accountname (sort keys(%cluster)) { + print "\tcluster(", $accountname, ")\n"; + print "\t => parent : ", $cluster{$accountname}{parent}, "\n"; + print "\t => share : ", $cluster{$accountname}{share}, "\n"; + print "\t => maxwall : ", $cluster{$accountname}{maxwall}, "\n"; + print "\t => maxnodes : ", $cluster{$accountname}{maxnodes}, "\n"; + } +} + +sub print() +{ + print "Account definitions for cluster ", $clustername, ":\n"; + foreach my $accountname (all_accounts_tree()) { + print $filename, ":", $clustername, ":", $accountname, "\n"; + print " => parent : ", get_value($accountname,"parent"), "\n"; + print " => share : ", get_value($accountname,"share"), "\n"; + print " => maxwall : ", get_value($accountname,"maxwall"), "\n"; + print " => maxnodes : ", get_value($accountname,"maxnodes"), "\n"; + } +} + +# Return the list of accounts associated to a group +sub group_to_slurm_accounts($) +{ + my ($groupname) = @_; + return () if !defined($groups{$groupname}); + return @{$groups{$groupname}}; +} + +sub new_account($) +{ + my ($accountname) = @_; + + my %new=(parent => "", share => "", maxwall => "", maxnodes => ""); + if (! defined($generic{$accountname})) { + $generic{$accountname} = {%new}; + } + if (! defined($cluster{$accountname})) { + $cluster{$accountname} = {%new}; + } +} + +# recursive routine to build the top-down list of accounts, matching the account dependence tree +sub account_tree +{ + my ($parent) = @_; + @ordered_list_of_accounts = () if $parent eq "root"; + + foreach my $accountname (keys(%generic)) { + if (get_value($accountname,"parent") eq $parent) { + if ( ! grep( /^$accountname$/, @ordered_list_of_accounts) ) { + push(@ordered_list_of_accounts,$accountname); + account_tree($accountname); + } + } + } +} + +sub init($$) +{ + my ($_clustername,$yaml_file) = @_; + + # reset global variables + %groups = (); + %generic = (); + %cluster = (); + + $clustername=$_clustername; + + $filename = $yaml_file; + my $accountDefinitions = YAML::LoadFile($yaml_file); + # print Dumper($accountDefinitions); + my %accounts = %{$accountDefinitions}; + + foreach my $_account (keys(%accounts)) { + # names are case unsensitive + my $accountname=lc$_account; + new_account($accountname); + my %elems = %{$accounts{$_account}}; + foreach my $elem (keys(%elems)) { + # print $accountname, " ", $elem, "\n"; + if ($elem eq "groups") { + my @groups = @{$accounts{$_account}{groups}}; + # print $account, " groups=", Log::array_to_string(@groups), "\n"; + foreach my $groupname (@groups) { + $groupname=lc$groupname; + push(@{$groups{$groupname}},$accountname); + } + } elsif ($elem eq $clustername) { + for (keys(%{$accounts{$_account}{$clustername}})) { + $cluster{$accountname}{$_} = $accounts{$_account}{$clustername}{lc$_}; + # print $clustername, " ", $accountname, " ", $_, "--->", $cluster{$accountname}{$_}, "\n"; + } + } elsif (defined($generic{$accountname}{$elem})) { + $generic{$accountname}{$elem} = $accounts{$_account}{lc$elem}; + # print " generic ", $accountname, " ", $elem, "--->", $generic{$accountname}{$elem}, "\n"; + } + } + } + + # get the list of accounts in the tree descendant order + account_tree("root"); +} + +1; + + diff --git a/lib/blib/Scratch.pm b/lib/blib/Scratch.pm new file mode 100644 index 0000000..9c1b5df --- /dev/null +++ b/lib/blib/Scratch.pm @@ -0,0 +1,132 @@ +# Scratch.pm - handling user /scratch directories +# +# ---- Perl script querying scratch file system usage +# +# Author : EPFL-SCITAS +# Date : January 2016 +# +# Revisions + +package Scratch; + +use strict; +use File::Basename; +use HPCUsers; +use Log; + +# ----------------------------------------------------------------------------- + + +# associative array for user's scratch directories, indexed by uid +# scratch{$uid} => dirs ; list of scratch directories belonging to user $uid +our %scratch; + +# For debugging +sub print_scratch () +{ + foreach my $uid (keys(%scratch)) { + print $uid, " ", Log::array_to_string(@{$scratch{$uid}{"dirs"}}), "\n"; + } +} + +# Return a comma separated string of all scratch directories belonging to the user +sub scratch_dirs_to_string($) +{ + my ($uid) = @_; + return "" if !defined($scratch{$uid}); + return Log::array_to_string(@{$scratch{$uid}{"dirs"}}); +} + +sub existing_user_scratch_dirs($) +{ + my ($uid) = @_; + return () if !defined($scratch{$uid}); + return @{$scratch{$uid}{"dirs"}}; +} + +sub user_scratch_dir($) +{ + my ($username) = @_; + return "/scratch/$username"; +} + +sub user_scratch_dir_exists($) +{ + my ($username) = @_; + return 1 if (-d "/scratch/$username"); + return 0; +} + +sub create_user_scratch_dir($$) +{ + my ($username,$gid) = @_; + my $pathname = user_scratch_dir($username); + my $uid = HPCUsers::uid($username); + my $cmd = "mkdir $pathname; chown ${uid}:${gid} $pathname; chmod 750 $pathname"; + return Cmd::run_command($cmd); +} + +# Check whether specified directory is empty, or not +sub dir_is_empty($) +{ + my ($dir) = @_; + + if (opendir my $h, $dir) { + while ( defined (my $entry = readdir $h) ) { + return unless $entry =~ /^[.][.]?\z/; + } + closedir $h + } + + return 1; +} + +# Delete, or move scratch directory belonging to given user +sub delete_user_scratch_dir($) +{ + my ($username) = @_; + + my $pathname = user_scratch_dir($username); + my $cmd = ""; + + if (dir_is_empty($pathname)) { + $cmd = "rmdir $pathname"; + } else { + my $destination_pathname = "/scratch/todelete/" . basename($pathname); + $destination_pathname .= "_1" if (-e "$destination_pathname"); + $cmd = "mv $pathname $destination_pathname"; + } + return Cmd::run_command($cmd); +} + +sub append_path($$) +{ + my ($username,$pathname) = @_; + push(@{$HPCUsers::users{$username}->{"scratch"}},$pathname); +} + +# Scan /scratch for users directories in the form /scratch/ +sub scan_scratch() +{ + open(SCRATCH,"find /scratch -mindepth 1 -maxdepth 1 -type d \! -user root -printf '%p %f %U %g\n' |"); + while() { + if ($_ =~ /^((\w|\/|-)+)\s((\w|-)+)\s(\d+)\s((\w|-)+)/) { + my $pathname = $1; + my $dirname = $3; # is also the username, in principle + my $owner_uid = $5; + my $group = $6; + push(@{$scratch{$owner_uid}->{"dirs"}},$pathname); + } + } + close(SCRATCH); +} + +# initialize %users associative array by searching /scratch filesystem +sub init () +{ + # search for all user directories + scan_scratch(); +} + +1; + diff --git a/lib/blib/Slurm.pm b/lib/blib/Slurm.pm new file mode 100644 index 0000000..1b65b55 --- /dev/null +++ b/lib/blib/Slurm.pm @@ -0,0 +1,392 @@ +# Slurm.pm - handling scitas slurm users and accounts +# +# ---- Perl script querying slurm db using 'sacctmgr' utility +# +# Author : EPFL-SCITAS +# Date : January 2016 +# +# Revisions +# - August 2018: refactoring, added processing of variables maxnodes and maxwall + +package Slurm; + +use strict; +use Cmd; + +# ----------------------------------------------------------------------------- + +my $sacctmgr="/usr/bin/sacctmgr"; + +# associative array for slurm user-account associations, indexed by username +# users{$username} => default_account ; user's default slurm account +# users{$username} => accounts ; list of user accounts +my %users; + +# associative array for slurm account associations, indexed by accountname +my %accounts; +# accounts{$accountname} => parent ; parent account +# accounts{$accountname} => share ; share +# accounts{$accountname} => maxnodes ; max number of nodes ("" means not specified) +# accounts{$accountname} => maxwall ; max walltime ("" means not specified) +# accounts{$accountname} => users ; list of users in this account +# accounts{$accountname} => children ; list of child accounts + +# ----------------------------------------------------------------------------- +sub _init_account($$$) +{ + my ($accountname,$parent,$share) = @_; + $accounts{$accountname}{parent} = $parent; + $accounts{$accountname}{share} = $share; + $accounts{$accountname}{maxwall} = ""; + $accounts{$accountname}{maxnodes} = -1; # numerical value for "no" value + @{$accounts{$accountname}{users}} = (); + @{$accounts{$accountname}{children}} = (); +} + +# reset all account children data +sub _set_account_children() +{ + foreach my $accountname (keys(%accounts)) { + @{$accounts{$accountname}{children}} = (); + } + foreach my $accountname (keys(%accounts)) { + my $parent = $accounts{$accountname}{parent}; + push(@{$accounts{$parent}{children}},$accountname) if $parent ne ""; + } +} +# ----------------------------------------------------------------------------- + + + +# return the list of all slurm users +sub all_users () +{ + return (sort keys(%users)); +} + +sub user_exists($) +{ + my ($username) = @_; + return defined($users{$username}); +} + +# return the list of all slurm accounts +sub all_accounts () +{ + return (sort keys(%accounts)); +} + +sub account_exists($) +{ + my ($accountname) = @_; + return defined($accounts{$accountname}); +} + +# get the specified account-key's value +sub get_account_value ($$) +{ + my ($accountname,$key) = @_; + my $value = $accounts{$accountname}{$key}; + return $value if (defined($value)); + return ""; +} +sub account_share($) {return get_account_value($_[0],"share");} +sub account_parent($) {return get_account_value($_[0],"parent");} +sub account_maxwall($) {return get_account_value($_[0],"maxwall") } +sub account_maxnodes($) +{ + my $maxnodes = get_account_value($_[0],"maxnodes"); + return -1 if $maxnodes eq ""; + return $maxnodes; +} + +# get the specified account-key's value +sub get_parent_account_value ($$) +{ + my ($accountname,$key) = @_; + my $parent = get_account_value($accountname,"parent"); + $parent = "root" if $parent eq ""; + return get_account_value($parent,$key); +} + +sub account_users($) +{ + my ($accountname) = @_; + return () if !defined($accounts{$accountname}); + return @{$accounts{$accountname}{users}}; +} + +# return the default account of the given user, or "" if none +sub default_account($) +{ + my ($username) = @_; + return "" if (!defined($users{$username})); + return $users{$username}{default_account}; +} + +# return the list of accounts the specified user belongs to +sub user_accounts($) +{ + my ($username) = @_; + return () if !user_exists($username); + return @{$users{$username}{accounts}}; +} + +sub is_account_empty($) +{ + my ($accountname) = @_; + return 0 if !defined($accounts{$accountname}); + my $nb_users = scalar @{$accounts{$accountname}{users}}; + my $nb_subaccounts = scalar @{$accounts{$accountname}{children}}; + return ($nb_users == 0 && $nb_subaccounts == 0); +} + +# return the number of accounts the specified user belongs to +sub nb_user_accounts($) +{ + my ($username) = @_; + return 0 if !user_exists($username); + return scalar(@{$users{$username}{accounts}}); +} + +# query slurm for the default account of the given user +sub get_default_account($) +{ + my ($username) = @_; + my $res = `$sacctmgr --noheader show user $username format=User%30,DefaultAccount%30`; + return $2 if ($res =~ /^\s*(\w+)\s+((\w|-)*)/); + return ""; +} + +# query slurm for the name of the current cluster +sub get_cluster_name() +{ + my $res = `$sacctmgr --noheader show cluster format=Cluster` or die "$@"; + return $1 if ($res =~ /^\s*(\w+)/); + return ""; +} + +#---------------------------------------------------------- +#------ slurm user and account modification routines ------ +#---------------------------------------------------------- + +# ----- user association creation, deletion ----- + +sub delete_user($) +{ + my ($username) = @_; + return Cmd::run_command("$sacctmgr -i delete user $username"); +} + +sub create_user_assoc($$) +{ + my ($username,$accountname) = @_; + return Cmd::run_command("$sacctmgr -i create user $username account=$accountname"); +} + +sub delete_user_assoc($$) +{ + my ($username,$accountname) = @_; + return Cmd::run_command("$sacctmgr -i delete user $username account=$accountname"); +} + +sub set_default_account($$) +{ + my ($username,$accountname) = @_; + my $cmd = ""; + if (defined($accountname)) { + return Cmd::run_command("$sacctmgr -i update user $username set defaultaccount=$accountname"); + } + return 0; +} + +# ----- account creation, deletion and value setting routines ----- + +# Create the specified slurm account +# - return 0 and do nothing, if an account with the same name already exist +# - run the command to create the account, set %result and return 1 +# - if dry run, don't run the command, set %result and return 2 +sub create_account($$$) +{ + my ($accountname,$parent,$share) = @_; + + return 0 if account_exists($accountname); + + my $res = Cmd::run_command("$sacctmgr -i add account $accountname parent=$parent share=$share"); + if ($res == 1 && $Cmd::result{exitcode} == 0) { + _init_account($accountname,$parent,$share); + _set_account_children(); + } + return $res; +} + +# Delete the specified slurm account +# - return 0 and do nothing, if the account doesn't exist +# - run the command to delete the account, set %Cmd::result and return 1 +# - if dry run, don't run the command, set %Cmd::result and return 2 +sub delete_account($) +{ + my ($accountname) = @_; + + return 0 if ! account_exists($accountname); + + my $res = Cmd::run_command("$sacctmgr -i delete account $accountname"); + if ($res == 1 && $Cmd::result{exitcode} == 0) { + # as the slurm account dependence tree has been modified, reinitialise %accounts and %users data + init(); + $Cmd::result{status} = "reinitialized"; + } + return $res; +} + +# Set the specified slurm account value by running sacctmgr +# - if the requested value is equal to the current one, do nothing and return 0 +# - run the appropriate sacctmgr command, set %Cmd::result and return 1 +# - if $dryrun is true, don't run the command, set %Cmd::result and return 2 +sub set_account_parent($$) +{ + my ($accountname,$parent) = @_; + return 0 if ($parent eq get_account_value($accountname,"parent")); + my $res = Cmd::run_command("$sacctmgr -i update account $accountname set parent=$parent"); + if ($res == 1 && $Cmd::result{exitcode} == 0) { + # as the slurm account dependence tree has been modified, reinitialise %accounts and %users data + init(); + $Cmd::result{status} = "reinitialized"; + } + return $res; +} + +sub set_account_share($$) +{ + my ($accountname,$share) = @_; + return 0 if ($share eq get_account_value($accountname,"share")); + $share = -1 if $share eq ""; + return Cmd::run_command("$sacctmgr -i update account $accountname set share=$share"); +} + +sub set_account_maxwall($$) +{ + my ($accountname,$maxwall) = @_; + + return 0 if ($maxwall eq get_account_value($accountname,"maxwall")); + $maxwall = -1 if $maxwall eq ""; + my $res = Cmd::run_command("$sacctmgr -i update account $accountname set maxwall=$maxwall"); + if ($res == 1 && $Cmd::result{exitcode} == 0 && scalar @{$accounts{$accountname}{children}} > 0) { + # due to slurm propagation of maxwall to children, reinitialise %accounts and %users data + init(); + $Cmd::result{status} = "reinitialized"; + } + return $res; +} + +sub set_account_maxnodes($$) +{ + my ($accountname,$maxnodes) = @_; + + # maxnodes is a numerical value + return 0 if $maxnodes == account_maxnodes($accountname); + + # FIXME Set value of -1 if any ancestor has the wished value, not only the direct parent + $maxnodes = -1 if $maxnodes == get_parent_account_value ($accountname,"maxnodes"); + + my $res = Cmd::run_command("$sacctmgr -i update account $accountname set maxnodes=$maxnodes"); + if ($res == 1 && $Cmd::result{exitcode} == 0 && scalar @{$accounts{$accountname}{children}} > 0) { + # due to slurm propagation of maxnodes to children, reinitialise %accounts and %users data + init(); + $Cmd::result{status} = "reinitialized"; + } + return $res; +} + +# --------------------------- +# ---- debug routines ------- +# --------------------------- +sub print_accounts() +{ + my $clustername=get_cluster_name(); + print "Slurm.pm: account settings for cluster ", $clustername, "\n"; + foreach my $accountname (all_accounts()) { + print "Slurm.pm: \$accounts{", $accountname, "}\n"; + print " => parent : ", get_account_value($accountname,"parent"), "\n"; + print " => share : ", get_account_value($accountname,"share"), "\n"; + print " => maxnodes : ", get_account_value($accountname,"maxnodes"), "\n"; + print " => maxwall : ", get_account_value($accountname,"maxwall"), "\n"; + print " => users : ", Log::array_to_string(account_users($accountname)), "\n"; + print " => children : ", Log::array_to_string(@{$accounts{$accountname}{children}}), "\n"; + } +} + +# ---------------------------------- +# ---- initialisation routine ------ +# ---------------------------------- + +# Dump slurm settings to perl %accounts and %users structures +sub init() +{ + # delete structures + %users = (); + %accounts = (); + + # FIXME: use 'sacctmgr dump ' to dump slurm data to perl structures + + # get all users and their default accounts + open(SLURM,"$sacctmgr --noheader show users format=User%30,DefaultAccount%30 |"); + while() { + if ($_ =~ /^\s*(\w+)\s+((\w|-)*).*/) { + $users{$1}{default_account} = $2; + @{$users{$1}{accounts}} = (); + } + } + close(SLURM); + + # get and initialize all existing accounts + open(SLURM,"$sacctmgr --noheader show accounts format=Account%30 |"); + while() { + _init_account($1,"",1) if ($_ =~ /^\s*((\w|-)*).*/); + } + close(SLURM); + + # get all user associations, and build the list of user accounts, and account users + open(SLURM,"$sacctmgr --noheader show assoc format=Account%30,User%30 |"); + while() { + if ($_ =~ /^\s+((\w|-)+)\s+((\w|-|\.)*).*/) { + my $accountname = $1; + my $username = $3; + if ($username ne "") { + push(@{$users{$username}{accounts}},$accountname); + push(@{$accounts{$accountname}{users}},$username); + } + }; + } + close(SLURM); + + # get accounts' Share and Parent values + open(SLURM,"$sacctmgr --noheader show assoc where user=\" \" format=Account%30,Share,ParentName%30 |"); + while() { + if ($_ =~ /^\s+((\w|-)+)\s+(\d+)\s+(\w*).*/) { + $accounts{$1}{parent} = $4; + $accounts{$1}{share} = $3; + }; + } + close(SLURM); + + # build the account depedence tree + _set_account_children(); + + # get accounts' MaxTRES=node= + open(SLURM,"$sacctmgr --noheader show assoc where user=\" \" format=Account%30,MaxTRES |"); + while() { + $accounts{$1}{maxnodes} = $3 if ($_ =~ /^\s+((\w|-)+)\s+node\=(\d+).*/); + } + close(SLURM); + + # get accounts' MaxWall + open(SLURM,"$sacctmgr --noheader show assoc where user=\" \" format=Account%30,MaxWall |"); + while() { + $accounts{$1}{maxwall} = $3 if ($_ =~ /^\s+((\w|-)+)\s+([^\s]*)/); + } + close(SLURM); +} + +1; + diff --git a/sbin/hpc_check b/sbin/hpc_check new file mode 100755 index 0000000..1093388 --- /dev/null +++ b/sbin/hpc_check @@ -0,0 +1,56 @@ +#!/usr/bin/perl -w + +use strict; +use FindBin; +use lib "$FindBin::RealBin/../lib/blib"; +use ParseAccounts; +use Slurm; +use Cwd 'abs_path'; + +my $accounts_file = "accounts.yaml"; +my $verbose = 0; + +# ----------------------------------------------------------------------------- +# Command line options +# +my %opt; + +sub print_usage() { + print STDERR << "EOF"; +Check account definition file $accounts_file + + usage: $0 [-hv] + + OPTIONS + -v : verbose, print internal hash data structures + -h : this (help) message + + example: $0 +EOF + exit; +} + +sub init() +{ + use Getopt::Std; + my $opt_string = 'hv'; + getopts("$opt_string", \%opt) or print_usage(); + # pathname of logfile + $accounts_file = abs_path("$FindBin::RealBin/../etc/" . $accounts_file); + print_usage() if $opt{h}; + print_usage() if (scalar @ARGV > 1); + $verbose = 1 if $opt{v}; +} + +&init(); +ParseAccounts::init(Slurm::get_cluster_name(),$accounts_file); +if ($verbose) { + print "Group:accounts\n"; + print "--------------\n"; + ParseAccounts::print_groups(); + + print "Accounts\n"; + print "--------\n"; + ParseAccounts::print_accounts(); +} + diff --git a/sbin/hpc_delaccounts b/sbin/hpc_delaccounts new file mode 100755 index 0000000..b506827 --- /dev/null +++ b/sbin/hpc_delaccounts @@ -0,0 +1,83 @@ +#!/usr/bin/perl -w + +# cleanup_accounts - delete empty slurm accounts +# +# Author : EPFL-SCITAS +# Date : March 2016 + +use strict; +use FindBin; +use lib "$FindBin::RealBin/../lib/blib"; +use HPCUsers; +use Slurm; +use Log; +use Cmd; +use Cwd 'abs_path'; + +# ----------------------------------------------------------------------------- +# Command line options +# +my %opt; + +my $logfile = "hpcusers.log"; + +sub print_usage() { + print STDERR << "EOF"; +Delete all empty slurm accounts. + +By default, perform a dry run by only printing commands to be run. + + usage: $0 [-g] [-h] + + OPTIONS + -g : go for real by applying the required modifications, and log commands in $logfile. + -h : this (help) message + + example: $0 -g +EOF + exit; +} + +sub init() +{ + use Getopt::Std; + my $opt_string = 'hg'; + getopts("$opt_string", \%opt) or print_usage(); + + # pathname of logfile + my $cluster = Slurm::get_cluster_name(); + my $logdir = "$FindBin::RealBin/../var/"; + if ($cluster ne "" && -e "$logdir") { + $Log::logfile = abs_path($logdir . $cluster . "_" . $logfile); + } else { + $Log::logfile = "/var/log/" . $logfile; + } + + print_usage() if $opt{h}; + $Cmd::dry_run = 1; + $Cmd::dry_run = 0 if $opt{g}; + print_usage() if (scalar @ARGV > 1); +} + +# Remove empty slurm accounts +sub cleanup_slurm_accounts() +{ + my $nb_iterations = 0; + +rescan: + return if ++$nb_iterations > 25; # we never know, to avoid infinite loop + + foreach my $accountname (Slurm::all_accounts()) { + if (Slurm::is_account_empty($accountname)) { + Log::output_last_cmd_result() if Slurm::delete_account($accountname); + # if slurm account tree is modified, we need to rescan accounts from scratch + goto rescan if ($Cmd::result{status} eq "reinitialized"); + } + } +} + +# ----------------------------------------------------------------------------- + +&init(); +&HPCUsers::init(); +&cleanup_slurm_accounts(); diff --git a/sbin/hpc_syncusers b/sbin/hpc_syncusers new file mode 100755 index 0000000..1028357 --- /dev/null +++ b/sbin/hpc_syncusers @@ -0,0 +1,248 @@ +#!/usr/bin/perl -w + +# hpc_syncusers - synchronise slurm user and account data with group memberships and YAML account definitions. +# - create/delete/modify slurm associations and scratch data +# +# Author : EPFL-SCITAS +# Date : March 2016 + +use strict; +use FindBin; +use lib "$FindBin::RealBin/../lib/blib"; +use HPCUsers; +use Slurm; +use Scratch; +use Cmd; +use ParseAccounts; +use Cwd 'abs_path'; + +# ----------------------------------------------------------------------------- +# Command line options +# +my %opt; + +my $account_def_file = "accounts.yaml"; +my $cluster = ""; + +sub print_usage() { + print STDERR << "EOF"; +Synchronize SCITAS cluster users with their group membership and account definitions: + - set slurm associations accordingly + - create scratch directory + - read account definitions in file $account_def_file + - log commands in $Log::logfile + +By default, does a dry run by only printing the commands required to bring users in sync. + + usage: $0 [-f] [-g] [-h] + + OPTIONS + -g : go for real by applying the required modifications, and log output. + -f : also free ressources of users having no more access to the cluster, i.e. delete slurm accounts and scratch data + -h : this (help) message + + example: $0 -g +EOF + exit; +} + + +# array_diff(A,B) : return an array with all of the elements in A not being in B +sub array_diff(\@\@) +{ + my %a = map{$_ => 1} @{$_[1]}; + return grep(!exists($a{$_}), @{$_[0]}); +} + +# return an array with no dupplicate +sub array_unique(@) { + return keys %{{map {$_ => 1} @_}}; +} + +sub init() +{ + use Getopt::Std; + my $opt_string = 'fhg'; + getopts("$opt_string", \%opt) or print_usage(); + + # pathname of logfile + $cluster = Slurm::get_cluster_name(); + my $logdir = "$FindBin::RealBin/../var/"; + if ($cluster ne "" && -e "$logdir") { + $Log::logfile = abs_path($logdir . $cluster . "_" . $Log::logfile); + } else { + $Log::logfile = "/var/log/" . $Log::logfile; + } + + # pathname of account definition file + my $pathname = abs_path("$FindBin::RealBin/../etc/" . $account_def_file); + if ( defined($pathname) && -e "$pathname" ) { + $account_def_file = $pathname; + } else { + $account_def_file = abs_path("$FindBin::RealBin/" . $account_def_file); + } + + print_usage() if $opt{h}; + $Cmd::dry_run = 1; + if ($opt{g}) { + $Cmd::dry_run = 0; + } + print_usage() if (scalar @ARGV > 1); +} + +sub check_modify_user_associations($) +{ + my ($username) = @_; + + my @tmp = (); + my @existing_accounts = array_unique(sort(Slurm::user_accounts($username))); + foreach my $groupname (HPCUsers::member_of($username)) { + push(@tmp, ParseAccounts::group_to_slurm_accounts($groupname)); + } + my @required_accounts = array_unique(sort(@tmp)); + my @to_add = array_diff(@required_accounts, @existing_accounts); + my @to_remove = array_diff(@existing_accounts, @required_accounts); + + if (scalar(@to_add) > 0 || scalar(@to_remove) > 0) { + Log::log_message(sprintf("modify user %s (%s)", $username, HPCUsers::user_to_string($username))); + Log::append_msg($Log::log_str) if ! $Cmd::dry_run; + print $Log::log_str, "\n"; + + # add new associations + foreach my $account (@to_add) { + Log::output_last_cmd_result() if Slurm::create_user_assoc($username,$account); + } + # remove obsolete associations + foreach my $account (@to_remove) { + Log::output_last_cmd_result() if Slurm::delete_user_assoc($username,$account); + } + # reset defaut account if necessary + if (! grep {$_ eq Slurm::default_account($username)} @required_accounts) { + # set default account to first account of @required_accounts + Log::output_last_cmd_result() if Slurm::set_default_account($username,$required_accounts[0]); + } + } +} + +# User is new, create the slurm association(s) and private scratch directory +sub new_user($) +{ + my ($username) = @_; + + Log::log_message(sprintf("new user %s (%s)", $username, HPCUsers::user_to_string($username))); + Log::append_msg($Log::log_str) if !$Cmd::dry_run; + print $Log::log_str, "\n"; + $Log::log_str = ""; + + # get the user accounts from the unix groups the user belongs to + my @groups = HPCUsers::member_of($username); + # print HPCUsers::groups_to_string($username), "\n"; + my @accounts = (); + foreach my $groupname (@groups) { + push(@accounts,ParseAccounts::group_to_slurm_accounts($groupname)); + } + # print $username, ":", Log::array_to_string(@accounts), "\n"; + + # Create slurm association(s) + foreach my $accountname (@accounts) { + Log::output_last_cmd_result() if Slurm::create_user_assoc($username,$accountname); + } + # Create scratch dir + Log::output_last_cmd_result() if Scratch::create_user_scratch_dir($username,HPCUsers::gid($username)); +} + +# User has no more access to SCITAS clusters: +# - delete the user from slurm dbd, along with all of its association(s) +# - delete its private scratch directory +sub free_user($) +{ + my ($username) = @_; + + return if (($username eq "query" || $username eq "root")); + + Log::log_message(sprintf("free user %s (%s)", $username, HPCUsers::user_to_string($username))); + Log::append_msg($Log::log_str) if ! $Cmd::dry_run; + print $Log::log_str, "\n"; + + # delete slurm data + Log::output_last_cmd_result() if Slurm::delete_user($username); + + # delete, or move scratch directory + Log::output_last_cmd_result() if Scratch::delete_user_scratch_dir($username); +} + +sub scan_users() +{ + foreach my $username (HPCUsers::all_users()) { + next if ($username eq "scitasbuild"); + if (! HPCUsers::is_in_hpc_group($username) || ! HPCUsers::is_accredited($username)) { + # if user has no more access to the cluster, we can remove it's account and data + free_user($username) if ($opt{f} && Scratch::user_scratch_dir_exists($username)); + } elsif (! Scratch::user_scratch_dir_exists($username)) { + # we assume that's a new user, because his private scratch directory doesn't exist + new_user($username); + } else { + check_modify_user_associations($username); + } + } +} + +# Scan and update slurm accounts with definitions found in account YAML file +sub scan_accounts() +{ + my $modified; + my $nb_iterations = 0; + +rescan: + return if ++$nb_iterations > 25; # we never know, to avoid infinite loop + + $modified = 0; + foreach my $account (ParseAccounts::all_accounts_tree()) { + my $parent = ParseAccounts::account_parent($account); + my $share = ParseAccounts::account_share($account); + if (! Slurm::account_exists($account)) { + # print "NOT EXISTS ", $account, " parent=", $parent, " share=", $share, "\n"; + Log::output_last_cmd_result() if (Slurm::create_account($account,$parent,$share)); + } + # check and set acount parent + if (Slurm::set_account_parent($account,$parent)) { + Log::output_last_cmd_result(); + # if slurm account depedence tree is modified, we need to rescan accounts from scratch + goto rescan if ($Cmd::result{status} eq "reinitialized"); + } + # check and set fairshare value + Log::output_last_cmd_result() if (Slurm::set_account_share($account,$share)); + # check and set maxwall value + if (Slurm::set_account_maxwall($account,ParseAccounts::account_maxwall($account))) { + Log::output_last_cmd_result(); + $modified = 1 if $Cmd::result{status} eq "reinitialized"; + } + # check and set maxnodes value + if (Slurm::set_account_maxnodes($account,ParseAccounts::account_maxnodes($account))) { + Log::output_last_cmd_result(); + $modified = 1 if $Cmd::result{status} eq "reinitialized"; + } + # if slurm data has been modified enough, it's safer to rescan all accounts from scratch + goto rescan if ($modified); + } +} + + +# ----------------------------------------------------------------------------- +init(); +HPCUsers::init(); +# search all groups to be able to associate users to accounts +HPCUsers::search_scoldap_for_groups(); + +# For debugging +# HPCUsers::print_users(); +# Slurm::print_accounts(); + +# parse YAML account definition file +ParseAccounts::init($cluster,$account_def_file); +#ParseAccounts::print(); + +# first scan, modify the accounts +scan_accounts(); +# then add, delete user associations +scan_users();