Skip to content

Commit 678836e

Browse files
authored
Add validation check to module name (#49)
Add validation check to module name
2 parents bfee8f3 + 67a3045 commit 678836e

File tree

9 files changed

+668
-91
lines changed

9 files changed

+668
-91
lines changed

changelog/49.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Module name is now validated against available modules on host. Can be skipped with `--skip-validation`.

mdbenchmark/generate.py

Lines changed: 104 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,82 @@
1919
# along with MDBenchmark. If not, see <http://www.gnu.org/licenses/>.
2020
import click
2121
import datreant.core as dtr
22-
from jinja2.exceptions import TemplateNotFound
2322

24-
from . import console
23+
from . import console, mdengines, utils
2524
from .cli import cli
26-
from .mdengines import detect_md_engine, namd
27-
from .utils import ENV, normalize_host, print_possible_hosts
25+
26+
27+
def validate_name(ctx, param, name=None):
28+
"""Validate that we are given a name argument."""
29+
if name is None:
30+
raise click.BadParameter(
31+
'Please specify the base name of your input files.',
32+
param_hint='"-n" / "--name"')
33+
34+
return name
35+
36+
37+
def validate_module(ctx, param, module=None):
38+
"""Validate that we are given a module argument."""
39+
if module is None or not module:
40+
raise click.BadParameter(
41+
'Please specify which MD engine module to use for the benchmarks.',
42+
param_hint='"-m" / "--module"')
43+
return module
44+
45+
46+
def validate_number_of_nodes(min_nodes, max_nodes):
47+
"""Validate that the minimal number of nodes is smaller than the maximal
48+
number.
49+
"""
50+
if min_nodes > max_nodes:
51+
raise click.BadParameter(
52+
'The minimal number of nodes needs to be smaller than the maximal number.',
53+
param_hint='"--min-nodes"')
54+
55+
56+
def print_known_hosts(ctx, param, value):
57+
"""Callback to print all available hosts to the user."""
58+
if not value or ctx.resilient_parsing:
59+
return
60+
utils.print_possible_hosts()
61+
ctx.exit()
62+
63+
64+
def validate_hosts(ctx, param, host=None):
65+
"""Callback to validate the hostname received as input.
66+
67+
If we were not given a hostname, we first try to guess it via
68+
`utils.guess_host`. If this fails, we give up and throw an error.
69+
70+
Otherwise we compare the provided/guessed host with the list of available
71+
templates. If the hostname matches the template name, we continue by
72+
returning the hostname.
73+
"""
74+
if host is None:
75+
host = utils.guess_host()
76+
if host is None:
77+
raise click.BadParameter(
78+
'Could not guess host. Please provide a value explicitly.',
79+
param_hint='"--host"')
80+
81+
known_hosts = utils.get_possible_hosts()
82+
if host not in known_hosts:
83+
console.info('Could not find template for host \'{}\'.', host)
84+
utils.print_possible_hosts()
85+
# TODO: Raise some appropriate error here
86+
ctx.exit()
87+
return
88+
89+
return host
2890

2991

3092
@cli.command()
3193
@click.option(
3294
'-n',
3395
'--name',
3496
help='Name of input files. All files must have the same base name.',
35-
show_default=True)
97+
callback=validate_name)
3698
@click.option(
3799
'-g',
38100
'--gpu',
@@ -43,8 +105,13 @@
43105
'-m',
44106
'--module',
45107
help='Name of the MD engine module to use.',
46-
multiple=True)
47-
@click.option('--host', help='Name of the job template.', default=None)
108+
multiple=True,
109+
callback=validate_module)
110+
@click.option(
111+
'--host',
112+
help='Name of the job template.',
113+
default=None,
114+
callback=validate_hosts)
48115
@click.option(
49116
'--min-nodes',
50117
help='Minimal number of nodes to request.',
@@ -64,48 +131,46 @@
64131
show_default=True,
65132
type=click.IntRange(1, 1440))
66133
@click.option(
67-
'--list-hosts', help='Show available job templates.', is_flag=True)
68-
def generate(name, gpu, module, host, min_nodes, max_nodes, time, list_hosts):
69-
"""Generate benchmarks."""
70-
if list_hosts:
71-
print_possible_hosts()
72-
return
73-
74-
if not name:
75-
raise click.BadParameter(
76-
'Please specify the base name of your input files.',
77-
param_hint='"-n" / "--name"')
78-
79-
if not module:
80-
raise click.BadParameter(
81-
'Please specify which mdengine module to use for the benchmarks.',
82-
param_hint='"-m" / "--module"')
83-
84-
if min_nodes > max_nodes:
85-
raise click.BadParameter(
86-
'The minimal number of nodes needs to be smaller than the maximal number.',
87-
param_hint='"--min-nodes"')
134+
'--list-hosts',
135+
help='Show available job templates.',
136+
is_flag=True,
137+
is_eager=True,
138+
callback=print_known_hosts,
139+
expose_value=False)
140+
@click.option(
141+
'--skip-validation',
142+
help='Skip the validation of module names.',
143+
default=False,
144+
is_flag=True)
145+
def generate(name, gpu, module, host, min_nodes, max_nodes, time,
146+
skip_validation):
147+
"""Generate benchmarks simulations from the CLI."""
148+
# Validate the number of nodes
149+
validate_number_of_nodes(min_nodes=min_nodes, max_nodes=max_nodes)
88150

89-
host = normalize_host(host)
90-
try:
91-
tmpl = ENV.get_template(host)
92-
except TemplateNotFound:
93-
raise click.BadParameter(
94-
'Could not find template for host \'{}\'.'.format(host),
95-
param_hint='"--host"')
151+
# Grab the template name for the host. This should always work because
152+
# click does the validation for us
153+
tmpl = utils.retrieve_host_template(host)
96154

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

164+
module = mdengines.normalize_modules(module, skip_validation)
165+
166+
# If several modules were given and we only cannot find one of them, we
167+
# continue.
168+
if not module:
169+
console.error('No requested modules available!')
170+
106171
for m in module:
107-
# Here we detect the mdengine (GROMACS or NAMD).
108-
engine = detect_md_engine(m)
172+
# Here we detect the MD engine (supported: GROMACS and NAMD).
173+
engine = mdengines.detect_md_engine(m)
109174

110175
directory = '{}_{}'.format(host, m)
111176
gpu_string = ''

mdbenchmark/mdengines/__init__.py

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,153 @@
1717
#
1818
# You should have received a copy of the GNU General Public License
1919
# along with MDBenchmark. If not, see <http://www.gnu.org/licenses/>.
20+
import os
21+
from collections import defaultdict
22+
2023
import six
2124

2225
from . import gromacs, namd
2326
from .. import console
2427

28+
SUPPORTED_ENGINES = {'gromacs': gromacs, 'namd': namd}
29+
2530

2631
def detect_md_engine(modulename):
2732
"""Detects the MD engine based on the available modules.
28-
Any newly implemented mdengines must be added here.
29-
Returns the python module."""
30-
_engines = {'gromacs': gromacs, 'namd': namd}
3133
32-
for name, engine in six.iteritems(_engines):
34+
Any newly implemented MD engines must be added here.
35+
36+
Returns
37+
-------
38+
39+
The corresponding MD engine module or `None` if the requested module is not
40+
supported.
41+
"""
42+
43+
for name, engine in six.iteritems(SUPPORTED_ENGINES):
3344
if name in modulename:
3445
return engine
3546

36-
console.error(
37-
"No suitable engine detected for '{}'. Known engines are: {}.",
38-
modulename, ', '.join(sorted(_engines.keys())))
47+
return None
48+
49+
50+
def prepare_module_name(module):
51+
"""Split the provided module name into its base MD engine and version.
52+
53+
Currently we only try to split via the delimiter `/`, but this could be
54+
changed upon request or made configurable on a per-host basis.
55+
"""
56+
try:
57+
basename, version = module.split('/')
58+
except (ValueError, AttributeError) as e:
59+
console.error('We were not able to determine the module name.')
60+
61+
return basename, version
62+
63+
64+
def get_available_modules():
65+
"""Return all available module versions for a given MD engine.
66+
67+
Returns
68+
-------
69+
If we cannot access the `MODULEPATH` environment variable, we return `None`.
70+
71+
available_modules : dict
72+
Dictionary containing all available engines as keys and their versions as a list.
73+
"""
74+
75+
MODULE_PATHS = os.environ.get('MODULEPATH', None)
76+
available_modules = dict((mdengine, []) for mdengine in SUPPORTED_ENGINES)
77+
78+
# Return `None` if the environment variable `MODULEPATH` does not exist.
79+
if not MODULE_PATHS:
80+
return None
81+
82+
# Go through the directory structure and grab all version of modules that we support.
83+
for paths in MODULE_PATHS.split(':'):
84+
for path, subdirs, files in os.walk(paths):
85+
for mdengine in SUPPORTED_ENGINES:
86+
if mdengine in path:
87+
for name in files:
88+
if not name.startswith('.'):
89+
available_modules[mdengine].append(name)
90+
91+
return available_modules
92+
93+
94+
def normalize_modules(modules, skip_validation):
95+
"""Validate that the provided module names are available.
96+
97+
We first check whether the requested MD engine is supported by the package.
98+
Next we try to discover all available modules on the host. If this is not
99+
possible, or if the user has used the `--skip-validation` option, we skip
100+
the check and notify the user.
101+
102+
If the user requested modules that were not found on the system, we inform
103+
the user and show all modules for that corresponding MD engine that were
104+
found.
105+
"""
106+
# Check if modules are from supported md engines
107+
d = defaultdict(list)
108+
for m in modules:
109+
engine, version = prepare_module_name(m)
110+
d[engine] = version
111+
for engine in d.keys():
112+
if detect_md_engine(engine) is None:
113+
console.error("There is currently no support for '{}'. "
114+
"Supported MD engines are: gromacs, namd.", engine)
115+
116+
if skip_validation:
117+
console.warn('Not performing module name validation.')
118+
return modules
119+
120+
available_modules = get_available_modules()
121+
if available_modules is None:
122+
console.warn(
123+
'Cannot locate modules available on this host. Not performing module name validation.'
124+
)
125+
return modules
126+
127+
good_modules = [
128+
m for m in modules if validate_module_name(m, available_modules)
129+
]
130+
131+
# Prepare to warn the user about missing modules
132+
missing_modules = set(modules).difference(good_modules)
133+
if missing_modules:
134+
d = defaultdict(list)
135+
for mm in sorted(missing_modules):
136+
engine, version = mm.split('/')
137+
d[engine].append(version)
138+
139+
err = 'We have problems finding all of your requested modules on this host.\n'
140+
args = []
141+
for engine in sorted(d.keys()):
142+
err += 'We were not able to find the following modules for MD engine {}: {}.\n'
143+
args.append(engine)
144+
args.extend(d[engine])
145+
# Show all available modules that we found for the requested MD engine
146+
err += 'Available modules are:\n{}\n'
147+
args.extend([
148+
'\n'.join([
149+
'{}/{}'.format(engine, mde)
150+
for mde in sorted(available_modules[engine])
151+
])
152+
])
153+
console.warn(err, bold=True, *args)
154+
155+
return good_modules
156+
157+
158+
def validate_module_name(module, available_modules=None):
159+
"""Validates that the specified module version is available on the host.
160+
161+
Returns
162+
-------
163+
164+
Returns True or False, indicating whether the specified version is
165+
available on the host.
166+
"""
167+
basename, version = prepare_module_name(module)
168+
169+
return version in available_modules[basename]

0 commit comments

Comments
 (0)