# Copyright 2015 Rackspace  All rights reserved.
#
# 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 docker
from docker import errors
import mock
import six

from magnum.common import docker_utils
from magnum.common import exception
from magnum.conductor.handlers import docker_conductor
from magnum import objects
from magnum.objects import fields
from magnum.tests import base


class TestDockerHandler(base.BaseTestCase):
    def setUp(self):
        super(TestDockerHandler, self).setUp()
        self.conductor = docker_conductor.Handler()
        dfc_patcher = mock.patch.object(docker_utils,
                                        'docker_for_container')
        docker_for_container = dfc_patcher.start()
        self.dfc_context_manager = docker_for_container.return_value
        self.mock_docker = mock.MagicMock()
        self.dfc_context_manager.__enter__.return_value = self.mock_docker
        self.addCleanup(dfc_patcher.stop)

    @mock.patch.object(docker_utils, 'is_docker_api_version_atleast')
    def _test_container_create(self, container_dict, expected_kwargs,
                               mock_version, expected_image='test_image',
                               expected_tag='some_tag',
                               api_version='1.18'):

        mock_version.return_value = (float(api_version) > 1.18)

        name = container_dict.pop('name')
        mock_container = mock.MagicMock(**container_dict)
        type(mock_container).name = mock.PropertyMock(return_value=name)

        container = self.conductor.container_create(
            None, mock_container)

        utf8_image = self.conductor._encode_utf8(mock_container.image)
        self.mock_docker.inspect_image.assert_called_once_with(utf8_image)
        self.mock_docker.pull.assert_called_once_with(expected_image,
                                                      tag=expected_tag)
        self.mock_docker.create_container.assert_called_once_with(
            mock_container.image, **expected_kwargs)
        self.assertEqual(fields.ContainerStatus.STOPPED, container.status)

    def test_container_create(self):
        container_dict = {
            'name': 'some-name',
            'uuid': 'some-uuid',
            'image': 'test_image:some_tag',
            'command': None,
            'memory': None,
            'environment': None,
        }
        expected_kwargs = {
            'name': 'some-name',
            'hostname': 'some-uuid',
            'command': None,
            'mem_limit': None,
            'environment': None,
        }
        self._test_container_create(container_dict, expected_kwargs)

    def test_container_create_api_1_19(self):
        container_dict = {
            'name': 'some-name',
            'uuid': 'some-uuid',
            'image': 'test_image:some_tag',
            'command': None,
            'memory': 512,
            'environment': None,
        }
        expected_kwargs = {
            'name': 'some-name',
            'hostname': 'some-uuid',
            'command': None,
            'host_config': {'mem_limit': 512},
            'environment': None,
        }
        self._test_container_create(container_dict, expected_kwargs,
                                    api_version='1.19')

    def test_container_create_with_command(self):
        container_dict = {
            'name': 'some-name',
            'uuid': 'some-uuid',
            'image': 'test_image:some_tag',
            'command': 'env',
            'memory': None,
            'environment': None,
        }
        expected_kwargs = {
            'name': 'some-name',
            'hostname': 'some-uuid',
            'command': 'env',
            'mem_limit': None,
            'environment': None,
        }
        self._test_container_create(container_dict, expected_kwargs)

    def test_container_create_with_memory(self):
        container_dict = {
            'name': 'some-name',
            'uuid': 'some-uuid',
            'image': 'test_image:some_tag',
            'command': None,
            'memory': '512m',
            'environment': None,
        }
        expected_kwargs = {
            'name': 'some-name',
            'hostname': 'some-uuid',
            'command': None,
            'mem_limit': '512m',
            'environment': None,
        }
        self._test_container_create(container_dict, expected_kwargs)

    def test_container_create_with_environment(self):
        container_dict = {
            'name': 'some-name',
            'uuid': 'some-uuid',
            'image': 'test_image:some_tag',
            'command': None,
            'memory': '512m',
            'environment': {'key1': 'val1', 'key2': 'val2'},
        }
        expected_kwargs = {
            'name': 'some-name',
            'hostname': 'some-uuid',
            'command': None,
            'mem_limit': '512m',
            'environment': {'key1': 'val1', 'key2': 'val2'},
        }
        self._test_container_create(container_dict, expected_kwargs)

    def test_encode_utf8_unicode(self):
        image = 'some_image:some_tag'
        unicode_image = six.u(image)
        utf8_image = self.conductor._encode_utf8(unicode_image)
        self.assertEqual(unicode_image.encode('utf-8'), utf8_image)

    @mock.patch.object(errors.APIError, '__str__')
    def test_container_create_with_failure(self, mock_init):
        mock_container = mock.MagicMock()
        mock_container.image = 'test_image:some_tag'
        mock_init.return_value = 'hit error'
        self.mock_docker.pull = mock.Mock(
            side_effect=errors.APIError('Error', '', ''))

        self.assertRaises(exception.ContainerException,
                          self.conductor.container_create,
                          None, mock_container)
        self.mock_docker.pull.assert_called_once_with(
            'test_image',
            tag='some_tag')
        self.assertFalse(self.mock_docker.create_container.called)
        mock_init.assert_called_with()
        self.assertEqual(fields.ContainerStatus.ERROR,
                         mock_container.status)

    def test_find_container_by_name_not_found(self):
        mock_docker = mock.MagicMock()
        fake_response = mock.MagicMock()
        fake_response.content = 'not_found'
        fake_response.status_code = 404
        mock_docker.list_instances.side_effect = errors.APIError(
            'not_found', fake_response)
        ret = self.conductor._find_container_by_name(mock_docker, '1')
        self.assertEqual({}, ret)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_delete(self, mock_find_container):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        self.conductor.container_delete(None, mock_container_uuid)
        self.mock_docker.remove_container.assert_called_once_with(
            mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_delete_with_container_not_exist(self,
                                                       mock_find_container):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = {}
        mock_find_container.return_value = mock_docker_id
        res = self.conductor.container_delete(None, mock_container_uuid)
        self.assertIsNone(res)
        self.assertFalse(self.mock_docker.remove_container.called)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)

    @mock.patch.object(errors.APIError, '__str__')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_delete_with_failure(self, mock_find_container,
                                           mock_init):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        mock_init.return_value = 'hit error'
        self.mock_docker.remove_container = mock.Mock(
            side_effect=errors.APIError('Error', '', ''))
        self.assertRaises(exception.ContainerException,
                          self.conductor.container_delete,
                          None, mock_container_uuid)
        self.mock_docker.remove_container.assert_called_once_with(
            mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)
        mock_init.assert_called_with()

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_action(self, mock_find_container, mock_get_by_uuid):
        mock_container = mock.MagicMock()
        mock_get_by_uuid.return_value = mock_container
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        self.conductor._container_action(None, mock_container_uuid,
                                         'fake-status', 'fake-func')
        self.assertEqual('fake-status', mock_container.status)

    def _test_container(self, action, docker_func_name, expected_status,
                        mock_find_container, mock_get_by_uuid):
        mock_container = mock.MagicMock()
        mock_get_by_uuid.return_value = mock_container
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        action_func = getattr(self.conductor, action)
        action_func(None, mock_container_uuid)
        docker_func = getattr(self.mock_docker, docker_func_name)
        docker_func.assert_called_once_with(mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)
        self.assertEqual(expected_status, mock_container.status)

    @mock.patch.object(errors.APIError, '__str__')
    def _test_container_with_failure(
            self, action, docker_func_name, mock_find_container, mock_init):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        mock_init.return_value = 'hit error'
        setattr(self.mock_docker, docker_func_name, mock.Mock(
            side_effect=errors.APIError('Error', '', '')))
        self.assertRaises(exception.ContainerException,
                          getattr(self.conductor, action),
                          None, mock_container_uuid)
        docker_func = getattr(self.mock_docker, docker_func_name)
        docker_func.assert_called_once_with(mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)
        mock_init.assert_called_with()

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_reboot(self, mock_find_container, mock_get_by_uuid):
        self._test_container(
            'container_reboot', 'restart', fields.ContainerStatus.RUNNING,
            mock_find_container, mock_get_by_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_reboot_with_failure(self, mock_find_container):
        self._test_container_with_failure(
            'container_reboot', 'restart', mock_find_container)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_start(self, mock_find_container, mock_get_by_uuid):
        self._test_container(
            'container_start', 'start', fields.ContainerStatus.RUNNING,
            mock_find_container, mock_get_by_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_start_with_failure(self, mock_find_container):
        self._test_container_with_failure(
            'container_start', 'start', mock_find_container)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_stop(self, mock_find_container, mock_get_by_uuid):
        self._test_container(
            'container_stop', 'stop', fields.ContainerStatus.STOPPED,
            mock_find_container, mock_get_by_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_stop_with_failure(self, mock_find_container):
        self._test_container_with_failure(
            'container_stop', 'stop', mock_find_container)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_pause(self, mock_find_container, mock_get_by_uuid):
        self._test_container(
            'container_pause', 'pause', fields.ContainerStatus.PAUSED,
            mock_find_container, mock_get_by_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_pause_with_failure(self, mock_find_container):
        self._test_container_with_failure(
            'container_pause', 'pause', mock_find_container)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_unpause(self, mock_find_container, mock_get_by_uuid):
        self._test_container(
            'container_unpause', 'unpause', fields.ContainerStatus.RUNNING,
            mock_find_container, mock_get_by_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_unpause_with_failure(self, mock_find_container):
        self._test_container_with_failure(
            'container_unpause', 'unpause', mock_find_container)

    def _test_container_show(
            self, mock_find_container, mock_get_by_uuid, container_detail=None,
            expected_status=None, mock_docker_id='2703ef2b705d'):
        mock_container = mock.MagicMock()
        mock_get_by_uuid.return_value = mock_container
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_find_container.return_value = mock_docker_id
        if container_detail is not None:
            self.mock_docker.inspect_container.return_value = container_detail

        self.conductor.container_show(None, mock_container_uuid)

        if mock_docker_id:
            self.mock_docker.inspect_container.assert_called_once_with(
                mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)
        if expected_status is not None:
            self.assertEqual(expected_status, mock_container.status)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show(self, mock_find_container, mock_get_by_uuid):
        self._test_container_show(mock_find_container, mock_get_by_uuid)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_running_state(self, mock_find_container,
                                               mock_get_by_uuid):
        mock_container_detail = {'State': {'Error': '',
                                           'Running': True,
                                           'Paused': False}}
        self._test_container_show(
            mock_find_container, mock_get_by_uuid, mock_container_detail,
            fields.ContainerStatus.RUNNING)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_stop_state(self, mock_find_container,
                                            mock_get_by_uuid):
        mock_container_detail = {'State': {'Error': '',
                                           'Running': False,
                                           'Paused': False}}
        self._test_container_show(
            mock_find_container, mock_get_by_uuid, mock_container_detail,
            fields.ContainerStatus.STOPPED)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_pause_state(self, mock_find_container,
                                             mock_get_by_uuid):
        mock_container_detail = {'State': {'Error': '',
                                           'Running': True,
                                           'Paused': True}}
        self._test_container_show(
            mock_find_container, mock_get_by_uuid, mock_container_detail,
            fields.ContainerStatus.PAUSED)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_error_status(self, mock_find_container,
                                              mock_get_by_uuid):
        mock_container_detail = {'State': {'Error': True,
                                           'Running': False,
                                           'Paused': False}}
        self._test_container_show(
            mock_find_container, mock_get_by_uuid, mock_container_detail,
            fields.ContainerStatus.ERROR)

    def _test_container_show_with_failure(
            self, mock_find_container, mock_get_by_uuid, error,
            assert_raise=True, expected_status=None):
        mock_container = mock.MagicMock()
        mock_get_by_uuid.return_value = mock_container
        mock_container_uuid = 'd545a92d-609a-428f-8edb-1d6b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        with mock.patch.object(errors.APIError, '__str__',
                               return_value=error) as mock_init:
            self.mock_docker.inspect_container = mock.Mock(
                side_effect=errors.APIError('Error', '', ''))

            if assert_raise:
                self.assertRaises(exception.ContainerException,
                                  self.conductor.container_show,
                                  None, mock_container_uuid)
            else:
                self.conductor.container_show(None, mock_container_uuid)

            self.mock_docker.inspect_container.assert_called_once_with(
                mock_docker_id)
            mock_find_container.assert_called_once_with(self.mock_docker,
                                                        mock_container_uuid)
            mock_init.assert_called_with()
            if expected_status is not None:
                self.assertEqual(expected_status, mock_container.status)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_failure(self, mock_find_container,
                                         mock_get_by_uuid):
        self._test_container_show_with_failure(
            mock_find_container, mock_get_by_uuid, error='hit error')

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_not_found(self, mock_find_container,
                                           mock_get_by_uuid):
        self._test_container_show_with_failure(
            mock_find_container, mock_get_by_uuid, error='404 error',
            assert_raise=False, expected_status=fields.ContainerStatus.ERROR)

    @mock.patch.object(objects.Container, 'get_by_uuid')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_show_with_not_found_from_docker(self,
                                                       mock_find_container,
                                                       mock_get_by_uuid):
        self._test_container_show(
            mock_find_container, mock_get_by_uuid, mock_docker_id={},
            expected_status=fields.ContainerStatus.ERROR)

    def _test_container_exec(self, mock_find_container, docker_version='1.2.2',
                             deprecated=False):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        docker.version = docker_version
        mock_find_container.return_value = mock_docker_id
        mock_create_res = mock.MagicMock()
        self.mock_docker.exec_create.return_value = mock_create_res
        self.conductor.container_exec(None, mock_container_uuid, 'ls')
        if deprecated:
            self.mock_docker.execute.assert_called_once_with(
                mock_docker_id, 'ls')
        else:
            self.mock_docker.exec_create.assert_called_once_with(
                mock_docker_id, 'ls', True, True, False)
            self. mock_docker.exec_start.assert_called_once_with(
                mock_create_res, False, False, False)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_exec(self, mock_find_container):
        self._test_container_exec(mock_find_container)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_exec_deprecated(self, mock_find_container):
        self._test_container_exec(
            mock_find_container, docker_version='0.7.0', deprecated=True)

    def _test_container_exec_with_failure(
            self, mock_find_container, docker_version='1.2.2',
            deprecated=False):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        docker.version = docker_version
        mock_find_container.return_value = mock_docker_id
        with mock.patch.object(errors.APIError, '__str__',
                               return_value='hit error') as mock_init:
            if deprecated:
                self.mock_docker.execute = mock.Mock(
                    side_effect=errors.APIError('Error', '', ''))
            else:
                self.mock_docker.exec_create = mock.Mock(
                    side_effect=errors.APIError('Error', '', ''))
            self.assertRaises(exception.ContainerException,
                              self.conductor.container_exec,
                              None, mock_container_uuid, 'ls')
            if deprecated:
                self.mock_docker.execute.assert_called_once_with(
                    mock_docker_id, 'ls')
            else:
                self.mock_docker.exec_create.assert_called_once_with(
                    mock_docker_id, 'ls', True, True, False)
            mock_find_container.assert_called_once_with(self.mock_docker,
                                                        mock_container_uuid)
            mock_init.assert_called_with()

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_exec_with_failure(self, mock_find_container):
        self._test_container_exec_with_failure(mock_find_container)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_exec_deprecated_with_failure(self, mock_find_container):
        self._test_container_exec_with_failure(
            mock_find_container, docker_version='0.7.0', deprecated=True)

    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_logs(self, mock_find_container):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        self.conductor.container_logs(None, mock_container_uuid)
        self.mock_docker.get_container_logs.assert_called_once_with(
            mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)

    @mock.patch.object(errors.APIError, '__str__')
    @mock.patch.object(docker_conductor.Handler, '_find_container_by_name')
    def test_container_logs_with_failure(self, mock_find_container, mock_init):
        mock_container_uuid = 'd545a92d-609a-428f-8edb-16b02ad20ca1'
        mock_docker_id = '2703ef2b705d'
        mock_find_container.return_value = mock_docker_id
        mock_init.return_value = 'hit error'
        self.mock_docker.get_container_logs = mock.Mock(
            side_effect=errors.APIError('Error', '', ''))
        self.assertRaises(exception.ContainerException,
                          self.conductor.container_logs,
                          None, mock_container_uuid)
        self.mock_docker.get_container_logs.assert_called_once_with(
            mock_docker_id)
        mock_find_container.assert_called_once_with(self.mock_docker,
                                                    mock_container_uuid)
        mock_init.assert_called_with()

    def test_container_common_exception(self):
        self.dfc_context_manager.__enter__.side_effect = Exception("So bad")
        for action in ('container_exec', 'container_logs', 'container_show',
                       'container_delete', 'container_create',
                       '_container_action'):
            func = getattr(self.conductor, action)
            self.assertRaises(exception.ContainerException,
                              func, None, None)
