#    Copyright 2015 Red Hat, Inc.
#
#    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 copy

from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import versionutils

from nova import exception
from nova.objects import base as obj_base
from nova.objects import fields

LOG = log.getLogger(__name__)
OS_VIF_DELEGATION = 'os_vif_delegation'


@obj_base.NovaObjectRegistry.register
class VIFMigrateData(obj_base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    # The majority of the fields here represent a port binding on the
    # **destination** host during a live migration. The vif_type, among
    # other fields, could be different from the existing binding on the
    # source host, which is represented by the "source_vif" field.
    fields = {
        'port_id': fields.StringField(),
        'vnic_type': fields.StringField(),  # TODO(sean-k-mooney): make enum?
        'vif_type': fields.StringField(),
        # vif_details is a dict whose contents are dependent on the vif_type
        # and can be any number of types for the values, so we just store it
        # as a serialized dict
        'vif_details_json': fields.StringField(),
        # profile is in the same random dict of terrible boat as vif_details
        # so it's stored as a serialized json string
        'profile_json': fields.StringField(),
        'host': fields.StringField(),
        # The source_vif attribute is a copy of the VIF network model
        # representation of the port on the source host which can be used
        # for filling in blanks about the VIF (port) when building a
        # configuration reference for the destination host.
        # NOTE(mriedem): This might not be sufficient based on how the
        # destination host is configured for all vif types. See the note in
        # the libvirt driver here: https://review.opendev.org/#/c/551370/
        # 29/nova/virt/libvirt/driver.py@7036
        'source_vif': fields.NetworkVIFModelField(),
    }

    @property
    def vif_details(self):
        if 'vif_details_json' not in self:
            return {}
        return jsonutils.loads(self.vif_details_json)

    @vif_details.setter
    def vif_details(self, vif_details_dict):
        self.vif_details_json = jsonutils.dumps(vif_details_dict)

    @property
    def profile(self):
        if 'profile_json' not in self:
            return {}
        return jsonutils.loads(self.profile_json)

    @profile.setter
    def profile(self, profile_dict):
        self.profile_json = jsonutils.dumps(profile_dict)

    @property
    def supports_os_vif_delegation(self):
        return self.profile.get(OS_VIF_DELEGATION, False)

    # TODO(stephenfin): add a proper delegation field instead of storing this
    # info in the profile catch-all blob
    @supports_os_vif_delegation.setter
    def supports_os_vif_delegation(self, supported):
        # we can't simply set the attribute using dict notation since the
        # getter returns a copy of the data, not the data itself
        self.profile = dict(
            self.profile or {}, **{OS_VIF_DELEGATION: supported})

    def get_dest_vif(self):
        """Get a destination VIF representation of this object.

        This method takes the source_vif and updates it to include the
        destination host port binding information using the other fields
        on this object.

        :return: nova.network.model.VIF object
        """
        if 'source_vif' not in self:
            raise exception.ObjectActionError(
                action='get_dest_vif', reason='source_vif is not set')
        vif = copy.deepcopy(self.source_vif)
        vif['type'] = self.vif_type
        vif['vnic_type'] = self.vnic_type
        vif['profile'] = self.profile
        vif['details'] = self.vif_details
        vif['delegate_create'] = self.supports_os_vif_delegation
        return vif

    @classmethod
    def create_skeleton_migrate_vifs(cls, vifs):
        """Create migrate vifs for live migration.

        :param vifs: a list of VIFs.
        :return: list of VIFMigrateData object corresponding to the provided
                 VIFs.
        """
        vif_mig_data = []

        for vif in vifs:
            mig_vif = cls(port_id=vif['id'], source_vif=vif)
            vif_mig_data.append(mig_vif)
        return vif_mig_data


@obj_base.NovaObjectRegistry.register
class LibvirtLiveMigrateNUMAInfo(obj_base.NovaObject):
    # Version 1.0: Initial version
    VERSION = '1.0'

    fields = {
        # NOTE(artom) We need a 1:many cardinality here, so DictOfIntegers with
        # its 1:1 cardinality cannot work here. cpu_pins can have a single
        # guest CPU pinned to multiple host CPUs.
        'cpu_pins': fields.DictOfSetOfIntegersField(),
        # NOTE(artom) Currently we never pin a guest cell to more than a single
        # host cell, so cell_pins could be a DictOfIntegers, but
        # DictOfSetOfIntegers is more future-proof.
        'cell_pins': fields.DictOfSetOfIntegersField(),
        'emulator_pins': fields.SetOfIntegersField(),
        'sched_vcpus': fields.SetOfIntegersField(),
        'sched_priority': fields.IntegerField(),
    }


@obj_base.NovaObjectRegistry.register_if(False)
class LiveMigrateData(obj_base.NovaObject):
    # Version 1.0: Initial version
    # Version 1.1: Added old_vol_attachment_ids field.
    # Version 1.2: Added wait_for_vif_plugged
    # Version 1.3: Added vifs field.
    # Version 1.4: Added pci_dev_map_src_dst field.
    VERSION = '1.4'

    fields = {
        'is_volume_backed': fields.BooleanField(),
        'migration': fields.ObjectField('Migration'),
        # old_vol_attachment_ids is a dict used to store the old attachment_ids
        # for each volume so they can be restored on a migration rollback. The
        # key is the volume_id, and the value is the attachment_id.
        # TODO(mdbooth): This field was made redundant by change Ibe9215c0. We
        # should eventually remove it.
        'old_vol_attachment_ids': fields.DictOfStringsField(),
        # wait_for_vif_plugged is set in pre_live_migration on the destination
        # compute host based on the [compute]/live_migration_wait_for_vif_plug
        # config option value; a default value is not set here since the
        # default for the config option may change in the future
        'wait_for_vif_plugged': fields.BooleanField(),
        'vifs': fields.ListOfObjectsField('VIFMigrateData'),
        'pci_dev_map_src_dst': fields.DictOfStringsField(),
    }


@obj_base.NovaObjectRegistry.register
class LibvirtLiveMigrateBDMInfo(obj_base.NovaObject):
    # VERSION 1.0 : Initial version
    # VERSION 1.1 : Added encryption_secret_uuid for tracking volume secret
    #               uuid created on dest during migration with encrypted vols.
    VERSION = '1.1'

    fields = {
        # FIXME(danms): some of these can be enums?
        'serial': fields.StringField(),
        'bus': fields.StringField(),
        'dev': fields.StringField(),
        'type': fields.StringField(),
        'format': fields.StringField(nullable=True),
        'boot_index': fields.IntegerField(nullable=True),
        'connection_info_json': fields.StringField(),
        'encryption_secret_uuid': fields.UUIDField(nullable=True),
    }

    def obj_make_compatible(self, primitive, target_version):
        super(LibvirtLiveMigrateBDMInfo, self).obj_make_compatible(
            primitive, target_version)
        target_version = versionutils.convert_version_to_tuple(target_version)
        if target_version < (1, 1) and 'encryption_secret_uuid' in primitive:
            del primitive['encryption_secret_uuid']

    # NOTE(danms): We don't have a connection_info object right
    # now, and instead mostly store/pass it as JSON that we're
    # careful with. When we get a connection_info object in the
    # future, we should use it here, so make this easy to convert
    # for later.
    @property
    def connection_info(self):
        return jsonutils.loads(self.connection_info_json)

    @connection_info.setter
    def connection_info(self, info):
        self.connection_info_json = jsonutils.dumps(info)

    def as_disk_info(self):
        info_dict = {
            'dev': self.dev,
            'bus': self.bus,
            'type': self.type,
        }
        if self.obj_attr_is_set('format') and self.format:
            info_dict['format'] = self.format
        if self.obj_attr_is_set('boot_index') and self.boot_index is not None:
            info_dict['boot_index'] = str(self.boot_index)
        return info_dict


@obj_base.NovaObjectRegistry.register
class LibvirtLiveMigrateData(LiveMigrateData):
    # Version 1.0: Initial version
    # Version 1.1: Added target_connect_addr
    # Version 1.2: Added 'serial_listen_ports' to allow live migration with
    #              serial console.
    # Version 1.3: Added 'supported_perf_events'
    # Version 1.4: Added old_vol_attachment_ids
    # Version 1.5: Added src_supports_native_luks
    # Version 1.6: Added wait_for_vif_plugged
    # Version 1.7: Added dst_wants_file_backed_memory
    # Version 1.8: Added file_backed_memory_discard
    # Version 1.9: Inherited vifs from LiveMigrateData
    # Version 1.10: Added dst_numa_info, src_supports_numa_live_migration, and
    #               dst_supports_numa_live_migration fields
    # Version 1.11: Added dst_supports_mdev_live_migration,
    #               source_mdev_types and target_mdevs fields
    # Version 1.12: Added dst_cpu_shared_set_info
    # Version 1.13: Inherited pci_dev_map_src_dst from LiveMigrateData
    VERSION = '1.13'

    fields = {
        'filename': fields.StringField(),
        # FIXME: image_type should be enum?
        'image_type': fields.StringField(),
        'block_migration': fields.BooleanField(),
        'disk_over_commit': fields.BooleanField(),
        'disk_available_mb': fields.IntegerField(nullable=True),
        'is_shared_instance_path': fields.BooleanField(),
        'is_shared_block_storage': fields.BooleanField(),
        'instance_relative_path': fields.StringField(),
        'graphics_listen_addr_vnc': fields.IPAddressField(nullable=True),
        'graphics_listen_addr_spice': fields.IPAddressField(nullable=True),
        'serial_listen_addr': fields.StringField(nullable=True),
        'serial_listen_ports': fields.ListOfIntegersField(),
        'bdms': fields.ListOfObjectsField('LibvirtLiveMigrateBDMInfo'),
        'target_connect_addr': fields.StringField(nullable=True),
        'supported_perf_events': fields.ListOfStringsField(),
        # TODO(lyarwood): No longer used, drop in version 2.0
        'src_supports_native_luks': fields.BooleanField(),
        'dst_wants_file_backed_memory': fields.BooleanField(),
        # TODO(lyarwood): No longer used, drop in version 2.0
        'file_backed_memory_discard': fields.BooleanField(),
        # TODO(artom) (src|dst)_supports_numa_live_migration are only used as
        # flags to indicate that the compute host is new enough to perform a
        # NUMA-aware live migration. Remove in version 2.0.
        'src_supports_numa_live_migration': fields.BooleanField(),
        'dst_supports_numa_live_migration': fields.BooleanField(),
        'dst_numa_info': fields.ObjectField('LibvirtLiveMigrateNUMAInfo'),
        # TODO(sbauza) dst_supports_mdev_live_migration is only used as
        # flag to indicate that the compute host is new enough to perform a
        # mediated-device-aware live migration. Remove in version 2.0.
        'dst_supports_mdev_live_migration': fields.BooleanField(),
        # key is mdev UUID and value is its type.
        'source_mdev_types': fields.DictOfStringsField(),
        # key is source mdev UUID and value is the destination mdev UUID.
        'target_mdevs': fields.DictOfStringsField(),
        'dst_cpu_shared_set_info': fields.SetOfIntegersField(),
    }

    def obj_make_compatible(self, primitive, target_version):
        super(LibvirtLiveMigrateData, self).obj_make_compatible(
            primitive, target_version)
        target_version = versionutils.convert_version_to_tuple(target_version)
        if (target_version < (1, 13)):
            primitive.pop('pci_dev_map_src_dst', None)
        if (target_version < (1, 12)):
            primitive.pop('dst_cpu_shared_set_info', None)
        if target_version < (1, 11):
            primitive.pop('target_mdevs', None)
            primitive.pop('source_mdev_types', None)
            primitive.pop('dst_supports_mdev_live_migration', None)
        if (target_version < (1, 10) and
                'src_supports_numa_live_migration' in primitive):
            del primitive['src_supports_numa_live_migration']
        if (target_version < (1, 10) and
                'dst_supports_numa_live_migration' in primitive):
            del primitive['dst_supports_numa_live_migration']
        if target_version < (1, 10) and 'dst_numa_info' in primitive:
            del primitive['dst_numa_info']
        if target_version < (1, 9) and 'vifs' in primitive:
            del primitive['vifs']
        if target_version < (1, 8):
            if 'file_backed_memory_discard' in primitive:
                del primitive['file_backed_memory_discard']
        if target_version < (1, 7):
            if 'dst_wants_file_backed_memory' in primitive:
                del primitive['dst_wants_file_backed_memory']
        if target_version < (1, 6) and 'wait_for_vif_plugged' in primitive:
            del primitive['wait_for_vif_plugged']
        if target_version < (1, 5):
            if 'src_supports_native_luks' in primitive:
                del primitive['src_supports_native_luks']
        if target_version < (1, 4):
            if 'old_vol_attachment_ids' in primitive:
                del primitive['old_vol_attachment_ids']
        if target_version < (1, 3):
            if 'supported_perf_events' in primitive:
                del primitive['supported_perf_events']
        if target_version < (1, 2):
            if 'serial_listen_ports' in primitive:
                del primitive['serial_listen_ports']
        if target_version < (1, 1) and 'target_connect_addr' in primitive:
            del primitive['target_connect_addr']

    def is_on_shared_storage(self):
        return self.is_shared_block_storage or self.is_shared_instance_path


# TODO(gmann): HyperV virt driver has been removed in Nova 29.0.0 (OpenStack
# 2024.1) release but we kept this object for a couple of cycle. This can be
# removed too in Nova 31.0.0 (OpenStack 2025.1) or later.
@obj_base.NovaObjectRegistry.register
class HyperVLiveMigrateData(LiveMigrateData):
    # Version 1.0: Initial version
    # Version 1.1: Added is_shared_instance_path
    # Version 1.2: Added old_vol_attachment_ids
    # Version 1.3: Added wait_for_vif_plugged
    # Version 1.4: Inherited vifs from LiveMigrateData
    # Version 1.5: Inherited pci_dev_map_src_dst from LiveMigrateData
    VERSION = '1.5'

    fields = {'is_shared_instance_path': fields.BooleanField()}

    def obj_make_compatible(self, primitive, target_version):
        super(HyperVLiveMigrateData, self).obj_make_compatible(
            primitive, target_version)
        target_version = versionutils.convert_version_to_tuple(target_version)
        if (target_version < (1, 5)):
            primitive.pop('pci_dev_map_src_dst', None)
        if target_version < (1, 4) and 'vifs' in primitive:
            del primitive['vifs']
        if target_version < (1, 3) and 'wait_for_vif_plugged' in primitive:
            del primitive['wait_for_vif_plugged']
        if target_version < (1, 2):
            if 'old_vol_attachment_ids' in primitive:
                del primitive['old_vol_attachment_ids']
        if target_version < (1, 1):
            if 'is_shared_instance_path' in primitive:
                del primitive['is_shared_instance_path']


@obj_base.NovaObjectRegistry.register
class VMwareLiveMigrateData(LiveMigrateData):
    # Version 1.0: Initial version
    # Version 1.1: Inherited pci_dev_map_src_dst from LiveMigrateData
    VERSION = '1.1'

    fields = {
        'cluster_name': fields.StringField(nullable=False),
        'datastore_regex': fields.StringField(nullable=False),
    }

    def obj_make_compatible(self, primitive, target_version):
        super(VMwareLiveMigrateData, self).obj_make_compatible(
            primitive, target_version)
        target_version = versionutils.convert_version_to_tuple(target_version)
        if (target_version < (1, 1)):
            primitive.pop('pci_dev_map_src_dst', None)
