instance.py 30.5 KB
Newer Older
1
from __future__ import absolute_import, unicode_literals
2
from datetime import timedelta
3
from logging import getLogger
Őry Máté committed
4
from importlib import import_module
5
from warnings import warn
6

7
import django.conf
Őry Máté committed
8 9
from django.contrib.auth.models import User
from django.core import signing
Dudás Ádám committed
10
from django.core.exceptions import PermissionDenied
11 12 13
from django.db.models import (BooleanField, CharField, DateTimeField,
                              IntegerField, ForeignKey, Manager,
                              ManyToManyField, permalink, SET_NULL, TextField)
14
from django.dispatch import Signal
15 16
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
Dudás Ádám committed
17

18 19
from model_utils import Choices
from model_utils.models import TimeStampedModel, StatusModel
20
from taggit.managers import TaggableManager
21

Dudás Ádám committed
22
from acl.models import AclBase
23
from common.operations import OperatedMixin
24
from storage.models import Disk
Dudás Ádám committed
25
from ..tasks import vm_tasks, agent_tasks
26 27
from .activity import (ActivityInProgressError, instance_activity,
                       InstanceActivity)
28
from .common import BaseResourceConfigModel, Lease
29 30
from .network import Interface
from .node import Node, Trait
31

32
logger = getLogger(__name__)
Őry Máté committed
33 34
pre_state_changed = Signal(providing_args=["new_state"])
post_state_changed = Signal(providing_args=["new_state"])
35
pwgen = User.objects.make_random_password
36
scheduler = import_module(name=django.conf.settings.VM_SCHEDULER)
Őry Máté committed
37

38
ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS
Őry Máté committed
39 40
ACCESS_METHODS = [(key, name) for key, (name, port, transport)
                  in ACCESS_PROTOCOLS.iteritems()]
Bach Dániel committed
41
VNC_PORT_RANGE = (20000, 65536)  # inclusive start, exclusive end
42 43


44 45 46 47 48 49 50 51 52 53 54 55 56 57
def find_unused_port(port_range, used_ports=[]):
    """Find an unused port in the specified range.

    The list of used ports can be specified optionally.

    :param port_range: a tuple representing a port range (w/ exclusive end)
                       e.g. (6000, 7000) represents ports 6000 through 6999
    """
    ports = xrange(*port_range)
    used = set(used_ports)
    unused = (port for port in ports if port not in used)
    return next(unused, None)  # first or None


58
def find_unused_vnc_port():
59 60 61 62 63
    port = find_unused_port(
        port_range=VNC_PORT_RANGE,
        used_ports=Instance.objects.values_list('vnc_port', flat=True))

    if port is None:
64
        raise Exception("No unused port could be found for VNC.")
65 66
    else:
        return port
67 68


69
class InstanceActiveManager(Manager):
Dudás Ádám committed
70

71 72
    def get_query_set(self):
        return super(InstanceActiveManager,
73
                     self).get_query_set().filter(destroyed_at=None)
74 75


76
class VirtualMachineDescModel(BaseResourceConfigModel):
77

78 79 80 81 82 83 84 85
    """Abstract base for virtual machine describing models.
    """
    access_method = CharField(max_length=10, choices=ACCESS_METHODS,
                              verbose_name=_('access method'),
                              help_text=_('Primary remote access method.'))
    boot_menu = BooleanField(verbose_name=_('boot menu'), default=False,
                             help_text=_(
                                 'Show boot device selection menu on boot.'))
86
    lease = ForeignKey(Lease, help_text=_("Preferred expiration periods."))
87 88
    raw_data = TextField(verbose_name=_('raw_data'), blank=True, help_text=_(
        'Additional libvirt domain parameters in XML format.'))
Dudás Ádám committed
89 90 91 92 93
    req_traits = ManyToManyField(Trait, blank=True,
                                 help_text=_("A set of traits required for a "
                                             "node to declare to be suitable "
                                             "for hosting the VM."),
                                 verbose_name=_("required traits"))
94 95 96 97
    system = TextField(verbose_name=_('operating system'),
                       help_text=(_('Name of operating system in '
                                    'format like "%s".') %
                                  'Ubuntu 12.04 LTS Desktop amd64'))
Dudás Ádám committed
98
    tags = TaggableManager(blank=True, verbose_name=_("tags"))
99 100 101 102 103

    class Meta:
        abstract = True


104
class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
105

106 107
    """Virtual machine template.
    """
108 109 110 111 112
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
Őry Máté committed
113 114 115 116 117 118 119 120 121 122
    name = CharField(max_length=100, unique=True,
                     verbose_name=_('name'),
                     help_text=_('Human readable name of template.'))
    description = TextField(verbose_name=_('description'), blank=True)
    parent = ForeignKey('self', null=True, blank=True,
                        verbose_name=_('parent template'),
                        help_text=_('Template which this one is derived of.'))
    disks = ManyToManyField(Disk, verbose_name=_('disks'),
                            related_name='template_set',
                            help_text=_('Disks which are to be mounted.'))
123
    owner = ForeignKey(User)
124 125

    class Meta:
Őry Máté committed
126 127
        app_label = 'vm'
        db_table = 'vm_instancetemplate'
128
        ordering = ('name', )
Őry Máté committed
129 130 131
        permissions = (
            ('create_template', _('Can create an instance template.')),
        )
132 133 134 135 136 137
        verbose_name = _('template')
        verbose_name_plural = _('templates')

    def __unicode__(self):
        return self.name

138
    @property
139
    def running_instances(self):
140
        """The number of running instances of the template.
141
        """
142
        return sum(1 for i in self.instance_set.all() if i.is_running)
143 144 145

    @property
    def os_type(self):
146
        """The type of the template's operating system.
147 148
        """
        if self.access_method == 'rdp':
149
            return 'windows'
150
        else:
151
            return 'linux'
152

153 154 155 156 157 158
    def save(self, *args, **kwargs):
        is_new = getattr(self, "pk", None) is None
        super(InstanceTemplate, self).save(*args, **kwargs)
        if is_new:
            self.set_level(self.owner, 'owner')

159 160 161 162
    @permalink
    def get_absolute_url(self):
        return ('dashboard.views.template-detail', None, {'pk': self.pk})

163

164
class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
165
               TimeStampedModel):
tarokkk committed
166

167 168
    """Virtual machine instance.
    """
169 170 171 172 173
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),  # console, networking, change state
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
174 175 176 177 178 179 180 181 182
    STATUS = Choices(
        ('NOSTATE', _('no state')),
        ('RUNNING', _('running')),
        ('STOPPED', _('stopped')),
        ('SUSPENDED', _('suspended')),
        ('ERROR', _('error')),
        ('PENDING', _('pending')),
        ('DESTROYED', _('destroyed')),
    )
Őry Máté committed
183
    name = CharField(blank=True, max_length=100, verbose_name=_('name'),
184
                     help_text=_("Human readable name of instance."))
Őry Máté committed
185 186
    description = TextField(blank=True, verbose_name=_('description'))
    template = ForeignKey(InstanceTemplate, blank=True, null=True,
187
                          related_name='instance_set', on_delete=SET_NULL,
188
                          help_text=_("Template the instance derives from."),
Őry Máté committed
189
                          verbose_name=_('template'))
190
    pw = CharField(help_text=_("Original password of the instance."),
Őry Máté committed
191 192 193
                   max_length=20, verbose_name=_('password'))
    time_of_suspend = DateTimeField(blank=True, default=None, null=True,
                                    verbose_name=_('time of suspend'),
194 195
                                    help_text=_("Proposed time of automatic "
                                                "suspension."))
Őry Máté committed
196 197
    time_of_delete = DateTimeField(blank=True, default=None, null=True,
                                   verbose_name=_('time of delete'),
198 199
                                   help_text=_("Proposed time of automatic "
                                               "deletion."))
Őry Máté committed
200
    active_since = DateTimeField(blank=True, null=True,
201 202
                                 help_text=_("Time stamp of successful "
                                             "boot report."),
Őry Máté committed
203 204 205
                                 verbose_name=_('active since'))
    node = ForeignKey(Node, blank=True, null=True,
                      related_name='instance_set',
206
                      help_text=_("Current hypervisor of this instance."),
Őry Máté committed
207 208
                      verbose_name=_('host node'))
    disks = ManyToManyField(Disk, related_name='instance_set',
209
                            help_text=_("Set of mounted disks."),
Őry Máté committed
210
                            verbose_name=_('disks'))
211 212 213
    vnc_port = IntegerField(blank=True, default=None, null=True,
                            help_text=_("TCP port where VNC console listens."),
                            unique=True, verbose_name=_('vnc_port'))
Őry Máté committed
214
    owner = ForeignKey(User)
215 216 217
    destroyed_at = DateTimeField(blank=True, null=True,
                                 help_text=_("The virtual machine's time of "
                                             "destruction."))
218 219
    objects = Manager()
    active = InstanceActiveManager()
220 221

    class Meta:
Őry Máté committed
222 223
        app_label = 'vm'
        db_table = 'vm_instance'
224
        ordering = ('pk', )
225 226 227 228 229 230
        permissions = (
            ('access_console', _('Can access the graphical console of a VM.')),
            ('change_resources', _('Can change resources of a running VM.')),
            ('set_resources', _('Can change resources of a new VM.')),
            ('config_ports', _('Can configure port forwards.')),
        )
231 232 233
        verbose_name = _('instance')
        verbose_name_plural = _('instances')

234 235 236 237 238 239 240 241 242 243 244
    class InstanceDestroyedError(Exception):

        def __init__(self, instance, message=None):
            if message is None:
                message = ("The instance (%s) has already been destroyed."
                           % instance)

            Exception.__init__(self, message)

            self.instance = instance

245 246 247 248 249 250
    class WrongStateError(Exception):

        def __init__(self, instance, message=None):
            if message is None:
                message = ("The instance's current state (%s) is "
                           "inappropriate for the invoked operation."
251
                           % instance.status)
252 253 254 255 256

            Exception.__init__(self, message)

            self.instance = instance

257
    def __unicode__(self):
258
        parts = (self.name, "(" + str(self.id) + ")")
259
        return " ".join(s for s in parts if s != "")
260

261
    @property
262 263 264 265
    def is_console_available(self):
        return self.is_running

    @property
266
    def is_running(self):
Guba Sándor committed
267 268
        """Check if VM is in running state.
        """
269
        return self.status == 'RUNNING'
270 271

    @property
272
    def state(self):
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
        warn('Use Instance.status (or get_status_display) instead.',
             DeprecationWarning)
        return self.status

    def _update_status(self):
        """Set the proper status of the instance to Instance.status.
        """
        old = self.status
        self.status = self._compute_status()
        if old != self.status:
            logger.info('Status of Instance#%d changed to %s',
                        self.pk, self.status)
            self.save()

    def _compute_status(self):
        """Return the proper status of the instance based on activities.
289
        """
290
        # check special cases
291 292 293 294
        if self.activity_log.filter(activity_code__endswith='migrate',
                                    finished__isnull=True).exists():
            return 'MIGRATING'

295 296 297 298 299 300
        # <<< add checks for special cases before this

        # default case
        acts = self.activity_log.filter(finished__isnull=False,
                                        resultant_state__isnull=False
                                        ).order_by('-finished')[:1]
301
        try:
302
            act = acts[0]
303
        except IndexError:
304 305 306
            return 'NOSTATE'
        else:
            return act.resultant_state
307

Dudás Ádám committed
308 309
    @classmethod
    def create(cls, params, disks, networks, req_traits, tags):
Guba Sándor committed
310 311
        """ Create new Instance object.
        """
Dudás Ádám committed
312 313
        # create instance and do additional setup
        inst = cls(**params)
314

Dudás Ádám committed
315 316 317 318
        # save instance
        inst.full_clean()
        inst.save()
        inst.set_level(inst.owner, 'owner')
319

Dudás Ádám committed
320 321
        def __on_commit(activity):
            activity.resultant_state = 'PENDING'
322

Dudás Ádám committed
323 324 325 326 327 328 329 330 331 332 333 334 335 336
        with instance_activity(code_suffix='create', instance=inst,
                               on_commit=__on_commit, user=inst.owner) as act:
            # create related entities
            inst.disks.add(*[disk.get_exclusive() for disk in disks])

            for net in networks:
                Interface.create(instance=inst, vlan=net.vlan,
                                 owner=inst.owner, managed=net.managed,
                                 base_activity=act)

            inst.req_traits.add(*req_traits)
            inst.tags.add(*tags)

            return inst
337

338
    @classmethod
339
    def create_from_template(cls, template, owner, disks=None, networks=None,
Dudás Ádám committed
340
                             req_traits=None, tags=None, **kwargs):
341 342 343 344 345
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
346 347 348 349 350 351 352 353 354 355 356 357 358 359
        insts = cls.mass_create_from_template(template, owner, disks=disks,
                                              networks=networks, tags=tags,
                                              req_traits=req_traits, **kwargs)
        return insts[0]

    @classmethod
    def mass_create_from_template(cls, template, owner, amount=1, disks=None,
                                  networks=None, req_traits=None, tags=None,
                                  **kwargs):
        """Mass-create new instances based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
360
        disks = template.disks.all() if disks is None else disks
361

362 363 364 365 366 367 368
        for disk in disks:
            if not disk.has_level(owner, 'user'):
                raise PermissionDenied()
            elif (disk.type == 'qcow2-snap'
                  and not disk.has_level(owner, 'owner')):
                raise PermissionDenied()

369 370 371
        networks = (template.interface_set.all() if networks is None
                    else networks)

372 373 374 375
        for network in networks:
            if not network.vlan.has_level(owner, 'user'):
                raise PermissionDenied()

Dudás Ádám committed
376 377 378
        req_traits = (template.req_traits.all() if req_traits is None
                      else req_traits)

Dudás Ádám committed
379 380
        tags = template.tags.all() if tags is None else tags

381
        # prepare parameters
Dudás Ádám committed
382 383
        common_fields = ['name', 'description', 'num_cores', 'ram_size',
                         'max_ram_size', 'arch', 'priority', 'boot_menu',
384
                         'raw_data', 'lease', 'access_method', 'system']
Dudás Ádám committed
385 386 387
        params = dict(template=template, owner=owner, pw=pwgen())
        params.update([(f, getattr(template, f)) for f in common_fields])
        params.update(kwargs)  # override defaults w/ user supplied values
388 389

        if amount > 1 and '%d' not in params['name']:
390
            params['name'] += ' %d'
Dudás Ádám committed
391

392 393 394 395 396
        customized_params = (dict(params,
                                  name=params['name'].replace('%d', str(i)))
                             for i in xrange(amount))
        return [cls.create(cps, disks, networks, req_traits, tags)
                for cps in customized_params]
397

Dudás Ádám committed
398 399 400 401
    def clean(self, *args, **kwargs):
        if self.time_of_delete is None:
            self._do_renew(which='delete')
        super(Instance, self).clean(*args, **kwargs)
402

Guba Sándor committed
403 404 405 406 407
    def manual_state_change(self, new_state="NOSTATE", reason=None, user=None):
        """ Manually change state of an Instance.

        Can be used to recover VM after administrator fixed problems.
        """
Dudás Ádám committed
408 409 410 411 412 413 414 415
        # TODO cancel concurrent activity (if exists)
        act = InstanceActivity.create(code_suffix='manual_state_change',
                                      instance=self, user=user)
        act.finished = act.started
        act.result = reason
        act.resultant_state = new_state
        act.succeeded = True
        act.save()
Dudás Ádám committed
416

Dudás Ádám committed
417 418 419 420 421 422 423 424
    def vm_state_changed(self, new_state):
        # log state change
        try:
            act = InstanceActivity.create(code_suffix='vm_state_changed',
                                          instance=self)
        except ActivityInProgressError:
            pass  # discard state change if another activity is in progress.
        else:
425 426 427 428
            if new_state == 'STOPPED':
                self.vnc_port = None
                self.node = None
                self.save()
Dudás Ádám committed
429 430 431 432
            act.finished = act.started
            act.resultant_state = new_state
            act.succeeded = True
            act.save()
433

Őry Máté committed
434
    @permalink
435
    def get_absolute_url(self):
436
        return ('dashboard.views.detail', None, {'pk': self.id})
437 438

    @property
439 440 441 442 443 444 445 446 447
    def vm_name(self):
        """Name of the VM instance.

        This is a unique identifier as opposed to the 'name' attribute, which
        is just for display.
        """
        return 'cloud-' + str(self.id)

    @property
448
    def mem_dump(self):
449
        """Return the path and datastore for the memory dump.
450 451 452

        It is always on the first hard drive storage named cloud-<id>.dump
        """
453 454 455 456 457 458 459
        try:
            datastore = self.disks.all()[0].datastore
        except:
            return None
        else:
            path = datastore.path + '/' + self.vm_name + '.dump'
            return {'datastore': datastore, 'path': path}
460 461

    @property
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476
    def primary_host(self):
        interfaces = self.interface_set.select_related('host')
        hosts = [i.host for i in interfaces if i.host]
        if not hosts:
            return None
        hs = [h for h in hosts if h.ipv6]
        if hs:
            return hs[0]
        hs = [h for h in hosts if not h.shared_ip]
        if hs:
            return hs[0]
        return hosts[0]

    @property
    def ipv4(self):
477 478
        """Primary IPv4 address of the instance.
        """
479 480 481 482
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
483 484
        """Primary IPv6 address of the instance.
        """
485 486 487 488
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
489 490
        """Primary MAC address of the instance.
        """
491 492 493 494 495 496 497 498 499 500 501
        return self.primary_host.mac if self.primary_host else None

    @property
    def uptime(self):
        """Uptime of the instance.
        """
        if self.active_since:
            return timezone.now() - self.active_since
        else:
            return timedelta()  # zero

502 503 504 505 506 507 508 509 510
    @property
    def os_type(self):
        """Get the type of the instance's operating system.
        """
        if self.template is None:
            return "unknown"
        else:
            return self.template.os_type

511 512 513 514 515 516 517 518 519 520 521
    def get_age(self):
        """Deprecated. Use uptime instead.

        Get age of VM in seconds.
        """
        return self.uptime.seconds

    @property
    def waiting(self):
        """Indicates whether the instance's waiting for an operation to finish.
        """
522
        return self.activity_log.filter(finished__isnull=True).exists()
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537

    def get_connect_port(self, use_ipv6=False):
        """Get public port number for default access method.
        """
        port, proto = ACCESS_PROTOCOLS[self.access_method][1:3]
        if self.primary_host:
            endpoints = self.primary_host.get_public_endpoints(port, proto)
            endpoint = endpoints['ipv6'] if use_ipv6 else endpoints['ipv4']
            return endpoint[1] if endpoint else None
        else:
            return None

    def get_connect_host(self, use_ipv6=False):
        """Get public hostname.
        """
538
        if not self.interface_set.exclude(host=None):
539 540
            return _('None')
        proto = 'ipv6' if use_ipv6 else 'ipv4'
541 542
        return self.interface_set.exclude(host=None)[0].host.get_hostname(
            proto=proto)
543

544
    def get_connect_command(self, use_ipv6=False):
Guba Sándor committed
545 546
        """Returns a formatted connect string.
        """
547 548 549 550 551 552 553 554 555
        try:
            port = self.get_connect_port(use_ipv6=use_ipv6)
            host = self.get_connect_host(use_ipv6=use_ipv6)
            proto = self.access_method
            if proto == 'rdp':
                return 'rdesktop %(host)s:%(port)d -u cloud -p %(pw)s' % {
                    'port': port, 'proto': proto, 'pw': self.pw,
                    'host': host}
            elif proto == 'ssh':
556
                return ('sshpass -p %(pw)s ssh -o StrictHostKeyChecking=no '
557 558 559 560 561 562
                        'cloud@%(host)s -p %(port)d') % {
                    'port': port, 'proto': proto, 'pw': self.pw,
                    'host': host}
        except:
            return

563 564 565 566 567 568 569 570 571
    def get_connect_uri(self, use_ipv6=False):
        """Get access parameters in URI format.
        """
        try:
            port = self.get_connect_port(use_ipv6=use_ipv6)
            host = self.get_connect_host(use_ipv6=use_ipv6)
            proto = self.access_method
            if proto == 'ssh':
                proto = 'sshterm'
572 573 574
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
575 576 577
        except:
            return

tarokkk committed
578
    def get_vm_desc(self):
Guba Sándor committed
579 580
        """Serialize Instance object to vmdriver.
        """
tarokkk committed
581
        return {
582
            'name': self.vm_name,
583
            'vcpu': self.num_cores,
584
            'memory': int(self.ram_size) * 1024,  # convert from MiB to KiB
Őry Máté committed
585
            'memory_max': int(self.max_ram_size) * 1024,  # convert MiB to KiB
586 587 588 589 590
            'cpu_share': self.priority,
            'arch': self.arch,
            'boot_menu': self.boot_menu,
            'network_list': [n.get_vmnetwork_desc()
                             for n in self.interface_set.all()],
591 592 593 594 595
            'disk_list': [d.get_vmdisk_desc() for d in self.disks.all()],
            'graphics': {
                'type': 'vnc',
                'listen': '0.0.0.0',
                'passwd': '',
Guba Sándor committed
596
                'port': self.vnc_port
597
            },
598
            'boot_token': signing.dumps(self.id, salt='activate'),
Guba Sándor committed
599
            'raw_data': "" if not self.raw_data else self.raw_data
600
        }
tarokkk committed
601

602 603 604 605
    def get_remote_queue_name(self, queue_id):
        """Get the remote worker queue name of this instance with the specified
           queue ID.
        """
606 607 608 609
        if self.node:
            return self.node.get_remote_queue_name(queue_id)
        else:
            raise Node.DoesNotExist()
610

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
    def _is_notified_about_expiration(self):
        renews = self.activity_log.filter(activity_code__endswith='renew')
        cond = {'activity_code__endswith': 'notification_about_expiration'}
        if len(renews) > 0:
            cond['finished__gt'] = renews[0].started
        return self.activity_log.filter(**cond).exists()

    def notify_owners_about_expiration(self, again=False):
        """Notify owners about vm expiring soon if they aren't already.

        :param again: Notify already notified owners.
        """
        if not again and self._is_notified_about_expiration():
            return False
        success, failed = [], []

        def on_commit(act):
            act.result = {'failed': failed, 'success': success}

        with instance_activity('notification_about_expiration', instance=self,
                               on_commit=on_commit):
            from dashboard.views import VmRenewView
            level = self.get_level_object("owner")
            for u, ulevel in self.get_users_with_level(level__pk=level.pk):
                try:
                    token = VmRenewView.get_token_url(self, u)
                    u.profile.notify(
                        _('%s expiring soon') % unicode(self),
                        'dashboard/notifications/vm-expiring.html',
                        {'instance': self, 'token': token}, valid_until=min(
                            self.time_of_delete, self.time_of_suspend))
                except Exception as e:
                    failed.append((u, e))
                else:
                    success.append(u)
        return True

648 649 650 651 652 653 654
    def is_expiring(self, threshold=0.1):
        """Returns if an instance will expire soon.

        Soon means that the time of suspend or delete comes in 10% of the
        interval what the Lease allows. This rate is configurable with the
        only parameter, threshold (0.1 = 10% by default).
        """
Bach Dániel committed
655 656
        return (self._is_suspend_expiring(threshold) or
                self._is_delete_expiring(threshold))
657 658 659

    def _is_suspend_expiring(self, threshold=0.1):
        interval = self.lease.suspend_interval
Bach Dániel committed
660 661 662
        if self.time_of_suspend is not None and interval is not None:
            limit = timezone.now() + timedelta(seconds=(
                threshold * self.lease.suspend_interval.total_seconds()))
663 664 665 666 667 668
            return limit > self.time_of_suspend
        else:
            return False

    def _is_delete_expiring(self, threshold=0.1):
        interval = self.lease.delete_interval
Bach Dániel committed
669 670 671
        if self.time_of_delete is not None and interval is not None:
            limit = timezone.now() + timedelta(seconds=(
                threshold * self.lease.delete_interval.total_seconds()))
672 673 674 675
            return limit > self.time_of_delete
        else:
            return False

676 677 678 679 680 681 682
    def get_renew_times(self):
        """Returns new suspend and delete times if renew would be called.
        """
        return (
            timezone.now() + self.lease.suspend_interval,
            timezone.now() + self.lease.delete_interval)

683 684 685 686 687 688 689 690 691
    def _do_renew(self, which='both'):
        """Set expiration times to renewed values.
        """
        time_of_suspend, time_of_delete = self.get_renew_times()
        if which in ('suspend', 'both'):
            self.time_of_suspend = time_of_suspend
        if which in ('delete', 'both'):
            self.time_of_delete = time_of_delete

692
    def renew(self, which='both', base_activity=None, user=None):
Dudás Ádám committed
693 694
        """Renew virtual machine instance leases.
        """
695
        if base_activity is None:
696 697
            act_ctx = instance_activity(code_suffix='renew', instance=self,
                                        user=user)
698
        else:
699 700 701
            act_ctx = base_activity.sub_activity('renew')

        with act_ctx:
702 703
            if which not in ('suspend', 'delete', 'both'):
                raise ValueError('No such expiration type.')
704
            self._do_renew(which)
705
            self.save()
Dudás Ádám committed
706

707 708 709 710 711 712 713 714 715 716 717
    def change_password(self, user=None):
        """Generate new password for the vm

        :param self: The virtual machine.

        :param user: The user who's issuing the command.
        """

        self.pw = pwgen()
        with instance_activity(code_suffix='change_password', instance=self,
                               user=user):
718
            queue = self.get_remote_queue_name("agent")
719 720 721 722 723
            agent_tasks.change_password.apply_async(queue=queue,
                                                    args=(self.vm_name,
                                                          self.pw))
        self.save()

724 725 726 727
    def select_node(self):
        """Returns the node the VM should be deployed or migrated to.
        """
        return scheduler.select_node(self, Node.objects.all())
728

Dudás Ádám committed
729
    def _schedule_vm(self, act):
730
        """Schedule the virtual machine as part of a higher level activity.
731 732 733 734 735 736 737 738 739

        :param act: Parent activity.
        """
        # Find unused port for VNC
        if self.vnc_port is None:
            self.vnc_port = find_unused_vnc_port()

        # Schedule
        if self.node is None:
740
            self.node = self.select_node()
741 742 743

        self.save()

Dudás Ádám committed
744
    def _deploy_vm(self, act, timeout=15):
745 746 747 748 749 750 751 752 753
        """Deploy the virtual machine.

        :param self: The virtual machine.

        :param act: Parent activity.
        """
        queue_name = self.get_remote_queue_name('vm')

        # Deploy VM on remote machine
754 755 756 757
        with act.sub_activity('deploying_vm') as deploy_act:
            deploy_act.result = vm_tasks.deploy.apply_async(
                args=[self.get_vm_desc()],
                queue=queue_name).get(timeout=timeout)
758 759 760 761 762 763 764 765 766

        # Estabilish network connection (vmdriver)
        with act.sub_activity('deploying_net'):
            for net in self.interface_set.all():
                net.deploy()

        # Resume vm
        with act.sub_activity('booting'):
            vm_tasks.resume.apply_async(args=[self.vm_name],
767
                                        queue=queue_name).get(timeout=timeout)
768

769
        self.renew(which='both', base_activity=act)
770

Dudás Ádám committed
771
    def _destroy_vm(self, act, timeout=15):
772 773 774 775 776 777 778 779 780 781 782 783 784 785
        """Destroy the virtual machine and its associated networks.

        :param self: The virtual machine.

        :param act: Parent activity.
        """
        # Destroy networks
        with act.sub_activity('destroying_net'):
            for net in self.interface_set.all():
                net.destroy()

        # Destroy virtual machine
        with act.sub_activity('destroying_vm'):
            queue_name = self.get_remote_queue_name('vm')
786 787
            try:
                vm_tasks.destroy.apply_async(args=[self.vm_name],
788 789
                                             queue=queue_name
                                             ).get(timeout=timeout)
790
            except Exception as e:
791
                if e.libvirtError and "Domain not found" in str(e):
792 793 794 795 796
                    logger.debug("Domain %s was not found at %s"
                                 % (self.vm_name, queue_name))
                else:
                    raise

Dudás Ádám committed
797
    def _cleanup_after_destroy_vm(self, act, timeout=15):
798 799 800 801 802 803 804 805
        """Clean up the virtual machine's data after destroy.

        :param self: The virtual machine.

        :param act: Parent activity.
        """
        # Delete mem. dump if exists
        try:
806 807
            queue_name = self.mem_dump['datastore'].get_remote_queue_name(
                'storage')
808 809
            from storage.tasks.remote_tasks import delete_dump
            delete_dump.apply_async(args=[self.mem_dump['path']],
810
                                    queue=queue_name).get(timeout=timeout)
811 812 813 814 815 816 817
        except:
            pass

        # Clear node and VNC port association
        self.node = None
        self.vnc_port = None

818 819
    def shutdown_and_save_as_template(self, name, user=None, task_uuid=None,
                                      **kwargs):
Dudás Ádám committed
820 821
        self.shutdown(user=user)
        self.save_as_template(name, user=user, **kwargs)
822 823 824 825 826 827 828 829 830 831 832

    def get_status_icon(self):
        return {
            'NOSTATE': 'icon-rocket',
            'RUNNING': 'icon-play',
            'STOPPED': 'icon-stop',
            'SUSPENDED': 'icon-pause',
            'ERROR': 'icon-warning_sign',
            'PENDING': 'icon-rocket',
            'DESTROYED': 'icon-trash',
            'MIGRATING': 'icon-truck'}.get(self.status, 'icon-question-sign')