# Copyright 2012 OpenStack Foundation
# 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.

from oslo_log import log as logging
import pytest
import allure

from tests.api.compute import base
from common.utils.linux import remote_client
from common import waiters
from configs import config
from lib.common.utils import data_utils
from lib import exceptions as lib_exc
from common.utils.net_utils import ping_check

CONF = config.CONF

LOG = logging.getLogger(__name__)


class ServerActionsBase(base.BaseV2ComputeTest):
    """Test server actions"""

    def setUp(self):
        # NOTE(afazekas): Normally we use the same server with all test cases,
        # but if it has an issue, we build a new one
        super().setUp()
        # Check if the server is in a clean state after test
        with allure.step("Setup for server action"):
            try:
                self.validation_resources = self.get_class_validation_resources(
                    self.os_primary
                )
                # _test_rebuild_server test compares ip address attached to the
                # server before and after the rebuild, in order to avoid
                # a situation when a newly created server doesn't have a floating
                # ip attached at the beginning of the test_rebuild_server let's
                # make sure right here the floating ip is attached
                # Only checking floating ip feature enabled, to avoid unnecessarily recreating the server
                if CONF.network_feature_enabled.floating_ips:
                    waiters.wait_for_server_floating_ip(
                        self.client,
                        self.client.show_server(self.server_id)["server"],
                        self.validation_resources["floating_ip"],
                    )

                waiters.wait_for_server_status(self.client, self.server_id, "ACTIVE")
            except lib_exc.NotFound:
                # The server was deleted by previous test, create a new one
                # Use class level validation resources to avoid them being
                # deleted once a test is over
                self.validation_resources = self.get_class_validation_resources(
                    self.os_primary
                )
                server = self.create_test_server(
                    validatable=True,
                    validation_resources=self.validation_resources,
                    wait_until="SSHABLE",
                )
                self.__class__.server_id = server["id"]
            except Exception:
                # Rebuild server if something happened to it during a test
                self.__class__.server_id = self.recreate_server(
                    self.server_id, validatable=True, wait_until="SSHABLE"
                )

    def tearDown(self):
        super(ServerActionsBase, self).tearDown()
        # NOTE(zhufl): Because server_check_teardown will raise Exception
        # which will prevent other cleanup steps from being executed, so
        # server_check_teardown should be called after super's tearDown.
        with allure.step("Teardown for server action"):
            self.server_check_teardown()

    @classmethod
    def setup_credentials(cls):
        cls.prepare_instance_network()
        super(ServerActionsBase, cls).setup_credentials()

    @classmethod
    def setup_clients(cls):
        super(ServerActionsBase, cls).setup_clients()
        cls.client = cls.servers_client

    @classmethod
    def resource_setup(cls):
        super(ServerActionsBase, cls).resource_setup()
        cls.server_id = cls.recreate_server(
            None, validatable=True, wait_until="SSHABLE"
        )

    def _test_reboot_server(self, reboot_type, server_id=None):
        if not server_id:
            server_id = self.server_id
        if CONF.validation.run_validation:
            # Get the time the server was last rebooted,
            server = self.client.show_server(server_id)["server"]
            linux_client = remote_client.RemoteClient(
                self.get_server_ip(server, self.validation_resources),
                self.ssh_user,
                self.password,
                self.validation_resources["keypair"]["private_key"],
                server=server,
                servers_client=self.client,
            )
            boot_time = linux_client.get_boot_time()

            # NOTE: This sync is for avoiding the loss of pub key data
            # in a server
            linux_client.exec_command("sync")
        try:
            self.reboot_server(server_id, type=reboot_type)
        except Exception as e:
            return False, f"Failed to reboot server {str(e)}"

        if CONF.validation.run_validation:
            # Log in and verify the boot time has changed
            linux_client = remote_client.RemoteClient(
                self.get_server_ip(server, self.validation_resources),
                self.ssh_user,
                self.password,
                self.validation_resources["keypair"]["private_key"],
                server=server,
                servers_client=self.client,
            )
            new_boot_time = linux_client.get_boot_time()
            self.assertGreater(
                new_boot_time, boot_time, "%s > %s" % (new_boot_time, boot_time)
            )
        return True, ""

    def _test_rebuild_server(self, server_id, is_other_image=True):
        # Get the IPs the server has before rebuilding it
        original_addresses = self.client.show_server(server_id)["server"]["addresses"]
        # The server should be rebuilt using the provided image and data
        meta = {"rebuild": "server"}
        new_name = data_utils.rand_name("server", prefix="vm")
        password = "rebuildPassw0rd"

        # If 'is_other_image' is False, Rebuild with same image (image_ref)
        rebuild_image = self.image_ref_alt
        if not is_other_image:
            rebuild_image = self.image_ref

        rebuilt_server = self.client.rebuild_server(
            server_id, rebuild_image, name=new_name, metadata=meta, adminPass=password
        )["server"]

        # Verify the properties in the initial response are correct
        if server_id != rebuilt_server["id"]:
            return False, "Failed to verify rebuilt server id in initial response"
        rebuilt_image_id = rebuilt_server["image"]["id"]
        if rebuild_image.endswith(rebuilt_image_id) is False:
            return False, "Failed to verify rebuilt image id in initial response"
        if self.flavor_ref != rebuilt_server["flavor"]["id"]:
            return False, "Failed to verify rebuilt flavor in initial response"

        # Verify the server properties after the rebuild completes
        try:
            waiters.wait_for_server_status(self.client, rebuilt_server["id"], "ACTIVE")
        except lib_exc.TimeoutException:
            return False, "Failed to validate rebuilt server state"

        server = self.client.show_server(rebuilt_server["id"])["server"]
        rebuilt_image_id = server["image"]["id"]
        if rebuild_image.endswith(rebuilt_image_id) is False:
            return False, "Failed to verify rebuilt image id"
        if new_name != server["name"]:
            return False, "Failed to verify rebuilt server name"
        if original_addresses != server["addresses"]:
            return False, "Failed to verify rebuilt server addresses"

        if CONF.validation.run_validation:
            # Authentication is attempted in the following order of priority:
            # 1.The key passed in, if one was passed in.
            # 2.Any key we can find through an SSH agent (if allowed).
            # 3.Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in
            #   ~/.ssh/ (if allowed).
            # 4.Plain username/password auth, if a password was given.
            linux_client = remote_client.RemoteClient(
                self.get_server_ip(rebuilt_server, self.validation_resources),
                self.ssh_alt_user,
                password,
                self.validation_resources["keypair"]["private_key"],
                server=rebuilt_server,
                servers_client=self.client,
            )
            linux_client.validate_authentication()
        return True, ""

    def _create_multi_volumes(self, vol_type, size):
        volume1 = self.create_volume(volume_type=vol_type, size=size)
        volume2 = self.create_volume(volume_type=vol_type, size=size)
        volume3 = self.create_volume(volume_type=vol_type, size=size)

        if not volume1["id"] or not volume2["id"] or not volume3["id"]:
            return None, "Failed to create multi volumes"
        return [volume1, volume2, volume3], ""

    def _attach_volumes_to_server(self, vol_list, server_id):
        for vol in vol_list:
            resp = self.servers_client.attach_volume(server_id, volumeId=vol["id"])
            if resp.response["status"] != "200":
                return False, f"Failed to attached volume({vol['id']}) to instance"
            try:
                waiters.wait_for_volume_resource_status(
                    self.volumes_client, vol["id"], "in-use"
                )
            except lib_exc.TimeoutException:
                self.addCleanup(self.delete_server, server_id)
                return (
                    False,
                    f"Failed to wait volume({vol['id']}) status to 'in-use' after volume attach",
                )
        return True, ""

    def _detach_volumes_from_server(self, vol_list, server_id):
        for vol in vol_list:
            resp = self.servers_client.detach_volume(server_id, volume_id=vol["id"])
            if resp.response["status"] != "202":
                return False, f"Failed to detached volume({vol['id']}) from server"
            try:
                waiters.wait_for_volume_resource_status(
                    self.volumes_client, vol["id"], "available"
                )
            except lib_exc.TimeoutException:
                self.addCleanup(self.delete_server, server_id)
                return (
                    False,
                    f"Failed to wait volume{vol['id']} status to 'available' after volume detach",
                )
        return True, ""

    def _check_server_ping(self, server_id, max_retry=5):
        server = self.client.show_server(server_id)["server"]
        if "addresses" in server and "private" in server["addresses"]:
            server_ip = server["addresses"]["private"][0]["addr"]
        if not server_ip:
            return False
        # Ping check with in retry
        retry = 0
        while max_retry > retry:
            if ping_check(server_ip):
                return True
            retry += 1
        # Failed all ping check
        self.addCleanup(self.delete_server, server_id)
        return False

    def _reboot_server_and_verify_status(
        self, vol_list, vol_type, server_id, reboot_type
    ):
        with allure.step(
            f"Reboot Server {reboot_type} attached multi volumes with VolumeType: {vol_type}"
        ):
            is_reboot_pass, msg = self._test_reboot_server(reboot_type, server_id)
            assert is_reboot_pass, msg
        with allure.step(
            f"Verify volumes maintain 'in-use' status with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "in-use"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) maintain to 'in-use' status"
        with allure.step("Verify server ping check alive with server ip"):
            assert self._check_server_ping(
                server_id=server_id
            ), f"Failed to ping check alive for server({server_id})"

    def _rebuild_server_and_verify_status(
        self, vol_list, vol_type, server, is_other_image
    ):
        with allure.step(
            f"Rebuild Server attached multi volumes with VolumeType: {vol_type}"
        ):
            is_rebuild_pass, msg = self._test_rebuild_server(
                server_id=server["id"], is_other_image=is_other_image
            )
            assert is_rebuild_pass, msg
        with allure.step(
            f"Verify volumes maintain 'in-use' status with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "in-use"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) maintain to 'in-use' status"
        with allure.step("Verify server ping check alive with server ip"):
            assert self._check_server_ping(
                server_id=server["id"]
            ), f"Failed to ping check alive for server({server['id']})"


class TestServerActionsAttachSsdNvmeVolumes(ServerActionsBase):
    @pytest.mark.positive
    @pytest.mark.skipif(
        not CONF.volume.volume_type_ssd, reason="SSD volume type not available."
    )
    def test_reboot_server_hard_soft_with_attach_and_detach_ssd_volumes(self):
        """Test hard and soft rebooting server with attach and detach ssd type volumes."""
        vol_type = CONF.volume.volume_type_ssd
        with allure.step("Create test server"):
            server = self.create_test_server(
                validatable=True,
                validation_resources=self.validation_resources,
                wait_until="ACTIVE",
            )
            assert server["id"], f"Failed to create test server({server['id']})"
        with allure.step(f"Create multi volumes with VolumeType: {vol_type}"):
            vol_list, msg = self._create_multi_volumes(vol_type, 10)
            assert vol_list, msg
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Verify Reboot Server HARD and Status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._reboot_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server_id=server["id"],
                reboot_type="HARD",
            )
        with allure.step(
            f"Verify Reboot Server SOFT and Status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._reboot_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server_id=server["id"],
                reboot_type="SOFT",
            )
        with allure.step(
            f"Verify detach multi volumes from server with VolumeType: {vol_type}"
        ):
            is_success, msg = self._detach_volumes_from_server(
                vol_list=vol_list, server_id=server["id"]
            )
            assert is_success, msg
        # Re-attach volume to test volume return 'available' status after instance deleted
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Delete Server with attached volumes. VolumeType : {vol_type}"
        ):
            self.servers_client.delete_server(server["id"])
            try:
                waiters.wait_for_server_termination(self.servers_client, server["id"])
            except lib_exc.TimeoutException:
                assert False, "Failed to instance server with attach volume"
        with allure.step(
            f"Verify volumes return to 'available' with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "available"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) return to 'available' status"

    @pytest.mark.positive
    @pytest.mark.skipif(
        not CONF.volume.volume_type_nvme, reason="NVME volume type not available."
    )
    def test_reboot_server_hard_soft_with_attach_and_detach_nvme_volumes(self):
        """Test hard and soft rebooting server with attach and detach nvme type volumes."""
        vol_type = CONF.volume.volume_type_nvme
        with allure.step("Create test server"):
            server = self.create_test_server(
                validatable=True,
                validation_resources=self.validation_resources,
                wait_until="ACTIVE",
            )
            assert server["id"], f"Failed to create test server({server['id']})"
        with allure.step(f"Create multi volumes with VolumeType: {vol_type}"):
            vol_list, msg = self._create_multi_volumes(vol_type, 10)
            assert vol_list, msg
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Verify Reboot Server HARD and Status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._reboot_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server_id=server["id"],
                reboot_type="HARD",
            )
        with allure.step(
            f"Verify Reboot Server SOFT and Status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._reboot_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server_id=server["id"],
                reboot_type="SOFT",
            )
        with allure.step(
            f"Verify detach multi volumes from server with VolumeType: {vol_type}"
        ):
            is_success, msg = self._detach_volumes_from_server(
                vol_list=vol_list, server_id=server["id"]
            )
            assert is_success, msg
        # Re-attach volume to test volume return 'available' status after instance deleted
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Delete Server with attached volumes. VolumeType : {vol_type}"
        ):
            self.servers_client.delete_server(server["id"])
            try:
                waiters.wait_for_server_termination(self.servers_client, server["id"])
            except lib_exc.TimeoutException:
                assert False, "Failed to instance server with attach volume"
        with allure.step(
            f"Verify volumes return to 'available' with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "available"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) return to 'available' status"

    @pytest.mark.positive
    @pytest.mark.skipif(
        not CONF.volume.volume_type_ssd, reason="SSD volume type not available."
    )
    def test_rebuild_server_with_attach_and_detach_ssd_volumes(self):
        """Test rebuilding server with attach and detach ssd type volumes."""
        vol_type = CONF.volume.volume_type_ssd
        with allure.step("Create test server"):
            server = self.create_test_server(
                validatable=True,
                validation_resources=self.validation_resources,
                wait_until="ACTIVE",
            )
            assert server["id"], f"Failed to create test server({server['id']})"
        with allure.step(f"Create multi volumes with VolumeType: {vol_type}"):
            vol_list, msg = self._create_multi_volumes(vol_type, 10)
            assert vol_list, msg
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Verify Rebuild Server with same image and status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._rebuild_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server=server,
                is_other_image=False,
            )
        with allure.step(
            f"Verify Rebuild Server with other image and status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._rebuild_server_and_verify_status(
                vol_list=vol_list, vol_type=vol_type, server=server, is_other_image=True
            )
        with allure.step(
            f"Verify detach multi volumes from server with VolumeType: {vol_type}"
        ):
            is_success, msg = self._detach_volumes_from_server(
                vol_list=vol_list, server_id=server["id"]
            )
            assert is_success, msg
        # Re-attach volume to test volume return 'available' status after instance deleted
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Delete Server with attached volumes. VolumeType : {vol_type}"
        ):
            self.servers_client.delete_server(server["id"])
            try:
                waiters.wait_for_server_termination(self.servers_client, server["id"])
            except lib_exc.TimeoutException:
                assert False, "Failed to instance server with attach volume"
        with allure.step(
            f"Verify volumes return to 'available' with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "available"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) return to 'available' status"

    @pytest.mark.positive
    @pytest.mark.skipif(
        not CONF.volume.volume_type_nvme, reason="NVME volume type not available."
    )
    def test_rebuild_server_with_attach_and_detach_nvme_volumes(self):
        """Test rebuilding server with attach and detach nvme type volumes."""
        vol_type = CONF.volume.volume_type_nvme
        with allure.step("Create test server"):
            server = self.create_test_server(
                validatable=True,
                validation_resources=self.validation_resources,
                wait_until="ACTIVE",
            )
            assert server["id"], f"Failed to create test server({server['id']})"
        with allure.step(f"Create multi volumes with VolumeType: {vol_type}"):
            vol_list, msg = self._create_multi_volumes(vol_type, 10)
            assert vol_list, msg
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Verify Rebuild Server with same image and status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._rebuild_server_and_verify_status(
                vol_list=vol_list,
                vol_type=vol_type,
                server=server,
                is_other_image=False,
            )
        with allure.step(
            f"Verify Rebuild Server with other image and status attached multi volumes with VolumeType: {vol_type}"
        ):
            self._rebuild_server_and_verify_status(
                vol_list=vol_list, vol_type=vol_type, server=server, is_other_image=True
            )
        with allure.step(
            f"Verify detach multi volumes from server with VolumeType: {vol_type}"
        ):
            is_success, msg = self._detach_volumes_from_server(
                vol_list=vol_list, server_id=server["id"]
            )
            assert is_success, msg
        # Re-attach volume to test volume return 'available' status after instance deleted
        with allure.step(f"Attach volumes to server with VolumeType: {vol_type}"):
            is_success, msg = self._attach_volumes_to_server(
                server_id=server["id"], vol_list=vol_list
            )
            assert is_success, msg
        with allure.step(
            f"Delete Server with attached volumes. VolumeType : {vol_type}"
        ):
            self.servers_client.delete_server(server["id"])
            try:
                waiters.wait_for_server_termination(self.servers_client, server["id"])
            except lib_exc.TimeoutException:
                assert False, "Failed to instance server with attach volume"
        with allure.step(
            f"Verify volumes return to 'available' with VolumeType:{vol_type}"
        ):
            for vol in vol_list:
                try:
                    waiters.wait_for_volume_resource_status(
                        self.volumes_client, vol["id"], "available"
                    )
                except lib_exc.TimeoutException:
                    assert (
                        False
                    ), f"Failed volume({vol['id']}) return to 'available' status"
