admin_tags.py 17.3 KB
Newer Older
1
# Copyright (C) 2010-2016 GRNET S.A.
2
#
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
3 4 5 6
# 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.
7
#
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
8 9 10 11
# 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.
12
#
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
13 14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 16
#

17
from importlib import import_module
18
from collections import OrderedDict
19
from django import template
20
from django.conf import settings
21
from django.core.exceptions import ObjectDoesNotExist
22
from django.utils.safestring import mark_safe
Olga Brani's avatar
Olga Brani committed
23
from datetime import date
24
import logging
25

26 27
import django_filters

28 29 30 31 32
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

Olga Brani's avatar
Olga Brani committed
33 34
from astakos.im.templatetags import filters as astakos_filters

35 36
import synnefo_admin.admin.resources.projects.utils as project_utils
import synnefo_admin.admin.resources.users.utils as user_utils
37
import synnefo_admin.admin.resources.vms.utils as vm_utils
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
38
from synnefo_admin.admin import utils
39
mod = import_module('astakos.im.management.commands.project-show')
40

41 42
register = template.Library()

43 44 45 46 47 48 49 50 51
status_map = {}
status_map['vm'] = {
    'BUILD': 'warning',
    'PENDING': 'warning',
    'ERROR': 'danger',
    'STOPPED': 'info',
    'STARTED': 'success',
    'ACTIVE': 'success',
    'DESTROYED': 'inverse'
52
}
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
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']
73
status_map['application']['APPROVED'] = 'success'
74 75


76
@register.filter()
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
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'
118
    else:
119
        state_cls = status_map[inst_type].get(state, 'info')
120

121
    label_cls = "label label-%s" % state_cls
122

123 124 125 126
    if inst_type in ["project", "application"]:
        name = inst_type.capitalize() + " Status: "
    else:
        name = ""
127

128
    deleted_label = ""
129
    if getattr(inst, 'deleted', False):
olga's avatar
olga committed
130
        deleted_label = '<span class="label label-danger">Deleted</span>'
131 132
    return '%s\n<span class="%s">%s%s</span>' % (deleted_label, label_cls,
                                                 name, state)
133 134 135 136 137 138 139 140 141 142


@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"


143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
@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:
163 164
        image_info = Image.objects.get(uuid=vm.imageid,
                                       version=vm.image_version)
165 166 167 168 169 170 171 172 173 174 175 176
    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)


177 178 179 180 181
@register.filter
def flavor_info(vm):
    return vm_utils.get_flavor_info(vm)


182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
@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 += '<dt>Backend ' + field.name + '</dt><dd>' + \
                   str(getattr(backend, field.name)) + '</dd>'
    return content
216 217


218 219 220 221 222 223 224 225 226 227 228 229 230
@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"
231 232 233 234
    elif type == "nic":
        return "Network Interfaces"
    elif type == "ip":
        return "IP Addresses"
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
235 236
    elif type == "volume":
        return "Volumes"
237
    elif type == "ip_log":
238
        return "IP History"
239 240 241 242
    else:
        return "Unknown type"


243 244 245 246 247 248 249
@register.filter
def admin_debug(var):
    """Print in the log a value."""
    logging.info("Template debugging: %s", var)
    return var


250 251 252 253 254
@register.filter
def get_details_template(type):
    """Get the correct template for the provided item."""
    template = 'admin/_' + type + '_details.html'
    return template
255 256 257


@register.filter
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
258 259
def get_filter_type(filter):
    """Get the flter type according to the filter class."""
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
260 261 262
    if isinstance(filter, django_filters.NumberFilter):
        type = "number"
    elif isinstance(filter, django_filters.CharFilter):
263 264 265 266 267 268 269 270 271
        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)
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
272 273 274 275 276 277 278 279 280 281
    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)
282 283
    template = 'admin/filter_' + type + '.html'
    return template
284 285


Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
286 287 288 289 290 291
@register.filter
def choices(filter):
    """Return an iterator with the available choices for a filter."""
    return [choice for choice, _ in filter.field.choices]


Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
@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


Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
312 313 314
@register.filter()
def details_url(inst, target):
    """Get a url for the details of an instance's field."""
315 316
    # Get instance type and import the appropriate utilities module.
    inst_type = utils.get_type_from_instance(inst)
317
    mod = import_module("synnefo_admin.admin.resources.{}s.utils".format(inst_type))
318

Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
319 320 321 322 323 324
    # 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))
325 326


327 328 329 330 331 332 333 334 335
@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.
    """
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
336 337 338 339 340 341 342 343 344 345
    try:
        return item.address
    except AttributeError:
        pass

    try:
        return item['project'].realname
    except TypeError:
        pass

346 347 348 349 350 351 352 353 354 355 356 357 358
    try:
        if item.realname:
            return item.realname
    except AttributeError:
        pass

    try:
        if item.name:
            return item.name
    except AttributeError:
        pass

    return item.__str__()
359 360


361 362 363 364 365
@register.filter
def get_groups(user):
    return user_utils.get_user_groups(user)


366 367 368 369 370 371 372 373 374 375
@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()


376 377 378 379 380 381
@register.filter
def get_project_members(project):
    members, _ = mod.members_fields(project)
    return members


382 383 384
@register.filter
def get_project_stats(project):
    """Create a dictionary with a summary for a project's stats."""
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    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
404

405

Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
406 407 408 409 410 411 412 413 414 415 416
@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"


417
@register.filter
418 419
def can_apply(action, item):
    """Return if action can apply on item."""
420
    if action.name == "Send e&#8209;mail" and action.target != 'user':
421 422
        return False
    return action.can_apply(item)
423 424 425 426 427 428 429 430 431 432 433


@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'
434 435 436 437 438 439 440 441 442


@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"
443 444 445


FILTER_NAME_ICON_MAP = {
446 447 448 449 450 451 452 453
    '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',
454 455 456 457 458 459 460 461 462 463 464
}


@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:
465
        label = '<span class="%s"></span>' % icon_cls
466
    else:
Alex Pyrgiotis's avatar
Alex Pyrgiotis committed
467
        label = filter_label + ":"
468 469
    return label

470

471 472 473 474
@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":
475 476
        return """</br>Alternatively, you may consult the "Members" tab of the project."""
    return ""
477 478


479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
@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 <dl> 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::
        <dt>foo1</dt><dd>bar1</dd><dt>foo3</dt><dd>bar3</dd>
    """
495
    l = []
496 497 498 499 500 501 502
    if isinstance(d, dict):
        stack = d.items()
        while stack:
            k, v = stack.pop()
            if isinstance(v, dict):
                stack.extend(v.iteritems())
            else:
503
                a = u'<dt>{0}</dt><dd>{1}</dd>'.format(k, v or default_if_empty)
504 505
                l.append(a)
    return mark_safe(''.join(reversed(l)))
Olga Brani's avatar
Olga Brani committed
506 507 508 509


@register.filter
def get_project_modifications(project):
510 511 512 513 514

    """
    Return a dictionary with a summary of a project's modifications as
    requested by the user, concerning project details, policies and resources.
    """
515
    last_app = project.last_pending_modification()
516 517
    if not last_app:
        return
Olga Brani's avatar
Olga Brani committed
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
    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,
553
            'diff': new_t - old_t,
Olga Brani's avatar
Olga Brani committed
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
        })

    # 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()
586 587
    policies_list = project_utils.get_policies(project)
    for p in policies_list:
Olga Brani's avatar
Olga Brani committed
588
        r = p.resource
589
        current_r[r.pluralized_display_name] = {
590 591 592
            'limit': p.display_project_capacity(),
            'member': p.display_member_capacity(),
        }
Olga Brani's avatar
Olga Brani committed
593 594 595
    for r in last_app.resource_set:
        old_member = 0
        old_project = 0
596 597 598
        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']
Olga Brani's avatar
Olga Brani committed
599 600

        resources.append({
601
            'label': r.resource.pluralized_display_name,
602
            'new_member': r.display_member_capacity(),
Olga Brani's avatar
Olga Brani committed
603
            'old_member': old_member,
604
            'diff_member': r.display_project_diff()[1] or '-',
605
            'new_project': r.display_project_capacity(),
Olga Brani's avatar
Olga Brani committed
606
            'old_project': old_project,
607
            'diff_project': r.display_project_diff()[0] or '-',
Olga Brani's avatar
Olga Brani committed
608 609 610 611 612 613 614 615 616 617 618 619 620
        })

    return {
        'details': details,
        'policies': policies,
        'resources': resources,
    }


@register.filter
def diff_cls(d):
    if d:
        d = str(d)
621 622 623 624 625
        cls = 'diff-positive'
        if d.startswith("-"):
            cls = 'diff-negative'
        if d == '-':
            cls = 'diff-zero'
Olga Brani's avatar
Olga Brani committed
626 627
        return cls
    return ''