Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,60 @@ Here is how the new deferred attribute is defined:

The behavior of the flexible task allocation is controlled by the ``--flex-alloc-tasks`` command line option.
See the corresponding `section <running.html#controlling-the-flexible-task-allocation>`__ for more information.


Testing containerized applications
----------------------------------

.. versionadded:: 2.20


ReFrame can be used also to test applications that run inside a container.
A container-based test can be written as :class:`RunOnlyRegressionTest <reframe.core.pipeline.RunOnlyRegressionTest>` that sets the :attr:`container_platform <reframe.core.pipeline.RegressionTest.container_platform>`.
The following example shows a simple test that runs some basic commands inside an Ubuntu 18.04 container and checks that the test has indeed run inside the container and that the stage directory was correctly mounted:

.. literalinclude:: ../tutorial/advanced/advanced_example10.py

A container-based test in ReFrame requires that the :attr:`container_platform <reframe.core.pipeline.RegressionTest.container_platform>` is set:

.. literalinclude:: ../tutorial/advanced/advanced_example10.py
:lines: 13

This attribute accepts a string that corresponds to the name of the platform and it instantiates the appropriate :class:`ContainerPlatform <reframe.core.containers.ContainerPlatform>` object behind the scenes.
In this case, the test will be using `Singularity <https://sylabs.io>`__ as a container platform.
If such a platform is not configured for the current system, the test will fail.
For a complete list of supported container platforms, the user is referred to the `configuration documentation <configure.html#partition-configuration>`__.

As soon as the container platform to be used is defined, you need to specify the container image to use and the commands to run inside the container:

.. literalinclude:: ../tutorial/advanced/advanced_example10.py
:lines: 14-17

These two attributes are mandatory for container-based check.
The :attr:`image <reframe.core.pipeline.RegressionTest.container_platform.image>` attribute specifies the name of an image from a registry, whereas the :attr:`commands <reframe.core.pipeline.RegressionTest.container_platform.commands>` attribute provides the list of commands to be run inside the container.
It is important to note that the :attr:`executable <reframe.core.pipeline.RegressionTest.executable>` and :attr:`executable_opts <reframe.core.pipeline.RegressionTest.executable_opts>` attributes of the :class:`RegressionTest <reframe.core.pipeline.RegressionTest>` are ignored in case of container-based tests.

In the above example, ReFrame will run the container as follows:

.. code:: shell

singularity exec -B"/path/to/test/stagedir:/rfm_workdir" docker://ubuntu:18.04 bash -c 'cd rfm_workdir; pwd; ls; cat /etc/os-release'

By default ReFrame will mount the stage directory of the test under ``/rfm_workdir`` inside the container and it will always prepend a ``cd`` command to that directory.
The user commands then are then run from that directory one after the other.
Once the commands are executed, the container is stopped and ReFrame goes on with the sanity and/or performance checks.

Users may also change the default mount point of the stage directory by using :attr:`workdir <reframe.core.pipeline.RegressionTest.container_platform.workdir>` attribute:

.. literalinclude:: ../tutorial/advanced/advanced_example10.py
:lines: 18

Besides the stage directory, additional mount points can be specified through the :attr:`mount_points <reframe.core.pipeline.RegressionTest.container_platform.mount_points>` attribute:

.. code-block:: python

self.container_platform.mount_points = [('/path/to/host/dir1', '/path/to/container/mount_point1'),
('/path/to/host/dir2', '/path/to/container/mount_point2')]


For a complete list of the available attributes of a specific container platform, the reader is referred to `reference guide <reference.html#container-platforms>`__.
37 changes: 37 additions & 0 deletions docs/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ The following example shows a minimal configuration for the `Piz Daint <https://
'access': ['--constraint=gpu'],
'environs': ['PrgEnv-cray', 'PrgEnv-gnu',
'PrgEnv-intel', 'PrgEnv-pgi'],
'container_platforms': {
'Singularity': {
'modules': ['Singularity']
}
},
'descr': 'Hybrid nodes (Haswell/P100)',
'max_jobs': 100
},
Expand All @@ -53,6 +58,11 @@ The following example shows a minimal configuration for the `Piz Daint <https://
'access': ['--constraint=mc'],
'environs': ['PrgEnv-cray', 'PrgEnv-gnu',
'PrgEnv-intel', 'PrgEnv-pgi'],
'container_platforms': {
'Singularity': {
'modules': ['Singularity']
}
},
'descr': 'Multicore nodes (Broadwell)',
'max_jobs': 100
}
Expand Down Expand Up @@ -150,6 +160,33 @@ The available partition attributes are the following:
* ``environs``: A list of environments, with which ReFrame will try to run any regression tests written for this partition (default ``[]``).
The environment names must be resolved inside the ``environments`` section of the ``site_configuration`` dictionary (see `Environments Configuration <#environments-configuration>`__ for more information).

* ``container_platforms``: *[new in 2.20]* A set of key/value pairs specifying the supported container platforms for this partition and how their environment is set up.
Supported platform names are the following (names are case sensitive):

- ``Docker``: The `Docker <https://www.docker.com/>`__ container runtime.
- ``Singularity``: The `Singularity <https://sylabs.io/>`__ container runtime.
- ``Sarus``: The `Sarus <https://sarus.readthedocs.io>`__ container runtime.

Each configured container runtime is associated optionally with an environment (modules and environment variables) that is providing it.
This environment is specified as a dictionary in the following format:

.. code:: python

{
'modules': ['mod1', 'mod2', ...]
'variables': {'ENV1': 'VAL1', 'ENV2': 'VAL2', ...}
}


If no special environment arrangement is needed for a configured container platform, you can simply specify an empty dictionary as an environment configuration, as it is shown in the following example:

.. code:: python

'container_platforms': {
'Docker': {}
}


* ``modules``: A list of modules to be loaded before running a regression test on that partition (default ``[]``).

* ``variables``: A set of environment variables to be set before running a regression test on that partition (default ``{}``).
Expand Down
16 changes: 16 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,19 @@ It is up to the concrete build system implementations on how to use or not these
:members:
:exclude-members: BuildSystemField
:show-inheritance:


Container platforms
-------------------

.. versionadded:: 2.20

ReFrame can run a regression test inside a container.
To achieve that you have to set the :attr:`reframe.core.pipeline.RegressionTest.container_platform` attribute and then set up the container platform (e.g., image to load, commands to execute).
The :class:`reframe.core.ContainerPlatform` abstract base class define the basic interface and a minimal set of attributes that all concrete container platforms must implement.
Concrete container platforms may also define additional fields that are specific to them.

.. automodule:: reframe.core.containers
:members:
:exclude-members: ContainerPlatformField
:show-inheritance:
6 changes: 0 additions & 6 deletions reframe/core/buildsystems.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,12 +644,6 @@ def emit_build_commands(self, environ):


class BuildSystemField(fields.TypedField):
'''A field representing a build system.

You may either assign an instance of :class:`BuildSystem` or a string
representing the name of the concrete class of a build system.
'''

def __init__(self, fieldname, *other_types):
super().__init__(fieldname, BuildSystem, *other_types)

Expand Down
75 changes: 43 additions & 32 deletions reframe/core/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,39 @@ class ContainerPlatform(abc.ABC):
'''The abstract base class of any container platform.

Concrete container platforms inherit from this class and must override the
:func:`emit_prepare_commands()` and :func:`launch_command()` abstract
methods.
:func:`emit_prepare_commands` and :func:`launch_command` abstract methods.
'''

#: The container image to be used for running the test.
#:
#: :type: :class:`str` or :class:`None`
#: :default: :class:`None`
image = fields.TypedField('image', str, type(None))

#: The commands to be executed within the container.
#:
#: :type: :class:`list[str]`
#: :default: ``[]``
commands = fields.TypedField('commands', typ.List[str])

#: List of mount point pairs for directories to mount inside the container.
#:
#: Each mount point is specified as a tuple of
#: ``(/path/in/host, /path/in/container)``.
#:
#: :type: :class:`list[tuple[str, str]]`
#: :default: ``[]``
mount_points = fields.TypedField('mount_points',
typ.List[typ.Tuple[str, str]])

#: The working directory of ReFrame inside the container.
#:
#: This is the directory where the test's stage directory is mounted inside
#: the container. This directory is always mounted regardless if
#: :attr:`mount_points` is set or not.
#:
#: :type: :class:`str`
#: :default: ``/rfm_workdir``
workdir = fields.TypedField('workdir', str, type(None))

def __init__(self):
Expand All @@ -27,36 +52,30 @@ def __init__(self):

@abc.abstractmethod
def emit_prepare_commands(self):
'''Returns commands that are necessary before running with this
container platform.
'''Returns commands for preparing this container for running.

:raises: `ContainerError` in case of errors.
Such a command could be for pulling the container image from a
repository.

.. note:

This method is relevant only to developers of new container
platforms.
platform backends.

'''

@abc.abstractmethod
def launch_command(self):
'''Returns the command for running with this container platform.

:raises: `ContainerError` in case of errors.
'''Returns the command for running :attr:`commands` with this container
platform.

.. note:
This method is relevant only to developers of new container
platforms.

'''

def validate(self):
'''Validates this container platform.

:raises: `ContainerError` in case of errors.

.. note:
This method is relevant only to developers of new container
platforms.
'''
if self.image is None:
raise ContainerError('no image specified')

Expand All @@ -65,8 +84,8 @@ def validate(self):


class Docker(ContainerPlatform):
'''An implementation of :class:`ContainerPlatform` for running containers
with Docker.'''
'''Container platform backend for running containers with `Docker
<https://www.docker.com/>`__.'''

def emit_prepare_commands(self):
return []
Expand All @@ -81,8 +100,7 @@ def launch_command(self):


class ShifterNG(ContainerPlatform):
'''An implementation of :class:`ContainerPlatform` for running containers
with ShifterNG.'''
'''Container platform backend for running containers with ShifterNG.'''

#: Add an option to the launch command to enable MPI support.
#:
Expand Down Expand Up @@ -112,19 +130,18 @@ def launch_command(self):


class Sarus(ShifterNG):
'''An implementation of :class:`ContainerPlatform` for running containers
with Sarus.'''
'''Container platform backend for running containers with Sarus.'''

def __init__(self):
super().__init__()
self._command = 'sarus'


class Singularity(ContainerPlatform):
'''An implementation of :class:`ContainerPlatform` for running containers
with Singularity.'''
'''Container platform backend for running containers with `Singularity
<https://sylabs.io/>`__.'''

#: Add an option to the launch command to enable CUDA support.
#: Enable CUDA support when launching the container.
#:
#: :type: boolean
#: :default: :class:`False`
Expand All @@ -150,12 +167,6 @@ def launch_command(self):


class ContainerPlatformField(fields.TypedField):
'''A field representing a container platforms.

You may either assign an instance of :class:`ContainerPlatform:` or a
string representing the name of the concrete class of a container platform.
'''

def __init__(self, fieldname, *other_types):
super().__init__(fieldname, ContainerPlatform, *other_types)

Expand Down
27 changes: 20 additions & 7 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,18 +236,31 @@ class RegressionTest(metaclass=RegressionTestMeta):
#: :default: ``[]``
executable_opts = fields.TypedField('executable_opts', typ.List[str])

#: The container platform to be used for this test.
#: The container platform to be used for launching this test.
#:
#: If the `self.container_platform` is defined on the test, both
#: `self.executable` and `self.executable_opts` are ignored.
#: If this field is set, the test will run inside a container using the
#: specified container runtime. Container-specific options must be defined
#: additionally after this field is set:
#:
#: .. code:: python
#:
#: self.container_platform = 'Singularity'
#: self.container_platform.image = 'docker://ubuntu:18.04'
#: self.container_platform.commands = ['cat /etc/os-release']
#:
#: If this field is set, :attr:`executable` and :attr:`executable_opts`
#: attributes are ignored. The container platform's :attr:`commands` will
#: be used instead. For more information on the container platform support,
#: see the `tutorial <advanced.html#testing-containerized-applications>`__
#: and the `reference guide <reference.html#container-platforms>`__.
#:
#: :type: :class:`str` or
#: :class:`reframe.core.containers.ContainerPlatform`.
#: :class:`reframe.core.containers.ContainerPlatform`.
#: :default: :class:`None`.
#:
#: .. versionadded:: 2.19
container_platform = ContainerPlatformField(
'container_platform', type(None))
#: .. versionadded:: 2.20
container_platform = ContainerPlatformField('container_platform',
type(None))

#: List of shell commands to execute before launching this job.
#:
Expand Down
26 changes: 26 additions & 0 deletions tutorial/advanced/advanced_example10.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import reframe as rfm
import reframe.utility.sanity as sn


@rfm.required_version('>=2.20-dev2')
@rfm.simple_test
class Example10Test(rfm.RunOnlyRegressionTest):
def __init__(self, **kwargs):
super().__init__()
self.descr = 'Run commands inside a container'
self.valid_systems = ['daint:gpu']
self.valid_prog_environs = ['PrgEnv-cray']
self.container_platform = 'Singularity'
self.container_platform.image = 'docker://ubuntu:18.04'
self.container_platform.commands = [
'pwd', 'ls', 'cat /etc/os-release'
]
self.container_platform.workdir = '/workdir'
self.sanity_patterns = sn.all([
sn.assert_found(r'^' + self.container_platform.workdir,
self.stdout),
sn.assert_found(r'^advanced_example1.c', self.stdout),
sn.assert_found(r'18.04.\d+ LTS \(Bionic Beaver\)', self.stdout),
])
self.maintainers = ['put-your-name-here']
self.tags = {'tutorial'}
10 changes: 10 additions & 0 deletions tutorial/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class ReframeSettings:
'access': ['--constraint=gpu'],
'environs': ['PrgEnv-cray', 'PrgEnv-gnu',
'PrgEnv-intel', 'PrgEnv-pgi'],
'container_platforms': {
'Singularity': {
'modules': ['Singularity']
}
},
'descr': 'Hybrid nodes (Haswell/P100)',
'max_jobs': 100
},
Expand All @@ -41,6 +46,11 @@ class ReframeSettings:
'access': ['--constraint=mc'],
'environs': ['PrgEnv-cray', 'PrgEnv-gnu',
'PrgEnv-intel', 'PrgEnv-pgi'],
'container_platforms': {
'Singularity': {
'modules': ['Singularity']
}
},
'descr': 'Multicore nodes (Broadwell)',
'max_jobs': 100
}
Expand Down