Merge "Implement the sandbox proposal - Part 1"

This commit is contained in:
Jenkins 2016-11-24 14:48:36 +00:00 committed by Gerrit Code Review
commit b4368be180
21 changed files with 353 additions and 73 deletions

View File

@ -25,7 +25,7 @@
# userrc for us if nova service is not enabled, check
# https://github.com/openstack-dev/devstack/blob/master/stack.sh#L1310
OVERRIDE_ENABLED_SERVICES="dstat,key,mysql,rabbit,n-api,n-cond,n-cpu,n-crt,n-obj,n-sch,tempest"
OVERRIDE_ENABLED_SERVICES="dstat,key,mysql,rabbit,n-api,n-cond,n-cpu,n-crt,n-obj,n-sch,g-api,g-reg,tempest"
export OVERRIDE_ENABLED_SERVICES
$BASE/new/devstack-gate/devstack-vm-gate.sh

View File

@ -64,8 +64,8 @@ else
ZUN_BIN_DIR=$(get_python_exec_prefix)
fi
DOCKER_GROUP=docker
DEFAULT_CONTAINER_DRIVER=docker
DOCKER_GROUP=${DOCKER_GROUP:-docker}
ZUN_DRIVER=${DEFAULT_ZUN_DRIVER:-docker}
ETCD_VERSION=v3.0.13
if is_ubuntu; then
@ -109,11 +109,19 @@ function configure_zun {
create_api_paste_conf
if [[ ${DEFAULT_CONTAINER_DRIVER} == "docker" ]]; then
if [[ ${ZUN_DRIVER} == "docker" ]]; then
check_docker || install_docker
fi
}
# upload_sandbox_image() - Upload sandbox image to glance
function upload_sandbox_image {
if [[ ${ZUN_DRIVER} == "docker" ]]; then
sg docker "docker pull kubernetes/pause"
sg docker "docker save kubernetes/pause" | openstack image create kubernetes/pause --public --container-format docker --disk-format raw
fi
}
# create_zun_accounts() - Set up common required ZUN accounts
#
# Project User Roles
@ -314,7 +322,11 @@ function start_zun_api {
# start_zun_compute() - Start Zun compute agent
function start_zun_compute {
echo "Start zun compute..."
run_process zun-compute "$ZUN_BIN_DIR/zun-compute" ${DOCKER_GROUP}
if [[ ${ZUN_DRIVER} == "docker" ]]; then
run_process zun-compute "$ZUN_BIN_DIR/zun-compute" ${DOCKER_GROUP}
else
run_process zun-compute "$ZUN_BIN_DIR/zun-compute"
fi
}
function start_zun_etcd {

View File

@ -32,6 +32,7 @@ if is_service_enabled zun-api zun-compute; then
# Start the zun API and zun compute
echo_summary "Starting zun"
start_zun
upload_sandbox_image
fi

View File

@ -256,6 +256,20 @@ class Dict(object):
return value
class Json(object):
type_name = 'Json'
@classmethod
def validate(self, value):
if value is None:
return None
if not isinstance(value, dict):
raise exception.InvalidValue(value=value, type=self.type_name)
return value
class DateTime(object):
type_name = "DateTime"

View File

@ -120,13 +120,16 @@ class Container(base.APIBase):
'labels': {
'validate': types.Dict(types.String, types.String).validate,
},
'addresses': {
'validate': types.Json.validate,
},
'image_pull_policy': {
'validate': types.EnumType.validate,
'validate_args': {
'name': 'image_pull_policy',
'values': ['never', 'always', 'ifnotpresent']
}
}
},
}
def __init__(self, **kwargs):
@ -138,7 +141,7 @@ class Container(base.APIBase):
container.unset_fields_except([
'uuid', 'name', 'image', 'command', 'status', 'cpu', 'memory',
'environment', 'task_state', 'workdir', 'ports', 'hostname',
'labels', 'image_pull_policy', 'status_reason'])
'labels', 'addresses', 'image_pull_policy', 'status_reason'])
container.links = [link.Link.make_link(
'self', url,
@ -171,6 +174,14 @@ class Container(base.APIBase):
ports=[80, 443],
hostname='testhost',
labels={'key1': 'val1', 'key2': 'val2'},
addresses={
'private': [
{'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:04:da:76',
'version': 4,
'addr': '10.0.0.12',
'OS-EXT-IPS:type': 'fixed'},
],
},
created_at=timeutils.utcnow(),
updated_at=timeutils.utcnow())
return cls._convert_with_links(sample, 'http://localhost:9517', expand)

View File

@ -118,9 +118,9 @@ def wrap_controller_exception(func, func_server_error, func_client_error):
# log the error message with its associated
# correlation id
log_correlation_id = uuidutils.generate_uuid()
LOG.error(_LE("%(correlation_id)s:%(excp)s") %
{'correlation_id': log_correlation_id,
'excp': str(excp)})
LOG.exception(_LE("%(correlation_id)s:%(excp)s") %
{'correlation_id': log_correlation_id,
'excp': str(excp)})
# raise a client error with an obfuscated message
return func_server_error(log_correlation_id, http_error_code)
else:
@ -364,3 +364,7 @@ class InvalidStateException(ZunException):
class DockerError(ZunException):
message = _("Docker internal error: %(error_msg)s.")
class PollTimeOut(ZunException):
message = _("Polling request timed out.")

View File

@ -23,6 +23,7 @@ import zun.conf
CONF = zun.conf.CONF
CFG_GROUP = 'keystone_auth'
CFG_LEGACY_GROUP = 'keystone_authtoken'
LOG = logging.getLogger(__name__)
keystone_auth_opts = (ka_loading.get_auth_common_conf_options() +
@ -46,7 +47,9 @@ class KeystoneClientV3(object):
@property
def auth_url(self):
return CONF.keystone_authtoken.auth_uri.replace('v2.0', 'v3')
# FIXME(pauloewerton): auth_url should be retrieved from keystone_auth
# section by default
return CONF[CFG_LEGACY_GROUP].auth_uri.replace('v2.0', 'v3')
@property
def auth_token(self):

View File

@ -77,6 +77,23 @@ class Manager(object):
LOG.debug('Creating container...', context=context,
container=container)
container.task_state = fields.TaskState.SANDBOX_CREATING
container.save()
sandbox_id = None
sandbox_image = 'kubernetes/pause'
repo, tag = utils.parse_image_name(sandbox_image)
try:
image = image_driver.pull_image(context, repo, tag, 'ifnotpresent')
sandbox_id = self.driver.create_sandbox(context, container,
image=sandbox_image)
except Exception as e:
with excutils.save_and_reraise_exception(reraise=reraise):
LOG.exception(_LE("Unexpected exception: %s"),
six.text_type(e))
self._fail_container(container, six.text_type(e))
return
self.driver.set_sandbox_id(container, sandbox_id)
container.task_state = fields.TaskState.IMAGE_PULLING
container.save()
repo, tag = utils.parse_image_name(container.image)
@ -107,7 +124,9 @@ class Manager(object):
container.task_state = fields.TaskState.CONTAINER_CREATING
container.save()
try:
container = self.driver.create(container, image)
container = self.driver.create(container, sandbox_id, image)
container.addresses = self._get_container_addresses(context,
container)
container.task_state = None
container.save()
return container
@ -154,7 +173,6 @@ class Manager(object):
if not force:
self._validate_container_state(container, 'delete')
self.driver.delete(container, force)
return container
except exception.DockerError as e:
LOG.error(_LE("Error occured while calling docker delete API: %s"),
six.text_type(e))
@ -163,6 +181,16 @@ class Manager(object):
LOG.exception(_LE("Unexpected exception: %s"), str(e))
raise e
sandbox_id = self.driver.get_sandbox_id(container)
if sandbox_id:
try:
self.driver.delete_sandbox(context, sandbox_id)
except Exception as e:
LOG.exception(_LE("Unexpected exception: %s"), str(e))
raise
return container
@translate_exception
def container_list(self, context):
LOG.debug('Listing container...', context=context)
@ -344,3 +372,16 @@ class Manager(object):
except Exception as e:
LOG.exception(_LE("Unexpected exception: %s"), str(e))
raise e
def _get_container_addresses(self, context, container):
LOG.debug('Showing container IP addresses...', context=context,
container=container)
try:
return self.driver.get_addresses(context, container)
except exception.DockerError as e:
LOG.error(_LE("Error occured while calling docker API: %s"),
six.text_type(e))
raise
except Exception as e:
LOG.exception(_LE("Unexpected exception: %s"), str(e))
raise e

View File

@ -28,7 +28,9 @@ Services which consume this:
Interdependencies to other options:
* None
""")
"""),
cfg.IntOpt('default_timeout', default=60 * 10,
help='Maximum time (in seconds) to wait for an event.'),
]

View File

@ -11,17 +11,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from docker import errors
import six
from docker import errors
from oslo_log import log as logging
from zun.common.i18n import _LW
from zun.common.utils import check_container_id
import zun.conf
from zun.container.docker import utils as docker_utils
from zun.container import driver
from zun.objects import fields
CONF = zun.conf.CONF
LOG = logging.getLogger(__name__)
@ -46,7 +49,7 @@ class DockerDriver(driver.ContainerDriver):
response = docker.images(repo, quiet)
return response
def create(self, container, image):
def create(self, container, sandbox_id, image):
with docker_utils.docker_client() as docker:
name = container.name
if image['path']:
@ -67,14 +70,18 @@ class DockerDriver(driver.ContainerDriver):
}
host_config = {}
host_config['publish_all_ports'] = True
host_config['network_mode'] = 'container:%s' % sandbox_id
# TODO(hongbin): Uncomment this after docker-py add support for
# container mode for pid namespace.
# host_config['pid_mode'] = 'container:%s' % sandbox_id
host_config['ipc_mode'] = 'container:%s' % sandbox_id
host_config['volumes_from'] = sandbox_id
if container.memory is not None:
host_config['mem_limit'] = container.memory
if container.cpu is not None:
host_config['cpu_quota'] = int(100000 * container.cpu)
host_config['cpu_period'] = 100000
kwargs['host_config'] = \
docker.create_host_config(**host_config)
kwargs['host_config'] = docker.create_host_config(**host_config)
response = docker.create_container(image, **kwargs)
container.container_id = response['Id']
@ -204,3 +211,45 @@ class DockerDriver(driver.ContainerDriver):
if six.PY2 and not isinstance(value, unicode):
value = unicode(value)
return value.encode('utf-8')
def create_sandbox(self, context, container, image='kubernetes/pause'):
with docker_utils.docker_client() as docker:
name = self.get_sandbox_name(container)
response = docker.create_container(image, name=name)
sandbox_id = response['Id']
docker.start(sandbox_id)
return sandbox_id
def delete_sandbox(self, context, sandbox_id):
with docker_utils.docker_client() as docker:
docker.remove_container(sandbox_id, force=True)
def get_sandbox_id(self, container):
if container.meta:
return container.meta.get('sandbox_id', None)
else:
LOG.warning(_LW("Unexpected missing of sandbox_id"))
return None
def set_sandbox_id(self, container, id):
if container.meta is None:
container.meta = {'sandbox_id': id}
else:
container.meta['sandbox_id'] = id
def get_sandbox_name(self, container):
return 'sandbox-' + container.uuid
def get_addresses(self, context, container):
sandbox_id = self.get_sandbox_id(container)
with docker_utils.docker_client() as docker:
response = docker.inspect_container(sandbox_id)
addr = response["NetworkSettings"]["IPAddress"]
addresses = {
'default': [
{
'addr': addr,
},
],
}
return addresses

View File

@ -58,7 +58,7 @@ def load_container_driver(container_driver=None):
class ContainerDriver(object):
'''Base class for container drivers.'''
def create(self, container):
def create(self, container, sandbox_name=None):
"""Create a container."""
raise NotImplementedError()
@ -105,3 +105,27 @@ class ContainerDriver(object):
def kill(self, container, signal):
"""kill signal to a container."""
raise NotImplementedError()
def create_sandbox(self, context, container, **kwargs):
"""Create a sandbox."""
raise NotImplementedError()
def delete_sandbox(self, context, id):
"""Delete a sandbox."""
raise NotImplementedError()
def get_sandbox_id(self, container):
"""Retrieve sandbox ID."""
raise NotImplementedError()
def set_sandbox_id(self, container, id):
"""Set sandbox ID."""
raise NotImplementedError()
def get_sandbox_name(self, container):
"""Retrieve sandbox name."""
raise NotImplementedError()
def get_addresses(self, context, container):
"""Retrieve IP addresses of the container."""
raise NotImplementedError()

View File

@ -0,0 +1,39 @@
# 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.
"""add meta addresses to container
Revision ID: 4a0c4f7a4a33
Revises: 43e1088c3389
Create Date: 2016-11-20 12:18:44.086036
"""
# revision identifiers, used by Alembic.
revision = '4a0c4f7a4a33'
down_revision = '43e1088c3389'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
from zun.db.sqlalchemy import models
def upgrade():
op.add_column('container',
sa.Column('meta', models.JSONEncodedDict(),
nullable=True))
op.add_column('container',
sa.Column('addresses', models.JSONEncodedDict(),
nullable=True))

View File

@ -142,6 +142,8 @@ class Container(Base):
ports = Column(JSONEncodedList)
hostname = Column(String(255))
labels = Column(JSONEncodedDict)
meta = Column(JSONEncodedDict)
addresses = Column(JSONEncodedDict)
image_pull_policy = Column(Text, nullable=True)

View File

@ -25,7 +25,9 @@ class Container(base.ZunPersistentObject, base.ZunObject,
# Version 1.2: Add memory column
# Version 1.3: Add task_state column
# Version 1.4: Add cpu, workdir, ports, hostname and labels columns
VERSION = '1.4'
# Version 1.5: Add meta column
# Version 1.6: Add addresses column
VERSION = '1.6'
fields = {
'id': fields.IntegerField(),
@ -46,7 +48,9 @@ class Container(base.ZunPersistentObject, base.ZunObject,
'ports': z_fields.ListOfIntegersField(nullable=True),
'hostname': fields.StringField(nullable=True),
'labels': fields.DictOfStringsField(nullable=True),
'image_pull_policy': fields.StringField(nullable=True)
'meta': fields.DictOfStringsField(nullable=True),
'addresses': z_fields.JsonField(nullable=True),
'image_pull_policy': fields.StringField(nullable=True),
}
@staticmethod

View File

@ -10,6 +10,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from oslo_serialization import jsonutils as json
from oslo_versionedobjects import fields
@ -31,9 +34,9 @@ class ContainerStatusField(fields.BaseEnumField):
class TaskState(fields.Enum):
ALL = (
IMAGE_PULLING, CONTAINER_CREATING,
IMAGE_PULLING, CONTAINER_CREATING, SANDBOX_CREATING,
) = (
'image_pulling', 'container_creating',
'image_pulling', 'container_creating', 'sandbox_creating',
)
def __init__(self):
@ -47,3 +50,21 @@ class TaskStateField(fields.BaseEnumField):
class ListOfIntegersField(fields.AutoTypedField):
AUTO_TYPE = fields.List(fields.Integer())
class Json(fields.FieldType):
def coerce(self, obj, attr, value):
if isinstance(value, six.string_types):
loaded = json.loads(value)
return loaded
return value
def from_primitive(self, obj, attr, value):
return self.coerce(obj, attr, value)
def to_primitive(self, obj, attr, value):
return json.dumps(value)
class JsonField(fields.AutoTypedField):
AUTO_TYPE = Json()

View File

@ -18,6 +18,7 @@ from tempest import manager
from zun.tests.tempest.api.models import container_model
from zun.tests.tempest.api.models import service_model
from zun.tests.tempest import utils
CONF = config.CONF
@ -105,3 +106,13 @@ class ZunClient(rest_client.RestClient):
resp, body = self.get(self.services_uri(filters), **kwargs)
return self.deserialize(resp, body,
service_model.ServiceCollection)
def ensure_container_created(self, container_id):
def container_created():
_, container = self.get_container(container_id)
if container.status == 'Creating':
return False
else:
return True
utils.wait_for_condition(container_created)

View File

@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import time
from tempest.lib import decorators
from zun.tests.tempest.api import clients
@ -43,6 +41,14 @@ class TestContainer(base.BaseZunTest):
super(TestContainer, cls).resource_setup()
def tearDown(self):
_, model = self.container_client.list_containers()
for c in model.containers:
self.container_client.ensure_container_created(c['uuid'])
self.container_client.delete_container(c['uuid'])
super(TestContainer, self).tearDown()
def _create_container(self, **kwargs):
model = datagen.container_data(**kwargs)
@ -52,26 +58,6 @@ class TestContainer(base.BaseZunTest):
self.container_client.delete_container(container_id, **kwargs)
def _wait_on_creation(self, container_id, timeout=60):
def _check_status():
resp, model = self.container_client.get_container(container_id)
status = model.status
if status == 'Creating':
return False
else:
return True
time.sleep(1)
start_time = time.time()
end_time = time.time() + timeout
while time.time() < end_time:
result = _check_status()
if result:
return result
time.sleep(1)
raise Exception(("Timed out after %s seconds. Started " +
"on %s and ended on %s") % (timeout, start_time,
end_time))
@decorators.idempotent_id('a04f61f2-15ae-4200-83b7-1f311b101f35')
def test_container_create_list_delete(self):
@ -82,8 +68,7 @@ class TestContainer(base.BaseZunTest):
self.assertEqual(200, resp.status)
self.assertGreater(len(model.containers), 0)
# NOTE(mkrai): Check and wait for container creation to get over
self._wait_on_creation(container.uuid)
self.container_client.ensure_container_created(container.uuid)
self._delete_container(container.uuid, headers={'force': True})
resp, model = self.container_client.list_containers()

View File

@ -0,0 +1,25 @@
# 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 time
def wait_for_condition(condition, interval=1, timeout=60):
start_time = time.time()
end_time = time.time() + timeout
while time.time() < end_time:
result = condition()
if result:
return result
time.sleep(interval)
raise Exception(("Timed out after %s seconds. Started on %s and ended "
"on %s") % (timeout, start_time, end_time))

View File

@ -62,56 +62,67 @@ class TestManager(base.TestCase):
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@mock.patch.object(fake_driver, 'create')
def test_container_create(self, mock_create, mock_pull, mock_save):
@mock.patch.object(fake_driver, 'create_sandbox')
def test_container_create(self, mock_create_sandbox, mock_create,
mock_pull, mock_save):
container = Container(self.context, **utils.get_test_container())
mock_pull.return_value = 'fake_path'
mock_create_sandbox.return_value = 'fake_id'
self.compute_manager._do_container_create(self.context, container)
mock_save.assert_called_with()
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_create.assert_called_once_with(container, 'fake_path')
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always')
mock_create.assert_called_once_with(container, 'fake_id', 'fake_path')
@mock.patch.object(Container, 'save')
@mock.patch.object(fake_driver, 'create_sandbox')
@mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container')
def test_container_create_pull_image_failed_docker_error(
self, mock_fail, mock_pull, mock_save):
self, mock_fail, mock_pull, mock_create_sandbox, mock_save):
container = Container(self.context, **utils.get_test_container())
mock_pull.side_effect = exception.DockerError("Pull Failed")
mock_create_sandbox.return_value = mock.MagicMock()
self.compute_manager._do_container_create(self.context, container)
mock_fail.assert_called_once_with(container, "Pull Failed")
@mock.patch.object(Container, 'save')
@mock.patch.object(fake_driver, 'create_sandbox')
@mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container')
def test_container_create_pull_image_failed_image_not_found(
self, mock_fail, mock_pull, mock_save):
self, mock_fail, mock_pull, mock_create_sandbox, mock_save):
container = Container(self.context, **utils.get_test_container())
mock_pull.side_effect = exception.ImageNotFound("Image Not Found")
mock_create_sandbox.return_value = mock.MagicMock()
self.compute_manager._do_container_create(self.context, container)
mock_fail.assert_called_once_with(container, "Image Not Found")
@mock.patch.object(Container, 'save')
@mock.patch.object(fake_driver, 'create_sandbox')
@mock.patch('zun.image.driver.pull_image')
@mock.patch.object(manager.Manager, '_fail_container')
def test_container_create_pull_image_failed_zun_exception(
self, mock_fail, mock_pull, mock_save):
self, mock_fail, mock_pull, mock_create_sandbox, mock_save):
container = Container(self.context, **utils.get_test_container())
mock_pull.side_effect = exception.ZunException(
message="Image Not Found")
mock_create_sandbox.return_value = mock.MagicMock()
self.compute_manager._do_container_create(self.context, container)
mock_fail.assert_called_once_with(container, "Image Not Found")
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@mock.patch.object(fake_driver, 'create')
@mock.patch.object(fake_driver, 'create_sandbox')
@mock.patch.object(manager.Manager, '_fail_container')
def test_container_create_docker_create_failed(self, mock_fail,
mock_create_sandbox,
mock_create, mock_pull,
mock_save):
container = Container(self.context, **utils.get_test_container())
mock_create.side_effect = exception.DockerError("Creation Failed")
mock_create_sandbox.return_value = mock.MagicMock()
self.compute_manager._do_container_create(self.context, container)
mock_fail.assert_called_once_with(container, "Creation Failed")
@ -127,10 +138,9 @@ class TestManager(base.TestCase):
container.status = 'Stopped'
self.compute_manager.container_run(self.context, container)
mock_save.assert_called_with()
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_create.assert_called_once_with(container, 'fake_path')
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always')
mock_create.assert_called_once_with(container, None, 'fake_path')
mock_start.assert_called_once_with(container)
@mock.patch.object(Container, 'save')
@ -147,9 +157,8 @@ class TestManager(base.TestCase):
container)
mock_save.assert_called_with()
mock_fail.assert_called_with(container, 'Image Not Found')
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_pull.assert_called_once_with(self.context, 'kubernetes/pause',
'latest', 'ifnotpresent')
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@ -165,9 +174,8 @@ class TestManager(base.TestCase):
container)
mock_save.assert_called_with()
mock_fail.assert_called_with(container, 'Image Not Found')
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_pull.assert_called_once_with(self.context, 'kubernetes/pause',
'latest', 'ifnotpresent')
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@ -183,9 +191,8 @@ class TestManager(base.TestCase):
container)
mock_save.assert_called_with()
mock_fail.assert_called_with(container, 'Docker Error occurred')
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_pull.assert_called_once_with(self.context, 'kubernetes/pause',
'latest', 'ifnotpresent')
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@ -204,10 +211,9 @@ class TestManager(base.TestCase):
container)
mock_save.assert_called_with()
mock_fail.assert_called_with(container, 'Docker Error occurred')
mock_pull.assert_called_once_with(self.context,
container.image,
'latest', 'always')
mock_create.assert_called_once_with(container,
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always')
mock_create.assert_called_once_with(container, None,
{'name': 'nginx', 'path': None})
@mock.patch.object(manager.Manager, '_validate_container_state')

View File

@ -68,3 +68,18 @@ class FakeDriver(driver.ContainerDriver):
@check_container_id
def kill(self, container, signal=None):
pass
def create_sandbox(self, context, name, **kwargs):
pass
def delete_sandbox(self, context, id):
pass
def get_sandbox_id(self, container):
pass
def set_sandbox_id(self, container, id):
pass
def get_addresses(self, context, container):
pass

View File

@ -44,6 +44,17 @@ def get_test_container(**kw):
'ports': kw.get('ports', [80, 443]),
'hostname': kw.get('hostname', 'testhost'),
'labels': kw.get('labels', {'key1': 'val1', 'key2': 'val2'}),
'meta': kw.get('meta', {'key1': 'val1', 'key2': 'val2'}),
'addresses': kw.get('addresses', {
'private': [
{
'OS-EXT-IPS-MAC:mac_addr': 'fa:16:3e:04:da:76',
'version': 4,
'addr': '10.0.0.12',
'OS-EXT-IPS:type': 'fixed'
},
],
}),
'image_pull_policy': kw.get('image_pull_policy', 'always'),
}