From 8b63023eec285a8c98b2aaf7472cee3794fa3b7d Mon Sep 17 00:00:00 2001 From: Sylvester Joosten <sylvester.joosten@gmail.com> Date: Tue, 31 Mar 2020 15:53:33 -0500 Subject: [PATCH] Added python-based deploy scripts, and updated Readme. --- README.md | 35 ++++++++- deploy.py | 160 ++++++++++++++++++++++++++++++++++++++++++ install/__init__.py | 7 ++ install/launcher.py | 111 +++++++++++++++++++++++++++++ install/modulefile.py | 46 ++++++++++++ install/util.py | 38 ++++++++++ 6 files changed, 396 insertions(+), 1 deletion(-) create mode 100755 deploy.py create mode 100644 install/__init__.py create mode 100644 install/launcher.py create mode 100644 install/modulefile.py create mode 100644 install/util.py diff --git a/README.md b/README.md index 0cd27916b..7e2c714ee 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,40 @@ EIC software container ============================================ Installation ------------ +------------ + +1. Clone the repository and go into the directory +```bash +git clone https://eicweb.phy.anl.gov/containers/eic_container.git +cd eic_container +``` + +2. Run the deploy script `deploy.py` to install to your `<PREFIX>` of choice + (e.g. $HOME/local/opt/eic_container_1.0.4). By default the + modelefile will be installed to `$PREFIX/../../etc/modulefiles`. + You can use the `-v` flag to select the version you want to deploy, or omit the + flag if you want to install the master build. The recommended stable + release version is `v1.0.4`. +```bash +./deploy.py -v 1.0.4 <PREFIX> +``` + +3. To use the container: load the modulefile, and then use the included apps as if + they are native apps on your system! +``` +module load eic_container +``` + +4. (Advanced) If you need to add additional bind directives for the internal singularity container, + you can add them with the `-b` flag. Run `./deploy.py -h` to see a list of all + supported options. + + +Installation (throug cmake) +--------------------------- + +*Use of the cmake-based deploy is deprecated, We recommend to use the `deploy.py` method +instead.* 1. Checkout the repository and create a build directory ``` diff --git a/deploy.py b/deploy.py new file mode 100755 index 000000000..0ed318154 --- /dev/null +++ b/deploy.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +## eic_container: Argonne Universal EIC Container + +''' +Deploy the singularity container built by the CI for this version of the software. + +The current version is determined from the currently loaded git branch or tag, +unless it is explicitly set on the command line. + +Authors: + - Whitney Armstrong <warmstrong@anl.gov> + - Sylvester Joosten <sjoosten@anl.gov> +''' + +import os +import argparse +import urllib.request +from install import make_launcher, make_modulefile +from install.util import smart_mkdir, project_version, InvalidArgumentError + +## Gitlab group and project/program name. +GROUP_NAME='containers' +PROJECT_NAME='eic_container' +PROGRAMS = [('container_dev', '/usr/bin/bash'), + 'ddsim', + 'geoConverter', + 'materialScan', + 'geoDisplay', + 'geoPluginRun', + 'teveDisplay', + 'ddeve', + 'g4FromXML' + 'geoDisplay', + 'listcomponents', + 'print_materials', + 'dumpBfield', + 'g4gdmlDisplay', + 'geoPluginRun', + 'materialBudget', + 'pyddg4', + 'dumpdetector', + 'graphicalScan', + 'root', + 'root-config', + 'rootbrowse', + 'rootls', + 'mongo', + 'mongod', + 'mongodump', + 'mongoexport', + 'mongoimport', + 'mongostat'] + +## URL for the current container (git tag will be filled in by the script) +CONTAINER_URL = r'https://eicweb.phy.anl.gov/{group}/{project}/-/jobs/artifacts/{version}/raw/build/eic.sif?job=eic_singularity' + +CONTAINER_ENV=r'''source /usr/local/bin/thisdd4hep.sh +ROOT_INCLUDE_PATH=/usr/local/include:/usr/include/eigen3:$ROOT_INCLUDE_PATH +''' + +## Singularity bind directive +BIND_DIRECTIVE= '-B {0}:{0}' + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + 'prefix', + help='Install prefix. This is where the container will be deployed.') + parser.add_argument( + '-v', '--version', + dest='version', + default=project_version(), + help='(opt.) project version. Default: current git branch/tag.') + parser.add_argument( + '-f', '--force', + action='store_true', + help='Force-overwrite already downloaded container', + default=False) + parser.add_argument( + '-b', '--bind-path', + dest='bind_paths', + action='append', + help='(opt.) extra bind paths for singularity.') + parser.add_argument( + '-m', '--module-path', + dest='module_path', + help='(opt.) Root module path where you want to install a modulefile. D: <prefix>/../../etc/modulefiles') + + args = parser.parse_args() + + print('Deploying', PROJECT_NAME, 'version', args.version) + + ## Check if our bind paths are valid + bind_directive = '' + if args.bind_paths and len(args.bind_paths): + print('Singularity bind paths:') + for path in args.bind_paths: + print(' -', path) + if not os.path.exists(path): + print('ERROR: path', path, 'does not exist.') + raise InvalidArgumentError() + bind_directive = ' '.join([BIND_DIRECTIVE.format(path) for path in args.bind_paths]) + + ## We want to slightly modify our version specifier: if it leads with a 'v' drop the v + ## for everything installed, but ensure we have the leading v as well where needed + version = '{}'.format(args.version) + vversion = '{}'.format(args.version) + if version[0] is 'v': + version = version[1:] + if vversion[0].isdigit(): + vversion= 'v{}'.format(args.version) + + ## Create our install prefix if needed and ensure it is writable + args.prefix = os.path.abspath(args.prefix) + if not args.module_path: + args.module_path = '{}/etc/modulefiles'.format(args.prefix) + print('Install prefix:', args.prefix) + print('Creating install prefix if needed...') + bindir = '{}/bin'.format(args.prefix) + libdir = '{}/lib'.format(args.prefix) + libexecdir = '{}/libexec'.format(args.prefix) + root_prefix = os.path.abspath('{}/..'.format(args.prefix)) + moduledir = '{}/etc/modulefiles/{}'.format(root_prefix, PROJECT_NAME) + for dir in [bindir, libdir, libexecdir, moduledir]: + print(' -', dir) + smart_mkdir(dir) + + ## At this point we know we can write to our desired prefix and that we have a set of + ## valid bind paths + + ## Get the container + ## We want to slightly modify our version specifier: if it leads with a 'v' drop the v + container = '{}/{}.sif.{}'.format(libdir, PROJECT_NAME, version) + if not os.path.exists(container) or args.force: + url = CONTAINER_URL.format(group=GROUP_NAME, project=PROJECT_NAME, version=vversion) + print('Downloading container from:', url) + print('Destination:', container) + urllib.request.urlretrieve(url, container) + else: + print('WARNING: Container found at', container) + print(' ---> run with -f to force a re-download') + + make_modulefile(PROJECT_NAME, version, moduledir, bindir) + + ## configure the application launchers + print('Configuring applications launchers: ') + for prog in PROGRAMS: + app = prog + exe = prog + if type(prog) == tuple: + app = prog[0] + exe = prog[1] + make_launcher(app, container, bindir, + bind=bind_directive, + libexecdir=libexecdir, + exe=exe, + env=CONTAINER_ENV) + + print('Container deployment successful!') diff --git a/install/__init__.py b/install/__init__.py new file mode 100644 index 000000000..7a9953f35 --- /dev/null +++ b/install/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +## eic_container: Argonne Universal EIC Container + +from install.util import smart_mkdir, project_version +from install.launcher import make_launcher +from install.modulefile import make_modulefile diff --git a/install/launcher.py b/install/launcher.py new file mode 100644 index 000000000..6ab5f0903 --- /dev/null +++ b/install/launcher.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +''' +Generic launcher script to launch applications in this container. + +The launcher script fires off an auxilary wrapper script in the container, +responsible to correctly setup the environment and then launch the application +of choice. + +Authors: + - Whitney Armstrong <warmstrong@anl.gov> + - Sylvester Joosten <sjoosten@anl.gov> +''' + +import os + +## generic launcher bash script to launch the application +_LAUNCHER='''#!/usr/bin/env bash + +## Boilerplate to make pipes work +piped_args= +if [ -p /dev/stdin ]; then + # If we want to read the input line by line + while IFS= read line; do + if [ -z "$piped_args" ]; then + piped_args="${{line}}" + else + piped_args="${{piped_args}}\n${{line}}" + fi + done +fi + +## Fire off the application wrapper +if [ ${{piped_args}} ] ; then + echo -e ${{piped_args}} | singularity exec {bind} {container} {wrapper} $@ +else + singularity exec {bind} {container} {wrapper} $@ +fi +''' + +## Wrapper script called from within the container that loads the propper environment and +## to then actually call our app +_WRAPPER='''#!/usr/bin/env bash + +## setup container environment +{env} + +## Boilerplate to make pipes work +piped_args= +if [ -p /dev/stdin ]; then + # If we want to read the input line by line + while IFS= read line; do + if [ -z "$piped_args" ]; then + piped_args="${{line}}" + else + piped_args="${{piped_args}}\n${{line}}" + fi + done +fi + +## Launch the exe +if [ ${{piped_args}} ] ; then + echo -e ${{piped_args}} | {exe} $@ +else + {exe} $@ +fi +''' + +def _write_script(path, content): + print(' - creating', path) + with open(path, 'w') as file: + file.write(content) + os.system('chmod +x {}'.format(path)) + +def make_launcher(app, container, bindir, + bind='', libexecdir=None, exe=None, env=''): + '''Configure and install a launcher/wrapper pair. + + Arguments: + - app: our application + - container: absolute path to container + - bindir: absolute launcher install path + Optional: + - bind: singularity bind directives + - libexecdir: absolute wrapper install path. + Default is bindir. + - exe: executable to be associated with app. + Default is app. + - env: environment directives to be added to the wrapper. + Multiline string. Default is nothing + ''' + ## assume bindir and libexecdir exist, are absolute, and are writable + if libexecdir is None: + libexecdir = bindir + + ## actual exe we want to run, default: same as app + exe=app + + ## paths + launcher_path = '{}/{}'.format(bindir, app) + wrapper_path = '{}/{}_wrap'.format(libexecdir, app) + + ## scripts --> use absolute path for wrapper path inside launcher + launcher = _LAUNCHER.format(container=container, + bind=bind, + wrapper=wrapper_path) + wrapper = _WRAPPER.format(env=env, exe=exe) + + ## write our scripts + _write_script(launcher_path, launcher) + _write_script(wrapper_path, wrapper) diff --git a/install/modulefile.py b/install/modulefile.py new file mode 100644 index 000000000..0c222bf96 --- /dev/null +++ b/install/modulefile.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +## eic_container: Argonne Universal EIC Container + +''' +Install modulefile for this container. + +Authors: + - Whitney Armstrong <warmstrong@anl.gov> + - Sylvester Joosten <sjoosten@anl.gov> +''' + +import os + +## Generic module file +_MODULEFILE='''#%Module1.0##################################################################### +## +## for {name} {version} +## +proc ModulesHelp {{ }} {{ + puts stderr "This module sets up the environment for the {name} container" +}} +module-whatis "{name} {version}" + +# For Tcl script use only +set version 4.1.4 + +prepend-path PATH {bindir} +''' + +def make_modulefile(project, version, moduledir, bindir): + '''Configure and install a modulefile for this project. + + Arguments: + - project: project name + - version: project version + - moduledir: root modulefile directory + - bindir: where executables for this project are located + ''' + + ## create our modulefile + content = _MODULEFILE.format(name=project, version=version, bindir=bindir) + fname = '{}/{}'.format(moduledir, version) + print(' - creating', fname) + with open(fname, 'w') as file: + file.write(content) diff --git a/install/util.py b/install/util.py new file mode 100644 index 000000000..47cb594a6 --- /dev/null +++ b/install/util.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +## eic_container: Argonne Universal EIC Container + +''' +Utility functions for this container + +Authors: + - Whitney Armstrong <warmstrong@anl.gov> + - Sylvester Joosten <sjoosten@anl.gov> +''' + +import os + +class InvalidArgumentError(Exception): + pass + +def smart_mkdir(dir): + '''functions as mkdir -p, with a write-check. + + Raises an exception if the directory is not writeable. + ''' + if not os.path.exists(dir): + try: + os.makedirs(dir) + except Exception as e: + print('ERROR: unable to create directory', dir) + raise e + if not os.access(dir, os.W_OK): + print('ERROR: We do not have the write privileges to', dir) + raise InvalidArgumentError() + +def project_version(): + '''Return the project version based on the current git branch/tag.''' + ## Shell command to get the current git version + git_version_cmd = 'git symbolic-ref -q --short HEAD || git describe --tags --exact-match' + ## Strip will remove the leading \n character + return os.popen(git_version_cmd).read().strip() -- GitLab