instance.py 42.9 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
import string
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
from celery.exceptions import TimeLimitExceeded
19
from model_utils.models import TimeStampedModel
20
from taggit.managers import TaggableManager
21

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

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

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


43
def find_unused_vnc_port():
44
    used = set(Instance.objects.values_list('vnc_port', flat=True))
45 46 47 48 49 50 51
    for p in xrange(*VNC_PORT_RANGE):
        if p not in used:
            return p
    else:
        raise Exception("No unused port could be found for VNC.")


52
class InstanceActiveManager(Manager):
Dudás Ádám committed
53

54 55 56 57 58
    def get_query_set(self):
        return super(InstanceActiveManager,
                     self).get_query_set().filter(destroyed=None)


59
class VirtualMachineDescModel(BaseResourceConfigModel):
60

61 62 63 64 65 66 67 68
    """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.'))
69
    lease = ForeignKey(Lease, help_text=_("Preferred expiration periods."))
70 71
    raw_data = TextField(verbose_name=_('raw_data'), blank=True, help_text=_(
        'Additional libvirt domain parameters in XML format.'))
Dudás Ádám committed
72 73 74 75 76
    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"))
77 78 79 80 81
    system = TextField(verbose_name=_('operating system'),
                       blank=True,
                       help_text=(_('Name of operating system in '
                                    'format like "%s".') %
                                  'Ubuntu 12.04 LTS Desktop amd64'))
Dudás Ádám committed
82
    tags = TaggableManager(blank=True, verbose_name=_("tags"))
83 84 85 86 87

    class Meta:
        abstract = True


88
class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
89

90 91 92 93 94 95 96 97 98 99 100 101 102 103
    """Virtual machine template.

    Every template has:
      * a name and a description
      * an optional parent template
      * state of the template
      * an OS name/description
      * a method of access to the system
      * default values of base resource configuration
      * list of attached images
      * set of interfaces
      * lease times (suspension & deletion)
      * time of creation and last modification
    """
104 105 106 107 108
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
Őry Máté committed
109 110 111 112 113 114 115 116 117 118
    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.'))
119
    owner = ForeignKey(User)
120 121

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

    def __unicode__(self):
        return self.name

    def running_instances(self):
        """Returns the number of running instances of the template.
        """
137 138
        return len([i for i in self.instance_set.all()
                    if i.state == 'RUNNING'])
139 140 141 142 143 144

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

149 150 151 152 153 154
    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')

155 156 157 158
    @permalink
    def get_absolute_url(self):
        return ('dashboard.views.template-detail', None, {'pk': self.pk})

159

160
class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
161

162 163 164 165 166 167 168 169 170 171 172
    """Virtual machine instance.

    Every instance has:
      * a name and a description
      * an optional parent template
      * associated share
      * a generated password for login authentication
      * time of deletion and time of suspension
      * lease times (suspension & deletion)
      * last boot timestamp
      * host node
173
      * current state (libvirt domain state)
174 175
      * time of creation and last modification
      * base resource configuration values
176
      * owner and privilege information
177
    """
178 179 180 181 182
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),  # console, networking, change state
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
Ő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)
Dudás Ádám committed
215
    destroyed = DateTimeField(blank=True, null=True,
216 217
                              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 251 252 253 254 255 256
    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."
                           % instance.state)

            Exception.__init__(self, message)

            self.instance = instance

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

261 262 263 264 265 266 267 268
    @property
    def state(self):
        """State of the virtual machine instance.
        """
        if self.activity_log.filter(activity_code__endswith='migrate',
                                    finished__isnull=True).exists():
            return 'MIGRATING'

269 270 271 272 273 274
        try:
            act = self.activity_log.filter(finished__isnull=False,
                                           resultant_state__isnull=False
                                           ).order_by('-finished').all()[0]
        except IndexError:
            act = None
275 276
        return 'NOSTATE' if act is None else act.resultant_state

277 278 279
    def is_console_available(self):
        return self.state in ('RUNNING', )

280
    def manual_state_change(self, new_state, reason=None, user=None):
281
        # TODO cancel concurrent activity (if exists)
282 283 284 285 286 287 288 289
        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()

290
    def vm_state_changed(self, new_state):
291
        try:
292 293 294 295 296 297 298
            act = InstanceActivity.create(
                code_suffix='monitor_event_%s' % new_state,
                instance=self)
            if new_state == "STOPPED":
                self.vnc_port = None
                self.node = None
                self.save()
299 300 301 302 303 304 305
        except ActivityInProgressError:
            pass  # discard state change if another activity is in progress.
        else:
            act.finished = act.started
            act.resultant_state = new_state
            act.succeeded = True
            act.save()
306

307 308
    def clean(self, *args, **kwargs):
        if self.time_of_delete is None:
309
            self._do_renew(which='delete')
310 311
        super(Instance, self).clean(*args, **kwargs)

312
    @classmethod
313
    def create_from_template(cls, template, owner, disks=None, networks=None,
Dudás Ádám committed
314
                             req_traits=None, tags=None, **kwargs):
315 316 317 318 319
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
320 321 322 323 324 325 326 327 328 329 330 331 332 333
        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.
        """
334
        disks = template.disks.all() if disks is None else disks
335

336 337 338 339 340 341 342
        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()

343 344 345
        networks = (template.interface_set.all() if networks is None
                    else networks)

346 347 348 349
        for network in networks:
            if not network.vlan.has_level(owner, 'user'):
                raise PermissionDenied()

Dudás Ádám committed
350 351 352
        req_traits = (template.req_traits.all() if req_traits is None
                      else req_traits)

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

355
        # prepare parameters
Dudás Ádám committed
356 357 358 359 360 361
        common_fields = ['name', 'description', 'num_cores', 'ram_size',
                         'max_ram_size', 'arch', 'priority', 'boot_menu',
                         'raw_data', 'lease', 'access_method']
        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
362 363
        if '%d' not in params['name']:
            params['name'] += ' %d'
Dudás Ádám committed
364

365 366 367 368 369 370 371
        instances = []
        for i in xrange(amount):
            real_params = params
            real_params['name'] = real_params['name'].replace('%d', str(i))
            instances.append(cls.__create_instance(real_params, disks,
                                                   networks, req_traits, tags))
        return instances
372 373 374

    @classmethod
    def __create_instance(cls, params, disks, networks, req_traits, tags):
375
        # create instance and do additional setup
Dudás Ádám committed
376 377
        inst = cls(**params)

378
        # save instance
379
        inst.clean()
380
        inst.save()
381
        inst.set_level(inst.owner, 'owner')
Dudás Ádám committed
382

383 384
        def __on_commit(activity):
            activity.resultant_state = 'PENDING'
385

386
        with instance_activity(code_suffix='create', instance=inst,
387
                               on_commit=__on_commit, user=inst.owner) as act:
388 389
            # create related entities
            inst.disks.add(*[disk.get_exclusive() for disk in disks])
390

391 392
            for net in networks:
                Interface.create(instance=inst, vlan=net.vlan,
393 394
                                 owner=inst.owner, managed=net.managed,
                                 base_activity=act)
Dudás Ádám committed
395

396 397 398 399
            inst.req_traits.add(*req_traits)
            inst.tags.add(*tags)

            return inst
400

Őry Máté committed
401
    @permalink
402
    def get_absolute_url(self):
403
        return ('dashboard.views.detail', None, {'pk': self.id})
404 405

    @property
406 407 408 409 410 411 412 413 414
    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
415
    def mem_dump(self):
416
        """Return the path and datastore for the memory dump.
417 418 419

        It is always on the first hard drive storage named cloud-<id>.dump
        """
420 421 422 423 424 425 426
        try:
            datastore = self.disks.all()[0].datastore
        except:
            return None
        else:
            path = datastore.path + '/' + self.vm_name + '.dump'
            return {'datastore': datastore, 'path': path}
427 428

    @property
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
    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):
444 445
        """Primary IPv4 address of the instance.
        """
446 447 448 449
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
450 451
        """Primary IPv6 address of the instance.
        """
452 453 454 455
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
456 457
        """Primary MAC address of the instance.
        """
458 459 460 461 462 463 464 465 466 467 468
        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

469 470 471 472 473 474 475 476 477
    @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

478 479 480 481 482 483 484 485 486 487 488
    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.
        """
489
        return self.activity_log.filter(finished__isnull=True).exists()
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504

    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.
        """
505
        if not self.interface_set.exclude(host=None):
506 507
            return _('None')
        proto = 'ipv6' if use_ipv6 else 'ipv4'
508 509
        return self.interface_set.exclude(host=None)[0].host.get_hostname(
            proto=proto)
510

511 512 513 514 515 516 517 518 519 520
    def get_connect_command(self, use_ipv6=False):
        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':
521
                return ('sshpass -p %(pw)s ssh -o StrictHostKeyChecking=no '
522 523 524 525 526 527
                        'cloud@%(host)s -p %(port)d') % {
                    'port': port, 'proto': proto, 'pw': self.pw,
                    'host': host}
        except:
            return

528 529 530 531 532 533 534 535 536
    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'
537 538 539
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
540 541 542
        except:
            return

tarokkk committed
543 544
    def get_vm_desc(self):
        return {
545
            'name': self.vm_name,
546
            'vcpu': self.num_cores,
547
            'memory': int(self.ram_size) * 1024,  # convert from MiB to KiB
Őry Máté committed
548
            'memory_max': int(self.max_ram_size) * 1024,  # convert MiB to KiB
549 550 551 552 553
            '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()],
554 555 556 557 558
            '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
559
                'port': self.vnc_port
560
            },
561
            'boot_token': signing.dumps(self.id, salt='activate'),
Guba Sándor committed
562
            'raw_data': "" if not self.raw_data else self.raw_data
563
        }
tarokkk committed
564

565 566 567 568
    def get_remote_queue_name(self, queue_id):
        """Get the remote worker queue name of this instance with the specified
           queue ID.
        """
569 570 571 572
        if self.node:
            return self.node.get_remote_queue_name(queue_id)
        else:
            raise Node.DoesNotExist()
573

574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
    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

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
    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).
        """
        return (self._is_suspend_expiring(self, threshold) or
                self._is_delete_expiring(self, threshold))

    def _is_suspend_expiring(self, threshold=0.1):
        interval = self.lease.suspend_interval
        if interval is not None:
            limit = timezone.now() + threshold * self.lease.suspend_interval
            return limit > self.time_of_suspend
        else:
            return False

    def _is_delete_expiring(self, threshold=0.1):
        interval = self.lease.delete_interval
        if interval is not None:
            limit = timezone.now() + threshold * self.lease.delete_interval
            return limit > self.time_of_delete
        else:
            return False

637 638 639 640 641 642 643
    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)

644 645 646 647 648 649 650 651 652
    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

653
    def renew(self, which='both', base_activity=None, user=None):
Dudás Ádám committed
654 655
        """Renew virtual machine instance leases.
        """
656 657 658 659 660 661 662 663
        if base_activity is None:
            act = instance_activity(code_suffix='renew', instance=self,
                                    user=user)
        else:
            act = base_activity.sub_activity('renew')
        with act:
            if which not in ('suspend', 'delete', 'both'):
                raise ValueError('No such expiration type.')
664
            self._do_renew(which)
665
            self.save()
Dudás Ádám committed
666

667 668 669 670 671 672 673 674 675 676 677
    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):
678
            queue = self.get_remote_queue_name("agent")
679 680 681 682 683
            agent_tasks.change_password.apply_async(queue=queue,
                                                    args=(self.vm_name,
                                                          self.pw))
        self.save()

684 685 686 687
    def select_node(self):
        """Returns the node the VM should be deployed or migrated to.
        """
        return scheduler.select_node(self, Node.objects.all())
688

689 690
    def __schedule_vm(self, act):
        """Schedule the virtual machine as part of a higher level activity.
691 692 693 694 695 696 697 698 699

        :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:
700
            self.node = self.select_node()
701 702 703

        self.save()

704
    def __deploy_vm(self, act, timeout=15):
705 706 707 708 709 710 711 712 713
        """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
714 715 716 717
        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)
718 719 720 721 722 723 724 725 726

        # 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],
727
                                        queue=queue_name).get(timeout=timeout)
728

729
        self.renew('suspend', act)
730

731
    def deploy(self, user=None, task_uuid=None):
Dudás Ádám committed
732 733 734 735 736 737 738 739 740 741 742
        """Deploy new virtual machine with network

        :param self: The virtual machine to deploy.
        :type self: vm.models.Instance

        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str
743
        """
744 745 746
        if self.destroyed:
            raise self.InstanceDestroyedError(self)

747 748
        def __on_commit(activity):
            activity.resultant_state = 'RUNNING'
749

750 751 752
        with instance_activity(code_suffix='deploy', instance=self,
                               on_commit=__on_commit, task_uuid=task_uuid,
                               user=user) as act:
753

754
            self.__schedule_vm(act)
tarokkk committed
755

756 757
            # Deploy virtual images
            with act.sub_activity('deploying_disks'):
758
                devnums = list(string.ascii_lowercase)  # a-z
759
                for disk in self.disks.all():
760 761 762 763 764 765 766
                    # assign device numbers
                    if disk.dev_num in devnums:
                        devnums.remove(disk.dev_num)
                    else:
                        disk.dev_num = devnums.pop(0)
                        disk.save()
                    # deploy disk
767
                    disk.deploy()
768

769
            self.__deploy_vm(act)
770

771 772 773
    def deploy_async(self, user=None):
        """Execute deploy asynchronously.
        """
774 775
        logger.debug('Calling async local_tasks.deploy(%s, %s)',
                     unicode(self), unicode(user))
776 777
        return local_tasks.deploy.apply_async(args=[self, user],
                                              queue="localhost.man")
778

779
    def __destroy_vm(self, act, timeout=15):
780 781 782 783 784 785 786 787 788 789 790 791 792 793
        """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')
794 795
            try:
                vm_tasks.destroy.apply_async(args=[self.vm_name],
796 797
                                             queue=queue_name
                                             ).get(timeout=timeout)
798
            except Exception as e:
799 800 801 802 803 804 805 806
                if e.libvirtError is True and "Domain not found" in str(e):
                    logger.debug("Domain %s was not found at %s"
                                 % (self.vm_name, queue_name))
                    pass
                else:
                    raise

    def __cleanup_after_destroy_vm(self, act, timeout=15):
807 808 809 810 811 812 813 814
        """Clean up the virtual machine's data after destroy.

        :param self: The virtual machine.

        :param act: Parent activity.
        """
        # Delete mem. dump if exists
        try:
815 816
            queue_name = self.mem_dump['datastore'].get_remote_queue_name(
                'storage')
817 818
            from storage.tasks.remote_tasks import delete_dump
            delete_dump.apply_async(args=[self.mem_dump['path']],
819
                                    queue=queue_name).get(timeout=timeout)
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
        except:
            pass

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

    def redeploy(self, user=None, task_uuid=None):
        """Redeploy virtual machine with network

        :param self: The virtual machine to redeploy.

        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str
        """
        with instance_activity(code_suffix='redeploy', instance=self,
                               task_uuid=task_uuid, user=user) as act:
            # Destroy VM
            if self.node:
                self.__destroy_vm(act)

            self.__cleanup_after_destroy_vm(act)

            # Deploy VM
            self.__schedule_vm(act)

            self.__deploy_vm(act)

    def redeploy_async(self, user=None):
        """Execute redeploy asynchronously.
        """
        return local_tasks.redeploy.apply_async(args=[self, user],
                                                queue="localhost.man")

858 859 860
    def shut_off(self, user=None, task_uuid=None):
        """Shut off VM. (plug-out)
        """
861 862 863
        def __on_commit(activity):
            activity.resultant_state = 'STOPPED'

864
        with instance_activity(code_suffix='shut_off', instance=self,
865 866
                               task_uuid=task_uuid, user=user,
                               on_commit=__on_commit) as act:
867 868 869 870 871
            # Destroy VM
            if self.node:
                self.__destroy_vm(act)

            self.__cleanup_after_destroy_vm(act)
872
            self.save()
873 874 875 876 877 878 879

    def shut_off_async(self, user=None):
        """Shut off VM. (plug-out)
        """
        return local_tasks.shut_off.apply_async(args=[self, user],
                                                queue="localhost.man")

880
    def destroy(self, user=None, task_uuid=None):
Dudás Ádám committed
881 882 883 884 885 886 887 888 889 890 891
        """Remove virtual machine and its networks.

        :param self: The virtual machine to destroy.
        :type self: vm.models.Instance

        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str
892
        """
893 894 895
        if self.destroyed:
            return  # already destroyed, nothing to do here

896 897 898
        def __on_commit(activity):
            activity.resultant_state = 'DESTROYED'

899
        with instance_activity(code_suffix='destroy', instance=self,
900 901
                               on_commit=__on_commit, task_uuid=task_uuid,
                               user=user) as act:
902

903
            if self.node:
904
                self.__destroy_vm(act)
905 906 907 908 909 910

            # Destroy disks
            with act.sub_activity('destroying_disks'):
                for disk in self.disks.all():
                    disk.destroy()

911
            self.__cleanup_after_destroy_vm(act)
912

Dudás Ádám committed
913
            self.destroyed = timezone.now()
914
            self.save()
915 916

    def destroy_async(self, user=None):
Dudás Ádám committed
917
        """Execute destroy asynchronously.
918
        """
919 920
        return local_tasks.destroy.apply_async(args=[self, user],
                                               queue="localhost.man")
921

922
    def sleep(self, user=None, task_uuid=None, timeout=60):
923 924
        """Suspend virtual machine with memory dump.
        """
925 926 927
        if self.state not in ['RUNNING']:
            raise self.WrongStateError(self)

928
        def __on_abort(activity, error):
929
            if isinstance(error, TimeLimitExceeded):
930 931 932 933 934 935 936
                activity.resultant_state = None
            else:
                activity.resultant_state = 'ERROR'

        def __on_commit(activity):
            activity.resultant_state = 'SUSPENDED'

937
        with instance_activity(code_suffix='sleep', instance=self,
938
                               on_abort=__on_abort, on_commit=__on_commit,
939
                               task_uuid=task_uuid, user=user) as act:
940

941 942 943 944 945 946 947 948 949 950
            # Destroy networks
            with act.sub_activity('destroying_net'):
                for net in self.interface_set.all():
                    net.destroy(delete_host=False)

            # Suspend vm
            with act.sub_activity('suspending'):
                queue_name = self.get_remote_queue_name('vm')
                vm_tasks.sleep.apply_async(args=[self.vm_name,
                                                 self.mem_dump['path']],
951 952
                                           queue=queue_name
                                           ).get(timeout=timeout)
953 954
                self.node = None
                self.save()
Guba Sándor committed
955

956
    def sleep_async(self, user=None):
Dudás Ádám committed
957
        """Execute sleep asynchronously.
958
        """
959 960
        return local_tasks.sleep.apply_async(args=[self, user],
                                             queue="localhost.man")
Guba Sándor committed
961

962
    def wake_up(self, user=None, task_uuid=None, timeout=60):
963 964 965
        if self.state not in ['SUSPENDED']:
            raise self.WrongStateError(self)

966 967 968 969 970 971
        def __on_abort(activity, error):
            activity.resultant_state = 'ERROR'

        def __on_commit(activity):
            activity.resultant_state = 'RUNNING'

972
        with instance_activity(code_suffix='wake_up', instance=self,
973
                               on_abort=__on_abort, on_commit=__on_commit,
974
                               task_uuid=task_uuid, user=user) as act:
975

976 977
            # Schedule vm
            self.__schedule_vm(act)
978
            queue_name = self.get_remote_queue_name('vm')
979 980 981 982 983

            # Resume vm
            with act.sub_activity('resuming'):
                vm_tasks.wake_up.apply_async(args=[self.vm_name,
                                                   self.mem_dump['path']],
984 985
                                             queue=queue_name
                                             ).get(timeout=timeout)
986 987 988 989 990

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

992
    def wake_up_async(self, user=None):
Dudás Ádám committed
993
        """Execute wake_up asynchronously.
994
        """
995 996
        return local_tasks.wake_up.apply_async(args=[self, user],
                                               queue="localhost.man")
997

998
    def shutdown(self, user=None, task_uuid=None, timeout=120):
999 1000
        """Shutdown virtual machine with ACPI signal.
        """
1001
        def __on_abort(activity, error):
1002
            if isinstance(error, TimeLimitExceeded):
1003 1004 1005 1006 1007 1008 1009
                activity.resultant_state = None
            else:
                activity.resultant_state = 'ERROR'

        def __on_commit(activity):
            activity.resultant_state = 'STOPPED'

1010
        with instance_activity(code_suffix='shutdown', instance=self,
1011
                               on_abort=__on_abort, on_commit=__on_commit,
1012 1013
                               task_uuid=task_uuid, user=user):
            queue_name = self.get_remote_queue_name('vm')
1014 1015
            logger.debug("RPC Shutdown at queue: %s, for vm: %s.",
                         self.vm_name, queue_name)
1016
            vm_tasks.shutdown.apply_async(kwargs={'name': self.vm_name},
1017 1018
                                          queue=queue_name
                                          ).get(timeout=timeout)
1019 1020 1021
            self.node = None
            self.vnc_port = None
            self.save()
Guba Sándor committed
1022

1023 1024
    def shutdown_async(self, user=None):
        """Execute shutdown asynchronously.
1025
        """
1026 1027
        return local_tasks.shutdown.apply_async(args=[self, user],
                                                queue="localhost.man")
Guba Sándor committed
1028

1029
    def reset(self, user=None, task_uuid=None, timeout=5):
1030 1031
        """Reset virtual machine (reset button)
        """
1032 1033
        with instance_activity(code_suffix='reset', instance=self,
                               task_uuid=task_uuid, user=user):
1034

1035
            queue_name = self.get_remote_queue_name('vm')
1036 1037 1038
            vm_tasks.reset.apply_async(args=[self.vm_name],
                                       queue=queue_name
                                       ).get(timeout=timeout)
Guba Sándor committed
1039

1040 1041
    def reset_async(self, user=None):
        """Execute reset asynchronously.
1042
        """
1043 1044
        return local_tasks.reset.apply_async(args=[self, user],
                                             queue="localhost.man")
Guba Sándor committed
1045

1046
    def reboot(self, user=None, task_uuid=None, timeout=5):
Dudás Ádám committed
1047
        """Reboot virtual machine with Ctrl+Alt+Del signal.
1048
        """
1049 1050
        with instance_activity(code_suffix='reboot', instance=self,
                               task_uuid=task_uuid, user=user):
1051

1052 1053
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.reboot.apply_async(args=[self.vm_name],
1054 1055
                                        queue=queue_name
                                        ).get(timeout=timeout)
1056

1057
    def reboot_async(self, user=None):
1058
        """Execute reboot asynchronously. """
1059 1060
        return local_tasks.reboot.apply_async(args=[self, user],
                                              queue="localhost.man")
1061

1062 1063 1064 1065 1066
    def migrate_async(self, to_node, user=None):
        """Execute migrate asynchronously. """
        return local_tasks.migrate.apply_async(args=[self, to_node, user],
                                               queue="localhost.man")

1067
    def migrate(self, to_node, user=None, task_uuid=None, timeout=120):
1068 1069 1070 1071 1072 1073
        """Live migrate running vm to another node. """
        with instance_activity(code_suffix='migrate', instance=self,
                               task_uuid=task_uuid, user=user) as act:
            # Destroy networks
            with act.sub_activity('destroying_net'):
                for net in self.interface_set.all():
1074
                    net.destroy(delete_host=False)
1075 1076 1077 1078 1079

            with act.sub_activity('migrate_vm'):
                queue_name = self.get_remote_queue_name('vm')
                vm_tasks.migrate.apply_async(args=[self.vm_name,
                                             to_node.host.hostname],
1080 1081
                                             queue=queue_name
                                             ).get(timeout=timeout)
1082 1083 1084 1085 1086 1087 1088 1089
            # Refresh node information
            self.node = to_node
            self.save()
            # Estabilish network connection (vmdriver)
            with act.sub_activity('deploying_net'):
                for net in self.interface_set.all():
                    net.deploy()

1090 1091 1092
    def save_as_template_async(self, name, user=None, **kwargs):
        return local_tasks.save_as_template.apply_async(
            args=[self, name, user, kwargs], queue="localhost.man")
1093

1094 1095 1096
    def save_as_template(self, name, task_uuid=None, user=None,
                         timeout=300, **kwargs):
        with instance_activity(code_suffix="save_as_template", instance=self,
1097
                               task_uuid=task_uuid, user=user) as act:
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
            # prepare parameters
            kwargs.setdefault('name', name)
            kwargs.setdefault('description', self.description)
            kwargs.setdefault('parent', self.template)
            kwargs.setdefault('num_cores', self.num_cores)
            kwargs.setdefault('ram_size', self.ram_size)
            kwargs.setdefault('max_ram_size', self.max_ram_size)
            kwargs.setdefault('arch', self.arch)
            kwargs.setdefault('priority', self.priority)
            kwargs.setdefault('boot_menu', self.boot_menu)
            kwargs.setdefault('raw_data', self.raw_data)
            kwargs.setdefault('lease', self.lease)
            kwargs.setdefault('access_method', self.access_method)
            kwargs.setdefault('system', self.template.system
                              if self.template else None)

            def __try_save_disk(disk):
                try:
                    return disk.save_as()  # can do in parallel
                except Disk.WrongDiskTypeError:
                    return disk

            # create template and do additional setup
            tmpl = InstanceTemplate(**kwargs)
1122
            tmpl.full_clean()  # Avoiding database errors.
1123
            tmpl.save()
1124 1125 1126 1127
            with act.sub_activity('saving_disks'):
                tmpl.disks.add(*[__try_save_disk(disk)
                               for disk in self.disks.all()])
                # save template
1128 1129 1130 1131 1132 1133 1134 1135 1136 1137
            tmpl.save()
            try:
                # create interface templates
                for i in self.interface_set.all():
                    i.save_as_template(tmpl)
            except:
                tmpl.delete()
                raise
            else:
                return tmpl
1138 1139 1140 1141 1142

    def shutdown_and_save_as_template(self, name, user=None, task_uuid=None,
                                      **kwargs):
        self.shutdown(user, task_uuid)
        self.save_as_template(name, **kwargs)