blob: 7309b67a1c46c621c7ef43df5d58087c0fcb6fcb [file] [log] [blame]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import grp
import os
import pwd
import subprocess
import mock
import pytest
from twitter.common.contextutil import temporary_dir
from apache.aurora.executor.common.sandbox import (
DefaultSandboxProvider,
DirectorySandbox,
DockerDirectorySandbox,
FileSystemImageSandbox
)
from gen.apache.aurora.api.ttypes import AssignedTask, Container, DockerContainer, TaskConfig
def test_sandbox_root_path():
with temporary_dir() as d:
mesos_dir = os.path.join(d, 'task1')
ds = DirectorySandbox(mesos_dir)
assert os.path.join(mesos_dir, DirectorySandbox.SANDBOX_NAME) == ds.root
def test_directory_sandbox():
with temporary_dir() as d:
ds1 = DirectorySandbox(os.path.join(d, 'task1'))
ds2 = DirectorySandbox(os.path.join(d, 'task2'))
ds1.create()
ds2.create()
assert os.path.exists(ds1.root)
assert os.path.exists(ds2.root)
ds1.destroy()
assert not os.path.exists(ds1.root)
assert os.path.exists(ds2.root)
ds2.destroy()
assert not os.path.exists(ds2.root)
@mock.patch('grp.getgrgid')
@mock.patch('pwd.getpwnam')
@mock.patch('os.chown')
@mock.patch('os.chmod')
def test_create(chmod, chown, getpwnam, getgrgid):
getgrgid.return_value.gr_name = 'foo'
getpwnam.return_value.pw_gid = 123
getpwnam.return_value.pw_uid = 456
with temporary_dir() as mesos_dir:
ds = DirectorySandbox(mesos_dir, 'cletus')
ds.create()
assert os.path.exists(ds.root)
getpwnam.assert_called_with('cletus')
getgrgid.assert_called_with(123)
chown.assert_called_with(ds.root, 456, 123)
chmod.assert_called_with(ds.root, 0700)
@mock.patch('grp.getgrgid')
@mock.patch('pwd.getpwnam')
@mock.patch('os.chown')
@mock.patch('os.chmod')
def test_create_no_user(*args):
with temporary_dir() as d:
ds = DirectorySandbox(d)
ds.create()
assert os.path.exists(ds.root)
for mocked in args:
mocked.assert_not_called()
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': '/some/path'})
def test_sandbox_provider_docker_container():
sandbox = DefaultSandboxProvider().from_assigned_task(
AssignedTask(task=TaskConfig(container=Container(docker=DockerContainer()))))
assert sandbox._user is None
@mock.patch('pwd.getpwnam')
def test_user_does_not_exist(getpwnam):
getpwnam.side_effect = KeyError('johndoe')
with temporary_dir() as d:
ds = DirectorySandbox(d, 'cletus')
with pytest.raises(DirectorySandbox.CreationError):
ds.create()
getpwnam.assert_called_with('cletus')
@mock.patch('os.chown')
def test_create_ioerror(chown):
chown.side_effect = IOError('Disk is borked')
with temporary_dir() as d:
ds = DirectorySandbox(d)
with pytest.raises(DirectorySandbox.CreationError):
ds.create()
@mock.patch('os.makedirs')
def test_docker_directory_sandbox_create_ioerror(makedirs):
makedirs.side_effect = IOError('Disk is borked')
with mock.patch.dict('os.environ', {
'MESOS_DIRECTORY': 'some-directory',
'MESOS_SANDBOX': 'some-sandbox'
}):
with temporary_dir() as d:
ds = DockerDirectorySandbox(d)
with pytest.raises(DirectorySandbox.CreationError):
ds.create()
def test_destroy_ioerror():
with temporary_dir() as d:
ds = DirectorySandbox(d)
ds.create()
with mock.patch('shutil.rmtree') as shutil_rmtree:
shutil_rmtree.side_effect = IOError('What even are you doing?')
with pytest.raises(DirectorySandbox.DeletionError):
ds.destroy()
MOCK_MESOS_DIRECTORY = '/some/path'
@mock.patch('subprocess.check_output')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_verify_group_match(mock_check_output):
with temporary_dir() as d:
sandbox = FileSystemImageSandbox(d, user='someuser', sandbox_mount_point='/some/path')
mock_check_output.return_value = 'test-group:x:2:'
# valid case
sandbox._verify_group_match_in_taskfs(2, 'test-group')
mock_check_output.assert_called_with(
['chroot', sandbox._task_fs_root, 'getent', 'group', 'test-group'])
# invalid group id
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_group_match_in_taskfs(3, 'test-group')
# invalid group name
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_group_match_in_taskfs(2, 'invalid-group')
# exception case
exception = subprocess.CalledProcessError(
returncode=1,
cmd='some command',
output=None)
mock_check_output.side_effect = exception
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_group_match_in_taskfs(2, 'test-group')
def test_verify_network_files():
with temporary_dir() as d:
task_fs_path = os.path.join(d, 'taskfs')
os.makedirs(os.path.join(task_fs_path, 'etc'))
with mock.patch.dict(os.environ, {'MESOS_DIRECTORY': d}):
sandbox = FileSystemImageSandbox(d, sandbox_mount_point='/some/sandbox/path')
sandbox._copy_files()
def verify_copy(path):
if os.path.exists(path):
assert os.path.exists(os.path.join(task_fs_path, path.lstrip('/')))
verify_copy('/etc/resolv.conf')
verify_copy('/etc/hostname')
verify_copy('/etc/hosts')
@mock.patch('subprocess.check_output')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_verify_user_match(mock_check_output):
with temporary_dir() as d:
sandbox = FileSystemImageSandbox(
os.path.join(d, 'sandbox'),
user='someuser',
sandbox_mount_point='/some/path')
mock_check_output.return_value = 'uid=1(test-user) gid=2(test-group) groups=2(test-group)'
# valid case
sandbox._verify_user_match_in_taskfs(1, 'test-user', 2, 'test-group')
mock_check_output.assert_called_with(['chroot', sandbox._task_fs_root, 'id', 'test-user'])
# invalid user id
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_user_match_in_taskfs(0, 'test-user', 2, 'test-group')
# invalid user name
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_user_match_in_taskfs(1, 'invalid-user', 2, 'test-group')
# invalid group id
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_user_match_in_taskfs(1, 'test-user', 0, 'test-group')
# invalid group name
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_user_match_in_taskfs(1, 'test-user', 2, 'invalid-group')
# exception case
exception = subprocess.CalledProcessError(
returncode=1,
cmd='some command',
output=None)
mock_check_output.side_effect = exception
with pytest.raises(FileSystemImageSandbox.CreationError):
sandbox._verify_user_match_in_taskfs(1, "test-user", 2, "test-group")
def assert_create_user_and_group(mock_check_call,
mock_verify,
gid_exists,
uid_exists,
group_name_exists,
user_name_exists):
mock_pwent = pwd.struct_passwd((
'someuser', # login name
'hunter2', # password
834, # uid
835, # gid
'Some User', # user name
'/home/someuser', # home directory
'/bin/sh')) # login shell
mock_grent = grp.struct_group((
'users', # group name
'*', # password
835, # gid
['someuser'])) # members
returncode = 0
if gid_exists or uid_exists:
returncode = FileSystemImageSandbox._USER_OR_GROUP_ID_EXISTS
elif group_name_exists or user_name_exists:
returncode = FileSystemImageSandbox._USER_OR_GROUP_NAME_EXISTS
exception = subprocess.CalledProcessError(
returncode=returncode,
cmd='some command',
output=None)
mock_check_call.side_effect = [
exception if gid_exists or group_name_exists else None,
exception if uid_exists or user_name_exists else None]
with temporary_dir() as d:
with mock.patch.object(
FileSystemImageSandbox,
'get_user_and_group',
return_value=(mock_pwent, mock_grent)):
sandbox = FileSystemImageSandbox(
os.path.join(d, 'sandbox'),
user='someuser',
sandbox_mount_point='/some/path')
sandbox._create_user_and_group_in_taskfs()
assert len(mock_check_call.mock_calls) == 2
assert len(mock_verify.mock_calls) == 1
@mock.patch('apache.aurora.executor.common.sandbox.' +
'FileSystemImageSandbox._verify_user_match_in_taskfs')
@mock.patch('subprocess.check_call')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_uid_exists(mock_check_call, mock_verify):
assert_create_user_and_group(mock_check_call, mock_verify, False, True, False, False)
@mock.patch('apache.aurora.executor.common.sandbox.' +
'FileSystemImageSandbox._verify_group_match_in_taskfs')
@mock.patch('subprocess.check_call')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_gid_exists(mock_check_call, mock_verify):
assert_create_user_and_group(mock_check_call, mock_verify, True, False, False, False)
@mock.patch('apache.aurora.executor.common.sandbox.' +
'FileSystemImageSandbox._verify_user_match_in_taskfs')
@mock.patch('subprocess.check_call')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_user_name_exists(mock_check_call, mock_verify):
assert_create_user_and_group(mock_check_call, mock_verify, False, False, False, True)
@mock.patch('apache.aurora.executor.common.sandbox.' +
'FileSystemImageSandbox._verify_group_match_in_taskfs')
@mock.patch('subprocess.check_call')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_group_name_exists(mock_check_call, mock_verify):
assert_create_user_and_group(mock_check_call, mock_verify, True, False, True, False)
@mock.patch('os.path.isfile')
@mock.patch('subprocess.check_call')
@mock.patch('apache.aurora.executor.common.sandbox.safe_mkdir')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_filesystem_sandbox_mounts_paths(mock_safe_mkdir, mock_check_call, mock_isfile):
sandbox_mount_point = '/some/mount/point'
sandbox_directory = os.path.join(MOCK_MESOS_DIRECTORY, 'sandbox')
sandbox = FileSystemImageSandbox(
MOCK_MESOS_DIRECTORY,
user='someuser',
no_create_user=True,
mounted_volume_paths=['/some/container/path', '/some/other/container/path'],
sandbox_mount_point=sandbox_mount_point)
mock_isfile.return_value = False
sandbox._mount_paths()
task_fs_path = os.path.join(MOCK_MESOS_DIRECTORY, 'taskfs')
# we should have mounted both of the paths we passed in as well as the sandbox directory itself.
assert mock_check_call.mock_calls == [
mock.call([
'mount',
'-n',
'--rbind',
'/some/container/path',
os.path.join(task_fs_path, 'some/container/path')
]),
mock.call([
'mount',
'-n',
'--rbind',
'/some/other/container/path',
os.path.join(task_fs_path, 'some/other/container/path')
]),
mock.call([
'mount',
'-n',
'--rbind',
sandbox_directory,
os.path.join(task_fs_path, sandbox_mount_point[1:])
])
]
@mock.patch('os.path.isfile')
@mock.patch('subprocess.check_call')
@mock.patch('apache.aurora.executor.common.sandbox.safe_mkdir')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_filesystem_sandbox_no_volumes(mock_safe_mkdir, mock_check_call, mock_isfile):
sandbox_mount_point = '/some/mount/point'
sandbox_directory = os.path.join(MOCK_MESOS_DIRECTORY, 'sandbox')
sandbox = FileSystemImageSandbox(
MOCK_MESOS_DIRECTORY,
user='someuser',
no_create_user=True,
mounted_volume_paths=None,
sandbox_mount_point=sandbox_mount_point)
mock_isfile.return_value = False
sandbox._mount_paths()
task_fs_path = os.path.join(MOCK_MESOS_DIRECTORY, 'taskfs')
assert mock_check_call.mock_calls == [
mock.call([
'mount',
'-n',
'--rbind',
sandbox_directory,
os.path.join(task_fs_path, sandbox_mount_point[1:])
])
]
@mock.patch('apache.aurora.executor.common.sandbox.touch')
@mock.patch('os.path.exists')
@mock.patch('os.path.isfile')
@mock.patch('subprocess.check_call')
@mock.patch('apache.aurora.executor.common.sandbox.safe_mkdir')
@mock.patch.dict(os.environ, {'MESOS_DIRECTORY': MOCK_MESOS_DIRECTORY})
def test_filesystem_sandbox_mounts_paths_source_is_file(
mock_safe_mkdir,
mock_check_call,
mock_isfile,
mock_exists,
mock_touch):
sandbox_mount_point = '/some/mount/point'
sandbox_directory = os.path.join(MOCK_MESOS_DIRECTORY, 'sandbox')
sandbox = FileSystemImageSandbox(
MOCK_MESOS_DIRECTORY,
user='someuser',
no_create_user=True,
mounted_volume_paths=['/some/path/to/file', '/some/path/to/directory'],
sandbox_mount_point=sandbox_mount_point)
def is_file_side_effect(arg):
return arg.endswith('file')
mock_isfile.side_effect = is_file_side_effect
mock_exists.return_value = False
sandbox._mount_paths()
task_fs_path = os.path.join(MOCK_MESOS_DIRECTORY, 'taskfs')
destination_path = os.path.join(task_fs_path, 'some/path/to/file')
# we should `touch` the mount point, but only for the file mount, not the directory mount.
assert mock_touch.mock_calls == [
mock.call(destination_path)
]
# we should have mounted the file path we passed in as well as the sandbox directory itself.
assert mock_check_call.mock_calls == [
mock.call([
'mount',
'-n',
'--rbind',
'/some/path/to/file',
destination_path
]),
mock.call([
'mount',
'-n',
'--rbind',
'/some/path/to/directory',
os.path.join(task_fs_path, 'some/path/to/directory')
]),
mock.call([
'mount',
'-n',
'--rbind',
sandbox_directory,
os.path.join(task_fs_path, sandbox_mount_point[1:])
])
]