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
109 changes: 109 additions & 0 deletions reframe/core/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import abc

import reframe.core.fields as fields
import reframe.utility.typecheck as typ
from reframe.core.exceptions import ContainerError


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_cmds` and :func:`emit_launch_cmds` abstract functions.
"""

registry = fields.TypedField('registry', str, type(None))
image = fields.TypedField('image', str, type(None))
requires_mpi = fields.TypedField('requires_mpi', bool)
commands = fields.TypedField('commands', typ.List[str])
mount_points = fields.TypedField('mount_points',
typ.List[typ.Tuple[str, str]])
workdir = fields.TypedField('workdir', str, type(None))

def __init__(self):
self.registry = None
self.image = None
self.requires_mpi = False
self.commands = []
self.mount_points = []
self.workdir = None

@abc.abstractmethod
def emit_prepare_cmds(self):
"""Returns commands that are necessary before running with this
container platform.

:raises: `ContainerError` in case of errors.

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

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

:raises: `ContainerError` in case of errors.

.. note:
This method is relevant only to developers of new container
platforms.
"""
if self.registry:
self.image = '/'.join([self.registry, self.image])

@abc.abstractmethod
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')

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


class Docker(ContainerPlatform):
"""An implementation of ContainerPlatform to run containers with Docker."""

def emit_prepare_cmds(self):
pass

def emit_launch_cmds(self):
super().emit_launch_cmds()
docker_opts = ['-v "%s":"%s"' % mp for mp in self.mount_points]
run_cmd = 'docker run %s %s bash -c ' % (' '.join(docker_opts),
self.image)
return run_cmd + "'" + '; '.join(
['cd ' + self.workdir] + self.commands) + "'"

def validate(self):
super().validate()


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)

def __set__(self, obj, value):
if isinstance(value, str):
try:
value = globals()[value]()
except KeyError:
raise ValueError(
'unknown container platform: %s' % value) from None

super().__set__(obj, value)
4 changes: 4 additions & 0 deletions reframe/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ class BuildSystemError(ReframeError):
"""Raised when a build system is not configured properly."""


class ContainerError(ReframeError):
"""Raised when a container platform is not configured properly."""


class BuildError(ReframeError):
"""Raised when a build fails."""

Expand Down
68 changes: 68 additions & 0 deletions unittests/test_containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import abc
import unittest

import pytest
import reframe.core.containers as containers
from reframe.core.exceptions import ContainerError


class _ContainerPlatformTest(abc.ABC):
@abc.abstractmethod
def create_container_platform(self):
pass

@property
@abc.abstractmethod
def exp_cmd_mount_points(self):
pass

@property
@abc.abstractmethod
def exp_cmd_custom_registry(self):
pass

def setUp(self):
self.container_platform = self.create_container_platform()

def test_mount_points(self):
self.container_platform.image = 'name:tag'
self.container_platform.mount_points = [('/path/one', '/one'),
('/path/two', '/two')]
self.container_platform.commands = ['cmd1', 'cmd2']
self.container_platform.workdir = '/stagedir'
assert (self.exp_cmd_mount_points ==
self.container_platform.emit_launch_cmds())

def test_missing_image(self):
self.container_platform.commands = ['cmd']
with pytest.raises(ContainerError):
self.container_platform.validate()

def test_missing_commands(self):
self.container_platform.image = 'name:tag'
with pytest.raises(ContainerError):
self.container_platform.validate()

def test_custom_registry(self):
self.container_platform.registry = 'registry/custom'
self.container_platform.image = 'name:tag'
self.container_platform.commands = ['cmd']
self.container_platform.mount_points = [('/path/one', '/one')]
self.container_platform.workdir = '/stagedir'
assert (self.exp_cmd_custom_registry ==
self.container_platform.emit_launch_cmds())


class TestDocker(_ContainerPlatformTest, unittest.TestCase):
def create_container_platform(self):
return containers.Docker()

@property
def exp_cmd_mount_points(self):
return ('docker run -v "/path/one":"/one" -v "/path/two":"/two" '
"name:tag bash -c 'cd /stagedir; cmd1; cmd2'")

@property
def exp_cmd_custom_registry(self):
return ('docker run -v "/path/one":"/one" registry/custom/name:tag '
"bash -c 'cd /stagedir; cmd'")