# Copyright 2012 Nebula, 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 logging

from django.conf import settings
from django.http import HttpResponse
from django import shortcuts
from django import template
from django.template.defaultfilters import title
from django import urls
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from django.utils.translation import npgettext_lazy
from django.utils.translation import pgettext_lazy
import netaddr

from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon.templatetags import sizeformat
from horizon.utils import filters

from openstack_dashboard import api
from openstack_dashboard.dashboards.project.floating_ips import workflows
from openstack_dashboard.dashboards.project.instances import tabs
from openstack_dashboard.dashboards.project.instances \
    import utils as instance_utils
from openstack_dashboard.dashboards.project.instances.workflows \
    import resize_instance
from openstack_dashboard.dashboards.project.instances.workflows \
    import update_instance
from openstack_dashboard import policy
from openstack_dashboard.views import get_url_with_pagination

LOG = logging.getLogger(__name__)

ACTIVE_STATES = ("ACTIVE",)
VOLUME_ATTACH_READY_STATES = ("ACTIVE", "SHUTOFF")
SNAPSHOT_READY_STATES = ("ACTIVE", "SHUTOFF", "PAUSED", "SUSPENDED")
SHELVE_READY_STATES = ("ACTIVE", "SHUTOFF", "PAUSED", "SUSPENDED")

POWER_STATES = {
    0: "NO STATE",
    1: "RUNNING",
    2: "BLOCKED",
    3: "PAUSED",
    4: "SHUTDOWN",
    5: "SHUTOFF",
    6: "CRASHED",
    7: "SUSPENDED",
    8: "FAILED",
    9: "BUILDING",
}

PAUSE = 0
UNPAUSE = 1
SUSPEND = 0
RESUME = 1
SHELVE = 0
UNSHELVE = 1


def is_deleting(instance):
    task_state = getattr(instance, "OS-EXT-STS:task_state", None)
    if not task_state:
        return False
    return task_state.lower() == "deleting"


class DeleteInstance(policy.PolicyTargetMixin, tables.DeleteAction):
    policy_rules = (("compute", "os_compute_api:servers:delete"),)
    help_text = _("Deleted instances are not recoverable.")
    default_message_level = "info"

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Delete Instance",
            "Delete Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Scheduled deletion of Instance",
            "Scheduled deletion of Instances",
            count
        )

    def allowed(self, request, instance=None):
        error_state = False
        if instance:
            error_state = (instance.status == 'ERROR')
        return error_state or not is_deleting(instance)

    def action(self, request, obj_id):
        api.nova.server_delete(request, obj_id)


class RebootInstance(policy.PolicyTargetMixin, tables.BatchAction):
    name = "reboot"
    classes = ('btn-reboot',)
    policy_rules = (("compute", "os_compute_api:servers:reboot"),)
    help_text = _("Restarted instances will lose any data"
                  " not saved in persistent storage.")
    action_type = "danger"

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Hard Reboot Instance",
            "Hard Reboot Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Hard Rebooted Instance",
            "Hard Rebooted Instances",
            count
        )

    def allowed(self, request, instance=None):
        if instance is None:
            return True
        return ((instance.status in ACTIVE_STATES or
                 instance.status == 'SHUTOFF') and
                not is_deleting(instance))

    def action(self, request, obj_id):
        api.nova.server_reboot(request, obj_id, soft_reboot=False)


class SoftRebootInstance(RebootInstance):
    name = "soft_reboot"

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Soft Reboot Instance",
            "Soft Reboot Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Soft Rebooted Instance",
            "Soft Rebooted Instances",
            count
        )

    def action(self, request, obj_id):
        api.nova.server_reboot(request, obj_id, soft_reboot=True)

    def allowed(self, request, instance=None):
        if instance is not None:
            return instance.status in ACTIVE_STATES
        return True


class RescueInstance(policy.PolicyTargetMixin, tables.LinkAction):
    name = "rescue"
    verbose_name = _("Rescue Instance")
    policy_rules = (("compute", "os_compute_api:os-rescue"),)
    classes = ("btn-rescue", "ajax-modal")
    url = "horizon:project:instances:rescue"

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        return urls.reverse(self.url, args=[instance_id])

    def allowed(self, request, instance):
        return instance.status in ACTIVE_STATES


class UnRescueInstance(tables.BatchAction):
    name = 'unrescue'
    classes = ("btn-unrescue",)

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Unrescue Instance",
            "Unrescue Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Unrescued Instance",
            "Unrescued Instances",
            count
        )

    def action(self, request, obj_id):
        api.nova.server_unrescue(request, obj_id)

    def allowed(self, request, instance=None):
        if instance:
            return instance.status == "RESCUE"
        return False


class TogglePause(tables.BatchAction):
    name = "pause"
    icon = "pause"

    @staticmethod
    def action_present(count):
        return (
            ngettext_lazy(
                "Pause Instance",
                "Pause Instances",
                count
            ),
            ngettext_lazy(
                "Resume Instance",
                "Resume Instances",
                count
            ),
        )

    @staticmethod
    def action_past(count):
        return (
            ngettext_lazy(
                "Paused Instance",
                "Paused Instances",
                count
            ),
            ngettext_lazy(
                "Resumed Instance",
                "Resumed Instances",
                count
            ),
        )

    def allowed(self, request, instance=None):
        if not instance:
            return False
        self.paused = instance.status == "PAUSED"
        if self.paused:
            self.current_present_action = UNPAUSE
            policy_rules = (
                ("compute", "os_compute_api:os-pause-server:unpause"),)
        else:
            self.current_present_action = PAUSE
            policy_rules = (
                ("compute", "os_compute_api:os-pause-server:pause"),)

        has_permission = policy.check(
            policy_rules, request,
            target={'project_id': getattr(instance, 'tenant_id', None)})

        return (has_permission and
                (instance.status in ACTIVE_STATES or self.paused) and
                not is_deleting(instance))

    def action(self, request, obj_id):
        if self.paused:
            api.nova.server_unpause(request, obj_id)
            self.current_past_action = UNPAUSE
        else:
            api.nova.server_pause(request, obj_id)
            self.current_past_action = PAUSE


class ToggleSuspend(tables.BatchAction):
    name = "suspend"
    classes = ("btn-suspend",)

    @staticmethod
    def action_present(count):
        return (
            ngettext_lazy(
                "Suspend Instance",
                "Suspend Instances",
                count
            ),
            ngettext_lazy(
                "Resume Instance",
                "Resume Instances",
                count
            ),
        )

    @staticmethod
    def action_past(count):
        return (
            ngettext_lazy(
                "Suspended Instance",
                "Suspended Instances",
                count
            ),
            ngettext_lazy(
                "Resumed Instance",
                "Resumed Instances",
                count
            ),
        )

    def allowed(self, request, instance=None):
        if not instance:
            return False
        self.suspended = instance.status == "SUSPENDED"
        if self.suspended:
            self.current_present_action = RESUME
            policy_rules = (
                ("compute", "os_compute_api:os-suspend-server:resume"),)
        else:
            self.current_present_action = SUSPEND
            policy_rules = (
                ("compute", "os_compute_api:os-suspend-server:suspend"),)

        has_permission = policy.check(
            policy_rules, request,
            target={'project_id': getattr(instance, 'tenant_id', None)})

        return (has_permission and
                (instance.status in ACTIVE_STATES or self.suspended) and
                not is_deleting(instance))

    def action(self, request, obj_id):
        if self.suspended:
            api.nova.server_resume(request, obj_id)
            self.current_past_action = RESUME
        else:
            api.nova.server_suspend(request, obj_id)
            self.current_past_action = SUSPEND


class ToggleShelve(tables.BatchAction):
    name = "shelve"
    icon = "shelve"

    @staticmethod
    def action_present(count):
        return (
            ngettext_lazy(
                "Shelve Instance",
                "Shelve Instances",
                count
            ),
            ngettext_lazy(
                "Unshelve Instance",
                "Unshelve Instances",
                count
            ),
        )

    @staticmethod
    def action_past(count):
        return (
            ngettext_lazy(
                "Shelved Instance",
                "Shelved Instances",
                count
            ),
            ngettext_lazy(
                "Unshelved Instance",
                "Unshelved Instances",
                count
            ),
        )

    def allowed(self, request, instance=None):
        if not instance:
            return False
        if not request.user.is_superuser and getattr(
                instance, 'locked', False):
            return False

        self.shelved = instance.status == "SHELVED_OFFLOADED"
        if self.shelved:
            self.current_present_action = UNSHELVE
            policy_rules = (("compute", "os_compute_api:os-shelve:unshelve"),)
        else:
            self.current_present_action = SHELVE
            policy_rules = (("compute", "os_compute_api:os-shelve:shelve"),)

        has_permission = policy.check(
            policy_rules, request,
            target={'project_id': getattr(instance, 'tenant_id', None)})

        return (has_permission and
                (instance.status in SHELVE_READY_STATES or self.shelved) and
                not is_deleting(instance))

    def action(self, request, obj_id):
        if self.shelved:
            api.nova.server_unshelve(request, obj_id)
            self.current_past_action = UNSHELVE
        else:
            api.nova.server_shelve(request, obj_id)
            self.current_past_action = SHELVE


class LaunchLinkNG(tables.LinkAction):
    name = "launch-ng"
    verbose_name = _("Launch Instance")
    url = "horizon:project:instances:index"
    ajax = False
    classes = ("btn-launch", )
    icon = "cloud-upload"
    policy_rules = (("compute", "os_compute_api:servers:create"),)

    def __init__(self, attrs=None, **kwargs):
        kwargs['preempt'] = True
        super().__init__(attrs, **kwargs)

    def allowed(self, request, datum):
        try:
            limits = api.nova.tenant_absolute_limits(request, reserved=True)

            instances_available = limits['maxTotalInstances'] \
                - limits['totalInstancesUsed']
            cores_available = limits['maxTotalCores'] \
                - limits['totalCoresUsed']
            ram_available = limits['maxTotalRAMSize'] - limits['totalRAMUsed']

            if instances_available <= 0 or cores_available <= 0 \
                    or ram_available <= 0:
                if "disabled" not in self.classes:
                    self.classes = list(self.classes) + ['disabled']
                    self.verbose_name = format_lazy(
                        '{verbose_name} {quota_exceeded}',
                        verbose_name=self.verbose_name,
                        quota_exceeded=_("(Quota exceeded)"))
            else:
                self.verbose_name = _("Launch Instance")
                classes = [c for c in self.classes if c != "disabled"]
                self.classes = classes
        except Exception:
            LOG.exception("Failed to retrieve quota information")
            # If we can't get the quota information, leave it to the
            # API to check when launching
        return True  # The action should always be displayed

    def single(self, table, request, object_id=None):
        self.allowed(request, None)
        return HttpResponse(self.render(is_table_action=True))

    def get_default_attrs(self):
        url = urls.reverse(self.url)
        ngclick = "modal.openLaunchInstanceWizard(" \
            "{ successUrl: '%s' })" % url
        self.attrs.update({
            'ng-controller': 'LaunchInstanceModalController as modal',
            'ng-click': ngclick
        })
        return super().get_default_attrs()

    def get_link_url(self, datum=None):
        return "javascript:void(0);"


class EditInstance(policy.PolicyTargetMixin, tables.LinkAction):
    name = "edit"
    verbose_name = _("Edit Instance")
    url = "horizon:project:instances:update"
    classes = ("ajax-modal",)
    icon = "pencil"
    policy_rules = (("compute", "os_compute_api:servers:update"),)

    def get_link_url(self, project):
        return self._get_link_url(project, 'instance_info')

    def _get_link_url(self, project, step_slug):
        base_url = urls.reverse(self.url, args=[project.id])
        next_url = self.table.get_full_url()
        params = {"step": step_slug,
                  update_instance.UpdateInstance.redirect_param_name: next_url}
        param = urlencode(params)
        return "?".join([base_url, param])

    def allowed(self, request, instance):
        return not is_deleting(instance)


class EditInstanceSecurityGroups(EditInstance):
    name = "edit_secgroups"
    verbose_name = _("Edit Security Groups")

    def get_link_url(self, project):
        return self._get_link_url(project, 'update_security_groups')

    def allowed(self, request, instance=None):
        if not api.base.is_service_enabled(request, 'network'):
            return False
        return (instance.status in ACTIVE_STATES and
                not is_deleting(instance) and
                request.user.tenant_id == instance.tenant_id)


class EditPortSecurityGroups(tables.LinkAction):
    name = "edit_port_secgroups"
    verbose_name = _("Edit Port Security Groups")
    policy_rules = (("network", "update_security_group"),)
    url = "horizon:project:instances:detail"
    icon = "pencil"

    def get_link_url(self, instance):
        base_url = urls.reverse(self.url, args=[instance.id])
        return '%s?tab=%s__%s' % (base_url, 'instance_details', 'interfaces')


class CreateSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
    name = "snapshot"
    verbose_name = _("Create Snapshot")
    url = "horizon:project:images:snapshots:create"
    classes = ("ajax-modal",)
    icon = "camera"
    policy_rules = (("compute", "os_compute_api:snapshot"),)

    def allowed(self, request, instance=None):
        return instance.status in SNAPSHOT_READY_STATES \
            and not is_deleting(instance)


class ConsoleLink(policy.PolicyTargetMixin, tables.LinkAction):
    name = "console"
    verbose_name = _("Console")
    url = "horizon:project:instances:detail"
    classes = ("btn-console",)
    policy_rules = (("compute", "os_compute_api:os-consoles:index"),)

    def allowed(self, request, instance=None):
        # We check if ConsoleLink is allowed only if settings.CONSOLE_TYPE is
        # not set at all, or if it's set to any value other than None or False.
        return (bool(settings.CONSOLE_TYPE) and
                instance.status in ACTIVE_STATES and
                not is_deleting(instance))

    def get_link_url(self, datum):
        base_url = super().get_link_url(datum)
        tab_query_string = tabs.ConsoleTab(
            tabs.InstanceDetailTabs).get_query_string()
        return "?".join([base_url, tab_query_string])


class LogLink(policy.PolicyTargetMixin, tables.LinkAction):
    name = "log"
    verbose_name = _("View Log")
    url = "horizon:project:instances:detail"
    classes = ("btn-log",)
    policy_rules = (("compute", "os_compute_api:os-console-output"),)

    def allowed(self, request, instance=None):
        return instance.status in ACTIVE_STATES and not is_deleting(instance)

    def get_link_url(self, datum):
        base_url = super().get_link_url(datum)
        tab_query_string = tabs.LogTab(
            tabs.InstanceDetailTabs).get_query_string()
        return "?".join([base_url, tab_query_string])


class ResizeLink(policy.PolicyTargetMixin, tables.LinkAction):
    name = "resize"
    verbose_name = _("Resize Instance")
    url = "horizon:project:instances:resize"
    classes = ("ajax-modal", "btn-resize")
    policy_rules = (("compute", "os_compute_api:servers:resize"),)
    action_type = "danger"

    def get_link_url(self, project):
        return self._get_link_url(project, 'flavor_choice')

    def _get_link_url(self, project, step_slug):
        base_url = urls.reverse(self.url, args=[project.id])
        next_url = self.table.get_full_url()
        params = {"step": step_slug,
                  resize_instance.ResizeInstance.redirect_param_name: next_url}
        param = urlencode(params)
        return "?".join([base_url, param])

    def allowed(self, request, instance):
        return ((instance.status in ACTIVE_STATES or
                 instance.status == 'SHUTOFF') and
                not is_deleting(instance))


class ConfirmResize(policy.PolicyTargetMixin, tables.Action):
    name = "confirm"
    verbose_name = _("Confirm Resize/Migrate")
    classes = ("btn-confirm", "btn-action-required")
    policy_rules = (("compute", "os_compute_api:servers:confirm_resize"),)

    def allowed(self, request, instance):
        return instance.status == 'VERIFY_RESIZE'

    def single(self, table, request, obj_id):
        instance = table.get_object_by_id(obj_id)
        try:
            api.nova.server_confirm_resize(request, instance.id)
        except Exception:
            exceptions.handle(request,
                              _('Unable to confirm resize instance "%s".')
                              % (instance.name or instance.id))
        return shortcuts.redirect(request.get_full_path())


class RevertResize(policy.PolicyTargetMixin, tables.Action):
    name = "revert"
    verbose_name = _("Revert Resize/Migrate")
    classes = ("btn-revert", "btn-action-required")
    policy_rules = (("compute", "os_compute_api:servers:revert_resize"),)

    def allowed(self, request, instance):
        return instance.status == 'VERIFY_RESIZE'

    def single(self, table, request, obj_id):
        instance = table.get_object_by_id(obj_id)
        try:
            api.nova.server_revert_resize(request, instance.id)
        except Exception:
            exceptions.handle(request,
                              _('Unable to revert resize instance "%s".')
                              % (instance.name or instance.id))


class RebuildInstance(policy.PolicyTargetMixin, tables.LinkAction):
    name = "rebuild"
    verbose_name = _("Rebuild Instance")
    classes = ("btn-rebuild", "ajax-modal")
    url = "horizon:project:instances:rebuild"
    policy_rules = (("compute", "os_compute_api:servers:rebuild"),)
    action_type = "danger"

    def allowed(self, request, instance):
        return ((instance.status in ACTIVE_STATES or
                 instance.status == 'SHUTOFF') and
                not is_deleting(instance))

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        return urls.reverse(self.url, args=[instance_id])


class DecryptInstancePassword(tables.LinkAction):
    name = "decryptpassword"
    verbose_name = _("Retrieve Password")
    classes = ("btn-decrypt", "ajax-modal")
    url = "horizon:project:instances:decryptpassword"

    def allowed(self, request, instance):
        return (settings.OPENSTACK_ENABLE_PASSWORD_RETRIEVE and
                (instance.status in ACTIVE_STATES or
                 instance.status == 'SHUTOFF') and
                not is_deleting(instance) and
                get_keyname(instance) is not None)

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        keypair_name = get_keyname(datum)
        return urls.reverse(self.url, args=[instance_id,
                                            keypair_name])


class AssociateIP(policy.PolicyTargetMixin, tables.LinkAction):
    name = "associate"
    verbose_name = _("Associate Floating IP")
    url = "horizon:project:floating_ips:associate"
    classes = ("ajax-modal",)
    icon = "link"
    policy_rules = (("network", "update_floatingip"),)

    def allowed(self, request, instance):
        if not api.base.is_service_enabled(request, 'network'):
            return False
        if not api.neutron.floating_ip_supported(request):
            return False
        if api.neutron.floating_ip_simple_associate_supported(request):
            return False
        if instance.status == "ERROR":
            return False
        for addresses in instance.addresses.values():
            for address in addresses:
                if address.get('OS-EXT-IPS:type') == "floating":
                    return False
        return not is_deleting(instance)

    def get_link_url(self, datum):
        base_url = urls.reverse(self.url)
        next_url = self.table.get_full_url()
        params = {
            "instance_id": self.table.get_object_id(datum),
            workflows.IPAssociationWorkflow.redirect_param_name: next_url}
        params = urlencode(params)
        return "?".join([base_url, params])


class DisassociateIP(tables.LinkAction):
    name = "disassociate"
    verbose_name = _("Disassociate Floating IP")
    url = "horizon:project:instances:disassociate"
    classes = ("btn-disassociate", 'ajax-modal')
    policy_rules = (("network", "update_floatingip"),)
    action_type = "danger"

    def allowed(self, request, instance):
        if not api.base.is_service_enabled(request, 'network'):
            return False
        if not api.neutron.floating_ip_supported(request):
            return False
        for addresses in instance.addresses.values():
            for address in addresses:
                if address.get('OS-EXT-IPS:type') == "floating":
                    return not is_deleting(instance)
        return False


class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
    name = "update_metadata"
    verbose_name = _("Update Metadata")
    ajax = False
    icon = "pencil"
    attrs = {"ng-controller": "MetadataModalHelperController as modal"}
    policy_rules = (("compute", "os_compute_api:server-metadata:update"),)

    def __init__(self, attrs=None, **kwargs):
        kwargs['preempt'] = True
        super().__init__(attrs, **kwargs)

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        self.attrs['ng-click'] = (
            "modal.openMetadataModal('instance', '%s', true, 'metadata')"
            % instance_id)
        return "javascript:void(0);"

    def allowed(self, request, instance=None):
        return (instance and
                instance.status.lower() != 'error')


def instance_fault_to_friendly_message(instance):
    fault = getattr(instance, 'fault', {})
    message = fault.get('message', _("Unknown"))
    default_message = _("Please try again later [Error: %s].") % message
    fault_map = {
        'NoValidHost': _("There is not enough capacity for this "
                         "flavor in the selected availability zone. "
                         "Try again later or select a different availability "
                         "zone.")
    }
    return fault_map.get(message, default_message)


def get_instance_error(instance):
    if instance.status.lower() != 'error':
        return None
    message = instance_fault_to_friendly_message(instance)
    preamble = _('Failed to perform requested operation on instance "%s", the '
                 'instance has an error status') % instance.name or instance.id
    message = format_lazy('{preamble}: {message}',
                          preamble=preamble, message=message)
    return message


class UpdateRow(tables.Row):
    ajax = True

    def get_data(self, request, instance_id):
        instance = api.nova.server_get(request, instance_id)
        try:
            instance.full_flavor = instance_utils.resolve_flavor(request,
                                                                 instance)
        except Exception:
            exceptions.handle(request,
                              _('Unable to retrieve flavor information '
                                'for instance "%s".') % instance_id,
                              ignore=True)
        try:
            api.network.servers_update_addresses(request, [instance])
        except Exception:
            exceptions.handle(request,
                              _('Unable to retrieve Network information '
                                'for instance "%s".') % instance_id,
                              ignore=True)
        error = get_instance_error(instance)
        if error:
            messages.error(request, error)
        return instance


class StartInstance(policy.PolicyTargetMixin, tables.BatchAction):
    name = "start"
    classes = ('btn-confirm',)
    policy_rules = (("compute", "os_compute_api:servers:start"),)

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Start Instance",
            "Start Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Started Instance",
            "Started Instances",
            count
        )

    def allowed(self, request, instance):
        return ((instance is None) or
                (instance.status in ("SHUTDOWN", "SHUTOFF", "CRASHED")))

    def action(self, request, obj_id):
        api.nova.server_start(request, obj_id)


class StopInstance(policy.PolicyTargetMixin, tables.BatchAction):
    name = "stop"
    policy_rules = (("compute", "os_compute_api:servers:stop"),)
    help_text = _("The instance(s) will be shut off.")
    action_type = "danger"

    @staticmethod
    def action_present(count):
        return npgettext_lazy(
            "Action to perform (the instance is currently running)",
            "Shut Off Instance",
            "Shut Off Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return npgettext_lazy(
            "Past action (the instance is currently already Shut Off)",
            "Shut Off Instance",
            "Shut Off Instances",
            count
        )

    def allowed(self, request, instance):
        return (instance is None or
                (get_power_state(instance) in ("RUNNING", "SUSPENDED") and
                 not is_deleting(instance)))

    def action(self, request, obj_id):
        api.nova.server_stop(request, obj_id)


class LockInstance(policy.PolicyTargetMixin, tables.BatchAction):
    name = "lock"
    policy_rules = (("compute", "os_compute_api:os-lock-server:lock"),)

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Lock Instance",
            "Lock Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Locked Instance",
            "Locked Instances",
            count
        )

    # to only allow unlocked instances to be locked
    def allowed(self, request, instance):
        if getattr(instance, 'locked', False):
            return False
        if not api.nova.is_feature_available(request, "locked_attribute"):
            return False
        return True

    def action(self, request, obj_id):
        api.nova.server_lock(request, obj_id)


class UnlockInstance(policy.PolicyTargetMixin, tables.BatchAction):
    name = "unlock"
    policy_rules = (("compute", "os_compute_api:os-lock-server:unlock"),)

    @staticmethod
    def action_present(count):
        return ngettext_lazy(
            "Unlock Instance",
            "Unlock Instances",
            count
        )

    @staticmethod
    def action_past(count):
        return ngettext_lazy(
            "Unlocked Instance",
            "Unlocked Instances",
            count
        )

    # to only allow locked instances to be unlocked
    def allowed(self, request, instance):
        if not getattr(instance, 'locked', True):
            return False
        if not api.nova.is_feature_available(request, "locked_attribute"):
            return False
        return True

    def action(self, request, obj_id):
        api.nova.server_unlock(request, obj_id)


class AttachVolume(tables.LinkAction):
    name = "attach_volume"
    verbose_name = _("Attach Volume")
    url = "horizon:project:instances:attach_volume"
    classes = ("ajax-modal",)
    policy_rules = (
        ("compute", "os_compute_api:os-volumes-attachments:create"),)

    # This action should be disabled if the instance
    # is not active, or the instance is being deleted
    # or cinder is not enabled
    def allowed(self, request, instance=None):
        return (instance.status in ("ACTIVE") and
                not is_deleting(instance) and
                api.cinder.is_volume_service_enabled(request))


class DetachVolume(AttachVolume):
    name = "detach_volume"
    verbose_name = _("Detach Volume")
    url = "horizon:project:instances:detach_volume"
    policy_rules = (
        ("compute", "os_compute_api:os-volumes-attachments:delete"),)

    # This action should be disabled if the instance
    # is not active, or the instance is being deleted
    # or cinder is not enabled
    def allowed(self, request, instance=None):
        return (instance.status in ("ACTIVE") and
                not is_deleting(instance) and
                api.cinder.is_volume_service_enabled(request))


class AttachInterface(policy.PolicyTargetMixin, tables.LinkAction):
    name = "attach_interface"
    verbose_name = _("Attach Interface")
    classes = ("btn-confirm", "ajax-modal")
    url = "horizon:project:instances:attach_interface"
    policy_rules = (("compute", "os_compute_api:os-attach-interfaces"),)

    def allowed(self, request, instance):
        return ((instance.status in ACTIVE_STATES or
                 instance.status == 'SHUTOFF') and
                not is_deleting(instance) and
                api.base.is_service_enabled(request, 'network'))

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        return urls.reverse(self.url, args=[instance_id])


class DetachInterface(policy.PolicyTargetMixin, tables.LinkAction):
    name = "detach_interface"
    verbose_name = _("Detach Interface")
    classes = ("btn-confirm", "ajax-modal")
    url = "horizon:project:instances:detach_interface"
    policy_rules = (("compute", "os_compute_api:os-attach-interfaces:delete"),)

    def allowed(self, request, instance):
        if not api.base.is_service_enabled(request, 'network'):
            return False
        if is_deleting(instance):
            return False
        if (instance.status not in ACTIVE_STATES and
                instance.status != 'SHUTOFF'):
            return False
        for addresses in instance.addresses.values():
            for address in addresses:
                if address.get('OS-EXT-IPS:type') == "fixed":
                    return True
        return False

    def get_link_url(self, datum):
        instance_id = self.table.get_object_id(datum)
        return urls.reverse(self.url, args=[instance_id])


def get_ips(instance):
    template_name = 'project/instances/_instance_ips.html'
    ip_groups = {}

    for ip_group, addresses in instance.addresses.items():
        ips = [addr['addr'] for addr in addresses]
        ips.sort(key=lambda ip: netaddr.IPAddress(ip).version)
        ip_groups[ip_group] = ips

    context = {
        "ip_groups": ip_groups,
    }
    return template.loader.render_to_string(template_name, context)


def get_flavor(instance):
    if hasattr(instance, "full_flavor"):
        template_name = 'project/instances/_instance_flavor.html'
        size_ram = sizeformat.mb_float_format(instance.full_flavor.ram)
        if instance.full_flavor.disk > 0:
            size_disk = sizeformat.diskgbformat(instance.full_flavor.disk)
        else:
            size_disk = _("%s GB") % "0"
        context = {
            "name": instance.full_flavor.name,
            "id": instance.id,
            "size_disk": size_disk,
            "size_ram": size_ram,
            "vcpus": instance.full_flavor.vcpus,
            "flavor_id": getattr(instance.full_flavor, 'id', None)
        }
        return template.loader.render_to_string(template_name, context)
    return _("Not available")


def get_keyname(instance):
    if hasattr(instance, "key_name"):
        keyname = instance.key_name
        return keyname
    return _("Not available")


def get_power_state(instance):
    return POWER_STATES.get(getattr(instance, "OS-EXT-STS:power_state", 0), '')


STATUS_DISPLAY_CHOICES = (
    ("deleted", pgettext_lazy("Current status of an Instance", "Deleted")),
    ("active", pgettext_lazy("Current status of an Instance", "Active")),
    ("shutoff", pgettext_lazy("Current status of an Instance", "Shutoff")),
    ("suspended", pgettext_lazy("Current status of an Instance",
                                "Suspended")),
    ("paused", pgettext_lazy("Current status of an Instance", "Paused")),
    ("error", pgettext_lazy("Current status of an Instance", "Error")),
    ("resize", pgettext_lazy("Current status of an Instance",
                             "Resize/Migrate")),
    ("verify_resize", pgettext_lazy("Current status of an Instance",
                                    "Confirm or Revert Resize/Migrate")),
    ("revert_resize", pgettext_lazy(
        "Current status of an Instance", "Revert Resize/Migrate")),
    ("reboot", pgettext_lazy("Current status of an Instance", "Reboot")),
    ("hard_reboot", pgettext_lazy("Current status of an Instance",
                                  "Hard Reboot")),
    ("password", pgettext_lazy("Current status of an Instance", "Password")),
    ("rebuild", pgettext_lazy("Current status of an Instance", "Rebuild")),
    ("migrating", pgettext_lazy("Current status of an Instance",
                                "Migrating")),
    ("build", pgettext_lazy("Current status of an Instance", "Build")),
    ("rescue", pgettext_lazy("Current status of an Instance", "Rescue")),
    ("soft-delete", pgettext_lazy("Current status of an Instance",
                                  "Soft Deleted")),
    ("shelved", pgettext_lazy("Current status of an Instance", "Shelved")),
    ("shelved_offloaded", pgettext_lazy("Current status of an Instance",
                                        "Shelved Offloaded")),
    # these vm states are used when generating CSV usage summary
    ("building", pgettext_lazy("Current status of an Instance", "Building")),
    ("stopped", pgettext_lazy("Current status of an Instance", "Stopped")),
    ("rescued", pgettext_lazy("Current status of an Instance", "Rescued")),
    ("resized", pgettext_lazy("Current status of an Instance", "Resized")),
)

TASK_DISPLAY_NONE = pgettext_lazy("Task status of an Instance", "None")

# Mapping of task states taken from Nova's nova/compute/task_states.py
TASK_DISPLAY_CHOICES = (
    ("scheduling", pgettext_lazy("Task status of an Instance",
                                 "Scheduling")),
    ("block_device_mapping", pgettext_lazy("Task status of an Instance",
                                           "Block Device Mapping")),
    ("networking", pgettext_lazy("Task status of an Instance",
                                 "Networking")),
    ("spawning", pgettext_lazy("Task status of an Instance", "Spawning")),
    ("image_snapshot", pgettext_lazy("Task status of an Instance",
                                     "Snapshotting")),
    ("image_snapshot_pending", pgettext_lazy("Task status of an Instance",
                                             "Image Snapshot Pending")),
    ("image_pending_upload", pgettext_lazy("Task status of an Instance",
                                           "Image Pending Upload")),
    ("image_uploading", pgettext_lazy("Task status of an Instance",
                                      "Image Uploading")),
    ("image_backup", pgettext_lazy("Task status of an Instance",
                                   "Image Backup")),
    ("updating_password", pgettext_lazy("Task status of an Instance",
                                        "Updating Password")),
    ("resize_prep", pgettext_lazy("Task status of an Instance",
                                  "Preparing Resize or Migrate")),
    ("resize_migrating", pgettext_lazy("Task status of an Instance",
                                       "Resizing or Migrating")),
    ("resize_migrated", pgettext_lazy("Task status of an Instance",
                                      "Resized or Migrated")),
    ("resize_finish", pgettext_lazy("Task status of an Instance",
                                    "Finishing Resize or Migrate")),
    ("resize_reverting", pgettext_lazy("Task status of an Instance",
                                       "Reverting Resize or Migrate")),
    ("resize_confirming", pgettext_lazy("Task status of an Instance",
                                        "Confirming Resize or Migrate")),
    ("rebooting", pgettext_lazy("Task status of an Instance", "Rebooting")),
    ("reboot_pending", pgettext_lazy("Task status of an Instance",
                                     "Reboot Pending")),
    ("reboot_started", pgettext_lazy("Task status of an Instance",
                                     "Reboot Started")),
    ("rebooting_hard", pgettext_lazy("Task status of an Instance",
                                     "Hard Rebooting")),
    ("reboot_pending_hard", pgettext_lazy("Task status of an Instance",
                                          "Hard Reboot Pending")),
    ("reboot_started_hard", pgettext_lazy("Task status of an Instance",
                                          "Hard Reboot Started")),
    ("pausing", pgettext_lazy("Task status of an Instance", "Pausing")),
    ("unpausing", pgettext_lazy("Task status of an Instance", "Resuming")),
    ("suspending", pgettext_lazy("Task status of an Instance",
                                 "Suspending")),
    ("resuming", pgettext_lazy("Task status of an Instance", "Resuming")),
    ("powering-off", pgettext_lazy("Task status of an Instance",
                                   "Powering Off")),
    ("powering-on", pgettext_lazy("Task status of an Instance",
                                  "Powering On")),
    ("rescuing", pgettext_lazy("Task status of an Instance", "Rescuing")),
    ("unrescuing", pgettext_lazy("Task status of an Instance",
                                 "Unrescuing")),
    ("rebuilding", pgettext_lazy("Task status of an Instance",
                                 "Rebuilding")),
    ("rebuild_block_device_mapping", pgettext_lazy(
        "Task status of an Instance", "Rebuild Block Device Mapping")),
    ("rebuild_spawning", pgettext_lazy("Task status of an Instance",
                                       "Rebuild Spawning")),
    ("migrating", pgettext_lazy("Task status of an Instance", "Migrating")),
    ("deleting", pgettext_lazy("Task status of an Instance", "Deleting")),
    ("soft-deleting", pgettext_lazy("Task status of an Instance",
                                    "Soft Deleting")),
    ("restoring", pgettext_lazy("Task status of an Instance", "Restoring")),
    ("shelving", pgettext_lazy("Task status of an Instance", "Shelving")),
    ("shelving_image_pending_upload", pgettext_lazy(
        "Task status of an Instance", "Shelving Image Pending Upload")),
    ("shelving_image_uploading", pgettext_lazy("Task status of an Instance",
                                               "Shelving Image Uploading")),
    ("shelving_offloading", pgettext_lazy("Task status of an Instance",
                                          "Shelving Offloading")),
    ("unshelving", pgettext_lazy("Task status of an Instance",
                                 "Unshelving")),
)

POWER_DISPLAY_CHOICES = (
    ("NO STATE", pgettext_lazy("Power state of an Instance", "No State")),
    ("RUNNING", pgettext_lazy("Power state of an Instance", "Running")),
    ("BLOCKED", pgettext_lazy("Power state of an Instance", "Blocked")),
    ("PAUSED", pgettext_lazy("Power state of an Instance", "Paused")),
    ("SHUTDOWN", pgettext_lazy("Power state of an Instance", "Shut Down")),
    ("SHUTOFF", pgettext_lazy("Power state of an Instance", "Shut Off")),
    ("CRASHED", pgettext_lazy("Power state of an Instance", "Crashed")),
    ("SUSPENDED", pgettext_lazy("Power state of an Instance", "Suspended")),
    ("FAILED", pgettext_lazy("Power state of an Instance", "Failed")),
    ("BUILDING", pgettext_lazy("Power state of an Instance", "Building")),
)

INSTANCE_FILTER_CHOICES = (
    ('uuid', _("Instance ID ="), True),
    ('name', _("Instance Name ="), True),
    ('image', _("Image ID ="), True),
    ('image_name', _("Image Name ="), True),
    ('ip', _("IPv4 Address ="), True),
    ('ip6', _("IPv6 Address ="), True, None,
     api.neutron.is_enabled_by_config('enable_ipv6')),
    ('flavor', _("Flavor ID ="), True),
    ('flavor_name', _("Flavor Name ="), True),
    ('key_name', _("Key Pair Name ="), True),
    ('status', _("Status ="), True),
    ('availability_zone', _("Availability Zone ="), True),
    ('changes-since', _("Changes Since"), True,
        _("Filter by an ISO 8061 formatted time, e.g. 2016-06-14T06:27:59Z")),
)


class InstancesFilterAction(tables.FilterAction):
    filter_type = "server"
    filter_choices = INSTANCE_FILTER_CHOICES


def render_locked(instance):
    if not hasattr(instance, 'locked'):
        return ""
    if instance.locked:
        icon_classes = "fa fa-fw fa-lock"
        help_tooltip = _("This instance is currently locked. To enable more "
                         "actions on it, please unlock it by selecting Unlock "
                         "Instance from the actions menu.")
    else:
        icon_classes = "fa fa-fw fa-unlock text-muted"
        help_tooltip = _("This instance is unlocked.")

    locked_status = ('<span data-toggle="tooltip" title="{}" class="{}">'
                     '</span>').format(help_tooltip, icon_classes)
    return mark_safe(locked_status)


def get_server_detail_link(obj, request):
    return get_url_with_pagination(request,
                                   InstancesTable._meta.pagination_param,
                                   InstancesTable._meta.prev_pagination_param,
                                   'horizon:project:instances:detail',
                                   obj.id)


class InstancesTable(tables.DataTable):
    TASK_STATUS_CHOICES = (
        (None, True),
        ("none", True)
    )
    STATUS_CHOICES = (
        ("active", True),
        ("shutoff", True),
        ("suspended", True),
        ("paused", True),
        ("error", False),
        ("rescue", True),
        ("shelved", True),
        ("shelved_offloaded", True),
    )
    name = tables.WrappingColumn("name",
                                 link=get_server_detail_link,
                                 verbose_name=_("Instance Name"))
    image_name = tables.WrappingColumn("image_name",
                                       verbose_name=_("Image Name"))
    ip = tables.Column(get_ips,
                       verbose_name=_("IP Address"),
                       attrs={'data-type': "ip"})
    flavor = tables.Column(get_flavor,
                           sortable=False,
                           verbose_name=_("Flavor"))
    keypair = tables.Column(get_keyname, verbose_name=_("Key Pair"))
    status = tables.Column("status",
                           filters=(title, filters.replace_underscores),
                           verbose_name=_("Status"),
                           status=True,
                           status_choices=STATUS_CHOICES,
                           display_choices=STATUS_DISPLAY_CHOICES)
    locked = tables.Column(render_locked,
                           verbose_name="",
                           sortable=False)
    az = tables.Column("availability_zone",
                       verbose_name=_("Availability Zone"))
    task = tables.Column("OS-EXT-STS:task_state",
                         verbose_name=_("Task"),
                         empty_value=TASK_DISPLAY_NONE,
                         status=True,
                         status_choices=TASK_STATUS_CHOICES,
                         display_choices=TASK_DISPLAY_CHOICES)
    state = tables.Column(get_power_state,
                          filters=(title, filters.replace_underscores),
                          verbose_name=_("Power State"),
                          display_choices=POWER_DISPLAY_CHOICES)
    created = tables.Column("created",
                            verbose_name=_("Age"),
                            filters=(filters.parse_isotime,
                                     filters.timesince_sortable),
                            attrs={'data-type': 'timesince'})

    class Meta(object):
        name = "instances"
        verbose_name = _("Instances")
        status_columns = ["status", "task"]
        row_class = UpdateRow
        table_actions_menu = (StartInstance, StopInstance, SoftRebootInstance)
        launch_actions = (LaunchLinkNG,)
        table_actions = launch_actions + (DeleteInstance,
                                          InstancesFilterAction)
        row_actions = (StartInstance, ConfirmResize, RevertResize,
                       CreateSnapshot, AssociateIP, DisassociateIP,
                       AttachInterface, DetachInterface, EditInstance,
                       AttachVolume, DetachVolume,
                       UpdateMetadata, DecryptInstancePassword,
                       EditInstanceSecurityGroups,
                       EditPortSecurityGroups,
                       ConsoleLink, LogLink,
                       RescueInstance, UnRescueInstance,
                       TogglePause, ToggleSuspend, ToggleShelve,
                       ResizeLink, LockInstance, UnlockInstance,
                       SoftRebootInstance, RebootInstance,
                       StopInstance, RebuildInstance, DeleteInstance)
