Sync remote application directories on first load¶
Some workstations and laptops mount a network share holding all kind of software installations that may be used on this machine, alongside their own local, faster disk. Reading application files directly from this network share on every use works, but it is slower than reading them from local disk, and it stops working entirely when the machine goes offline.
This recipe describes how to keep modulefiles pointing at application
directories on a network share mounted under /remote_apps, while
transparently copying, with the rsync tool, the directory of a given
application to /local_apps the first time the corresponding module
is loaded. Every later load of the same module finds its application
directory already synced locally and skips the copy.
Implementation¶
A .module_appdir_map file, located at the root of the
/remote_apps network share, maps each module name and version to the
basename of its application directory under /remote_apps:
foo/2.1 foo-2.1-build3
The .modulerc file at the root of the modulepath reads this map file
and applies the remote tag, with the module-tag modulefile
command, to every module listed in it whose application directory has not
been synced locally yet -- tracked by the presence of a
.<basename>.synced marker file under /local_apps:
#%Module5.7
# tag every module listed in the remote application directory map as
# 'remote', unless its application directory has already been synced to
# local disk (see siteconfig.tcl for how the sync itself is triggered)
set mapfile [file join /remote_apps .module_appdir_map]
if {[file readable $mapfile]} {
set fid [open $mapfile r]
set fdata [split [read $fid] "\n"]
close $fid
foreach fline $fdata {
if {[llength $fline] != 2} {
continue
}
lassign $fline modnamevr appdir
set syncedfile [file join /local_apps ".$appdir.synced"]
if {![file exists $syncedfile]} {
module-tag remote $modnamevr
}
}
}
The actual sync is performed by a before-modulefile-eval hook,
registered with the add-hook siteconfig command (see the Hook API
design notes and the Hooks section of module(1) man page). The
hook procedure returns immediately if the evaluation mode is not load,
or if the module being evaluated is not tagged remote -- checked with
module-info tags, run in the modulefile Tcl
interpreter reached through getCurrentModfileInterpName, exactly as
already documented for a hook procedure that needs to run modulefile
commands. Otherwise, it looks up the application directory basename mapped
to the module in .module_appdir_map, and skips the sync if this
directory was already marked as synced by a previous load. Otherwise it
copies the application directory with rsync and, on success, touches
the .<basename>.synced marker file so later loads skip the copy.
Because the modulepath root .modulerc tags a module remote ahead
of the sync actually happening, this tag would otherwise still be recorded
as applying to the module once loaded, which is misleading once its
application directory has just been synced locally. The remote tag is
therefore added to the non_exportable_tags configuration option,
introduced in Modules v5.7 together with the hook API, so it is dropped from
the tag list persisted once a module is loaded, without affecting how it is
reported beforehand, for instance on an avail listing. The
remote tag is also given its own abbreviation and color, so modules
whose application directory has not been synced yet stand out on an
avail or spider listing. These module config calls
are set in /etc/environment-modules/initrc, evaluated once when the module shell
function initializes with autoinit, the only context module
config is usable from within a file evaluated by Modules itself:
#%Module5.7
# give the 'remote' module tag its own abbreviation and color, and make
# sure it is not persisted onto a module once it gets loaded (see the "Sync
# remote application directories on first load" cookbook recipe)
module config tag_abbrev "+remote=R"
module config colors "+R=38;5;202"
module config tag_color_name +remote
module config non_exportable_tags +remote
The sync itself is triggered by the hook procedure, defined and registered
in siteconfig.tcl:
#
# siteconfig.tcl - Site specific configuration script that copies, the first
# time a module tagged 'remote' is loaded, its application directory from
# a remote network share to local disk with rsync, so this and every later
# load of the same module read from local disk instead of the network
# share.
#
# Author: Xavier Delaruelle <xavier.delaruelle@cea.fr>
# Compatibility: Modules v5.7+
#
# Installation: put this file in the 'etc' directory of your Modules
# installation. Refer to the "Modulecmd startup" section in the
# module(1) man page to get this location.
# root of the mounted network share and of its local counterpart
set g_remoteAppDir /remote_apps
set g_localAppDir /local_apps
# return the application directory basename mapped to given bare module name
# and version, or an empty string if this module has no mapped directory
proc getAppDirBasename {modname} {
set mapfile [file join $::g_remoteAppDir .module_appdir_map]
if {![file readable $mapfile]} {
return {}
}
set fid [open $mapfile r]
set fdata [split [read $fid] "\n"]
close $fid
foreach fline $fdata {
if {[llength $fline] == 2 && [lindex $fline 0] eq $modname} {
return [lindex $fline 1]
}
}
return {}
}
# copy application directory from the remote network share to local disk, on
# the first load of a module tagged 'remote' (see the modulepath root
# .modulerc file for how this tag gets applied)
proc syncRemoteAppDir {modfile modname modnamevr modspec mode requested} {
if {$mode ne {load}} {
return
}
set itrp [getCurrentModfileInterpName]
if {![interp eval $itrp {module-info tags remote}]} {
return
}
set appdir [getAppDirBasename $modname]
if {$appdir eq {}} {
return
}
set syncedfile [file join $::g_localAppDir ".$appdir.synced"]
if {[file exists $syncedfile]} {
return
}
report "Syncing '$appdir' application directory from remote share..."
file mkdir $::g_localAppDir
set srcdir [file join $::g_remoteAppDir $appdir]
set destdir [file join $::g_localAppDir $appdir]
if {[catch {exec rsync -a --delete $srcdir/ $destdir/} errMsg]} {
reportError "Failed to sync '$appdir' from remote share\n$errMsg"
return
}
# mark this application directory as synced so it does not get copied
# again on a later load
close [open $syncedfile w]
}
add-hook before-modulefile-eval syncRemoteAppDir
# vim:set tabstop=3 shiftwidth=3 expandtab autoindent:
Since the hook fires before the modulefile itself is evaluated, and the sync
is performed with a blocking exec call, module load waits for the
copy to complete before the modulefile that relies on the now-local
application directory gets evaluated. A hook procedure cannot abort the
evaluation it wraps, so a sync failure is reported as an error but does not
prevent the module from loading afterward, even though its application
directory may still be missing locally in that case.
Compatible with Modules v5.7+
Installation¶
Create site-specific configuration directory if it does not exist yet:
$ mkdir /etc/environment-modules
Copy the site-specific configuration script and initialization file of this recipe:
$ cp example/sync-remote-appdir/siteconfig.tcl /etc/environment-modules/ $ cp example/sync-remote-appdir/initrc /etc/environment-modules/
Note
Defined location for the site-specific configuration script may vary from
one installation to another. To determine the expected location for this
file on your setup, check the value of the siteconfig configuration
option:
$ module config siteconfig
Adapt modulefiles/.modulerc to your modulepath, and copy it at its
root, next to the modulefiles it applies to. Finally, create
/remote_apps/.module_appdir_map on the network share, with one
<module_name_and_version> <software_install_directory_basename> entry
per line for each application directory that should be synced this way.
Usage example¶
The application directory of foo/2.1 has not been synced locally yet, so
it shows up tagged remote on an avail listing, using the
abbreviation and color configured for this tag:
$ module avail foo --------------- /path/to/modulefiles --------------- foo/2.1 <R> Key: <module-tag> <R>=remote
Loading it triggers the sync, then proceeds with the load once the copy completes:
$ module load -v foo/2.1 Syncing 'foo-2.1-build3' application directory from remote share... Loading foo/2.1
Once loaded, the module no longer carries the remote tag, since its
application directory now lives on local disk:
$ module list
Currently Loaded Modulefiles:
1) foo/2.1
A later load, after the module has been unloaded, finds the
.foo-2.1-build3.synced marker file and skips the sync entirely,
and the module no longer shows up tagged remote on avail
either:
$ module unload foo/2.1 $ module avail foo --------------- /path/to/modulefiles --------------- foo/2.1 $ module load -v foo/2.1 Loading foo/2.1