Skip to content

Commit

Permalink
Add validation check to module name (#49)
Browse files Browse the repository at this point in the history
Add validation check to module name
  • Loading branch information
mimischi authored Apr 8, 2018
2 parents bfee8f3 + 67a3045 commit 678836e
Show file tree
Hide file tree
Showing 9 changed files with 668 additions and 91 deletions.
1 change: 1 addition & 0 deletions changelog/49.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Module name is now validated against available modules on host. Can be skipped with `--skip-validation`.
143 changes: 104 additions & 39 deletions mdbenchmark/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,82 @@
# along with MDBenchmark. If not, see <http://www.gnu.org/licenses/>.
import click
import datreant.core as dtr
from jinja2.exceptions import TemplateNotFound

from . import console
from . import console, mdengines, utils
from .cli import cli
from .mdengines import detect_md_engine, namd
from .utils import ENV, normalize_host, print_possible_hosts


def validate_name(ctx, param, name=None):
"""Validate that we are given a name argument."""
if name is None:
raise click.BadParameter(
'Please specify the base name of your input files.',
param_hint='"-n" / "--name"')

return name


def validate_module(ctx, param, module=None):
"""Validate that we are given a module argument."""
if module is None or not module:
raise click.BadParameter(
'Please specify which MD engine module to use for the benchmarks.',
param_hint='"-m" / "--module"')
return module


def validate_number_of_nodes(min_nodes, max_nodes):
"""Validate that the minimal number of nodes is smaller than the maximal
number.
"""
if min_nodes > max_nodes:
raise click.BadParameter(
'The minimal number of nodes needs to be smaller than the maximal number.',
param_hint='"--min-nodes"')


def print_known_hosts(ctx, param, value):
"""Callback to print all available hosts to the user."""
if not value or ctx.resilient_parsing:
return
utils.print_possible_hosts()
ctx.exit()


def validate_hosts(ctx, param, host=None):
"""Callback to validate the hostname received as input.
If we were not given a hostname, we first try to guess it via
`utils.guess_host`. If this fails, we give up and throw an error.
Otherwise we compare the provided/guessed host with the list of available
templates. If the hostname matches the template name, we continue by
returning the hostname.
"""
if host is None:
host = utils.guess_host()
if host is None:
raise click.BadParameter(
'Could not guess host. Please provide a value explicitly.',
param_hint='"--host"')

known_hosts = utils.get_possible_hosts()
if host not in known_hosts:
console.info('Could not find template for host \'{}\'.', host)
utils.print_possible_hosts()
# TODO: Raise some appropriate error here
ctx.exit()
return

return host


@cli.command()
@click.option(
'-n',
'--name',
help='Name of input files. All files must have the same base name.',
show_default=True)
callback=validate_name)
@click.option(
'-g',
'--gpu',
Expand All @@ -43,8 +105,13 @@
'-m',
'--module',
help='Name of the MD engine module to use.',
multiple=True)
@click.option('--host', help='Name of the job template.', default=None)
multiple=True,
callback=validate_module)
@click.option(
'--host',
help='Name of the job template.',
default=None,
callback=validate_hosts)
@click.option(
'--min-nodes',
help='Minimal number of nodes to request.',
Expand All @@ -64,48 +131,46 @@
show_default=True,
type=click.IntRange(1, 1440))
@click.option(
'--list-hosts', help='Show available job templates.', is_flag=True)
def generate(name, gpu, module, host, min_nodes, max_nodes, time, list_hosts):
"""Generate benchmarks."""
if list_hosts:
print_possible_hosts()
return

if not name:
raise click.BadParameter(
'Please specify the base name of your input files.',
param_hint='"-n" / "--name"')

if not module:
raise click.BadParameter(
'Please specify which mdengine module to use for the benchmarks.',
param_hint='"-m" / "--module"')

if min_nodes > max_nodes:
raise click.BadParameter(
'The minimal number of nodes needs to be smaller than the maximal number.',
param_hint='"--min-nodes"')
'--list-hosts',
help='Show available job templates.',
is_flag=True,
is_eager=True,
callback=print_known_hosts,
expose_value=False)
@click.option(
'--skip-validation',
help='Skip the validation of module names.',
default=False,
is_flag=True)
def generate(name, gpu, module, host, min_nodes, max_nodes, time,
skip_validation):
"""Generate benchmarks simulations from the CLI."""
# Validate the number of nodes
validate_number_of_nodes(min_nodes=min_nodes, max_nodes=max_nodes)

host = normalize_host(host)
try:
tmpl = ENV.get_template(host)
except TemplateNotFound:
raise click.BadParameter(
'Could not find template for host \'{}\'.'.format(host),
param_hint='"--host"')
# Grab the template name for the host. This should always work because
# click does the validation for us
tmpl = utils.retrieve_host_template(host)

# Make sure we only warn the user once, if they are using NAMD.
# Warn the user that NAMD support is still experimental.
if any(['namd' in m for m in module]):
console.warn(
'NAMD support is experimental. '
'All input files must be in the current directory. '
'Parameter paths must be absolute. Only crude file checks are performed!'
'Parameter paths must be absolute. Only crude file checks are performed! '
'If you use the {} option make sure you use the GPU compatible NAMD module!',
'--gpu')

module = mdengines.normalize_modules(module, skip_validation)

# If several modules were given and we only cannot find one of them, we
# continue.
if not module:
console.error('No requested modules available!')

for m in module:
# Here we detect the mdengine (GROMACS or NAMD).
engine = detect_md_engine(m)
# Here we detect the MD engine (supported: GROMACS and NAMD).
engine = mdengines.detect_md_engine(m)

directory = '{}_{}'.format(host, m)
gpu_string = ''
Expand Down
145 changes: 138 additions & 7 deletions mdbenchmark/mdengines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,153 @@
#
# You should have received a copy of the GNU General Public License
# along with MDBenchmark. If not, see <http://www.gnu.org/licenses/>.
import os
from collections import defaultdict

import six

from . import gromacs, namd
from .. import console

SUPPORTED_ENGINES = {'gromacs': gromacs, 'namd': namd}


def detect_md_engine(modulename):
"""Detects the MD engine based on the available modules.
Any newly implemented mdengines must be added here.
Returns the python module."""
_engines = {'gromacs': gromacs, 'namd': namd}
for name, engine in six.iteritems(_engines):
Any newly implemented MD engines must be added here.
Returns
-------
The corresponding MD engine module or `None` if the requested module is not
supported.
"""

for name, engine in six.iteritems(SUPPORTED_ENGINES):
if name in modulename:
return engine

console.error(
"No suitable engine detected for '{}'. Known engines are: {}.",
modulename, ', '.join(sorted(_engines.keys())))
return None


def prepare_module_name(module):
"""Split the provided module name into its base MD engine and version.
Currently we only try to split via the delimiter `/`, but this could be
changed upon request or made configurable on a per-host basis.
"""
try:
basename, version = module.split('/')
except (ValueError, AttributeError) as e:
console.error('We were not able to determine the module name.')

return basename, version


def get_available_modules():
"""Return all available module versions for a given MD engine.
Returns
-------
If we cannot access the `MODULEPATH` environment variable, we return `None`.
available_modules : dict
Dictionary containing all available engines as keys and their versions as a list.
"""

MODULE_PATHS = os.environ.get('MODULEPATH', None)
available_modules = dict((mdengine, []) for mdengine in SUPPORTED_ENGINES)

# Return `None` if the environment variable `MODULEPATH` does not exist.
if not MODULE_PATHS:
return None

# Go through the directory structure and grab all version of modules that we support.
for paths in MODULE_PATHS.split(':'):
for path, subdirs, files in os.walk(paths):
for mdengine in SUPPORTED_ENGINES:
if mdengine in path:
for name in files:
if not name.startswith('.'):
available_modules[mdengine].append(name)

return available_modules


def normalize_modules(modules, skip_validation):
"""Validate that the provided module names are available.
We first check whether the requested MD engine is supported by the package.
Next we try to discover all available modules on the host. If this is not
possible, or if the user has used the `--skip-validation` option, we skip
the check and notify the user.
If the user requested modules that were not found on the system, we inform
the user and show all modules for that corresponding MD engine that were
found.
"""
# Check if modules are from supported md engines
d = defaultdict(list)
for m in modules:
engine, version = prepare_module_name(m)
d[engine] = version
for engine in d.keys():
if detect_md_engine(engine) is None:
console.error("There is currently no support for '{}'. "
"Supported MD engines are: gromacs, namd.", engine)

if skip_validation:
console.warn('Not performing module name validation.')
return modules

available_modules = get_available_modules()
if available_modules is None:
console.warn(
'Cannot locate modules available on this host. Not performing module name validation.'
)
return modules

good_modules = [
m for m in modules if validate_module_name(m, available_modules)
]

# Prepare to warn the user about missing modules
missing_modules = set(modules).difference(good_modules)
if missing_modules:
d = defaultdict(list)
for mm in sorted(missing_modules):
engine, version = mm.split('/')
d[engine].append(version)

err = 'We have problems finding all of your requested modules on this host.\n'
args = []
for engine in sorted(d.keys()):
err += 'We were not able to find the following modules for MD engine {}: {}.\n'
args.append(engine)
args.extend(d[engine])
# Show all available modules that we found for the requested MD engine
err += 'Available modules are:\n{}\n'
args.extend([
'\n'.join([
'{}/{}'.format(engine, mde)
for mde in sorted(available_modules[engine])
])
])
console.warn(err, bold=True, *args)

return good_modules


def validate_module_name(module, available_modules=None):
"""Validates that the specified module version is available on the host.
Returns
-------
Returns True or False, indicating whether the specified version is
available on the host.
"""
basename, version = prepare_module_name(module)

return version in available_modules[basename]
Loading

0 comments on commit 678836e

Please sign in to comment.