From 2c6ad5a119b97bc127d7ad3945adfd805c3ce4ba Mon Sep 17 00:00:00 2001 From: Pradeep Kumar Singh Date: Thu, 17 Nov 2016 11:16:48 +0000 Subject: [PATCH] Implements image_pull_policy This patch implements image_pull_policy, which determines if the image should be pulled prior to starting the container. It can have following values: ifnotpresent: only pull the image if it does not already exist on the node. always: Always pull the image from repositery. never: never pull the image If image_pull_policy is not specified then: 1. If the tag is latest, defaults image_pull_policy is always. 2. Otherwise default is ifnotpresent Partial-Implements: blueprint image-pull-policy Change-Id: If2715d7724fd5336fb25d380f125e1485ca302d0 --- zun/api/controllers/types.py | 17 +++++ zun/api/controllers/v1/containers.py | 9 ++- zun/common/utils.py | 18 +++++ zun/compute/manager.py | 5 +- ...e1088c3389_add_image_pull_policy_column.py | 34 +++++++++ zun/db/sqlalchemy/models.py | 1 + zun/image/docker/driver.py | 25 ++++++- zun/image/driver.py | 30 +------- zun/image/glance/driver.py | 30 +++++++- zun/objects/container.py | 1 + zun/tests/unit/api/controllers/test_types.py | 23 ++++++ .../api/controllers/v1/test_containers.py | 5 +- zun/tests/unit/common/test_utils.py | 19 +++++ .../unit/compute/test_compute_manager.py | 2 +- zun/tests/unit/db/utils.py | 1 + zun/tests/unit/image/docker/test_driver.py | 70 ++++++++++++++++--- zun/tests/unit/image/glance/test_driver.py | 61 +++++++++++++--- 17 files changed, 296 insertions(+), 55 deletions(-) create mode 100644 zun/db/sqlalchemy/alembic/versions/43e1088c3389_add_image_pull_policy_column.py diff --git a/zun/api/controllers/types.py b/zun/api/controllers/types.py index f4dc398ef..08ad5d46b 100644 --- a/zun/api/controllers/types.py +++ b/zun/api/controllers/types.py @@ -311,3 +311,20 @@ class ImageSize(object): in both cases""") raise exception.InvalidValue(message=message, value=value, type=cls.type_name) + + +class EnumType(object): + type_name = 'EnumType' + + @classmethod + def validate(cls, value, name=None, values=None): + if value is None: + return None + if value.lower() not in set(values): + message = _( + "%(name)s should be one of: %(values)s") % { + 'name': name, + 'values': ', '.join(map(six.text_type, values))} + raise exception.InvalidValue(message) + else: + return value.lower() diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index c33ee6122..cfe9fa297 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -120,6 +120,13 @@ class Container(base.APIBase): 'labels': { 'validate': types.Dict(types.String, types.String).validate, }, + 'image_pull_policy': { + 'validate': types.EnumType.validate, + 'validate_args': { + 'name': 'image_pull_policy', + 'values': ['never', 'always', 'ifnotpresent'] + } + } } def __init__(self, **kwargs): @@ -131,7 +138,7 @@ class Container(base.APIBase): container.unset_fields_except([ 'uuid', 'name', 'image', 'command', 'status', 'cpu', 'memory', 'environment', 'task_state', 'workdir', 'ports', 'hostname', - 'labels']) + 'labels', 'image_pull_policy', 'status_reason']) container.links = [link.Link.make_link( 'self', url, diff --git a/zun/common/utils.py b/zun/common/utils.py index 219f27f43..d41a4e1ed 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -141,3 +141,21 @@ def check_container_id(function): return function(*args, **kwargs) return decorated_function + + +def get_image_pull_policy(image_pull_policy, image_tag): + if not image_pull_policy: + if image_tag == 'latest': + image_pull_policy = 'always' + else: + image_pull_policy = 'ifnotpresent' + return image_pull_policy + + +def should_pull_image(image_pull_policy, present): + if image_pull_policy == 'never': + return False + if image_pull_policy == 'always' or \ + (image_pull_policy == 'ifnotpresent' and not present): + return True + return False diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 2950548bc..2b6cf9961 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -69,8 +69,11 @@ class Manager(object): container.task_state = fields.TaskState.IMAGE_PULLING container.save() repo, tag = utils.parse_image_name(container.image) + image_pull_policy = utils.get_image_pull_policy( + container.image_pull_policy, tag) try: - image = image_driver.pull_image(context, repo, tag) + image = image_driver.pull_image(context, repo, + tag, image_pull_policy) except exception.ImageNotFound as e: LOG.error(six.text_type(e)) self._fail_container(container, six.text_type(e)) diff --git a/zun/db/sqlalchemy/alembic/versions/43e1088c3389_add_image_pull_policy_column.py b/zun/db/sqlalchemy/alembic/versions/43e1088c3389_add_image_pull_policy_column.py new file mode 100644 index 000000000..03d3c24c2 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/43e1088c3389_add_image_pull_policy_column.py @@ -0,0 +1,34 @@ +# 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 image_pull_policy column + +Revision ID: 43e1088c3389 +Revises: c5565cbaa3de +Create Date: 2016-11-17 09:26:22.756296 + +""" + +# revision identifiers, used by Alembic. +revision = '43e1088c3389' +down_revision = 'c5565cbaa3de' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('container', + sa.Column('image_pull_policy', sa.Text(), + nullable=True)) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index 13007a6bb..7794e4e3c 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -142,6 +142,7 @@ class Container(Base): ports = Column(JSONEncodedList) hostname = Column(String(255)) labels = Column(JSONEncodedDict) + image_pull_policy = Column(Text, nullable=True) class Image(Base): diff --git a/zun/image/docker/driver.py b/zun/image/docker/driver.py index 5886f99a7..c3dc03c01 100644 --- a/zun/image/docker/driver.py +++ b/zun/image/docker/driver.py @@ -21,6 +21,7 @@ from oslo_utils import excutils from zun.common import exception from zun.common.i18n import _ +from zun.common import utils from zun.container.docker import utils as docker_utils from zun.image import driver @@ -32,6 +33,18 @@ class DockerDriver(driver.ContainerImageDriver): def __init__(self): super(DockerDriver, self).__init__() + def _search_image_on_host(self, repo, tag): + with docker_utils.docker_client() as docker: + image = repo + ":" + tag + LOG.debug('Inspecting image locally %s' % image) + try: + image_dict = docker.inspect_image(image) + if image_dict: + return {'image': repo, 'path': None} + except errors.NotFound: + LOG.debug('Image %s not found locally' % image) + return None + def _pull_image(self, repo, tag): with docker_utils.docker_client() as docker: for line in docker.pull(repo, tag=tag, stream=True): @@ -42,7 +55,17 @@ class DockerDriver(driver.ContainerImageDriver): else: raise exception.DockerError(error['message']) - def pull_image(self, context, repo, tag): + def pull_image(self, context, repo, tag, image_pull_policy): + image = self._search_image_on_host(repo, tag) + if not utils.should_pull_image(image_pull_policy, bool(image)): + if image: + LOG.debug('Image %s present locally' % repo) + return image + else: + message = _('Image %s not present with pull policy of Never' + ) % repo + raise exception.ImageNotFound(message) + try: LOG.debug('Pulling image from docker %s,' ' context %s' % (repo, context)) diff --git a/zun/image/driver.py b/zun/image/driver.py index d3d14cee3..0b8363760 100644 --- a/zun/image/driver.py +++ b/zun/image/driver.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os import sys from oslo_log import log as logging @@ -23,7 +22,6 @@ from zun.common.i18n import _ from zun.common.i18n import _LE from zun.common.i18n import _LI import zun.conf -from zun.image.glance import utils CONF = zun.conf.CONF LOG = logging.getLogger(__name__) @@ -57,35 +55,13 @@ def load_image_driver(image_driver=None): sys.exit(1) -def search_image_on_host(context, repo): - LOG.debug('Searching for image %s locally' % repo) - CONF.import_opt('images_directory', 'zun.image.glance.driver', - group='glance') - images_directory = CONF.glance.images_directory - try: - # TODO(mkrai): Change this to search image entry in zun db - # after the image endpoint is merged. - image_meta = utils.find_image(context, repo) - except exception.ImageNotFound: - return None - if image_meta: - out_path = os.path.join(images_directory, image_meta.id + '.tar') - if os.path.isfile(out_path): - return {'image': repo, 'path': out_path} - else: - return None - - -def pull_image(context, repo, tag): - image = search_image_on_host(context, repo) - if image: - LOG.debug('Found image %s locally.' % repo) - return image +def pull_image(context, repo, tag, image_pull_policy): image_driver_list = CONF.image_driver_list for driver in image_driver_list: try: image_driver = load_image_driver(driver) - image = image_driver.pull_image(context, repo, tag) + image = image_driver.pull_image(context, repo, + tag, image_pull_policy) if image: break except exception.ImageNotFound: diff --git a/zun/image/glance/driver.py b/zun/image/glance/driver.py index cb1bdb2fb..beaa03de6 100644 --- a/zun/image/glance/driver.py +++ b/zun/image/glance/driver.py @@ -20,6 +20,7 @@ from oslo_utils import fileutils from zun.common import exception from zun.common.i18n import _ +from zun.common import utils as common_utils import zun.conf from zun.image import driver from zun.image.glance import utils @@ -34,9 +35,36 @@ class GlanceDriver(driver.ContainerImageDriver): def __init__(self): super(GlanceDriver, self).__init__() - def pull_image(self, context, repo, tag): + def _search_image_on_host(self, context, repo): + LOG.debug('Searching for image %s locally' % repo) + images_directory = CONF.glance.images_directory + try: + # TODO(mkrai): Change this to search image entry in zun db + # after the image endpoint is merged. + image_meta = utils.find_image(context, repo) + except exception.ImageNotFound: + return None + if image_meta: + out_path = os.path.join(images_directory, + image_meta.id + '.tar') + if os.path.isfile(out_path): + return {'image': repo, 'path': out_path} + else: + return None + + def pull_image(self, context, repo, tag, image_pull_policy): # TODO(shubhams): glance driver does not handle tags # once metadata is stored in db then handle tags + image = self._search_image_on_host(context, repo) + if not common_utils.should_pull_image(image_pull_policy, bool(image)): + if image: + LOG.debug('Image %s present locally' % repo) + return image + else: + message = _('Image %s not present with pull policy of Never' + ) % repo + raise exception.ImageNotFound(message) + LOG.debug('Pulling image from glance %s' % repo) try: glance = utils.create_glanceclient(context) diff --git a/zun/objects/container.py b/zun/objects/container.py index e99f91c25..63e200833 100644 --- a/zun/objects/container.py +++ b/zun/objects/container.py @@ -46,6 +46,7 @@ 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) } @staticmethod diff --git a/zun/tests/unit/api/controllers/test_types.py b/zun/tests/unit/api/controllers/test_types.py index 5b06a139e..50d1c6fbb 100644 --- a/zun/tests/unit/api/controllers/test_types.py +++ b/zun/tests/unit/api/controllers/test_types.py @@ -243,3 +243,26 @@ class TestTypes(test_base.BaseTestCase): expected_value = 4096 value = types.ImageSize.validate(test_value) self.assertEqual(value, expected_value) + + def test_enum_type(self): + test_value = 'always' + self.assertEqual(test_value, types.EnumType.validate( + test_value, name='image_pull_policy', + values=['always', 'never', 'ifnotpresent'])) + + test_value = 'ALWAYS' + self.assertEqual('always', types.EnumType.validate( + test_value, name='image_pull_policy', + values=['always', 'never', 'ifnotpresent'])) + + test_value = None + self.assertEqual(None, types.EnumType.validate( + test_value, name='image_pull_policy', + values=['always', 'never', 'ifnotpresent'])) + + test_value = 'xyz' + self.assertRaises(exception.InvalidValue, + types.EnumType.validate, + test_value, + name='image_pull_policy', + values=['always', 'never', 'ifnotpresent']) diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 13fdab42c..8c8e3bc47 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -226,8 +226,8 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') - def test_get_all_has_status_reason(self, mock_container_list, - mock_container_show): + def test_get_all_has_status_reason_and_image_pull_policy( + self, mock_container_list, mock_container_show): test_container = utils.get_test_container() containers = [objects.Container(self.context, **test_container)] mock_container_list.return_value = containers @@ -240,6 +240,7 @@ class TestContainerController(api_base.FunctionalTest): self.assertEqual(test_container['uuid'], actual_containers[0].get('uuid')) self.assertIn('status_reason', actual_containers[0].keys()) + self.assertIn('image_pull_policy', actual_containers[0].keys()) @patch('zun.compute.api.API.container_show') @patch('zun.objects.Container.list') diff --git a/zun/tests/unit/common/test_utils.py b/zun/tests/unit/common/test_utils.py index 7cdd08456..a2454b536 100644 --- a/zun/tests/unit/common/test_utils.py +++ b/zun/tests/unit/common/test_utils.py @@ -54,3 +54,22 @@ class TestUtils(base.BaseTestCase): utils.parse_image_name('test:test')) self.assertEqual(('test-test', 'test'), utils.parse_image_name('test-test:test')) + + def test_get_image_pull_policy(self): + self.assertEqual('always', + utils.get_image_pull_policy('always', + 'latest')) + self.assertEqual('always', + utils.get_image_pull_policy(None, + 'latest')) + self.assertEqual('ifnotpresent', + utils.get_image_pull_policy(None, + '2.0')) + + def test_should_pull_image(self): + self.assertFalse(utils.should_pull_image('never', True)) + self.assertFalse(utils.should_pull_image('never', False)) + self.assertTrue(utils.should_pull_image('always', True)) + self.assertTrue(utils.should_pull_image('always', False)) + self.assertTrue(utils.should_pull_image('ifnotpresent', False)) + self.assertFalse(utils.should_pull_image('ifnotpresent', True)) diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index 59636d83d..ac744ce1b 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -69,7 +69,7 @@ class TestManager(base.TestCase): mock_save.assert_called_with() mock_pull.assert_called_once_with(self.context, container.image, - 'latest') + 'latest', 'always') mock_create.assert_called_once_with(container, 'fake_path') @mock.patch.object(Container, 'save') diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index 2dec79a7e..6bd92c20e 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -41,6 +41,7 @@ def get_test_container(**kw): 'ports': kw.get('ports', [80, 443]), 'hostname': kw.get('hostname', 'testhost'), 'labels': kw.get('labels', {'key1': 'val1', 'key2': 'val2'}), + 'image_pull_policy': kw.get('image_pull_policy', 'always'), } diff --git a/zun/tests/unit/image/docker/test_driver.py b/zun/tests/unit/image/docker/test_driver.py index 6e2fbf959..787cd7670 100644 --- a/zun/tests/unit/image/docker/test_driver.py +++ b/zun/tests/unit/image/docker/test_driver.py @@ -34,8 +34,34 @@ class TestDriver(base.BaseTestCase): self.dfc_context_manager.__enter__.return_value = self.mock_docker self.addCleanup(dfc_patcher.stop) - def test_pull_image_success(self): - ret = self.driver.pull_image(None, 'test_image', 'latest') + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_should_pull_no_image_not_present_locally( + self, mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = False + mock_search.return_value = None + self.assertRaises(exception.ImageNotFound, self.driver.pull_image, + None, 'nonexisting', 'tag', 'never') + + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_should_pull_no_image_present_locally( + self, mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = False + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} + self.assertEqual({'image': 'nginx', 'path': 'xyz'}, + self.driver.pull_image(None, 'nonexisting', + 'tag', 'never')) + + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_success(self, mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} + ret = self.driver.pull_image(None, 'test_image', 'latest', 'always') self.assertEqual({'image': 'test_image', 'path': None}, ret) self.mock_docker.pull.assert_called_once_with( 'test_image', @@ -43,14 +69,20 @@ class TestDriver(base.BaseTestCase): stream=True) @mock.patch('zun.common.utils.parse_image_name') - def test_pull_image_raises_API_error(self, mock_parse_image): + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_raises_API_error(self, mock_should_pull_image, + mock_search, mock_parse_image): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_parse_image.return_value = ('repo', 'tag') with mock.patch.object(errors.APIError, '__str__', return_value='404 Not Found') as mock_init: self.mock_docker.pull = mock.Mock( side_effect=errors.APIError('Error', '', '')) self.assertRaises(exception.ZunException, self.driver.pull_image, - None, 'repo', 'tag') + None, 'repo', 'tag', 'always') self.mock_docker.pull.assert_called_once_with( 'repo', tag='tag', @@ -58,7 +90,13 @@ class TestDriver(base.BaseTestCase): self.assertEqual(1, mock_init.call_count) @mock.patch('zun.common.utils.parse_image_name') - def test_pull_image_not_found(self, mock_parse_image): + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_not_found(self, mock_should_pull_image, + mock_search, mock_parse_image): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_parse_image.return_value = ('repo', 'tag') pull_return_value = '{"errorDetail":{"message":'\ '"Error: image library/repo not found"},'\ @@ -67,7 +105,7 @@ class TestDriver(base.BaseTestCase): with mock.patch.object(self.mock_docker, 'pull', return_value=[pull_return_value]) as mock_init: self.assertRaises(exception.ImageNotFound, self.driver.pull_image, - None, 'repo', 'tag') + None, 'repo', 'tag', 'always') self.mock_docker.pull.assert_called_once_with( 'repo', tag='tag', @@ -75,7 +113,13 @@ class TestDriver(base.BaseTestCase): self.assertEqual(1, mock_init.call_count) @mock.patch('zun.common.utils.parse_image_name') - def test_pull_image_raises_docker_error(self, mock_parse_image): + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_raises_docker_error(self, mock_should_pull_image, + mock_search, mock_parse_image): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_parse_image.return_value = ('repo', 'tag') pull_return_value = '{"errorDetail":{"message":'\ '"Error: image library/repo not"},'\ @@ -84,7 +128,7 @@ class TestDriver(base.BaseTestCase): with mock.patch.object(self.mock_docker, 'pull', return_value=[pull_return_value]) as mock_init: self.assertRaises(exception.DockerError, self.driver.pull_image, - None, 'repo', 'tag') + None, 'repo', 'tag', 'always') self.mock_docker.pull.assert_called_once_with( 'repo', tag='tag', @@ -92,14 +136,20 @@ class TestDriver(base.BaseTestCase): self.assertEqual(1, mock_init.call_count) @mock.patch('zun.common.utils.parse_image_name') - def test_pull_image_exception(self, mock_parse_image): + @mock.patch.object(driver.DockerDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_exception(self, mock_should_pull_image, + mock_search, mock_parse_image): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_parse_image.return_value = ('repo', 'tag') with mock.patch.object(errors.APIError, '__str__', return_value='hit error') as mock_init: self.mock_docker.pull = mock.Mock( side_effect=errors.APIError('Error', '', '')) self.assertRaises(exception.ZunException, self.driver.pull_image, - None, 'repo', 'tag') + None, 'repo', 'tag', 'always') self.mock_docker.pull.assert_called_once_with( 'repo', tag='tag', diff --git a/zun/tests/unit/image/glance/test_driver.py b/zun/tests/unit/image/glance/test_driver.py index caeccc8f5..3592fdbf2 100644 --- a/zun/tests/unit/image/glance/test_driver.py +++ b/zun/tests/unit/image/glance/test_driver.py @@ -36,28 +36,67 @@ class TestDriver(base.BaseTestCase): super(TestDriver, self).tearDown() shutil.rmtree(self.test_dir) + @mock.patch.object(driver.GlanceDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_should_pull_no_image_not_present_locally( + self, mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = False + mock_search.return_value = None + self.assertRaises(exception.ImageNotFound, self.driver.pull_image, + None, 'nonexisting', 'tag', 'never') + + @mock.patch.object(driver.GlanceDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_should_pull_no_image_present_locally( + self, mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = False + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} + self.assertEqual({'image': 'nginx', 'path': 'xyz'}, + self.driver.pull_image(None, 'nonexisting', + 'tag', 'never')) + @mock.patch('zun.image.glance.utils.create_glanceclient') - def test_pull_image_failure(self, mock_glance): + @mock.patch.object(driver.GlanceDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_failure(self, mock_should_pull_image, + mock_search, mock_glance): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_glance.side_effect = Exception self.assertRaises(exception.ZunException, self.driver.pull_image, - None, 'nonexisting', 'tag') - - @mock.patch('zun.image.glance.utils.create_glanceclient') - def test_pull_image_not_found(self, mock_glance): - with mock.patch('zun.image.glance.utils.find_image') as mock_find: - mock_find.side_effect = exception.ImageNotFound - self.assertRaises(exception.ImageNotFound, self.driver.pull_image, - None, 'nonexisting', 'tag') + None, 'nonexisting', 'tag', 'always') + @mock.patch.object(driver.GlanceDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') @mock.patch('zun.image.glance.utils.create_glanceclient') @mock.patch('zun.image.glance.utils.find_image') - def test_pull_image_found(self, mock_find_image, mock_glance): + def test_pull_image(self, mock_find_image, mock_glance, + mock_should_pull_image, mock_search): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} mock_glance.images.data = mock.MagicMock(return_value='content') image_meta = mock.MagicMock() image_meta.id = '1234' mock_find_image.return_value = image_meta CONF.set_override('images_directory', self.test_dir, group='glance') out_path = os.path.join(self.test_dir, '1234' + '.tar') - ret = self.driver.pull_image(None, 'image', 'latest') + ret = self.driver.pull_image(None, 'image', 'latest', 'always') self.assertEqual({'image': 'image', 'path': out_path}, ret) self.assertTrue(os.path.isfile(ret['path'])) + + @mock.patch('zun.image.glance.utils.create_glanceclient') + @mock.patch.object(driver.GlanceDriver, + '_search_image_on_host') + @mock.patch('zun.common.utils.should_pull_image') + def test_pull_image_not_found(self, mock_should_pull_image, + mock_search, mock_glance): + mock_should_pull_image.return_value = True + mock_search.return_value = {'image': 'nginx', 'path': 'xyz'} + with mock.patch('zun.image.glance.utils.find_image') as mock_find: + mock_find.side_effect = exception.ImageNotFound + self.assertRaises(exception.ImageNotFound, self.driver.pull_image, + None, 'nonexisting', 'tag', 'always')