blob: 3b294f87c9d477809f5722fdc0865ca89a31d933 [file] [log] [blame]
# Copyright (c) 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from datetime import datetime
import collections
import docker
import mock
import requests
import sys
import unittest
from infra.services.swarm_docker import containers
class FakeClient(object):
"""Mocks the client object returned from docker's client API.
containers.DockerClient wraps it. Mocked here to verify wrapper class
bheaves correctly.
"""
def __init__(self):
self.containers = None
self.images = FakeImageList()
self.creds = None
self.responsive = True
def ping(self):
if self.responsive:
return True
else:
raise docker.errors.APIError('omg engine not running')
class FakeImage(object):
def __init__(self, image_id, image_url):
self.id = image_id
self.tags = [image_url]
class FakeImageList(object):
def __init__(self):
self.images = []
def list(self):
return self.images
def get(self, image_url):
for i in self.images:
if image_url in i.tags:
return True
raise docker.errors.ImageNotFound('omg no image')
def remove(self, image_id):
for i in self.images[:]:
if i.id == image_id:
self.images.remove(i)
def pull(self, image):
self.images.append(image)
class FakeContainer(object):
"""Used to mock containers.Container"""
def __init__(self, name, uptime=None):
self._container = FakeContainerBackend(name)
self.name = name
self.uptime = uptime
self.swarming_bot_killed = False
def get_container_uptime(self, _):
return self.uptime
def kill_swarming_bot(self, *_, **__):
self.swarming_bot_killed = True
class FakeContainerBackend(object):
"""Mocks the container objects returned from docker's client API.
containers.Container wraps each one. Mocked here to verify the wrapper class
behaves correctly.
"""
ExecResult = collections.namedtuple('ExecResult', 'exit_code,output')
def __init__(self, name, devices='not set'):
self.name = name
self.was_deleted = False
self.was_started = False
self.was_stopped = False
self.is_paused = False
self.exec_outputs = []
self.exec_inputs = []
self.attrs = {}
self.devices = devices
self.image = ''
def remove(self, **_kwargs):
self.was_deleted = True
def start(self):
self.was_started = True
def pause(self):
assert not self.is_paused
self.is_paused = True
def unpause(self):
assert self.is_paused
self.is_paused = False
def stop(self, **_kwargs):
self.was_stopped = True
def exec_run(self, cmd, **_kwargs):
self.exec_inputs.append(cmd)
return self.ExecResult(0, self.exec_outputs.pop(0))
class FakeContainerList(object):
"""Mocks the container list objects returned from docker's client API."""
def __init__(self, containers_list):
self._list = containers_list
def create(self, **kwargs):
return FakeContainerBackend(kwargs['name'], kwargs['devices'])
def list(self, filters=None, **_kwargs): # pylint: disable=unused-argument
if filters is None:
filters = {}
status = filters.get('status')
if status == 'paused':
return [c for c in self._list if c.is_paused]
elif status == 'running':
return [c for c in self._list if not c.is_paused]
elif status == 'created':
return [
c for c in self._list if c.attrs.get('State', {}).get(
'Status') == 'created']
else:
return self._list
def get(self, name):
for c in self._list:
if c.name == name:
return c
raise docker.errors.NotFound('omg container missing')
class TestContainerDescriptor(unittest.TestCase):
def setUp(self):
self.desc = containers.ContainerDescriptor('7')
def test_name(self):
self.assertEquals(self.desc.name, '7')
@mock.patch('socket.gethostname')
def test_hostname(self, mock_gethostname):
mock_gethostname.return_value = 'build123-a4'
self.assertEquals(self.desc.hostname, 'build123-a4--7')
def test_log_started_smoke(self):
self.desc.log_started()
def test_shutdown_file(self):
self.assertEqual(self.desc.shutdown_file, '/b/7.shutdown.stamp')
def test_lock_file(self):
self.assertEqual(self.desc.lock_file, '/var/lock/swarm_docker.7.lock')
def test_should_create_container(self):
self.assertTrue(self.desc.should_create_container())
class TestDockerClient(unittest.TestCase):
def setUp(self):
self.fake_client = FakeClient()
mock.patch('docker.from_env', return_value=self.fake_client).start()
self.container_names = ['5', '6']
self.fake_client.containers = FakeContainerList(
[FakeContainerBackend(name) for name in self.container_names])
@mock.patch('time.sleep')
def test_ping_success(self, mock_sleep):
self.fake_client.responsive = True
mock_sleep.return_value = None
client = containers.DockerClient()
self.assertTrue(client.ping())
@mock.patch('time.sleep')
def test_ping_fail(self, mock_sleep):
self.fake_client.responsive = False
mock_sleep.return_value = None
client = containers.DockerClient()
self.assertFalse(client.ping(retries=5))
mock_sleep.assert_has_calls(
[mock.call(1), mock.call(2), mock.call(4), mock.call(8)])
def test_images(self):
img1 = FakeImage('image1-id', 'image1-url')
self.fake_client.images.images.append(img1)
client = containers.DockerClient()
self.assertEqual(client.images(), [img1])
def test_has_image(self):
self.fake_client.images.images.append(FakeImage('image1-id', 'image1-url'))
client = containers.DockerClient()
self.assertTrue(client.has_image('image1-url'))
self.assertFalse(client.has_image('image99-url'))
def test_remove_image(self):
self.fake_client.images.images.append(FakeImage('image1-id', 'image1-url'))
client = containers.DockerClient()
client.remove_image('image1-id')
self.assertEqual(client.images(), [])
def test_remove_outdated_images(self):
old_img = FakeImage('old-image-id', 'old-image-url')
new_img = FakeImage('new-image-id', 'new-image-url')
self.fake_client.images.images = [old_img, new_img]
client = containers.DockerClient()
client.remove_outdated_images('new-image-url')
self.assertEqual(client.images(), [new_img])
def test_remove_outdated_images_no_op(self):
"""remove_outdated_images() is a no-op. Needed for 100% coverage."""
new_img = FakeImage('new-image-id', 'new-image-url')
self.fake_client.images.images = [new_img]
client = containers.DockerClient()
client.remove_outdated_images('new-image-url')
self.assertEqual(client.images(), [new_img])
def test_pull(self):
client = containers.DockerClient()
client.logged_in = True
client.pull('image1')
self.assertTrue('image1' in self.fake_client.images.images)
def test_get_running_containers(self):
running_containers = containers.DockerClient().get_running_containers()
self.assertEqual(
set(c.name for c in running_containers), set(self.container_names))
def test_get_paused_containers(self):
self.fake_client.containers.get('5').pause()
paused_containers = containers.DockerClient().get_paused_containers()
self.assertEqual(len(paused_containers), 1)
self.assertEqual(paused_containers[0].name, '5')
def test_get_created_containers(self):
self.fake_client.containers.get('5').attrs['State'] = {'Status': 'created'}
created_containers = containers.DockerClient().get_created_containers()
self.assertEqual(len(created_containers), 1)
self.assertEqual(created_containers[0].name, '5')
def test_get_container(self):
container = containers.DockerClient().get_container(
containers.ContainerDescriptor('5'))
self.assertEqual(container.name, '5')
def test_get_missing_container(self):
container = containers.DockerClient().get_container(
containers.ContainerDescriptor('1'))
self.assertEqual(container, None)
def test_stop_old_containers(self):
young_container = FakeContainer('young_container', uptime=10)
old_container = FakeContainer('old_container', uptime=999)
containers.DockerClient().stop_old_containers(
[young_container, old_container], 100)
self.assertFalse(young_container.swarming_bot_killed)
self.assertTrue(old_container.swarming_bot_killed)
def test_stop_frozen_containers(self):
def _raise_frozen_container(*_args, **_kwargs):
raise containers.FrozenContainerError()
frozen_container1 = FakeContainer('frozen_container1', uptime=999)
frozen_container1.kill_swarming_bot = _raise_frozen_container
frozen_container2 = FakeContainer('frozen_container2', uptime=999)
frozen_container2.kill_swarming_bot = _raise_frozen_container
with self.assertRaises(containers.FrozenEngineError):
containers.DockerClient().stop_old_containers(
[frozen_container1, frozen_container2], 100)
def test_delete_stopped_containers(self):
created_c = FakeContainerBackend('11')
created_c.attrs['State'] = {'Status': 'created'}
self.fake_client.containers._list.append(created_c)
containers.DockerClient().delete_stopped_containers()
self.assertTrue(
all(c.was_deleted for c in self.fake_client.containers.list()))
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
def test_create_container(self, mock_getpwnam, mock_exists, mock_mkdir,
mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.return_value = False
running_containers = [FakeContainer('1'), FakeContainer('2')]
self.fake_client.containers = FakeContainerList(running_containers)
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {})
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
def test_create_container_with_env(self, mock_getpwnam, mock_exists,
mock_mkdir, mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.return_value = False
running_containers = [FakeContainer('1'), FakeContainer('2')]
self.fake_client.containers = FakeContainerList(running_containers)
additional_env = {'SOME_ENV': 'SOME_VAL'}
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {},
additional_env)
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
@mock.patch('sys.platform', 'darwin')
def test_create_container_darwin(self, mock_getpwnam, mock_exists, mock_mkdir,
mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.side_effect = lambda d: d == containers._KVM_DEVICE
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {})
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
self.assertEquals(container.devices, None)
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
@mock.patch('sys.platform', 'linux2')
def test_create_container_linux_no_kvm(self, mock_getpwnam, mock_exists,
mock_mkdir, mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.return_value = False
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {})
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
self.assertEquals(container.devices, None)
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
@mock.patch('sys.platform', 'linux2')
def test_create_container_linux_kvm(self, mock_getpwnam, mock_exists,
mock_mkdir, mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.side_effect = lambda d: d == containers._KVM_DEVICE
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {})
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
self.assertEquals(container.devices,
['{0}:{0}'.format(containers._KVM_DEVICE)])
@mock.patch('os.chown')
@mock.patch('os.mkdir')
@mock.patch('os.path.exists')
@mock.patch('pwd.getpwnam')
@mock.patch('sys.platform', 'linux2')
def test_create_container_linux_tun(self, mock_getpwnam, mock_exists,
mock_mkdir, mock_chown):
mock_getpwnam.return_value = collections.namedtuple(
'pwnam', 'pw_uid, pw_gid')(1,2)
mock_exists.side_effect = lambda d: d in (
containers._KVM_DEVICE, containers._TUN_DEVICE)
container = containers.DockerClient().create_container(
containers.ContainerDescriptor('1'), 'image', 'swarm-url.com', {})
self.assertEquals(container.name, '1')
mock_chown.assert_called_with(mock_mkdir.call_args[0][0], 1, 2)
self.assertEquals(container.devices,
['{0}:{0}'.format(containers._KVM_DEVICE),
'{0}:{0}'.format(containers._TUN_DEVICE)])
def test_num_containers_is_set(self):
client = containers.DockerClient()
self.assertIsNone(client._get_env('').get('NUM_CONFIGURED_CONTAINERS'))
client.set_num_configured_containers(42)
self.assertEquals(client._get_env('').get('NUM_CONFIGURED_CONTAINERS'), 42)
@mock.patch('socket.getfqdn')
def test_host_hostname_is_set(self, mock_getfqdn):
mock_getfqdn.return_value = 'hostofa_hostofa_hostofa_host'
client = containers.DockerClient()
self.assertEquals(
client._get_env('').get('DOCKER_HOST_HOSTNAME'),
'hostofa_hostofa_hostofa_host')
class TestContainer(unittest.TestCase):
def setUp(self):
self.container_backend = FakeContainerBackend('container1')
self.container = containers.Container(self.container_backend)
def test_get_labels(self):
self.container_backend.attrs = {'Config': {'Labels': {'label1': 'val1'}}}
self.assertEquals(self.container.labels, {'label1': 'val1'})
def test_get_exit_code(self):
self.container_backend.attrs['State'] = {'ExitCode': 111}
self.assertEqual(self.container.exit_code, 111)
def test_get_state(self):
self.container_backend.attrs = {'State': {'Status': 'running'}}
status = self.container.state
self.assertEquals(status, 'running')
def test_get_image(self):
self.container_backend.image = 'test-image'
self.assertEquals(self.container.image, 'test-image')
def test_get_container_uptime(self):
now = datetime.strptime(
'2000-01-01T01:30:00.000000', '%Y-%m-%dT%H:%M:%S.%f')
self.container_backend.attrs = {
'State': {'StartedAt': '2000-01-01T00:00:00.0000000000'}
}
uptime = self.container.get_container_uptime(now)
self.assertEquals(uptime, 90)
def test_get_swarming_bot_pid(self):
self.container_backend.exec_outputs = ['123']
pid = self.container.get_swarming_bot_pid()
self.assertEquals(pid, 123)
def test_get_swarming_bot_pid_backend_error(self):
self.container_backend.exec_outputs = ['rpc error: omg failure']
pid = self.container.get_swarming_bot_pid()
self.assertEquals(pid, None)
def test_get_swarming_bot_pid_lsof_error(self):
self.container_backend.exec_outputs = ['omg lsof failure']
pid = self.container.get_swarming_bot_pid()
self.assertEquals(pid, None)
def test_get_swarming_bot_pid_404_error(self):
def _raises_docker_not_found(*_args, **_kwargs):
raise docker.errors.NotFound('404')
self.container_backend.exec_run = _raises_docker_not_found
pid = self.container.get_swarming_bot_pid()
self.assertEquals(pid, None)
def test_kill_swarming_bot(self):
self.container_backend.exec_outputs = ['123', '']
self.container.kill_swarming_bot()
self.assertEquals(self.container_backend.exec_inputs[-1], 'kill -15 123')
def test_kill_swarming_bot_error_no_shutdown(self):
self.container_backend.attrs = {
'State': {
'StartedAt': '2000-01-01T00:00:00.0000000000'
}
}
# 1 hour uptime.
now = datetime.strptime('2000-01-01T01:00:00.000000',
'%Y-%m-%dT%H:%M:%S.%f')
self.container_backend.exec_outputs = ['omg failure']
self.container.kill_swarming_bot(now=now, max_uptime=60)
# Ensure nothing was killed when the bot's pid couldn't be found and its
# uptime is much less than max_uptime.
self.assertFalse(
any('kill -15' in cmd for cmd in self.container_backend.exec_inputs))
self.assertFalse(self.container_backend.was_stopped)
def test_kill_swarming_bot_error_shutdown(self):
self.container_backend.attrs = {
'State': {
'StartedAt': '2000-01-01T00:00:00.0000000000'
}
}
# 12 hour uptime.
now = datetime.strptime('2000-01-01T12:00:00.000000',
'%Y-%m-%dT%H:%M:%S.%f')
self.container_backend.exec_outputs = ['omg failure']
self.container.kill_swarming_bot(now=now, max_uptime=60)
# Ensure the container was shutdown when the bot's pid couldn't be found
# and its uptime was much larger than max_uptime.
self.assertFalse(
any('kill -15' in cmd for cmd in self.container_backend.exec_inputs))
self.assertTrue(self.container_backend.was_stopped)
def test_kill_swarming_bot_cant_kill(self):
def _raise_requests_timeout(**_kwargs):
raise requests.exceptions.ReadTimeout()
self.container_backend.attrs = {
'State': {
'StartedAt': '2000-01-01T00:00:00.0000000000'
}
}
# 1 hour uptime.
now = datetime.strptime('2000-01-01T01:00:00.000000',
'%Y-%m-%dT%H:%M:%S.%f')
self.container_backend.exec_outputs = ['omg failure']
self.container_backend.stop = _raise_requests_timeout
self.container.kill_swarming_bot(now=now)
# Ensure nothing was killed when the bot's pid couldn't be found.
self.assertFalse(
any('kill -15' in cmd for cmd in self.container_backend.exec_inputs))
self.assertFalse(self.container_backend.was_stopped)
self.assertTrue(self.container_backend.was_deleted)
def test_kill_swarming_bot_cant_remove(self):
def _raise_requests_timeout(**_kwargs):
raise requests.exceptions.ReadTimeout()
def _raise_docker_api_error(**_kwargs):
raise docker.errors.APIError('omg error')
self.container_backend.attrs = {
'State': {
'StartedAt': '2000-01-01T00:00:00.0000000000'
}
}
# 1 hour uptime.
now = datetime.strptime('2000-01-01T01:00:00.000000',
'%Y-%m-%dT%H:%M:%S.%f')
self.container_backend.exec_outputs = ['omg failure']
self.container_backend.stop = _raise_requests_timeout
self.container_backend.remove = _raise_docker_api_error
with self.assertRaises(containers.FrozenContainerError):
self.container.kill_swarming_bot(now=now)
# Ensure nothing was killed when the bot's pid couldn't be found.
self.assertFalse(
any('kill -15' in cmd for cmd in self.container_backend.exec_inputs))
self.assertFalse(self.container_backend.was_stopped)
self.assertFalse(self.container_backend.was_deleted)
def test_pause_unpause(self):
self.container.pause()
self.assertTrue(self.container_backend.is_paused)
self.container.unpause()
self.assertFalse(self.container_backend.is_paused)
def test_exec_run(self):
self.container_backend.exec_outputs = ['', '']
self.container.exec_run('ls')
self.container.exec_run('cd')
self.assertEquals(self.container_backend.exec_inputs, ['ls', 'cd'])
def test_attrs(self):
self.container_backend.attrs = {'Id': '123'}
self.assertEquals(self.container.attrs['Id'], '123')
if __name__ == '__main__':
unittest.main()