#    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 fixtures
from unittest import mock

from nova import context
from nova import objects
from nova import test
from nova.tests import fixtures as nova_fixtures
from nova.tests.functional.api import client as api_client
from nova.tests.functional import fixtures as func_fixtures
from nova.tests.functional import integrated_helpers


class BootFromVolumeTest(integrated_helpers._IntegratedTestBase):

    def _get_hypervisor_stats(self):
        response = self.admin_api.api_get('/os-hypervisors/statistics')
        return response.body['hypervisor_statistics']

    def _verify_zero_local_gb_used(self):
        stats = self._get_hypervisor_stats()
        self.assertEqual(0, stats['local_gb_used'])

    def _verify_instance_flavor_not_zero(self, instance_uuid):
        # We are trying to avoid saving instance records with root_gb=0
        ctxt = context.RequestContext('fake', self.api.project_id)
        instance = objects.Instance.get_by_uuid(ctxt, instance_uuid)
        self.assertNotEqual(0, instance.root_gb)
        self.assertNotEqual(0, instance.flavor.root_gb)

    def _verify_request_spec_flavor_not_zero(self, instance_uuid):
        # We are trying to avoid saving request spec records with root_gb=0
        ctxt = context.RequestContext('fake', self.api.project_id)
        rspec = objects.RequestSpec.get_by_instance_uuid(ctxt, instance_uuid)
        self.assertNotEqual(0, rspec.flavor.root_gb)

    def setUp(self):
        # These need to be set up before services are started, else they
        # won't be reflected in the running service.
        self.flags(allow_resize_to_same_host=True)
        super(BootFromVolumeTest, self).setUp()
        self.admin_api = self.api_fixture.admin_api
        self.useFixture(nova_fixtures.CinderFixture(self))
        self.useFixture(fixtures.MockPatch(
            'nova.compute.manager.ComputeVirtAPI.wait_for_instance_event'))

    def test_boot_from_volume_larger_than_local_gb(self):
        # Verify no local disk is being used currently
        self._verify_zero_local_gb_used()

        # Create flavors with disk larger than available host local disk
        flavor_id = self._create_flavor(memory_mb=64, vcpu=1, disk=8192,
                                        ephemeral=0)
        flavor_id_alt = self._create_flavor(memory_mb=64, vcpu=1, disk=16384,
                                            ephemeral=0)

        # Boot a server with a flavor disk larger than the available local
        # disk. It should succeed for boot from volume.
        server = self._build_server(image_uuid='', flavor_id=flavor_id)
        volume_uuid = nova_fixtures.CinderFixture.IMAGE_BACKED_VOL
        bdm = {'boot_index': 0,
               'uuid': volume_uuid,
               'source_type': 'volume',
               'destination_type': 'volume'}
        server['block_device_mapping_v2'] = [bdm]
        created_server = self.api.post_server({"server": server})
        server_id = created_server['id']
        self._wait_for_state_change(created_server, 'ACTIVE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Do actions that could change local disk reporting and verify they
        # don't change local disk reporting.

        # Resize
        post_data = {'resize': {'flavorRef': flavor_id_alt}}
        self.api.post_server_action(server_id, post_data)
        self._wait_for_state_change(created_server, 'VERIFY_RESIZE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Confirm the resize
        post_data = {'confirmResize': None}
        self.api.post_server_action(server_id, post_data)
        self._wait_for_state_change(created_server, 'ACTIVE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Shelve
        post_data = {'shelve': None}
        self.api.post_server_action(server_id, post_data)
        self._wait_for_state_change(created_server, 'SHELVED_OFFLOADED')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Unshelve
        post_data = {'unshelve': None}
        self.api.post_server_action(server_id, post_data)
        self._wait_for_state_change(created_server, 'ACTIVE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Rebuild
        # The image_uuid is from CinderFixture for the
        # volume representing IMAGE_BACKED_VOL.
        image_uuid = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
        post_data = {'rebuild': {'imageRef': image_uuid}}
        self.api.post_server_action(server_id, post_data)

    def test_rebuild_volume_backed_larger_than_local_gb(self):
        # Verify no local disk is being used currently
        self._verify_zero_local_gb_used()

        # Create flavors with disk larger than available host local disk
        flavor_id = self._create_flavor(memory_mb=64, vcpu=1, disk=8192,
                                        ephemeral=0)

        # Boot a server with a flavor disk larger than the available local
        # disk. It should succeed for boot from volume.
        server = self._build_server(image_uuid='', flavor_id=flavor_id)
        volume_uuid = nova_fixtures.CinderFixture.IMAGE_BACKED_VOL
        bdm = {'boot_index': 0,
               'uuid': volume_uuid,
               'source_type': 'volume',
               'destination_type': 'volume'}
        server['block_device_mapping_v2'] = [bdm]
        created_server = self.api.post_server({"server": server})
        server_id = created_server['id']
        self._wait_for_state_change(created_server, 'ACTIVE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

        # Rebuild
        # The image_uuid is from CinderFixture for the
        # volume representing IMAGE_BACKED_VOL.
        self.api.microversion = '2.93'
        image_uuid = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
        post_data = {'rebuild': {'imageRef': image_uuid}}
        self.api.post_server_action(server_id, post_data)
        self._wait_for_state_change(created_server, 'ACTIVE')

        # Check that hypervisor local disk reporting is still 0
        self._verify_zero_local_gb_used()
        # Check that instance has not been saved with 0 root_gb
        self._verify_instance_flavor_not_zero(server_id)
        # Check that request spec has not been saved with 0 root_gb
        self._verify_request_spec_flavor_not_zero(server_id)

    def test_max_local_block_devices_0_force_bfv(self):
        """Tests that when the API is configured with max_local_block_devices=0
        a user cannot boot from image, they must boot from volume.
        """
        self.flags(max_local_block_devices=0)
        server = self._build_server()
        ex = self.assertRaises(api_client.OpenStackApiException,
                               self.admin_api.post_server,
                               {'server': server})
        self.assertEqual(400, ex.response.status_code)
        self.assertIn('You specified more local devices than the limit allows',
                      str(ex))

    def test_reboot_bfv_instance(self):
        # verify bdm 'source_type': 'image' and 'destination_type': 'volume'
        server = self._create_server_boot_from_volume()
        server = self._reboot_server(server, hard=True)
        self.assertEqual(server['status'], 'ACTIVE')

    def test_reboot_bfv_instance_snapshot(self):
        # verify bdm 'source_type': 'snapshot' and 'destination_type': 'volume'
        server = self._create_server_boot_from_volume()
        self._snapshot_server(server, 'snap1')
        images = self.api.get_images()
        snap_img = [img for img in images if img['name'] == 'snap1'][0]
        server1 = self._create_server(image_uuid=snap_img['id'])
        self._reboot_server(server1, hard=True)
        self.assertEqual(server['status'], 'ACTIVE')


class BootFromVolumeLargeRequestTest(test.TestCase,
                                     integrated_helpers.InstanceHelperMixin):

    def setUp(self):
        super(BootFromVolumeLargeRequestTest, self).setUp()
        self.useFixture(nova_fixtures.RealPolicyFixture())
        self.useFixture(nova_fixtures.NeutronFixture(self))
        self.glance = self.useFixture(nova_fixtures.GlanceFixture(self))
        self.useFixture(nova_fixtures.CinderFixture(self))
        self.useFixture(func_fixtures.PlacementFixture())

        self.api = self.useFixture(nova_fixtures.OSAPIFixture(
            api_version='v2.1')).admin_api
        # The test cases will handle starting compute/conductor/scheduler
        # services if they need them.

    def test_boot_from_volume_10_servers_255_volumes_2_images(self):
        """Create 10 servers with 255 BDMs each using the same image for 200
        of the BDMs and another image for 55 other BDMs. This is a bit silly
        but it just shows that it's possible and there is no rate limiting
        involved in this type of very heavy request.
        """
        # We only care about API performance in this test case so stub out
        # conductor to not do anything.
        self.useFixture(nova_fixtures.NoopConductorFixture())
        # NOTE(gibi): Do not use 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' image
        # as that is defined with a separate kernel image, leading to one extra
        # call to nova.image.glance.API.get from compute.api
        # _handle_kernel_and_ramdisk()
        image1 = 'a2459075-d96c-40d5-893e-577ff92e721c'
        image2 = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
        server = self._build_server()
        server.pop('imageRef')
        server['min_count'] = 10
        bdms = []
        # Create 200 BDMs using image1 and 55 using image2.
        for boot_index in range(255):
            image_uuid = image2 if boot_index >= 200 else image1
            bdms.append({
                'source_type': 'image',
                'destination_type': 'volume',
                'delete_on_termination': True,
                'volume_size': 1,
                'boot_index': boot_index,
                'uuid': image_uuid
            })
        server['block_device_mapping_v2'] = bdms

        # Wrap the image service get method to check how many times it was
        # called.
        with mock.patch('nova.image.glance.API.get',
                        wraps=self.glance.show) as mock_image_get:
            self.api.post_server({'server': server})
            # Assert that there was caching of the GET /v2/images/{image_id}
            # calls. The expected total in this case is 3: one for validating
            # the root BDM image, one for image1 and one for image2 during bdm
            # validation - only the latter two cases use a cache.
            self.assertEqual(3, mock_image_get.call_count)
