# Copyright (c) 2013 dotCloud, Inc.
# 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 contextlib
import os
import socket

import mock
from oslo_config import fixture as config_fixture
from oslo_utils import units

from nova.compute import task_states
from nova import context
from nova import exception
from nova import test
import nova.tests.unit.image.fake
from nova.tests.unit import matchers
from nova.tests.unit import utils
from nova.tests.unit.virt import test_virt_drivers
from novadocker.tests.virt.docker import mock_client
import novadocker.virt.docker
from novadocker.virt.docker import driver as docker_driver
from novadocker.virt.docker import hostinfo
from novadocker.virt.docker import network


class DockerDriverTestCase(test_virt_drivers._VirtDriverTestCase,
                           test.TestCase):

    driver_module = 'novadocker.virt.docker.DockerDriver'

    def setUp(self):
        super(DockerDriverTestCase, self).setUp()

        self.mock_client = mock_client.MockClient()
        self.stubs.Set(novadocker.virt.docker.driver.DockerDriver, 'docker',
                       self.mock_client)

        def fake_plug_vifs(self, instance, network_info):
            return

        self.stubs.Set(novadocker.virt.docker.driver.DockerDriver,
                       'plug_vifs',
                       fake_plug_vifs)

        def fake_attach_vifs(self, instance, network_info):
            return

        self.stubs.Set(novadocker.virt.docker.driver.DockerDriver,
                       '_attach_vifs',
                       fake_attach_vifs)

        # Note: using mock.object.path on class throws
        # errors in test_virt_drivers
        def fake_teardown_network(container_id):
            return

        self.stubs.Set(network, 'teardown_network', fake_teardown_network)
        self.context = context.RequestContext('fake_user', 'fake_project')

        self.connection.init_host(None)
        self.fixture = self.useFixture(
            config_fixture.Config(novadocker.virt.docker.driver.CONF))

    def test_driver_capabilities(self):
        self.assertFalse(self.connection.capabilities['has_imagecache'])
        self.assertFalse(self.connection.capabilities['supports_recreate'])

    # NOTE(bcwaldon): This exists only because _get_running_instance on the
    # base class will not let us set a custom disk/container_format.
    def _get_running_instance(self, obj=True):
        instance_ref = utils.get_test_instance(obj=obj)
        network_info = utils.get_test_network_info()
        network_info[0]['network']['subnets'][0]['meta']['dhcp_server'] = \
            '1.1.1.1'
        image_info = utils.get_test_image_info(None, instance_ref)
        image_info['disk_format'] = 'raw'
        image_info['container_format'] = 'docker'
        self.connection.spawn(self.ctxt, instance_ref, image_info,
                              [], 'herp', network_info=network_info)
        return instance_ref, network_info

    @mock.patch.object(hostinfo, 'get_total_vcpus', return_value=1)
    @mock.patch.object(hostinfo, 'get_vcpus_used', return_value=0)
    def test_get_host_stats(self, mock_total, mock_used):
        memory = {
            'total': 4 * units.Mi,
            'used': 1 * units.Mi
        }
        self.mox.StubOutWithMock(socket, 'gethostname')
        self.mox.StubOutWithMock(hostinfo, 'get_memory_usage')
        socket.gethostname().AndReturn('foo')
        hostinfo.get_memory_usage().AndReturn(memory)
        socket.gethostname().AndReturn('bar')
        hostinfo.get_memory_usage().AndReturn(memory)
        self.mox.ReplayAll()
        self.assertEqual('foo',
                         self.connection.get_host_stats()['host_hostname'])
        self.assertEqual('foo',
                         self.connection.get_host_stats()['host_hostname'])

    def test_get_available_resource(self):
        memory = {
            'total': 4 * units.Mi,
            'used': 1 * units.Mi
        }
        disk = {
            'total': 50 * units.Gi,
            'available': 25 * units.Gi,
            'used': 25 * units.Gi
        }
        # create the mocks
        with contextlib.nested(
            mock.patch.object(hostinfo, 'get_total_vcpus',
                              return_value=1),
            mock.patch.object(hostinfo, 'get_vcpus_used',
                              return_value=0),
            mock.patch.object(hostinfo, 'get_memory_usage',
                              return_value=memory),
            mock.patch.object(hostinfo, 'get_disk_usage',
                              return_value=disk)
        ) as (
            get_total_vcpus,
            get_vcpus_used,
            get_memory_usage,
            get_disk_usage
        ):
            # run the code
            stats = self.connection.get_available_resource(nodename='test')
            # make our assertions
            get_memory_usage.assert_called_once_with()
            get_disk_usage.assert_called_once_with()
            expected_stats = {
                'vcpus': 1,
                'vcpus_used': 0,
                'memory_mb': 4,
                'memory_mb_used': 1,
                'local_gb': 50L,
                'local_gb_used': 25L,
                'disk_available_least': 25L,
                'hypervisor_type': 'docker',
                'hypervisor_version': 1000,
                'hypervisor_hostname': 'test',
                'cpu_info': '?',
                'numa_topology': None,
                'supported_instances': ('[["i686", "docker", "exe"],'
                                        ' ["x86_64", "docker", "exe"]]')
            }
            self.assertEqual(expected_stats, stats)

    def test_create_container(self, image_info=None, instance_href=None,
                              network_info=None):
        if instance_href is None:
            instance_href = utils.get_test_instance()
        if image_info is None:
            image_info = utils.get_test_image_info(None, instance_href)
            image_info['disk_format'] = 'raw'
            image_info['container_format'] = 'docker'
        self.connection.spawn(self.context, instance_href, image_info,
                              'fake_files', 'fake_password',
                              network_info=network_info)
        self._assert_cpu_shares(instance_href)
        self.assertEqual(self.mock_client.name, "nova-{0}".format(
            instance_href['uuid']))

    def test_create_container_empty_cmd(self, image_info=None,
                                        instance_href=None,
                                        network_info=None):
        if instance_href is None:
            instance_href = utils.get_test_instance()
        if image_info is None:
            image_info = utils.get_test_image_info(None, instance_href)
            image_info['disk_format'] = 'raw'
            image_info['container_format'] = 'docker'
        with mock.patch.object(self.mock_client, 'create_container') as mc:
            self.connection.spawn(self.context, instance_href, image_info,
                                  'fake_files', 'fake_password',
                                  network_info=network_info)
            self.assertIsNone(mc.call_args[1].get('command'))

    @mock.patch.object(novadocker.virt.docker.driver.DockerDriver,
                       '_inject_key', return_value='/tmp/.ssh')
    def test_create_container_inject_key(self, mock_inject_key):
        self.fixture.config(inject_key=True, group='docker')
        instance_href = utils.get_test_instance()
        instance_href.key_data = 'my_key'
        image_info = utils.get_test_image_info(None, instance_href)
        image_info['disk_format'] = 'raw'
        image_info['container_format'] = 'docker'
        with mock.patch.object(self.mock_client, 'create_container'):
            with mock.patch.object(self.mock_client, 'start') as ms:
                self.connection.spawn(self.context, instance_href, image_info,
                                      'fake_files', 'fake_password',
                                      network_info=None)
                command = ms.call_args[1]
                expected = {'binds': {'/tmp/.ssh':
                            {'bind': '/root/.ssh', 'ro': True}},
                            'dns': None,
                            'privileged': False}
                self.assertEqual(expected, command)

    def test_create_container_glance_cmd(self, image_info=None,
                                         instance_href=None,
                                         network_info=None):
        if instance_href is None:
            instance_href = utils.get_test_instance()
        if image_info is None:
            image_info = utils.get_test_image_info(None, instance_href)
            image_info['disk_format'] = 'raw'
            image_info['container_format'] = 'docker'
            image_info['properties'] = {'os_command_line': 'uname'}
        with mock.patch.object(self.mock_client, 'create_container') as mc:
            self.connection.spawn(self.context, instance_href, image_info,
                                  'fake_files', 'fake_password',
                                  network_info=network_info)
            command = mc.call_args[1]['command']
            self.assertEqual('uname', command)

    def test_create_container_vcpus_2(self, image_info=None):
        flavor = utils.get_test_flavor(options={
            'name': 'vcpu_2',
            'flavorid': 'vcpu_2',
            'vcpus': 2
        })
        instance_href = utils.get_test_instance(flavor=flavor)
        if image_info is None:
            image_info = utils.get_test_image_info(None, instance_href)
            image_info['disk_format'] = 'raw'
            image_info['container_format'] = 'docker'
        self.connection.spawn(self.context, instance_href, image_info,
                              'fake_files', 'fake_password')
        self._assert_cpu_shares(instance_href, vcpus=2)
        self.assertEqual(self.mock_client.name, "nova-{0}".format(
            instance_href['uuid']))

    def _assert_cpu_shares(self, instance_href, vcpus=4):
        container_id = self.connection._find_container_by_name(
            instance_href['name']).get('id')
        container_info = self.connection.docker.inspect_container(container_id)
        self.assertEqual(vcpus * 1024, container_info['Config']['CpuShares'])

    @mock.patch('novadocker.virt.docker.driver.DockerDriver.plug_vifs',
                side_effect=Exception)
    def test_create_container_net_setup_fails(self, mock_plug_vifs):
        self.assertRaises(exception.InstanceDeployFailure,
                          self.test_create_container,
                          network_info=utils.get_test_network_info())
        self.assertEqual(0, len(self.mock_client.containers()))

    def test_create_container_wrong_image(self):
        instance_href = utils.get_test_instance()
        image_info = utils.get_test_image_info(None, instance_href)
        image_info['disk_format'] = 'raw'
        image_info['container_format'] = 'invalid_format'
        self.assertRaises(exception.InstanceDeployFailure,
                          self.test_create_container,
                          image_info, instance_href)

    @mock.patch.object(novadocker.virt.docker.driver.DockerDriver,
                       'cleanup')
    @mock.patch.object(novadocker.virt.docker.driver.DockerDriver,
                       '_find_container_by_name',
                       return_value={'id': 'fake_id'})
    def test_destroy_container(self, byname_mock, cleanup_mock):
        instance = utils.get_test_instance()
        self.connection.destroy(self.context, instance, 'fake_networkinfo')
        cleanup_mock.assert_called_with(self.context, instance,
                                        'fake_networkinfo', None, True)