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
64 changes: 51 additions & 13 deletions docs/tutorial_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -671,13 +671,14 @@ ReFrame can be used also to test applications that run inside a container.
First, we need to enable the container platform support in ReFrame's configuration and, specifically, at the partition configuration level:

.. literalinclude:: ../tutorials/config/settings.py
:lines: 38-58
:emphasize-lines: 15-20
:lines: 38-62
:emphasize-lines: 15-24

For each partition, users can define a list of container platforms supported using the :js:attr:`container_platforms` `configuration parameter <config_reference.html#.systems[].partitions[].container_platforms>`__.
In this case, we define the `Singularity <https://sylabs.io>`__ platform, for which we set the :js:attr:`modules` parameter in order to instruct ReFrame to load the ``singularity`` module, whenever it needs to run with this container platform.
In this case, we define the `Sarus <https://github.com/eth-cscs/sarus>`__ platform for which we set the :js:attr:`modules` parameter in order to instruct ReFrame to load the ``sarus`` module, whenever it needs to run with this container platform.
Similarly, we add an entry for the `Singularity <https://sylabs.io>`__ platform.

The following test will use a Singularity container to run:
The following parameterized test, will create two tests, one for each of the supported container platforms:

.. code-block:: console

Expand All @@ -690,29 +691,66 @@ The following test will use a Singularity container to run:

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>` attribute.
This attribute accepts a string that corresponds to the name of the container platform that will be used to run the container for this test.
In this case, the test will be using `Singularity <https://sylabs.io>`__ as a container platform.
If such a platform is not `configured <config_reference.html#container-platform-configuration>`__ for the current system, the test will fail.

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 by setting the :attr:`image <reframe.core.containers.ContainerPlatform.image>` and the :attr:`commands <reframe.core.containers.ContainerPlatform.commands>` container platform attributes.
These two attributes are mandatory for container-based checks.
As soon as the container platform to be used is defined, you need to specify the container image to use by setting the :attr:`image <reframe.core.containers.ContainerPlatform.image>`.
In the ``Singularity`` test variant, we add the ``docker://`` prefix to the image name, in order to instruct ``Singularity`` to pull the image from `DockerHub <https://hub.docker.com/>`__.
The default command that the container runs can be overwritten by setting the :attr:`command <reframe.core.containers.ContainerPlatform.command>` attribute of the container platform.

The :attr:`image <reframe.core.containers.ContainerPlatform.image>` is the only mandatory attribute for container-based checks.
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 actual test are ignored in case of container-based tests.

ReFrame will run the container as follows:
ReFrame will run the container according to the given platform as follows:

.. code-block:: bash

# Sarus
sarus run --mount=type=bind,source="/path/to/test/stagedir",destination="/rfm_workdir" ubuntu:18.04 bash -c 'cat /etc/os-release | tee /rfm_workdir/release.txt'

# Singularity
singularity exec -B"/path/to/test/stagedir:/rfm_workdir" docker://ubuntu:18.04 bash -c 'cat /etc/os-release | tee /rfm_workdir/release.txt'

.. code-block:: console

singularity exec -B"/path/to/test/stagedir:/workdir" docker://ubuntu:18.04 bash -c 'cd rfm_workdir; pwd; ls; cat /etc/os-release'
In the ``Sarus`` case, ReFrame will prepend the following command in order to pull the container image before running the container:

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 are then run from that directory one after the other.
.. code-block:: bash

sarus pull ubuntu:18.04


This is the default behavior of ReFrame, which can be changed if pulling the image is not desired by setting the :attr:`pull_image <reframe.core.containers.ContainerPlatform.pull_image>` attribute to :class:`False`.
By default ReFrame will mount the stage directory of the test under ``/rfm_workdir`` inside the container.
Once the commands are executed, the container is stopped and ReFrame goes on with the sanity and 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:
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')]


The container filesystem is ephemeral, therefore, ReFrame mounts the stage directory under ``/rfm_workdir`` inside the container where the user can copy artifacts as needed.
These artifacts will therefore be available inside the stage directory after the container execution finishes.
This is very useful if the artifacts are needed for the sanity or performance checks.
If the copy is not performed by the default container command, the user can override this command by settings the :attr:`command <reframe.core.containers.ContainerPlatform.command>` attribute such as to include the appropriate copy commands.
In the current test, the output of the ``cat /etc/os-release`` is available both in the standard output as well as in the ``release.txt`` file, since we have used the command:

.. code-block:: bash

bash -c 'cat /etc/os-release | tee /rfm_workdir/release.txt'


and ``/rfm_workdir`` corresponds to the stage directory on the host system.
Therefore, the ``release.txt`` file can now be used in the subsequent sanity checks:

.. code-block:: python

os_release_pattern = r'18.04.\d+ LTS \(Bionic Beaver\)'
self.sanity_patterns = sn.all([
sn.assert_found(os_release_pattern, 'release.txt'),
sn.assert_found(os_release_pattern, self.stdout)
])


For a complete list of the available attributes of a specific container platform, please have a look at the :ref:`container-platforms` section of the :doc:`regression_test_api` guide.
On how to configure ReFrame for running containerized tests, please have a look at the :ref:`container-platform-configuration` section of the :doc:`config_reference`.
4 changes: 2 additions & 2 deletions docs/tutorial_basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ Note that you should *not* edit this configuration file in place.
Here is how the new configuration file looks like with the needed additions highlighted:

.. literalinclude:: ../tutorials/config/settings.py
:lines: 10-24,76-97,130-
:lines: 10-24,80-101,134-
:emphasize-lines: 3-15,31-42

Here we define a system named ``catalina`` that has one partition named ``default``.
Expand Down Expand Up @@ -807,7 +807,7 @@ Let's extend our configuration file for Piz Daint.


.. literalinclude:: ../tutorials/config/settings.py
:lines: 10-45,58-66,73-
:lines: 10-45,62-70,77-
:emphasize-lines: 16-48,70-101,114-120


Expand Down
158 changes: 117 additions & 41 deletions reframe/core/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,65 @@
from reframe.core.exceptions import ContainerError


_STAGEDIR_MOUNT = '/rfm_workdir'


class ContainerPlatform(abc.ABC):
'''The abstract base class of any container platform.'''

#: The default mount location of the test case stage directory inside the
#: container

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

#: The command to be executed within the container.
#:
#: If no command is given, then the default command of the corresponding
#: container image is going to be executed.
#:
#: .. versionadded:: 3.5.0
#: Changed the attribute name from `commands` to `command` and its type
#: to a string.
#:
#: :type: :class:`str` or :class:`None`
#: :default: :class:`None`
command = fields.TypedField(str, type(None))

_commands = fields.TypedField(typ.List[str])
#: The commands to be executed within the container.
#:
#: .. deprecated:: 3.5.0
#: Please use the `command` field instead.
#:
#: :type: :class:`list[str]`
#: :default: ``[]``
commands = fields.TypedField(typ.List[str])
commands = fields.DeprecatedField(
_commands,
'The `commands` field is deprecated, please use the `command` field '
'to set the command to be executed by the container.',
fields.DeprecatedField.OP_SET, from_version='3.5.0'
)

#: Pull the container image before running.
#:
#: This does not have any effect for the `Singularity` container platform.
#:
#: .. versionadded:: 3.5
#:
#: :type: :class:`bool`
#: :default: ``True``
pull_image = fields.TypedField(bool)

#: 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)``.
#: ``(/path/in/host, /path/in/container)``. The stage directory of the
#: ReFrame test is always mounted under ``/rfm_workdir`` inside the
#: container, independelty of this field.
#:
#: :type: :class:`list[tuple[str, str]]`
#: :default: ``[]``
Expand All @@ -40,25 +80,40 @@ class ContainerPlatform(abc.ABC):
#: :default: ``[]``
options = fields.TypedField(typ.List[str])

_workdir = fields.TypedField(str, type(None))
#: 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.
#:
#: .. deprecated:: 3.5
#: Please use the `options` field to set the working directory.
#:
#: :type: :class:`str`
#: :default: ``/rfm_workdir``
workdir = fields.TypedField(str, type(None))
workdir = fields.DeprecatedField(
_workdir,
'The `workdir` field is deprecated, please use the `options` field to '
'set the container working directory',
fields.DeprecatedField.OP_SET, from_version='3.5.0'
)

def __init__(self):
self.image = None
self.commands = []
self.command = None

# NOTE: Here we set the target fields directly to avoid the deprecation
# warnings
self._commands = []
self._workdir = _STAGEDIR_MOUNT

self.mount_points = []
self.options = []
self.workdir = '/rfm_workdir'
self.pull_image = True

@abc.abstractmethod
def emit_prepare_commands(self):
def emit_prepare_commands(self, stagedir):
'''Returns commands for preparing this container for running.

Such a command could be for pulling the container image from a
Expand All @@ -70,10 +125,12 @@ def emit_prepare_commands(self):
platform backends.

:meta private:

:arg stagedir: The stage directory of the test.
'''

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

Expand All @@ -82,15 +139,14 @@ def launch_command(self):
platforms.

:meta private:

:arg stagedir: The stage directory of the test.
'''

def validate(self):
if self.image is None:
raise ContainerError('no image specified')

if not self.commands:
raise ContainerError('no commands specified')

def __str__(self):
return type(self).__name__

Expand All @@ -102,17 +158,24 @@ class Docker(ContainerPlatform):
'''Container platform backend for running containers with `Docker
<https://www.docker.com/>`__.'''

def emit_prepare_commands(self):
return []
def emit_prepare_commands(self, stagedir):
return [f'docker pull {self.image}'] if self.pull_image else []

def launch_command(self):
super().launch_command()
run_opts = ['-v "%s":"%s"' % mp for mp in self.mount_points]
def launch_command(self, stagedir):
super().launch_command(stagedir)
mount_points = self.mount_points + [(stagedir, _STAGEDIR_MOUNT)]
run_opts = [f'-v "{mp[0]}":"{mp[1]}"' for mp in mount_points]
run_opts += self.options
run_cmd = 'docker run --rm %s %s bash -c ' % (' '.join(run_opts),
self.image)
return run_cmd + "'" + '; '.join(
['cd ' + self.workdir] + self.commands) + "'"

if self.command:
return (f'docker run --rm {" ".join(run_opts)} '
f'{self.image} {self.command}')

if self.commands:
return (f"docker run --rm {' '.join(run_opts)} {self.image} "
f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")

return f'docker run --rm {" ".join(run_opts)} {self.image}'


class Sarus(ContainerPlatform):
Expand All @@ -130,27 +193,34 @@ def __init__(self):
self.with_mpi = False
self._command = 'sarus'

def emit_prepare_commands(self):
def emit_prepare_commands(self, stagedir):
# The format that Sarus uses to call the images is
# <reposerver>/<user>/<image>:<tag>. If an image was loaded
# locally from a tar file, the <reposerver> is 'load'.
if self.image.startswith('load/'):
if not self.pull_image or self.image.startswith('load/'):
return []

return [self._command + ' pull %s' % self.image]

def launch_command(self):
super().launch_command()
run_opts = ['--mount=type=bind,source="%s",destination="%s"' %
mp for mp in self.mount_points]
else:
return [f'{self._command} pull {self.image}']

def launch_command(self, stagedir):
super().launch_command(stagedir)
mount_points = self.mount_points + [(stagedir, _STAGEDIR_MOUNT)]
run_opts = [f'--mount=type=bind,source="{mp[0]}",destination="{mp[1]}"'
for mp in mount_points]
if self.with_mpi:
run_opts.append('--mpi')

run_opts += self.options
run_cmd = self._command + ' run %s %s bash -c ' % (' '.join(run_opts),
self.image)
return run_cmd + "'" + '; '.join(
['cd ' + self.workdir] + self.commands) + "'"

if self.command:
return (f'{self._command} run {" ".join(run_opts)} {self.image} '
f'{self.command}')

if self.commands:
return (f"{self._command} run {' '.join(run_opts)} {self.image} "
f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")

return f'{self._command} run {" ".join(run_opts)} {self.image}'


class Shifter(Sarus):
Expand All @@ -177,20 +247,26 @@ def __init__(self):
super().__init__()
self.with_cuda = False

def emit_prepare_commands(self):
def emit_prepare_commands(self, stagedir):
return []

def launch_command(self):
super().launch_command()
run_opts = ['-B"%s:%s"' % mp for mp in self.mount_points]
def launch_command(self, stagedir):
super().launch_command(stagedir)
mount_points = self.mount_points + [(stagedir, _STAGEDIR_MOUNT)]
run_opts = [f'-B"{mp[0]}:{mp[1]}"' for mp in mount_points]
if self.with_cuda:
run_opts.append('--nv')

run_opts += self.options
run_cmd = 'singularity exec %s %s bash -c ' % (' '.join(run_opts),
self.image)
return run_cmd + "'" + '; '.join(
['cd ' + self.workdir] + self.commands) + "'"
if self.command:
return (f'singularity exec {" ".join(run_opts)} '
f'{self.image} {self.command}')

if self.commands:
return (f"singularity exec {' '.join(run_opts)} {self.image} "
f"bash -c 'cd {self.workdir}; {'; '.join(self.commands)}'")

return f'singularity run {" ".join(run_opts)} {self.image}'


class ContainerPlatformField(fields.TypedField):
Expand All @@ -203,6 +279,6 @@ def __set__(self, obj, value):
value = globals()[value]()
except KeyError:
raise ValueError(
'unknown container platform: %s' % value) from None
f'unknown container platform: {value}') from None

super().__set__(obj, value)
Loading