models.py 9.47 KB
Newer Older
Kálmán Viktor committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE 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.
#
# CIRCLE 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 CIRCLE.  If not, see <http://www.gnu.org/licenses/>.
17 18 19
import json
import logging

Kálmán Viktor committed
20 21 22
from django.db.models import (
    Model, CharField, IntegerField, TextField, ForeignKey, ManyToManyField,
)
23
from django.db.models.signals import post_save
24
from django.conf import settings
Kálmán Viktor committed
25 26
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
Kálmán Viktor committed
27 28
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
29 30 31
from django.utils.translation import (
    ugettext_lazy as _, ugettext_noop, ungettext
)
Kálmán Viktor committed
32 33
from django.core.urlresolvers import reverse

34
import requests
Kálmán Viktor committed
35 36 37 38 39
from model_utils.models import TimeStampedModel
from model_utils import Choices

from vm.models import Instance, InstanceTemplate, Lease

40 41
logger = logging.getLogger(__name__)

Kálmán Viktor committed
42 43 44 45 46 47

class RequestAction(Model):

    def accept(self):
        raise NotImplementedError

48 49 50 51
    @property
    def accept_msg(self):
        raise NotImplementedError

Kálmán Viktor committed
52 53 54
    class Meta:
        abstract = True

Kálmán Viktor committed
55 56

class RequestType(Model):
57
    name = CharField(max_length=100, verbose_name=_("Name"))
Kálmán Viktor committed
58 59 60 61

    def __unicode__(self):
        return self.name

Kálmán Viktor committed
62 63 64
    class Meta:
        abstract = True

Kálmán Viktor committed
65 66 67 68 69 70 71

class Request(TimeStampedModel):
    STATUSES = Choices(
        ('PENDING', _('pending')),
        ('ACCEPTED', _('accepted')),
        ('DECLINED', _('declined')),
    )
72
    status = CharField(choices=STATUSES, default=STATUSES.PENDING,
Kálmán Viktor committed
73
                       max_length=10)
74 75
    user = ForeignKey(User, related_name="user")
    closed_by = ForeignKey(User, related_name="closed_by", null=True)
Kálmán Viktor committed
76 77 78
    TYPES = Choices(
        ('resource', _('resource request')),
        ('lease', _("lease request")),
79
        ('template', _("template access request")),
Kálmán Viktor committed
80 81
    )
    type = CharField(choices=TYPES, max_length=10)
82
    message = TextField(verbose_name=_("Message"))
83
    reason = TextField(verbose_name=_("Reason"))
Kálmán Viktor committed
84 85 86 87 88

    content_type = ForeignKey(ContentType)
    object_id = IntegerField()
    action = GenericForeignKey("content_type", "object_id")

89 90 91
    def get_absolute_url(self):
        return reverse("request.views.request-detail", kwargs={'pk': self.pk})

Kálmán Viktor committed
92 93 94
    def get_readable_status(self):
        return self.STATUSES[self.status]

95 96 97
    def get_readable_type(self):
        return self.TYPES[self.type]

98
    def get_request_icon(self):
Kálmán Viktor committed
99 100 101 102 103 104 105 106 107 108 109 110
        return {
            'resource': "tasks",
            'lease': "clock-o",
            'template': "puzzle-piece"
        }.get(self.type)

    def get_effect(self):
        return {
            "PENDING": "warning",
            "ACCEPTED": "success",
            "DECLINED": "danger",
        }.get(self.status)
Kálmán Viktor committed
111

112 113 114 115 116 117 118
    def get_status_icon(self):
        return {
            "PENDING": "exclamation-triangle",
            "ACCEPTED": "check",
            "DECLINED": "times",
        }.get(self.status)

119 120
    def accept(self, user):
        self.action.accept(user)
121
        self.status = "ACCEPTED"
122
        self.closed_by = user
123 124
        self.save()

125 126 127 128 129
        self.user.profile.notify(
            ugettext_noop("Request accepted"),
            self.action.accept_msg
        )

130
    def decline(self, user, reason):
131
        self.status = "DECLINED"
132
        self.closed_by = user
133
        self.reason = reason
134 135
        self.save()

136 137 138 139 140 141 142 143 144 145
        decline_msg = ugettext_noop(
            'Your <a href="%(url)s">request</a> was declined because of the '
            'following reason: %(reason)s'
        )

        self.user.profile.notify(
            ugettext_noop("Request declined"),
            decline_msg, url=self.get_absolute_url(), reason=self.reason,
        )

Kálmán Viktor committed
146 147

class LeaseType(RequestType):
148
    lease = ForeignKey(Lease, verbose_name=_("Lease"))
Kálmán Viktor committed
149

150 151 152 153 154 155
    def __unicode__(self):
        return _("%(name)s (suspend: %(s)s, remove: %(r)s)") % {
            'name': self.name,
            's': self.lease.get_readable_suspend_time(),
            'r': self.lease.get_readable_delete_time()}

Kálmán Viktor committed
156 157 158 159 160 161
    def get_absolute_url(self):
        return reverse("request.views.lease-type-detail",
                       kwargs={'pk': self.pk})


class TemplateAccessType(RequestType):
162
    templates = ManyToManyField(InstanceTemplate, verbose_name=_("Templates"))
Kálmán Viktor committed
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

    def get_absolute_url(self):
        return reverse("request.views.template-type-detail",
                       kwargs={'pk': self.pk})


class ResourceChangeAction(RequestAction):
    instance = ForeignKey(Instance)
    num_cores = IntegerField(verbose_name=_('number of cores'),
                             help_text=_('Number of virtual CPU cores '
                                         'available to the virtual machine.'),
                             validators=[MinValueValidator(0)])
    ram_size = IntegerField(verbose_name=_('RAM size'),
                            help_text=_('Mebibytes of memory.'),
                            validators=[MinValueValidator(0)])
    priority = IntegerField(verbose_name=_('priority'),
                            help_text=_('CPU priority.'),
                            validators=[MinValueValidator(0)])

182
    def accept(self, user):
183 184 185 186 187
        self.instance.resources_change.async(
            user=user, num_cores=self.num_cores, ram_size=self.ram_size,
            max_ram_size=self.ram_size, priority=self.priority,
            with_shutdown=True)

188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    @property
    def accept_msg(self):
        return _(
            'The resources of <a href="%(url)s">%(name)s</a> were changed. '
            'Number of cores: %(num_cores)d, RAM size: '
            '<span class="nowrap">%(ram_size)d MiB</span>, '
            'CPU priority: %(priority)d/100.'
        ) % {
            'url': self.instance.get_absolute_url(),
            'name': self.instance.name,
            'num_cores': self.num_cores,
            'ram_size': self.ram_size,
            'priority': self.priority,
        }

Kálmán Viktor committed
203 204 205 206 207

class ExtendLeaseAction(RequestAction):
    instance = ForeignKey(Instance)
    lease_type = ForeignKey(LeaseType)

208 209 210
    def accept(self, user):
        self.instance.renew(lease=self.lease_type.lease, save=True, force=True,
                            user=user)
Kálmán Viktor committed
211

212 213 214 215 216 217 218 219 220 221
    @property
    def accept_msg(self):
        return _(
            'The lease of <a href="%(url)s">%(name)s</a> got extended. '
            '(suspend: %(suspend)s, remove: %(remove)s)'
        ) % {'name': self.instance.name,
             'url': self.instance.get_absolute_url(),
             'suspend': self.lease_type.lease.get_readable_suspend_time(),
             'remove': self.lease_type.lease.get_readable_delete_time(), }

Kálmán Viktor committed
222 223 224 225 226 227 228 229 230 231 232

class TemplateAccessAction(RequestAction):
    template_type = ForeignKey(TemplateAccessType)
    LEVELS = Choices(
        ('user', _('user')),
        ('operator', _('operator')),
    )
    level = CharField(choices=LEVELS, default=LEVELS.user,
                      max_length=10)
    user = ForeignKey(User)

Kálmán Viktor committed
233 234 235
    def get_readable_level(self):
        return self.LEVELS[self.level]

236 237 238
    def accept(self, user):
        for t in self.template_type.templates.all():
            t.set_user_level(self.user, self.level)
239

240 241 242 243 244 245 246 247
    @property
    def accept_msg(self):
        return ungettext(
            "You got access to the following template: %s",
            "You got access to the following templates: %s",
            self.template_type.templates.count()
        ) % ", ".join([x.name for x in self.template_type.templates.all()])

248

249
def send_notifications(sender, instance, created, **kwargs):
250 251 252 253 254 255 256 257 258 259
    if not created:
        return

    notification_msg = ugettext_noop(
        'A new <a href="%(request_url)s">%(request_type)s</a> was submitted '
        'by <a href="%(user_url)s">%(display_name)s</a>.')
    context = {
        'display_name': instance.user.profile.get_display_name(),
        'user_url': instance.user.profile.get_absolute_url(),
        'request_url': instance.get_absolute_url(),
260
        'request_type': u"%s" % instance.get_readable_type()
261 262 263 264 265 266 267
    }

    for u in User.objects.filter(is_superuser=True):
        u.profile.notify(
            ugettext_noop("New %(request_type)s"), notification_msg, context
        )

268 269 270 271 272
    instance.user.profile.notify(
        ugettext_noop("Request submitted"),
        ugettext_noop('You can view the request\'s status at this '
                      '<a href="%(request_url)s">link</a>.'), context
    )
273

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
    if settings.REQUEST_HOOK_URL:
        context.update({
            'object_kind': "request",
            'site_url': settings.DJANGO_URL,
        })
        try:
            r = requests.post(settings.REQUEST_HOOK_URL, timeout=3,
                              data=json.dumps(context, indent=2))
            r.raise_for_status()
        except requests.RequestException as e:
            logger.warning("Error in HTTP POST: %s. url: %s params: %s",
                           str(e), settings.REQUEST_HOOK_URL, context)
        else:
            logger.info("Successful HTTP POST. url: %s params: %s",
                        settings.REQUEST_HOOK_URL, context)


post_save.connect(send_notifications, sender=Request)