# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from importlib import import_module
from collections import OrderedDict
from django import template
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe
from datetime import date
import logging
import django_filters
from snf_django.lib.api import faults
from synnefo.api.util import get_image
from synnefo.lib.dict import SnfOrderedDict
from synnefo.db.models import Image
from astakos.im.templatetags import filters as astakos_filters
import synnefo_admin.admin.resources.projects.utils as project_utils
import synnefo_admin.admin.resources.users.utils as user_utils
import synnefo_admin.admin.resources.vms.utils as vm_utils
from synnefo_admin.admin import utils
mod = import_module('astakos.im.management.commands.project-show')
register = template.Library()
status_map = {}
status_map['vm'] = {
'BUILD': 'warning',
'PENDING': 'warning',
'ERROR': 'danger',
'STOPPED': 'info',
'STARTED': 'success',
'ACTIVE': 'success',
'DESTROYED': 'inverse'
}
status_map['volume'] = {
'AVAILABLE': 'success',
'IN_USE': 'success',
'DELETING': 'warning',
'ERROR': 'danger',
'ERROR_DELETING': 'danger',
'ERROR_RESTORING': 'danger',
}
status_map['network'] = {
'ACTIVE': 'success',
'ERROR': 'danger',
}
status_map['project'] = {
'PENDING': 'warning',
'ACTIVE': 'success',
'DENIED': 'danger',
'DELETED': 'danger',
'SUSPENDED': 'warning',
}
status_map['application'] = status_map['project']
status_map['application']['APPROVED'] = 'success'
@register.filter()
def get_status_from_instance(inst):
"""Generic function to get the status of any instance."""
try:
return inst.state_display()
except AttributeError:
pass
try:
return inst.status_display
except AttributeError:
pass
try:
return inst.operstate
except AttributeError:
pass
try:
return inst.state
except AttributeError:
pass
return inst.status
@register.filter(is_safe=True)
def status_label(inst):
"""Return a span label styled based on the instance's current status"""
inst_type = utils.get_type_from_instance(inst)
state = get_status_from_instance(inst).upper()
state_cls = 'info'
if inst_type == 'user':
if not inst.email_verified or not inst.moderated:
state_cls = 'warning'
elif inst.is_rejected:
state_cls = 'danger'
elif inst.is_active:
state_cls = 'success'
elif not inst.is_active:
state_cls = 'inverse'
else:
state_cls = status_map[inst_type].get(state, 'info')
label_cls = "label label-%s" % state_cls
if inst_type in ["project", "application"]:
name = inst_type.capitalize() + " Status: "
else:
name = ""
deleted_label = ""
if getattr(inst, 'deleted', False):
deleted_label = 'Deleted'
return '%s\n%s%s' % (deleted_label, label_cls,
name, state)
@register.filter(name="get_os", is_safe=True)
def get_os(vm):
try:
return vm.metadata.filter(meta_key="OS").get().meta_value
except:
return "unknown"
@register.filter
def image_info(vm):
"""Retrieve Image details.
If the Image is deleted or no longer visible by the user, then provide
whatever information is stored by Cyclades.
"""
# Use the same order of appearance for Plankton and Cyclades image info
order = ['name', 'id', 'owner', 'version', 'size', 'created_at',
'updated_at', 'deleted_at', 'is_public', 'is_snapshot',
'is_system', 'location', 'os', 'osfamily', 'properties']
# TODO: Add this code when deferred loading works.
# Try to retrieve Image info using Plankton.
#try:
# image_info = get_image(vm.imageid, vm.userid)
#except faults.ItemNotFound:
# Check if Cyclades DB has any info about this Image.
try:
image_info = Image.objects.get(uuid=vm.imageid,
version=vm.image_version)
except ObjectDoesNotExist:
# If all else fails, gather whatever info are available from the
# VirtualMachine instance.
image_info = {
'id': vm.imageid,
'version': vm.image_version,
'os': get_os(vm),
}
return SnfOrderedDict(image_info, order, strict=False)
@register.filter
def flavor_info(vm):
return vm_utils.get_flavor_info(vm)
@register.filter(name="network_vms", is_safe=True)
def network_vms(network, account, show_deleted=False):
vms = []
nics = network.nics.filter(machine__userid=account)
if not show_deleted:
nics = nics.filter(machine__deleted=False).distinct()
for nic in nics:
vms.append(nic.machine)
return vms
@register.filter(name="network_nics")
def network_nics(network, account, show_deleted=False):
nics = network.nics.filter(machine__userid=account)
if not show_deleted:
nics = nics.filter(machine__deleted=False).distinct()
return nics
@register.filter(name="backend_info", is_safe=True)
def backend_info(vm):
content = ""
backend = vm.backend
excluded = ['password_hash', 'hash', 'username']
if not vm.backend:
content = "No backend"
return content
for field in vm.backend._meta.fields:
if field.name in excluded:
continue
content += '
Backend ' + field.name + '' + \
str(getattr(backend, field.name)) + ''
return content
@register.filter
def display_list_type(type):
"""Display the type of an item list in a human readable way."""
if type == "user":
return "Users"
elif type == "project":
return "Projects"
elif type == "quota":
return "Quotas"
elif type == "vm":
return "Virtual Machines"
elif type == "network":
return "Networks"
elif type == "nic":
return "Network Interfaces"
elif type == "ip":
return "IP Addresses"
elif type == "volume":
return "Volumes"
elif type == "ip_log":
return "IP History"
else:
return "Unknown type"
@register.filter
def admin_debug(var):
"""Print in the log a value."""
logging.info("Template debugging: %s", var)
return var
@register.filter
def get_details_template(type):
"""Get the correct template for the provided item."""
template = 'admin/_' + type + '_details.html'
return template
@register.filter
def get_filter_type(filter):
"""Get the flter type according to the filter class."""
if isinstance(filter, django_filters.NumberFilter):
type = "number"
elif isinstance(filter, django_filters.CharFilter):
type = "char"
elif isinstance(filter, django_filters.BooleanFilter):
type = "bool"
elif isinstance(filter, django_filters.ChoiceFilter):
type = "choice"
elif isinstance(filter, django_filters.MultipleChoiceFilter):
type = "multichoice"
else:
raise Exception("Unknown filter type: %s", filter)
return type
@register.filter
def get_filter_template(filter):
"""Get the correct flter template according to the filter type.
This only works for filters that are instances of django-filter's Filter.
"""
type = get_filter_type(filter)
template = 'admin/filter_' + type + '.html'
return template
@register.filter
def choices(filter):
"""Return an iterator with the available choices for a filter."""
return [choice for choice, _ in filter.field.choices]
@register.filter
def id(item):
try:
return item['project'].uuid
except TypeError:
pass
try:
return item.uuid
except AttributeError:
pass
try:
return item.id
except AttributeError:
pass
return item.pk
@register.filter()
def details_url(inst, target):
"""Get a url for the details of an instance's field."""
# Get instance type and import the appropriate utilities module.
inst_type = utils.get_type_from_instance(inst)
mod = import_module("synnefo_admin.admin.resources.{}s.utils".format(inst_type))
# Call the details_href function for the provided target.
func = getattr(mod, "get_{}_details_href".format(target), None)
if func:
return func(inst)
else:
raise Exception("Wrong target name: {}".format(target))
@register.filter
def repr(item):
"""Return the string representation of an item.
If an item has a "realname" attribute that is not emprty, we return this.
Else, if an item has a "name" attribute that is not empty, we return this.
Finally, if an item has none of the above attributes, or the attributes are
empty, return the result of the __str__ method of the item.
"""
try:
return item.address
except AttributeError:
pass
try:
return item['project'].realname
except TypeError:
pass
try:
if item.realname:
return item.realname
except AttributeError:
pass
try:
if item.name:
return item.name
except AttributeError:
pass
return item.__str__()
@register.filter
def get_groups(user):
return user_utils.get_user_groups(user)
@register.filter
def verbify(action):
"""Create verb from action name.
If action has more than one words, then we keep the first one which, by
convention, will be a verb.
"""
return action.split()[0].capitalize()
@register.filter
def get_project_members(project):
members, _ = mod.members_fields(project)
return members
@register.filter
def get_project_stats(project):
"""Create a dictionary with a summary for a project's stats."""
limit = project_utils.get_project_quota_category(project, "limit")
usage = project_utils.get_project_usage(project)
member = project_utils.get_project_quota_category(project, "member")
if not usage:
usage = [(name, '-',) for name, _ in limit]
if project.is_base:
all_stats = zip(limit, usage)
else:
all_stats = zip(member, limit, usage)
new_stats = OrderedDict()
for row in all_stats:
resource_name = row[0][0]
new_stats[resource_name] = []
for _, value in row:
new_stats[resource_name].append(value)
return new_stats
@register.filter
def show_auth_providers(user, category):
"""Show auth providers for a user."""
func = getattr(user, "get_%s_auth_providers" % category)
providers = [prov.module for prov in func()]
if providers:
return ", ".join(providers)
else:
return "None"
@register.filter
def can_apply(action, item):
"""Return if action can apply on item."""
if action.name == "Send e‑mail" and action.target != 'user':
return False
return action.can_apply(item)
@register.filter
def default_value(f):
"""Set default value for filters.
By default the value is all, except for filters with "NOT" in their label.
"""
if 'NOT' in f.label:
return 'None'
return 'All'
@register.filter
def present_excluded(assoc):
"""Present what are the excluded entries."""
if assoc.type == "user":
return "users that are not project members"
else:
return "deleted entries"
FILTER_NAME_ICON_MAP = {
'vm': 'snf-pc-full',
'user': 'snf-user-full',
'vol': 'snf-volume-full',
'volume': 'snf-volume-full',
'net': 'snf-network-full',
'network': 'snf-network-full',
'proj': 'snf-clipboard-h',
'project': 'snf-clipboard-h',
}
@register.filter
def label_to_icon(filter_name, filter_label):
"""
Return a span icon based on the filter name
If no icon is found, return filter label
"""
icon_cls = FILTER_NAME_ICON_MAP.get(filter_name)
if icon_cls:
label = '' % icon_cls
else:
label = filter_label + ":"
return label
@register.filter
def show_more_exception_message(assoc):
"""Show an extra message for an instance in the popover for "Show More"."""
if assoc.type == "user":
return """Alternatively, you may consult the "Members" tab of the project."""
return ""
@register.simple_tag
def flatten_dict_to_dl(d, default_if_empty='-'):
"""
Recursively takes a self-nested dict and returns an HTML definition list --
WITHOUT opening and closing tags.
The dict is assumed to be in the proper format. For example, if ``var``
contains: ``{
'foo1': 'bar1',
'foo2': {
'foo3': 'bar3',
}
}``,
then ``{{ var|flatten_dict_to_dl }}`` would return::
- foo1
- bar1
- foo3
- bar3
"""
l = []
if isinstance(d, dict):
stack = d.items()
while stack:
k, v = stack.pop()
if isinstance(v, dict):
stack.extend(v.iteritems())
else:
a = u'- {0}
- {1}
'.format(k, v or default_if_empty)
l.append(a)
return mark_safe(''.join(reversed(l)))
@register.filter
def get_project_modifications(project):
"""
Return a dictionary with a summary of a project's modifications as
requested by the user, concerning project details, policies and resources.
"""
last_app = project.last_pending_modification()
if not last_app:
return
details = []
policies = []
resources = []
# details
if last_app.name is not None:
details.append({
'label': 'name',
'new': last_app.name,
'old': project.name,
})
if last_app.homepage is not None:
details.append({
'label': 'homepage',
'new': last_app.homepage,
'old': project.homepage,
})
if last_app.description is not None:
details.append({
'label': 'description',
'new': last_app.description,
'old': project.description,
})
if last_app.comments:
details.append({
'label': 'comments',
'new': mark_safe(last_app.comments),
})
if last_app.end_date is not None:
new_t = last_app.end_date
old_t = project.end_date
details.append({
'label': 'end date',
'new': new_t,
'old': old_t,
'diff': new_t - old_t,
})
# policies
if last_app.member_join_policy is not None:
policies.append({
'label': 'join policy',
'new': last_app.member_join_policy_display,
'old': project.member_join_policy_display,
})
if last_app.member_leave_policy is not None:
policies.append({
'label': 'join policy',
'new': last_app.member_leave_policy_display,
'old': project.member_leave_policy_display,
})
if last_app.limit_on_members_number is not None:
new_n = last_app.limit_on_members_number
old_n = project.limit_on_members_number
diff = new_n-old_n
if astakos_filters.inf_display(new_n) == 'Unlimited':
diff = 'Unlimited'
if astakos_filters.inf_display(old_n) == 'Unlimited':
diff = '- Unlimited'
policies.append({
'label': 'max members',
'new': astakos_filters.inf_display(new_n),
'old': astakos_filters.inf_display(old_n),
'diff': diff,
})
# resources
current_r = OrderedDict()
policies_list = project_utils.get_policies(project)
for p in policies_list:
r = p.resource
current_r[r.pluralized_display_name] = {
'limit': p.display_project_capacity(),
'member': p.display_member_capacity(),
}
for r in last_app.resource_set:
old_member = 0
old_project = 0
if r.resource.pluralized_display_name in current_r:
old_member = current_r[r.resource.pluralized_display_name]['member']
old_project = current_r[r.resource.pluralized_display_name]['limit']
resources.append({
'label': r.resource.pluralized_display_name,
'new_member': r.display_member_capacity(),
'old_member': old_member,
'diff_member': r.display_project_diff()[1] or '-',
'new_project': r.display_project_capacity(),
'old_project': old_project,
'diff_project': r.display_project_diff()[0] or '-',
})
return {
'details': details,
'policies': policies,
'resources': resources,
}
@register.filter
def diff_cls(d):
if d:
d = str(d)
cls = 'diff-positive'
if d.startswith("-"):
cls = 'diff-negative'
if d == '-':
cls = 'diff-zero'
return cls
return ''