# Copyright 2013: Rackspace UK
# 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 os
from unittest import mock

import ddt

from rally.common import validation
from rally import exceptions

from rally_openstack.task.scenarios.vm import vmtasks
from tests.unit import test


BASE = "rally_openstack.task.scenarios.vm.vmtasks"


@ddt.ddt
class VMTasksTestCase(test.ScenarioTestCase):

    def setUp(self):
        super(VMTasksTestCase, self).setUp()
        self.context.update({"user": {"keypair": {"name": "keypair_name"},
                                      "credential": mock.MagicMock()}})

        cinder_patcher = mock.patch(
            "rally_openstack.common.services.storage.block.BlockStorage")
        self.cinder = cinder_patcher.start().return_value
        self.cinder.create_volume.return_value = mock.Mock(id="foo_volume")
        self.addCleanup(cinder_patcher.stop)

    def create_env(self, scenario):
        self.ip = {"id": "foo_id", "ip": "foo_ip", "is_floating": True}
        scenario._boot_server_with_fip = mock.Mock(
            return_value=("foo_server", self.ip))
        scenario._wait_for_ping = mock.Mock()
        scenario._delete_server_with_fip = mock.Mock()
        scenario._run_command = mock.MagicMock(
            return_value=(0, "{\"foo\": 42}", "foo_err"))
        scenario.add_output = mock.Mock()
        return scenario

    def test_boot_runcommand_delete(self):
        scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))
        scenario._run_command = mock.MagicMock(
            return_value=(0, "{\"foo\": 42}", "foo_err"))
        scenario.run("foo_flavor", image="foo_image",
                     command={"script_file": "foo_script",
                              "interpreter": "foo_interpreter"},
                     username="foo_username",
                     password="foo_password",
                     use_floating_ip="use_fip",
                     floating_network="ext_network",
                     force_delete="foo_force",
                     volume_args={"size": 16},
                     foo_arg="foo_value")

        self.cinder.create_volume.assert_called_once_with(16, imageRef=None)
        scenario._boot_server_with_fip.assert_called_once_with(
            "foo_image", "foo_flavor", key_name="keypair_name",
            use_floating_ip="use_fip", floating_network="ext_network",
            block_device_mapping={"vdrally": "foo_volume:::1"},
            foo_arg="foo_value")

        scenario._wait_for_ping.assert_called_once_with("foo_ip")
        scenario._run_command.assert_called_once_with(
            "foo_ip", 22, "foo_username", "foo_password",
            command={"script_file": "foo_script",
                     "interpreter": "foo_interpreter"})
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete="foo_force")
        scenario.add_output.assert_called_once_with(
            complete={"chart_plugin": "TextArea",
                      "data": [
                          "StdErr: foo_err",
                          "StdOut:",
                          "{\"foo\": 42}"],
                      "title": "Script Output"})

    @ddt.data(
        {"output": (0, "", ""),
         "expected": [{"complete": {"chart_plugin": "TextArea",
                                    "data": [
                                        "StdErr: (none)",
                                        "StdOut:",
                                        ""],
                                    "title": "Script Output"}}]},
        {"output": (1, "{\"foo\": 42}", ""), "raises": exceptions.ScriptError},
        {"output": ("", 1, ""), "raises": TypeError},
        {"output": (0, "{\"foo\": 42}", ""),
         "expected": [{"complete": {"chart_plugin": "TextArea",
                                    "data": [
                                        "StdErr: (none)",
                                        "StdOut:",
                                        "{\"foo\": 42}"],
                                    "title": "Script Output"}}]},
        {"output": (0, "{\"additive\": [1, 2]}", ""),
         "expected": [{"complete": {"chart_plugin": "TextArea",
                                    "data": [
                                        "StdErr: (none)",
                                        "StdOut:", "{\"additive\": [1, 2]}"],
                                    "title": "Script Output"}}]},
        {"output": (0, "{\"complete\": [3, 4]}", ""),
         "expected": [{"complete": {"chart_plugin": "TextArea",
                                    "data": [
                                        "StdErr: (none)",
                                        "StdOut:",
                                        "{\"complete\": [3, 4]}"],
                                    "title": "Script Output"}}]},
        {"output": (0, "{\"additive\": [1, 2], \"complete\": [3, 4]}", ""),
         "expected": [{"additive": 1}, {"additive": 2},
                      {"complete": 3}, {"complete": 4}]}
    )
    @ddt.unpack
    def test_boot_runcommand_delete_add_output(self, output,
                                               expected=None, raises=None):
        scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))

        scenario._run_command.return_value = output
        kwargs = {"flavor": "foo_flavor",
                  "image": "foo_image",
                  "command": {"remote_path": "foo"},
                  "username": "foo_username",
                  "password": "foo_password",
                  "use_floating_ip": "use_fip",
                  "floating_network": "ext_network",
                  "force_delete": "foo_force",
                  "volume_args": {"size": 16},
                  "foo_arg": "foo_value"}
        if raises:
            self.assertRaises(raises, scenario.run, **kwargs)
            self.assertFalse(scenario.add_output.called)
        else:
            scenario.run(**kwargs)
            calls = [mock.call(**kw) for kw in expected]
            scenario.add_output.assert_has_calls(calls, any_order=True)

            self.cinder.create_volume.assert_called_once_with(
                16, imageRef=None)
            scenario._boot_server_with_fip.assert_called_once_with(
                "foo_image", "foo_flavor", key_name="keypair_name",
                use_floating_ip="use_fip", floating_network="ext_network",
                block_device_mapping={"vdrally": "foo_volume:::1"},
                foo_arg="foo_value")

            scenario._run_command.assert_called_once_with(
                "foo_ip", 22, "foo_username", "foo_password",
                command={"remote_path": "foo"})
            scenario._delete_server_with_fip.assert_called_once_with(
                "foo_server", self.ip, force_delete="foo_force")

    def test_boot_runcommand_delete_command_timeouts(self):
        scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))

        scenario._run_command.side_effect = exceptions.SSHTimeout()
        self.assertRaises(exceptions.SSHTimeout,
                          scenario.run,
                          "foo_flavor", "foo_image", "foo_interpreter",
                          "foo_script", "foo_username")
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete=False)
        self.assertFalse(scenario.add_output.called)

    def test_boot_runcommand_delete_ping_wait_timeouts(self):
        scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))

        scenario._wait_for_ping.side_effect = exceptions.TimeoutException(
            resource_type="foo_resource",
            resource_name="foo_name",
            resource_id="foo_id",
            desired_status="foo_desired_status",
            resource_status="foo_resource_status",
            timeout=2)
        exc = self.assertRaises(exceptions.TimeoutException,
                                scenario.run,
                                "foo_image", "foo_flavor", "foo_interpreter",
                                "foo_script", "foo_username",
                                wait_for_ping=True)
        self.assertEqual(exc.kwargs["resource_type"], "foo_resource")
        self.assertEqual(exc.kwargs["resource_name"], "foo_name")
        self.assertEqual(exc.kwargs["resource_id"], "foo_id")
        self.assertEqual(exc.kwargs["desired_status"], "foo_desired_status")
        self.assertEqual(exc.kwargs["resource_status"], "foo_resource_status")

        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete=False)
        self.assertFalse(scenario.add_output.called)

    @mock.patch("%s.json" % BASE)
    def test_boot_runcommand_delete_json_fails(self, mock_json):
        scenario = self.create_env(vmtasks.BootRuncommandDelete(self.context))

        mock_json.loads.side_effect = ValueError()
        scenario.run("foo_image", "foo_flavor", "foo_interpreter",
                     "foo_script", "foo_username")
        scenario.add_output.assert_called_once_with(complete={
            "chart_plugin": "TextArea", "data": ["StdErr: foo_err",
                                                 "StdOut:", "{\"foo\": 42}"],
            "title": "Script Output"})
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete=False)

    def test_boot_runcommand_delete_custom_image(self):
        context = {
            "user": {
                "tenant_id": "tenant_id",
                "keypair": {"name": "foo_keypair_name"},
                "credential": mock.Mock()
            },
            "tenant": {
                "custom_image": {"id": "image_id"}
            }
        }

        scenario = self.create_env(vmtasks.BootRuncommandDelete(context))
        scenario._run_command = mock.MagicMock(
            return_value=(0, "{\"foo\": 42}", "foo_err"))
        scenario.run("foo_flavor",
                     command={"script_file": "foo_script",
                              "interpreter": "foo_interpreter"},
                     username="foo_username",
                     password="foo_password",
                     use_floating_ip="use_fip",
                     floating_network="ext_network",
                     force_delete="foo_force",
                     volume_args={"size": 16},
                     foo_arg="foo_value")

        self.cinder.create_volume.assert_called_once_with(16, imageRef=None)
        scenario._boot_server_with_fip.assert_called_once_with(
            "image_id", "foo_flavor", key_name="foo_keypair_name",
            use_floating_ip="use_fip", floating_network="ext_network",
            block_device_mapping={"vdrally": "foo_volume:::1"},
            foo_arg="foo_value")

        scenario._wait_for_ping.assert_called_once_with("foo_ip")
        scenario._run_command.assert_called_once_with(
            "foo_ip", 22, "foo_username", "foo_password",
            command={"script_file": "foo_script",
                     "interpreter": "foo_interpreter"})
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete="foo_force")
        scenario.add_output.assert_called_once_with(
            complete={"chart_plugin": "TextArea",
                      "data": [
                          "StdErr: foo_err",
                          "StdOut:", "{\"foo\": 42}"],
                      "title": "Script Output"})

    @mock.patch("%s.heat" % BASE)
    @mock.patch("%s.sshutils" % BASE)
    def test_runcommand_heat(self, mock_sshutils, mock_heat):
        fake_ssh = mock.Mock()
        fake_ssh.execute.return_value = [0, "key:val", ""]
        mock_sshutils.SSH.return_value = fake_ssh
        fake_stack = mock.Mock()
        fake_stack.stack.outputs = [{"output_key": "gate_node",
                                     "output_value": "ok"}]
        mock_heat.main.Stack.return_value = fake_stack
        context = {
            "user": {"keypair": {"name": "name", "private": "pk"},
                     "credential": mock.MagicMock()},
            "tenant": {"networks": [{"router_id": "1"}]}
        }
        scenario = vmtasks.RuncommandHeat(context)
        scenario.generate_random_name = mock.Mock(return_value="name")
        scenario.add_output = mock.Mock()
        workload = {"username": "admin",
                    "resource": ["foo", "bar"]}
        scenario.run(workload, "template",
                     {"file_key": "file_value"},
                     {"param_key": "param_value"})
        expected = {"chart_plugin": "Table",
                    "data": {"rows": [["key", "val"]],
                             "cols": ["key", "value"]},
                    "description": "Data generated by workload",
                    "title": "Workload summary"}
        scenario.add_output.assert_called_once_with(complete=expected)

    def create_env_for_designate(self, zone_config=None):
        scenario = vmtasks.CheckDesignateDNSResolving(self.context)
        self.ip = {"id": "foo_id", "ip": "foo_ip", "is_floating": True}
        scenario._boot_server_with_fip = mock.Mock(
            return_value=("foo_server", self.ip))
        scenario._delete_server_with_fip = mock.Mock()
        scenario._run_command = mock.MagicMock(
            return_value=(0, "ANSWER SECTION", "foo_err"))
        scenario.add_output = mock.Mock()
        if zone_config is None:
            zone_config = {
                "test_existing_designate_from_VM": {
                    "bind_ip": "192.168.1.123"
                }
            }
        self.context.update(
            {
                "config": {
                    "zones@openstack": zone_config
                },
                "user": {
                    "keypair": {"name": "keypair_name"},
                    "credential": mock.MagicMock()
                },
                "tenant": {
                    "id": "0",
                    "name": "tenant1",
                    "zones": [
                        {"name": "zone1.com."}
                    ],
                    "networks": [
                        {
                            "name": "net1",
                            "subnets": [
                                {
                                    "name": "subnet1",
                                    "dns_nameservers": "1.2.3.4"
                                }
                            ]
                        }
                    ]
                }
            }
        )
        args = {"image": "some_image", "flavor": "m1.small",
                "username": "chuck norris"}
        return scenario, args

    @mock.patch("rally.task.utils.get_from_manager")
    @mock.patch("rally.task.utils.wait_for_status")
    def test_check_designate_dns_resolving_ok(
            self,
            mock_rally_task_utils_wait_for_status,
            mock_rally_task_utils_get_from_manager):
        scenario, args = self.create_env_for_designate()
        scenario.run(**args)

        scenario._boot_server_with_fip.assert_called_once_with(
            "some_image", "m1.small", floating_network=None,
            key_name="keypair_name", use_floating_ip=True)
        mock_rally_task_utils_wait_for_status.assert_called_once_with(
            "foo_server", ready_statuses=["ACTIVE"], update_resource=mock.ANY)
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", {"id": "foo_id", "ip": "foo_ip",
                           "is_floating": True},
            force_delete=False)
        scenario.add_output.assert_called_with(
            complete={"chart_plugin": "TextArea",
                      "data": [
                          "foo_err"],
                      "title": "Script StdErr"})

    @mock.patch("rally.task.utils.get_from_manager")
    @mock.patch("rally.task.utils.wait_for_status")
    def test_test_existing_designate_from_vm_command_timeout(
            self,
            mock_rally_task_utils_wait_for_status,
            mock_rally_task_utils_get_from_manager):
        scenario, _ = self.create_env_for_designate()

        scenario._run_command.side_effect = exceptions.SSHTimeout()
        self.assertRaises(exceptions.SSHTimeout,
                          scenario.run,
                          "foo_flavor", "foo_image", "foo_interpreter",
                          "foo_script", "foo_username")
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete=False)
        self.assertFalse(scenario.add_output.called)

    @mock.patch("rally.task.utils.get_from_manager")
    @mock.patch("rally.task.utils.wait_for_status")
    def test_test_existing_designate_from_vm_wait_timeout(
            self,
            mock_rally_task_utils_wait_for_status,
            mock_rally_task_utils_get_from_manager):
        scenario, args = self.create_env_for_designate()

        mock_rally_task_utils_wait_for_status.side_effect = \
            exceptions.TimeoutException(
                resource_type="foo_resource",
                resource_name="foo_name",
                resource_id="foo_id",
                desired_status="foo_desired_status",
                resource_status="foo_resource_status",
                timeout=2)
        self.assertRaises(exceptions.TimeoutException,
                          scenario.run,
                          "foo_flavor", "foo_image", "foo_interpreter",
                          "foo_script", "foo_username")
        scenario._delete_server_with_fip.assert_called_once_with(
            "foo_server", self.ip, force_delete=False)
        self.assertFalse(scenario.add_output.called)

    @ddt.data(
        {"output": (1, "x y z", "error message"),
         "raises": exceptions.ScriptError},
        {"output": (0, "[1, 2, 3, 4]", ""),
         "raises": exceptions.ScriptError}
    )
    @ddt.unpack
    def test_test_existing_designate_from_vm_add_output(self, output,
                                                        expected=None,
                                                        raises=None):
        scenario, _ = self.create_env_for_designate()

        scenario._run_command.return_value = output
        kwargs = {"flavor": "foo_flavor",
                  "image": "foo_image",
                  "username": "foo_username",
                  "password": "foo_password",
                  "use_floating_ip": "use_fip",
                  "floating_network": "ext_network",
                  "force_delete": "foo_force"}

        self.assertRaises(raises, scenario.run, **kwargs)
        self.assertFalse(scenario.add_output.called)


@ddt.ddt
class ValidCommandValidatorTestCase(test.TestCase):

    def setUp(self):
        super(ValidCommandValidatorTestCase, self).setUp()
        self.context = {"admin": {"credential": mock.MagicMock()},
                        "users": [{"credential": mock.MagicMock()}]}

    @ddt.data({"command": {"script_inline": "foobar",
                           "interpreter": ["ENV=bar", "/bin/foo"],
                           "local_path": "bar",
                           "remote_path": "/bin/foo"}},
              {"command": {"script_inline": "foobar", "interpreter": "foo"}})
    @ddt.unpack
    def test_check_command_dict(self, command=None):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)
        self.assertIsNone(validator.check_command_dict(command))

    @ddt.data({"raises_message": "Command must be a dictionary"},
              {"command": "foo",
               "raises_message": "Command must be a dictionary"},
              {"command": {"interpreter": "foobar", "script_file": "foo",
                           "script_inline": "bar"},
               "raises_message": "Exactly one of "},
              {"command": {"script_file": "foobar"},
               "raises_message": "Supplied dict specifies no"},
              {"command": {"script_inline": "foobar",
                           "interpreter": "foo",
                           "local_path": "bar"},
               "raises_message": "When uploading an interpreter its path"},
              {"command": {"interpreter": "/bin/bash",
                           "script_path": "foo"},
               "raises_message": ("Unexpected command parameters: "
                                  "script_path")})
    @ddt.unpack
    def test_check_command_dict_failed(
            self, command=None, raises_message=None):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)
        e = self.assertRaises(
            validation.ValidationError,
            validator.check_command_dict, command)
        self.assertIn(raises_message, e.message)

    @mock.patch("rally.plugins.common.validators.FileExistsValidator"
                "._file_access_ok")
    def test_validate(self, mock__file_access_ok):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)
        mock__file_access_ok.return_value = None
        command = {"script_file": "foobar", "interpreter": "foo"}
        result = validator.validate(self.context, {"args": {"p": command}},
                                    None, None)
        self.assertIsNone(result)
        mock__file_access_ok.assert_called_once_with(
            filename="foobar", mode=os.R_OK, param_name="p",
            required=True)

    def test_valid_command_not_required(self):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=False)
        result = validator.validate(self.context, {"args": {"p": None}},
                                    None, None)
        self.assertIsNone(result)

    def test_valid_command_required(self):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)

        e = self.assertRaises(
            validation.ValidationError,
            validator.validate, {"args": {"p": None}},
            self.context, None, None)
        self.assertEqual("Command must be a dictionary", e.message)

    @mock.patch("rally.plugins.common.validators.FileExistsValidator"
                "._file_access_ok")
    def test_valid_command_unreadable_script_file(self, mock__file_access_ok):
        mock__file_access_ok.side_effect = validation.ValidationError("O_o")

        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)

        command = {"script_file": "foobar", "interpreter": "foo"}
        e = self.assertRaises(
            validation.ValidationError,
            validator.validate, self.context, {"args": {"p": command}},
            None, None)
        self.assertEqual("O_o", e.message)

    @mock.patch("%s.ValidCommandValidator.check_command_dict" % BASE)
    def test_valid_command_fail_check_command_dict(self,
                                                   mock_check_command_dict):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)

        mock_check_command_dict.side_effect = validation.ValidationError(
            "foobar")
        e = self.assertRaises(
            validation.ValidationError,
            validator.validate, {"args": {"p": {"foo": "bar"}}},
            self.context, None, None)
        self.assertEqual("foobar", e.message)

    def test_valid_command_script_inline(self):
        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)

        command = {"script_inline": "bar", "interpreter": "/bin/sh"}
        result = validator.validate(self.context, {"args": {"p": command}},
                                    None, None)
        self.assertIsNone(result)

    @mock.patch("rally.plugins.common.validators.FileExistsValidator"
                "._file_access_ok")
    def test_valid_command_local_path(self, mock__file_access_ok):
        mock__file_access_ok.side_effect = validation.ValidationError("")

        validator = vmtasks.ValidCommandValidator(param_name="p",
                                                  required=True)

        command = {"remote_path": "bar", "local_path": "foobar"}
        self.assertRaises(
            validation.ValidationError,
            validator.validate, self.context, {"args": {"p": command}},
            None, None)
        mock__file_access_ok.assert_called_once_with(
            filename="foobar", mode=os.R_OK, param_name="p",
            required=True)
