Merge "Implement APIs for mounting Cinder volumes"

This commit is contained in:
Zuul 2017-10-25 03:04:44 +00:00 committed by Gerrit Code Review
commit 7d9403a85c
16 changed files with 409 additions and 65 deletions

View File

@ -35,6 +35,7 @@ from zun.common import validation
import zun.conf import zun.conf
from zun.network import neutron from zun.network import neutron
from zun import objects from zun import objects
from zun.volume import cinder_api as cinder
CONF = zun.conf.CONF CONF = zun.conf.CONF
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -242,6 +243,17 @@ class ContainersController(base.Controller):
nets = container_dict.get('nets', []) nets = container_dict.get('nets', [])
requested_networks = self._build_requested_networks(context, nets) requested_networks = self._build_requested_networks(context, nets)
mounts = container_dict.pop('mounts', [])
if mounts:
req_version = pecan.request.version
min_version = versions.Version('', '', '', '1.11')
if req_version < min_version:
raise exception.InvalidParamInVersion(param='mounts',
req_version=req_version,
min_version=min_version)
requested_volumes = self._build_requested_volumes(context, mounts)
# Valiadtion accepts 'None' so need to convert it to None # Valiadtion accepts 'None' so need to convert it to None
if container_dict.get('image_driver'): if container_dict.get('image_driver'):
container_dict['image_driver'] = api_utils.string_or_none( container_dict['image_driver'] = api_utils.string_or_none(
@ -275,6 +287,7 @@ class ContainersController(base.Controller):
kwargs = {} kwargs = {}
kwargs['extra_spec'] = extra_spec kwargs['extra_spec'] = extra_spec
kwargs['requested_networks'] = requested_networks kwargs['requested_networks'] = requested_networks
kwargs['requested_volumes'] = requested_volumes
kwargs['run'] = run kwargs['run'] = run
compute_api.container_create(context, new_container, **kwargs) compute_api.container_create(context, new_container, **kwargs)
# Set the HTTP Location Header # Set the HTTP Location Header
@ -331,6 +344,25 @@ class ContainersController(base.Controller):
self._check_external_network_attach(context, requested_networks) self._check_external_network_attach(context, requested_networks)
return requested_networks return requested_networks
def _build_requested_volumes(self, context, mounts):
# NOTE(hongbin): We assume cinder is the only volume provider here.
# The logic needs to be re-visited if a second volume provider
# (i.e. Manila) is introduced.
cinder_api = cinder.CinderAPI(context)
requested_volumes = []
for mount in mounts:
volume = cinder_api.search_volume(mount['source'])
cinder_api.ensure_volume_usable(volume)
volmapp = objects.VolumeMapping(
context,
volume_id=volume.id, volume_provider='cinder',
container_path=mount['destination'],
user_id=context.user_id,
project_id=context.project_id)
requested_volumes.append(volmapp)
return requested_volumes
def _check_security_group(self, context, security_group, container): def _check_security_group(self, context, security_group, container):
if security_group.get("uuid"): if security_group.get("uuid"):
security_group_id = security_group.get("uuid") security_group_id = security_group.get("uuid")

View File

@ -30,6 +30,7 @@ _container_properties = {
'image_driver': parameter_types.image_driver, 'image_driver': parameter_types.image_driver,
'security_groups': parameter_types.security_groups, 'security_groups': parameter_types.security_groups,
'hints': parameter_types.hints, 'hints': parameter_types.hints,
'mounts': parameter_types.mounts,
'nets': parameter_types.nets, 'nets': parameter_types.nets,
'runtime': parameter_types.runtime, 'runtime': parameter_types.runtime,
'hostname': parameter_types.hostname, 'hostname': parameter_types.hostname,

View File

@ -43,10 +43,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 1.8 - Support attach a network to a container * 1.8 - Support attach a network to a container
* 1.9 - Add support set container's hostname * 1.9 - Add support set container's hostname
* 1.10 - Make delete container async * 1.10 - Make delete container async
* 1.11 - Add mounts to container create
""" """
BASE_VER = '1.1' BASE_VER = '1.1'
CURRENT_MAX_VER = '1.10' CURRENT_MAX_VER = '1.11'
class Version(object): class Version(object):

View File

@ -86,3 +86,14 @@ user documentation.
---- ----
Make container delete API async. Delete operation for a container Make container delete API async. Delete operation for a container
can take long time, so making it async to improve user experience. can take long time, so making it async to improve user experience.
1.11
----
Add a new attribute 'mounts' to the request to create a container.
Users can use this attribute to specify one or multiple mounts for
the container. Each mount could specify the source and destination.
The source is the Cinder volume id or name, and the destination is
the path where the file or directory will be mounted in the container.
For examples:
[{'source': 'my-vol', 'destination': '/data'}]

View File

@ -113,6 +113,23 @@ nets = {
'type': ['array', 'null'] 'type': ['array', 'null']
} }
mounts = {
'type': ['array', 'null'],
'items': {
'type': 'object',
'properties': {
'source': {
'type': ['string'],
},
'destination': {
'type': ['string'],
}
},
'additionalProperties': False,
'required': ['source', 'destination']
}
}
environment = { environment = {
'type': ['object', 'null'], 'type': ['object', 'null'],
'patternProperties': { 'patternProperties': {

View File

@ -29,7 +29,7 @@ class API(object):
super(API, self).__init__() super(API, self).__init__()
def container_create(self, context, new_container, extra_spec, def container_create(self, context, new_container, extra_spec,
requested_networks, run): requested_networks, requested_volumes, run):
host_state = None host_state = None
try: try:
host_state = self._schedule_container(context, new_container, host_state = self._schedule_container(context, new_container,
@ -42,7 +42,8 @@ class API(object):
self.rpcapi.container_create(context, host_state['host'], self.rpcapi.container_create(context, host_state['host'],
new_container, host_state['limits'], new_container, host_state['limits'],
requested_networks, run) requested_networks, requested_volumes,
run)
def _schedule_container(self, context, new_container, extra_spec): def _schedule_container(self, context, new_container, extra_spec):
dests = self.scheduler_client.select_destinations(context, dests = self.scheduler_client.select_destinations(context,

View File

@ -49,6 +49,12 @@ class Manager(periodic_task.PeriodicTasks):
self.use_sandbox = False self.use_sandbox = False
def _fail_container(self, context, container, error, unset_host=False): def _fail_container(self, context, container, error, unset_host=False):
try:
self._detach_volumes(context, container)
except Exception as e:
LOG.exception("Failed to detach volumes: %s",
six.text_type(e))
container.status = consts.ERROR container.status = consts.ERROR
container.status_reason = error container.status_reason = error
container.task_state = None container.task_state = None
@ -56,16 +62,19 @@ class Manager(periodic_task.PeriodicTasks):
container.host = None container.host = None
container.save(context) container.save(context)
def container_create(self, context, limits, requested_networks, container, def container_create(self, context, limits, requested_networks,
run): requested_volumes, container, run):
@utils.synchronized(container.uuid) @utils.synchronized(container.uuid)
def do_container_create(run, context, *args): def do_container_create():
created_container = self._do_container_create(context, *args) if not self._attach_volumes(context, container, requested_volumes):
return
created_container = self._do_container_create(
context, container, requested_networks, requested_volumes,
limits)
if run and created_container: if run and created_container:
self._do_container_start(context, created_container) self._do_container_start(context, created_container)
utils.spawn_n(do_container_create, run, context, container, utils.spawn_n(do_container_create)
requested_networks, limits)
def _do_sandbox_cleanup(self, context, container): def _do_sandbox_cleanup(self, context, container):
sandbox_id = container.get_sandbox_id() sandbox_id = container.get_sandbox_id()
@ -83,6 +92,7 @@ class Manager(periodic_task.PeriodicTasks):
container.save(context) container.save(context)
def _do_container_create_base(self, context, container, requested_networks, def _do_container_create_base(self, context, container, requested_networks,
requested_volumes,
sandbox=None, limits=None, reraise=False): sandbox=None, limits=None, reraise=False):
self._update_task_state(context, container, consts.IMAGE_PULLING) self._update_task_state(context, container, consts.IMAGE_PULLING)
repo, tag = utils.parse_image_name(container.image) repo, tag = utils.parse_image_name(container.image)
@ -127,7 +137,8 @@ class Manager(periodic_task.PeriodicTasks):
with rt.container_claim(context, container, container.host, with rt.container_claim(context, container, container.host,
limits): limits):
container = self.driver.create(context, container, image, container = self.driver.create(context, container, image,
requested_networks) requested_networks,
requested_volumes)
self._update_task_state(context, container, None) self._update_task_state(context, container, None)
return container return container
except exception.DockerError as e: except exception.DockerError as e:
@ -148,23 +159,66 @@ class Manager(periodic_task.PeriodicTasks):
return return
def _do_container_create(self, context, container, requested_networks, def _do_container_create(self, context, container, requested_networks,
requested_volumes,
limits=None, reraise=False): limits=None, reraise=False):
LOG.debug('Creating container: %s', container.uuid) LOG.debug('Creating container: %s', container.uuid)
sandbox = None sandbox = None
if self.use_sandbox: if self.use_sandbox:
sandbox = self._create_sandbox(context, container, sandbox = self._create_sandbox(context, container,
requested_networks, reraise) requested_networks,
requested_volumes,
reraise)
if sandbox is None: if sandbox is None:
return return
created_container = self._do_container_create_base(context, created_container = self._do_container_create_base(context,
container, container,
requested_networks, requested_networks,
requested_volumes,
sandbox, limits, sandbox, limits,
reraise) reraise)
return created_container return created_container
def _attach_volumes(self, context, container, volumes):
try:
for volume in volumes:
volume.container_uuid = container.uuid
self._attach_volume(context, volume)
return True
except Exception as e:
with excutils.save_and_reraise_exception(reraise=False):
self._fail_container(context, container, six.text_type(e),
unset_host=True)
def _attach_volume(self, context, volume):
volume.create(context)
context = context.elevated()
LOG.info('Attaching volume %(volume_id)s to %(host)s',
{'volume_id': volume.volume_id,
'host': CONF.host})
try:
self.driver.attach_volume(context, volume)
except Exception:
with excutils.save_and_reraise_exception():
volume.destroy()
def _detach_volumes(self, context, container, reraise=True):
volumes = objects.VolumeMapping.list_by_container(context,
container.uuid)
for volume in volumes:
self._detach_volume(context, volume, reraise=reraise)
def _detach_volume(self, context, volume, reraise=True):
context = context.elevated()
try:
self.driver.detach_volume(context, volume)
except Exception:
with excutils.save_and_reraise_exception(reraise=reraise):
LOG.error("Failed to detach %(volume_id)s",
{'volume_id': volume.volume_id})
volume.destroy()
def _use_sandbox(self): def _use_sandbox(self):
if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]: if CONF.use_sandbox and self.driver.capabilities["support_sandbox"]:
return True return True
@ -179,7 +233,7 @@ class Manager(periodic_task.PeriodicTasks):
'driver': self.driver}) 'driver': self.driver})
def _create_sandbox(self, context, container, requested_networks, def _create_sandbox(self, context, container, requested_networks,
reraise=False): requested_volumes, reraise=False):
self._update_task_state(context, container, consts.SANDBOX_CREATING) self._update_task_state(context, container, consts.SANDBOX_CREATING)
sandbox_image = CONF.sandbox_image sandbox_image = CONF.sandbox_image
sandbox_image_driver = CONF.sandbox_image_driver sandbox_image_driver = CONF.sandbox_image_driver
@ -193,7 +247,8 @@ class Manager(periodic_task.PeriodicTasks):
self.driver.load_image(image['path']) self.driver.load_image(image['path'])
sandbox_id = self.driver.create_sandbox( sandbox_id = self.driver.create_sandbox(
context, container, image=sandbox_image, context, container, image=sandbox_image,
requested_networks=requested_networks) requested_networks=requested_networks,
requested_volumes=requested_volumes)
return sandbox_id return sandbox_id
except Exception as e: except Exception as e:
with excutils.save_and_reraise_exception(reraise=reraise): with excutils.save_and_reraise_exception(reraise=reraise):
@ -245,6 +300,8 @@ class Manager(periodic_task.PeriodicTasks):
LOG.exception("Unexpected exception: %s", six.text_type(e)) LOG.exception("Unexpected exception: %s", six.text_type(e))
self._fail_container(context, container, six.text_type(e)) self._fail_container(context, container, six.text_type(e))
self._detach_volumes(context, container, reraise=reraise)
self._update_task_state(context, container, None) self._update_task_state(context, container, None)
container.destroy(context) container.destroy(context)
self._get_resource_tracker() self._get_resource_tracker()

View File

@ -56,9 +56,11 @@ class API(rpc_service.API):
transport, context, topic=zun.conf.CONF.compute.topic) transport, context, topic=zun.conf.CONF.compute.topic)
def container_create(self, context, host, container, limits, def container_create(self, context, host, container, limits,
requested_networks, run): requested_networks, requested_volumes, run):
self._cast(host, 'container_create', limits=limits, self._cast(host, 'container_create', limits=limits,
requested_networks=requested_networks, container=container, requested_networks=requested_networks,
requested_volumes=requested_volumes,
container=container,
run=run) run=run)
@check_container_host @check_container_host

View File

@ -31,6 +31,7 @@ from zun.container.docker import utils as docker_utils
from zun.container import driver from zun.container import driver
from zun.network import network as zun_network from zun.network import network as zun_network
from zun import objects from zun import objects
from zun.volume import driver as vol_driver
CONF = zun.conf.CONF CONF = zun.conf.CONF
@ -112,7 +113,8 @@ class DockerDriver(driver.ContainerDriver):
except Exception: except Exception:
LOG.warning("Unable to read image data from tarfile") LOG.warning("Unable to read image data from tarfile")
def create(self, context, container, image, requested_networks): def create(self, context, container, image, requested_networks,
requested_volumes):
sandbox_id = container.get_sandbox_id() sandbox_id = container.get_sandbox_id()
with docker_utils.docker_client() as docker: with docker_utils.docker_client() as docker:
@ -121,6 +123,7 @@ class DockerDriver(driver.ContainerDriver):
LOG.debug('Creating container with image %(image)s name %(name)s', LOG.debug('Creating container with image %(image)s name %(name)s',
{'image': image['image'], 'name': name}) {'image': image['image'], 'name': name})
self._provision_network(context, network_api, requested_networks) self._provision_network(context, network_api, requested_networks)
binds = self._get_binds(context, requested_volumes)
kwargs = { kwargs = {
'name': self.get_container_name(container), 'name': self.get_container_name(container),
'command': container.command, 'command': container.command,
@ -147,6 +150,9 @@ class DockerDriver(driver.ContainerDriver):
# host_config['pid_mode'] = 'container:%s' % sandbox_id # host_config['pid_mode'] = 'container:%s' % sandbox_id
host_config['ipc_mode'] = 'container:%s' % sandbox_id host_config['ipc_mode'] = 'container:%s' % sandbox_id
host_config['volumes_from'] = sandbox_id host_config['volumes_from'] = sandbox_id
else:
host_config['binds'] = binds
kwargs['volumes'] = [b['bind'] for b in binds.values()]
if container.auto_remove: if container.auto_remove:
host_config['auto_remove'] = container.auto_remove host_config['auto_remove'] = container.auto_remove
if container.memory is not None: if container.memory is not None:
@ -179,6 +185,15 @@ class DockerDriver(driver.ContainerDriver):
self._get_or_create_docker_network( self._get_or_create_docker_network(
context, network_api, rq_network['network']) context, network_api, rq_network['network'])
def _get_binds(self, context, requested_volumes):
binds = {}
for volume in requested_volumes:
volume_driver = vol_driver.driver(provider=volume.volume_provider,
context=context)
source, destination = volume_driver.bind_mount(volume)
binds[source] = {'bind': destination}
return binds
def _setup_network_for_container(self, context, container, def _setup_network_for_container(self, context, container,
requested_networks, network_api): requested_networks, network_api):
security_group_ids = utils.get_security_group_ids(context, container. security_group_ids = utils.get_security_group_ids(context, container.
@ -675,14 +690,19 @@ class DockerDriver(driver.ContainerDriver):
return value.encode('utf-8') return value.encode('utf-8')
def create_sandbox(self, context, container, requested_networks, def create_sandbox(self, context, container, requested_networks,
requested_volumes,
image='kubernetes/pause'): image='kubernetes/pause'):
with docker_utils.docker_client() as docker: with docker_utils.docker_client() as docker:
network_api = zun_network.api(context=context, docker_api=docker) network_api = zun_network.api(context=context, docker_api=docker)
self._provision_network(context, network_api, requested_networks) self._provision_network(context, network_api, requested_networks)
binds = self._get_binds(context, requested_volumes)
host_config = docker.create_host_config(binds=binds)
name = self.get_sandbox_name(container) name = self.get_sandbox_name(container)
sandbox = docker.create_container( volumes = [b['bind'] for b in binds.values()]
image, name=name, sandbox = docker.create_container(image, name=name,
hostname=container.hostname or name[:63]) hostname=name[:63],
volumes=volumes,
host_config=host_config)
container.set_sandbox_id(sandbox['Id']) container.set_sandbox_id(sandbox['Id'])
addresses = self._setup_network_for_container( addresses = self._setup_network_for_container(
context, container, requested_networks, network_api) context, container, requested_networks, network_api)
@ -696,6 +716,18 @@ class DockerDriver(driver.ContainerDriver):
docker.start(sandbox['Id']) docker.start(sandbox['Id'])
return sandbox['Id'] return sandbox['Id']
def attach_volume(self, context, volume_mapping):
volume_driver = vol_driver.driver(
provider=volume_mapping.volume_provider,
context=context)
volume_driver.attach(volume_mapping)
def detach_volume(self, context, volume_mapping):
volume_driver = vol_driver.driver(
provider=volume_mapping.volume_provider,
context=context)
volume_driver.detach(volume_mapping)
def _get_or_create_docker_network(self, context, network_api, def _get_or_create_docker_network(self, context, network_api,
neutron_net_id): neutron_net_id):
docker_net_name = self._get_docker_network_name(context, docker_net_name = self._get_docker_network_name(context,

View File

@ -192,6 +192,12 @@ class ContainerDriver(object):
def get_cpu_used(self): def get_cpu_used(self):
raise NotImplementedError() raise NotImplementedError()
def attach_volume(self, context, volume_mapping):
raise NotImplementedError()
def detach_volume(self, context, volume_mapping):
raise NotImplementedError()
def add_security_group(self, context, container, security_group, **kwargs): def add_security_group(self, context, container, security_group, **kwargs):
raise NotImplementedError() raise NotImplementedError()

View File

@ -47,6 +47,10 @@ class BaseTestCase(testscenarios.WithScenarios, base.BaseTestCase):
self.addCleanup(CONF.reset) self.addCleanup(CONF.reset)
class TestingException(Exception):
pass
class TestCase(base.BaseTestCase): class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests.""" """Test case base class for all unit tests."""

View File

@ -27,7 +27,7 @@ from zun.tests.unit.db import base
PATH_PREFIX = '/v1' PATH_PREFIX = '/v1'
CURRENT_VERSION = "container 1.9" CURRENT_VERSION = "container 1.11"
class FunctionalTest(base.DbTestCase): class FunctionalTest(base.DbTestCase):

View File

@ -28,7 +28,7 @@ class TestRootController(api_base.FunctionalTest):
'default_version': 'default_version':
{'id': 'v1', {'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}],
'max_version': '1.10', 'max_version': '1.11',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}, 'status': 'CURRENT'},
'description': 'Zun is an OpenStack project which ' 'description': 'Zun is an OpenStack project which '
@ -37,7 +37,7 @@ class TestRootController(api_base.FunctionalTest):
'versions': [{'id': 'v1', 'versions': [{'id': 'v1',
'links': [{'href': 'http://localhost/v1/', 'links': [{'href': 'http://localhost/v1/',
'rel': 'self'}], 'rel': 'self'}],
'max_version': '1.10', 'max_version': '1.11',
'min_version': '1.1', 'min_version': '1.1',
'status': 'CURRENT'}]} 'status': 'CURRENT'}]}

View File

@ -721,6 +721,53 @@ class TestContainerController(api_base.FunctionalTest):
mock_authorize.return_value = fake_admin_authorize mock_authorize.return_value = fake_admin_authorize
self.assertEqual(202, response.status_int) self.assertEqual(202, response.status_int)
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create')
@patch('zun.common.context.RequestContext.can')
@patch('zun.volume.cinder_api.CinderAPI.search_volume')
@patch('zun.volume.cinder_api.CinderAPI.ensure_volume_usable')
@patch('zun.compute.api.API.image_search')
def test_create_container_with_volume(
self, mock_search, mock_ensure_volume_usable, mock_search_volume,
mock_authorize, mock_container_create, mock_container_show,
mock_neutron_get_network):
fake_network = {'id': 'foo'}
mock_neutron_get_network.return_value = fake_network
fake_volume_id = 'fakevolid'
fake_volume = mock.Mock(id=fake_volume_id)
mock_search_volume.return_value = fake_volume
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
'"command": "env", "memory": "512",'
'"mounts": [{"source": "s", "destination": "d"}]}')
response = self.post('/v1/containers/',
params=params,
content_type='application/json')
self.assertEqual(202, response.status_int)
# get all containers
container = objects.Container.list(self.context)[0]
container.status = 'Stopped'
mock_container_show.return_value = container
response = self.app.get('/v1/containers/')
self.assertEqual(200, response.status_int)
self.assertEqual(2, len(response.json))
c = response.json['containers'][0]
self.assertIsNotNone(c.get('uuid'))
self.assertEqual('MyDocker', c.get('name'))
self.assertEqual('env', c.get('command'))
self.assertEqual('Stopped', c.get('status'))
self.assertEqual('512M', c.get('memory'))
requested_networks = \
mock_container_create.call_args[1]['requested_networks']
self.assertEqual(1, len(requested_networks))
self.assertEqual(fake_network['id'], requested_networks[0]['network'])
mock_search_volume.assert_called_once()
requested_volumes = \
mock_container_create.call_args[1]['requested_volumes']
self.assertEqual(1, len(requested_volumes))
self.assertEqual(fake_volume_id, requested_volumes[0].volume_id)
@patch('zun.network.neutron.NeutronAPI.get_available_network') @patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show') @patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create') @patch('zun.compute.api.API.container_create')

View File

@ -24,6 +24,7 @@ from zun.compute import manager
import zun.conf import zun.conf
from zun.objects.container import Container from zun.objects.container import Container
from zun.objects.image import Image from zun.objects.image import Image
from zun.objects.volume_mapping import VolumeMapping
from zun.tests import base from zun.tests import base
from zun.tests.unit.container.fake_driver import FakeDriver as fake_driver from zun.tests.unit.container.fake_driver import FakeDriver as fake_driver
from zun.tests.unit.db import utils from zun.tests.unit.db import utils
@ -35,6 +36,27 @@ class FakeResourceTracker(object):
return claims.NopClaim() return claims.NopClaim()
class FakeVolumeMapping(object):
volume_provider = 'fake_provider'
container_path = 'fake_path'
container_uuid = 'fake-cid'
volume_id = 'fake-vid'
def __init__(self):
self.__class__.volumes = []
def create(self, context):
self.__class__.volumes.append(self)
def destroy(self):
self.__class__.volumes.remove(self)
@classmethod
def list_by_container(cls, context, container_id):
return cls.volumes
class TestManager(base.TestCase): class TestManager(base.TestCase):
def setUp(self): def setUp(self):
@ -62,13 +84,14 @@ class TestManager(base.TestCase):
mock_pull.return_value = image, False mock_pull.return_value = image, False
self.compute_manager._resource_tracker = FakeResourceTracker() self.compute_manager._resource_tracker = FakeResourceTracker()
networks = [] networks = []
volumes = []
self.compute_manager._do_container_create(self.context, container, self.compute_manager._do_container_create(self.context, container,
networks) networks, volumes)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_pull.assert_any_call(self.context, container.image, 'latest', mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance') 'always', 'glance')
mock_create.assert_called_once_with(self.context, container, image, mock_create.assert_called_once_with(self.context, container, image,
networks) networks, volumes)
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@ -78,8 +101,9 @@ class TestManager(base.TestCase):
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
mock_pull.side_effect = exception.DockerError("Pull Failed") mock_pull.side_effect = exception.DockerError("Pull Failed")
networks = [] networks = []
volumes = []
self.compute_manager._do_container_create(self.context, container, self.compute_manager._do_container_create(self.context, container,
networks) networks, volumes)
mock_fail.assert_called_once_with(self.context, mock_fail.assert_called_once_with(self.context,
container, "Pull Failed") container, "Pull Failed")
@ -91,8 +115,9 @@ class TestManager(base.TestCase):
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
mock_pull.side_effect = exception.ImageNotFound("Image Not Found") mock_pull.side_effect = exception.ImageNotFound("Image Not Found")
networks = [] networks = []
volumes = []
self.compute_manager._do_container_create(self.context, container, self.compute_manager._do_container_create(self.context, container,
networks) networks, volumes)
mock_fail.assert_called_once_with(self.context, mock_fail.assert_called_once_with(self.context,
container, "Image Not Found") container, "Image Not Found")
@ -105,8 +130,9 @@ class TestManager(base.TestCase):
mock_pull.side_effect = exception.ZunException( mock_pull.side_effect = exception.ZunException(
message="Image Not Found") message="Image Not Found")
networks = [] networks = []
volumes = []
self.compute_manager._do_container_create(self.context, container, self.compute_manager._do_container_create(self.context, container,
networks) networks, volumes)
mock_fail.assert_called_once_with(self.context, mock_fail.assert_called_once_with(self.context,
container, "Image Not Found") container, "Image Not Found")
@ -124,18 +150,25 @@ class TestManager(base.TestCase):
mock_create.side_effect = exception.DockerError("Creation Failed") mock_create.side_effect = exception.DockerError("Creation Failed")
self.compute_manager._resource_tracker = FakeResourceTracker() self.compute_manager._resource_tracker = FakeResourceTracker()
networks = [] networks = []
volumes = []
self.compute_manager._do_container_create(self.context, container, self.compute_manager._do_container_create(self.context, container,
networks) networks, volumes)
mock_fail.assert_called_once_with( mock_fail.assert_called_once_with(
self.context, container, "Creation Failed", unset_host=True) self.context, container, "Creation Failed", unset_host=True)
@mock.patch('zun.common.utils.spawn_n') @mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@mock.patch.object(fake_driver, 'detach_volume')
@mock.patch.object(fake_driver, 'attach_volume')
@mock.patch.object(fake_driver, 'create') @mock.patch.object(fake_driver, 'create')
@mock.patch.object(fake_driver, 'start') @mock.patch.object(fake_driver, 'start')
def test_container_run(self, mock_start, def test_container_run(
mock_create, mock_pull, mock_save, mock_spawn_n): self, mock_start, mock_create, mock_attach_volume,
mock_detach_volume, mock_pull, mock_list_by_container, mock_save,
mock_spawn_n):
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance'} image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance'}
mock_create.return_value = container mock_create.return_value = container
@ -144,25 +177,74 @@ class TestManager(base.TestCase):
container.status = 'Stopped' container.status = 'Stopped'
self.compute_manager._resource_tracker = FakeResourceTracker() self.compute_manager._resource_tracker = FakeResourceTracker()
networks = [] networks = []
volumes = [FakeVolumeMapping()]
self.compute_manager.container_create( self.compute_manager.container_create(
self.context, self.context,
requested_networks=networks, requested_networks=networks,
requested_volumes=volumes,
container=container, container=container,
limits=None, run=True) limits=None, run=True)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_pull.assert_any_call(self.context, container.image, 'latest', mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance') 'always', 'glance')
mock_create.assert_called_once_with(self.context, container, image, mock_create.assert_called_once_with(self.context, container, image,
networks) networks, volumes)
mock_start.assert_called_once_with(self.context, container) mock_start.assert_called_once_with(self.context, container)
mock_attach_volume.assert_called_once()
mock_detach_volume.assert_not_called()
self.assertEqual(1, len(FakeVolumeMapping.volumes))
@mock.patch('zun.common.utils.spawn_n') @mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(fake_driver, 'detach_volume')
def test_container_run_image_not_found(self, mock_fail, @mock.patch.object(fake_driver, 'attach_volume')
mock_pull, mock_save, @mock.patch.object(fake_driver, 'create')
mock_spawn_n): @mock.patch.object(fake_driver, 'start')
def test_container_run_driver_attach_failed(
self, mock_start, mock_create, mock_attach_volume,
mock_detach_volume, mock_pull, mock_list_by_container, mock_save,
mock_spawn_n):
mock_attach_volume.side_effect = [None, base.TestingException("fake")]
container = Container(self.context, **utils.get_test_container())
vol = FakeVolumeMapping()
vol2 = FakeVolumeMapping()
image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance'}
mock_create.return_value = container
mock_pull.return_value = image, False
mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y)
container.status = 'Stopped'
self.compute_manager._resource_tracker = FakeResourceTracker()
networks = []
volumes = [vol, vol2]
self.compute_manager.container_create(
self.context,
requested_networks=networks,
requested_volumes=volumes,
container=container,
limits=None, run=True)
mock_save.assert_called_with(self.context)
mock_pull.assert_not_called()
mock_create.assert_not_called()
mock_start.assert_not_called()
mock_attach_volume.assert_has_calls([
mock.call(mock.ANY, vol), mock.call(mock.ANY, vol2)])
mock_detach_volume.assert_has_calls([
mock.call(mock.ANY, vol)])
self.assertEqual(0, len(FakeVolumeMapping.volumes))
@mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch.object(fake_driver, 'detach_volume')
@mock.patch.object(fake_driver, 'attach_volume')
@mock.patch('zun.image.driver.pull_image')
def test_container_run_image_not_found(
self, mock_pull, mock_attach_volume, mock_detach_volume,
mock_list_by_container, mock_save, mock_spawn_n):
container_dict = utils.get_test_container( container_dict = utils.get_test_container(
image='test:latest', image_driver='docker', image='test:latest', image_driver='docker',
image_pull_policy='ifnotpresent') image_pull_policy='ifnotpresent')
@ -171,24 +253,32 @@ class TestManager(base.TestCase):
message="Image Not Found") message="Image Not Found")
mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y)
networks = [] networks = []
volumes = [FakeVolumeMapping()]
self.compute_manager.container_create( self.compute_manager.container_create(
self.context, self.context,
requested_networks=networks, requested_networks=networks,
requested_volumes=volumes,
container=container, container=container,
limits=None, run=True) limits=None, run=True)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_fail.assert_called_with(self.context, self.assertEqual('Error', container.status)
container, 'Image Not Found') self.assertEqual('Image Not Found', container.status_reason)
mock_pull.assert_called_once_with(self.context, 'test', 'latest', mock_pull.assert_called_once_with(self.context, 'test', 'latest',
'ifnotpresent', 'docker') 'ifnotpresent', 'docker')
mock_attach_volume.assert_called_once()
mock_detach_volume.assert_called_once()
self.assertEqual(0, len(FakeVolumeMapping.volumes))
@mock.patch('zun.common.utils.spawn_n') @mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch.object(fake_driver, 'detach_volume')
@mock.patch.object(fake_driver, 'attach_volume')
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container') def test_container_run_image_pull_exception_raised(
def test_container_run_image_pull_exception_raised(self, mock_fail, self, mock_pull, mock_attach_volume, mock_detach_volume,
mock_pull, mock_save, mock_list_by_container, mock_save, mock_spawn_n):
mock_spawn_n):
container_dict = utils.get_test_container( container_dict = utils.get_test_container(
image='test:latest', image_driver='docker', image='test:latest', image_driver='docker',
image_pull_policy='ifnotpresent') image_pull_policy='ifnotpresent')
@ -197,24 +287,32 @@ class TestManager(base.TestCase):
message="Image Not Found") message="Image Not Found")
mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y)
networks = [] networks = []
volumes = [FakeVolumeMapping()]
self.compute_manager.container_create( self.compute_manager.container_create(
self.context, self.context,
requested_networks=networks, requested_networks=networks,
requested_volumes=volumes,
container=container, container=container,
limits=None, run=True) limits=None, run=True)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_fail.assert_called_with(self.context, self.assertEqual('Error', container.status)
container, 'Image Not Found') self.assertEqual('Image Not Found', container.status_reason)
mock_pull.assert_called_once_with(self.context, 'test', 'latest', mock_pull.assert_called_once_with(self.context, 'test', 'latest',
'ifnotpresent', 'docker') 'ifnotpresent', 'docker')
mock_attach_volume.assert_called_once()
mock_detach_volume.assert_called_once()
self.assertEqual(0, len(FakeVolumeMapping.volumes))
@mock.patch('zun.common.utils.spawn_n') @mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch.object(fake_driver, 'detach_volume')
@mock.patch.object(fake_driver, 'attach_volume')
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container') def test_container_run_image_pull_docker_error(
def test_container_run_image_pull_docker_error(self, mock_fail, self, mock_pull, mock_attach_volume, mock_detach_volume,
mock_pull, mock_save, mock_list_by_container, mock_save, mock_spawn_n):
mock_spawn_n):
container_dict = utils.get_test_container( container_dict = utils.get_test_container(
image='test:latest', image_driver='docker', image='test:latest', image_driver='docker',
image_pull_policy='ifnotpresent') image_pull_policy='ifnotpresent')
@ -223,26 +321,34 @@ class TestManager(base.TestCase):
message="Docker Error occurred") message="Docker Error occurred")
mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y)
networks = [] networks = []
volumes = [FakeVolumeMapping()]
self.compute_manager.container_create( self.compute_manager.container_create(
self.context, self.context,
requested_networks=networks, requested_networks=networks,
requested_volumes=volumes,
container=container, container=container,
limits=None, run=True) limits=None, run=True)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_fail.assert_called_with(self.context, self.assertEqual('Error', container.status)
container, 'Docker Error occurred') self.assertEqual('Docker Error occurred', container.status_reason)
mock_pull.assert_called_once_with(self.context, 'test', 'latest', mock_pull.assert_called_once_with(self.context, 'test', 'latest',
'ifnotpresent', 'docker') 'ifnotpresent', 'docker')
mock_attach_volume.assert_called_once()
mock_detach_volume.assert_called_once()
self.assertEqual(0, len(FakeVolumeMapping.volumes))
@mock.patch('zun.common.utils.spawn_n') @mock.patch('zun.common.utils.spawn_n')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container',
side_effect=FakeVolumeMapping.list_by_container)
@mock.patch.object(fake_driver, 'detach_volume')
@mock.patch.object(fake_driver, 'attach_volume')
@mock.patch('zun.image.driver.pull_image') @mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container')
@mock.patch.object(fake_driver, 'create') @mock.patch.object(fake_driver, 'create')
def test_container_run_create_raises_docker_error(self, mock_create, def test_container_run_create_raises_docker_error(
mock_fail, self, mock_create, mock_pull, mock_attach_volume,
mock_pull, mock_save, mock_detach_volume, mock_list_by_container, mock_save,
mock_spawn_n): mock_spawn_n):
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance', image = {'image': 'repo', 'path': 'out_path', 'driver': 'glance',
'repo': 'test', 'tag': 'testtag'} 'repo': 'test', 'tag': 'testtag'}
@ -252,26 +358,34 @@ class TestManager(base.TestCase):
mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y) mock_spawn_n.side_effect = lambda f, *x, **y: f(*x, **y)
self.compute_manager._resource_tracker = FakeResourceTracker() self.compute_manager._resource_tracker = FakeResourceTracker()
networks = [] networks = []
volumes = [FakeVolumeMapping()]
self.compute_manager.container_create( self.compute_manager.container_create(
self.context, self.context,
requested_networks=networks, requested_networks=networks,
requested_volumes=volumes,
container=container, container=container,
limits=None, run=True) limits=None, run=True)
mock_save.assert_called_with(self.context) mock_save.assert_called_with(self.context)
mock_fail.assert_called_with( self.assertEqual('Error', container.status)
self.context, container, 'Docker Error occurred', unset_host=True) self.assertEqual('Docker Error occurred', container.status_reason)
mock_pull.assert_any_call(self.context, container.image, 'latest', mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance') 'always', 'glance')
mock_create.assert_called_once_with( mock_create.assert_called_once_with(
self.context, container, image, networks) self.context, container, image, networks, volumes)
mock_attach_volume.assert_called_once()
mock_detach_volume.assert_called_once()
self.assertEqual(0, len(FakeVolumeMapping.volumes))
@mock.patch.object(compute_node_tracker.ComputeNodeTracker, @mock.patch.object(compute_node_tracker.ComputeNodeTracker,
'remove_usage_from_container') 'remove_usage_from_container')
@mock.patch.object(Container, 'destroy') @mock.patch.object(Container, 'destroy')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container')
@mock.patch.object(fake_driver, 'delete') @mock.patch.object(fake_driver, 'delete')
def test_container_delete(self, mock_delete, mock_save, mock_cnt_destroy, def test_container_delete(
mock_remove_usage): self, mock_delete, mock_list_by_container, mock_save,
mock_cnt_destroy, mock_remove_usage):
mock_list_by_container.return_value = []
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
self.compute_manager._do_container_delete(self. context, container, self.compute_manager._do_container_delete(self. context, container,
False) False)
@ -307,10 +421,14 @@ class TestManager(base.TestCase):
@mock.patch.object(Container, 'destroy') @mock.patch.object(Container, 'destroy')
@mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(manager.Manager, '_fail_container')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container')
@mock.patch.object(fake_driver, 'delete') @mock.patch.object(fake_driver, 'delete')
def test_container_delete_failed_force(self, mock_delete, mock_save, def test_container_delete_failed_force(self, mock_delete,
mock_list_by_container,
mock_save,
mock_fail, mock_destroy, mock_fail, mock_destroy,
mock_remove_usage): mock_remove_usage):
mock_list_by_container.return_value = []
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
mock_delete.side_effect = exception.DockerError( mock_delete.side_effect = exception.DockerError(
message="Docker Error occurred") message="Docker Error occurred")
@ -354,12 +472,15 @@ class TestManager(base.TestCase):
@mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(manager.Manager, '_fail_container')
@mock.patch.object(manager.Manager, '_delete_sandbox') @mock.patch.object(manager.Manager, '_delete_sandbox')
@mock.patch.object(Container, 'save') @mock.patch.object(Container, 'save')
@mock.patch.object(VolumeMapping, 'list_by_container')
@mock.patch.object(fake_driver, 'delete') @mock.patch.object(fake_driver, 'delete')
def test_container_delete_sandbox_failed_force(self, mock_delete, def test_container_delete_sandbox_failed_force(self, mock_delete,
mock_list_by_container,
mock_save, mock_save,
mock_delete_sandbox, mock_delete_sandbox,
mock_fail, mock_destroy, mock_fail, mock_destroy,
mock_remove_usage): mock_remove_usage):
mock_list_by_container.return_value = []
self.compute_manager.use_sandbox = True self.compute_manager.use_sandbox = True
container = Container(self.context, **utils.get_test_container()) container = Container(self.context, **utils.get_test_container())
container.set_sandbox_id("sandbox_id") container.set_sandbox_id("sandbox_id")

View File

@ -91,14 +91,17 @@ class TestDockerDriver(base.DriverTestCase):
return_value={'Id': 'val1', 'key1': 'val2'}) return_value={'Id': 'val1', 'key1': 'val2'})
image = {'path': '', 'image': '', 'repo': '', 'tag': ''} image = {'path': '', 'image': '', 'repo': '', 'tag': ''}
mock_container = self.mock_default_container mock_container = self.mock_default_container
networks = []
volumes = []
result_container = self.driver.create(self.context, mock_container, result_container = self.driver.create(self.context, mock_container,
image, []) image, networks, volumes)
host_config = {} host_config = {}
host_config['mem_limit'] = '512m' host_config['mem_limit'] = '512m'
host_config['cpu_quota'] = 100000 host_config['cpu_quota'] = 100000
host_config['cpu_period'] = 100000 host_config['cpu_period'] = 100000
host_config['restart_policy'] = {'Name': 'no', 'MaximumRetryCount': 0} host_config['restart_policy'] = {'Name': 'no', 'MaximumRetryCount': 0}
host_config['runtime'] = 'runc' host_config['runtime'] = 'runc'
host_config['binds'] = {}
self.mock_docker.create_host_config.assert_called_once_with( self.mock_docker.create_host_config.assert_called_once_with(
**host_config) **host_config)
@ -112,6 +115,7 @@ class TestDockerDriver(base.DriverTestCase):
'stdin_open': True, 'stdin_open': True,
'tty': True, 'tty': True,
'hostname': 'testhost', 'hostname': 'testhost',
'volumes': [],
} }
self.mock_docker.create_container.assert_called_once_with( self.mock_docker.create_container.assert_called_once_with(
image['repo'] + ":" + image['tag'], **kwargs) image['repo'] + ":" + image['tag'], **kwargs)
@ -364,15 +368,19 @@ class TestDockerDriver(base.DriverTestCase):
mock_get_sandbox_name.return_value = sandbox_name mock_get_sandbox_name.return_value = sandbox_name
self.mock_docker.create_container = mock.Mock( self.mock_docker.create_container = mock.Mock(
return_value={'Id': 'val1', 'key1': 'val2'}) return_value={'Id': 'val1', 'key1': 'val2'})
fake_host_config = mock.Mock()
self.mock_docker.create_host_config.return_value = fake_host_config
mock_container = mock.MagicMock() mock_container = mock.MagicMock()
hostname = 'my_hostname' hostname = 'my_hostname'
mock_container.hostname = hostname mock_container.hostname = hostname
requested_networks = [] requested_networks = []
requested_volumes = []
result_sandbox_id = self.driver.create_sandbox( result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, requested_networks, self.context, mock_container, requested_networks,
'kubernetes/pause') requested_volumes, 'kubernetes/pause')
self.mock_docker.create_container.assert_called_once_with( self.mock_docker.create_container.assert_called_once_with(
'kubernetes/pause', name=sandbox_name, hostname=hostname) 'kubernetes/pause', name=sandbox_name, hostname=sandbox_name,
host_config=fake_host_config, volumes=[])
self.assertEqual(result_sandbox_id, 'val1') self.assertEqual(result_sandbox_id, 'val1')
@mock.patch('zun.network.kuryr_network.KuryrNetwork' @mock.patch('zun.network.kuryr_network.KuryrNetwork'
@ -386,14 +394,18 @@ class TestDockerDriver(base.DriverTestCase):
mock_get_sandbox_name.return_value = sandbox_name mock_get_sandbox_name.return_value = sandbox_name
self.mock_docker.create_container = mock.Mock( self.mock_docker.create_container = mock.Mock(
return_value={'Id': 'val1', 'key1': 'val2'}) return_value={'Id': 'val1', 'key1': 'val2'})
fake_host_config = mock.Mock()
self.mock_docker.create_host_config.return_value = fake_host_config
mock_container = mock.MagicMock() mock_container = mock.MagicMock()
mock_container.hostname = None mock_container.hostname = None
requested_networks = [] requested_networks = []
requested_volumes = []
result_sandbox_id = self.driver.create_sandbox( result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, requested_networks, self.context, mock_container, requested_networks,
'kubernetes/pause') requested_volumes, 'kubernetes/pause')
self.mock_docker.create_container.assert_called_once_with( self.mock_docker.create_container.assert_called_once_with(
'kubernetes/pause', name=sandbox_name, hostname=sandbox_name[:63]) 'kubernetes/pause', name=sandbox_name, hostname=sandbox_name[:63],
host_config=fake_host_config, volumes=[])
self.assertEqual(result_sandbox_id, 'val1') self.assertEqual(result_sandbox_id, 'val1')
def test_delete_sandbox(self): def test_delete_sandbox(self):