instance.py 36.4 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
8 9 10
from django.db.models import (BooleanField, CharField, DateTimeField,
                              IntegerField, ForeignKey, Manager,
                              ManyToManyField, permalink, TextField)
Őry Máté committed
11 12
from django.contrib.auth.models import User
from django.core import signing
Dudás Ádám committed
13
from django.core.exceptions import PermissionDenied
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"))
Dudás Ádám committed
77
    tags = TaggableManager(blank=True, verbose_name=_("tags"))
78 79 80 81 82

    class Meta:
        abstract = True


83
class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
84

85 86 87 88 89 90 91 92 93 94 95 96 97 98
    """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
    """
99 100 101 102 103
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
Őry Máté committed
104
    STATES = [('NEW', _('new')),        # template has just been created
105
              ('SAVING', _('saving')),  # changes are being saved
Őry Máté committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
              ('READY', _('ready'))]    # template is ready for instantiation
    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.'))
    system = TextField(verbose_name=_('operating system'),
                       blank=True,
                       help_text=(_('Name of operating system in '
                                    'format like "%s".') %
                                  'Ubuntu 12.04 LTS Desktop amd64'))
    state = CharField(max_length=10, choices=STATES, default='NEW')
    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 138 139 140
        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.
        """
141 142
        return len([i for i in self.instance_set.all()
                    if i.state == 'RUNNING'])
143 144 145 146 147 148

    @property
    def os_type(self):
        """Get the type of the template's operating system.
        """
        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
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 187
    description = TextField(blank=True, verbose_name=_('description'))
    template = ForeignKey(InstanceTemplate, blank=True, null=True,
                          related_name='instance_set',
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
    def manual_state_change(self, new_state, reason=None, user=None):
278
        # TODO cancel concurrent activity (if exists)
279 280 281 282 283 284 285 286
        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()

287
    def vm_state_changed(self, new_state):
288 289 290 291 292 293 294 295 296 297
        try:
            act = InstanceActivity.create(code_suffix='vm_state_changed',
                                          instance=self)
        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()
298

299 300 301 302 303
    def clean(self, *args, **kwargs):
        if self.time_of_delete is None:
            self.renew(which='delete')
        super(Instance, self).clean(*args, **kwargs)

304
    @classmethod
305
    def create_from_template(cls, template, owner, disks=None, networks=None,
Dudás Ádám committed
306
                             req_traits=None, tags=None, **kwargs):
307 308 309 310 311
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
312 313 314 315 316 317 318 319 320 321 322 323 324 325
        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.
        """
326
        disks = template.disks.all() if disks is None else disks
327

328 329 330 331 332 333 334
        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()

335 336 337
        networks = (template.interface_set.all() if networks is None
                    else networks)

338 339 340 341
        for network in networks:
            if not network.vlan.has_level(owner, 'user'):
                raise PermissionDenied()

Dudás Ádám committed
342 343 344
        req_traits = (template.req_traits.all() if req_traits is None
                      else req_traits)

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

347
        # prepare parameters
Dudás Ádám committed
348 349 350 351 352 353 354
        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

355 356 357 358 359
        return [cls.__create_instance(params, disks, networks, req_traits,
                                      tags) for i in xrange(amount)]

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

363
        # save instance
364
        inst.clean()
365
        inst.save()
366
        inst.set_level(inst.owner, 'owner')
Dudás Ádám committed
367

368 369
        def __on_commit(activity):
            activity.resultant_state = 'PENDING'
370

371 372 373 374
        with instance_activity(code_suffix='create', instance=inst,
                               on_commit=__on_commit, user=inst.owner):
            # create related entities
            inst.disks.add(*[disk.get_exclusive() for disk in disks])
375

376 377 378
            for net in networks:
                Interface.create(instance=inst, vlan=net.vlan,
                                 owner=inst.owner, managed=net.managed)
Dudás Ádám committed
379

380 381 382 383
            inst.req_traits.add(*req_traits)
            inst.tags.add(*tags)

            return inst
384

Őry Máté committed
385
    @permalink
386
    def get_absolute_url(self):
387
        return ('dashboard.views.detail', None, {'pk': self.id})
388 389

    @property
390 391 392 393 394 395 396 397 398
    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
399
    def mem_dump(self):
400
        """Return the path and datastore for the memory dump.
401 402 403

        It is always on the first hard drive storage named cloud-<id>.dump
        """
404 405 406
        datastore = self.disks.all()[0].datastore
        path = datastore.path + '/' + self.vm_name + '.dump'
        return {'datastore': datastore, 'path': path}
407 408

    @property
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    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):
424 425
        """Primary IPv4 address of the instance.
        """
426 427 428 429
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
430 431
        """Primary IPv6 address of the instance.
        """
432 433 434 435
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
436 437
        """Primary MAC address of the instance.
        """
438 439 440 441 442 443 444 445 446 447 448
        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

449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
    @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

    @property
    def system(self):
        """Get the instance's operating system.
        """
        if self.template is None:
            return _("Unknown")
        else:
            return self.template.system

467 468 469 470 471 472 473 474 475 476 477
    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.
        """
478
        return self.activity_log.filter(finished__isnull=True).exists()
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493

    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.
        """
494
        if not self.interface_set.exclude(host=None):
495 496
            return _('None')
        proto = 'ipv6' if use_ipv6 else 'ipv4'
497 498
        return self.interface_set.exclude(host=None)[0].host.get_hostname(
            proto=proto)
499

500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
    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':
                return ('sshpass -p %(pw)s ssh -o StrictHostKeyChecking=n '
                        'cloud@%(host)s -p %(port)d') % {
                    'port': port, 'proto': proto, 'pw': self.pw,
                    'host': host}
        except:
            return

517 518 519 520 521 522 523 524 525
    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'
526 527 528
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
529 530 531
        except:
            return

tarokkk committed
532 533
    def get_vm_desc(self):
        return {
534
            'name': self.vm_name,
535
            'vcpu': self.num_cores,
536
            'memory': int(self.ram_size) * 1024,  # convert from MiB to KiB
Őry Máté committed
537
            'memory_max': int(self.max_ram_size) * 1024,  # convert MiB to KiB
538 539 540 541 542
            '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()],
543 544 545 546 547
            '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
548
                'port': self.vnc_port
549
            },
550
            'boot_token': signing.dumps(self.id, salt='activate'),
Guba Sándor committed
551
            'raw_data': "" if not self.raw_data else self.raw_data
552
        }
tarokkk committed
553

554 555 556 557
    def get_remote_queue_name(self, queue_id):
        """Get the remote worker queue name of this instance with the specified
           queue ID.
        """
558 559 560 561
        if self.node:
            return self.node.get_remote_queue_name(queue_id)
        else:
            raise Node.DoesNotExist()
562

Dudás Ádám committed
563 564 565 566 567 568 569 570 571 572 573
    def renew(self, which='both'):
        """Renew virtual machine instance leases.
        """
        if which not in ['suspend', 'delete', 'both']:
            raise ValueError('No such expiration type.')
        if which in ['suspend', 'both']:
            self.time_of_suspend = timezone.now() + self.lease.suspend_interval
        if which in ['delete', 'both']:
            self.time_of_delete = timezone.now() + self.lease.delete_interval
        self.save()

574 575 576 577 578 579 580 581 582 583 584
    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):
585
            queue = self.get_remote_queue_name("agent")
586 587 588 589 590
            agent_tasks.change_password.apply_async(queue=queue,
                                                    args=(self.vm_name,
                                                          self.pw))
        self.save()

591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
    def __schedule_vm(self, act):
        """Schedule the virtual machine.

        :param self: The virtual machine.

        :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:
            self.node = scheduler.select_node(self, Node.objects.all())

        self.save()

    def __deploy_vm(self, act):
        """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
        with act.sub_activity('deploying_vm'):
            vm_tasks.deploy.apply_async(args=[self.get_vm_desc()],
                                        queue=queue_name).get()

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

        self.renew('suspend')

634
    def deploy(self, user=None, task_uuid=None):
Dudás Ádám committed
635 636 637 638 639 640 641 642 643 644 645
        """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
646
        """
647 648 649
        if self.destroyed:
            raise self.InstanceDestroyedError(self)

650 651
        def __on_commit(activity):
            activity.resultant_state = 'RUNNING'
652

653 654 655
        with instance_activity(code_suffix='deploy', instance=self,
                               on_commit=__on_commit, task_uuid=task_uuid,
                               user=user) as act:
656

657
            self.__schedule_vm(act)
tarokkk committed
658

659 660
            # Deploy virtual images
            with act.sub_activity('deploying_disks'):
661
                devnums = list(string.ascii_lowercase)  # a-z
662
                for disk in self.disks.all():
663 664 665 666 667 668 669
                    # 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
670
                    disk.deploy()
671

672
            self.__deploy_vm(act)
673

674 675 676
    def deploy_async(self, user=None):
        """Execute deploy asynchronously.
        """
677 678
        logger.debug('Calling async local_tasks.deploy(%s, %s)',
                     unicode(self), unicode(user))
679 680
        return local_tasks.deploy.apply_async(args=[self, user],
                                              queue="localhost.man")
681

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
    def __destroy_vm(self, act):
        """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')
697 698 699 700 701 702 703
            try:
                vm_tasks.destroy.apply_async(args=[self.vm_name],
                                             queue=queue_name).get()
            except Exception as e:
                if e.libvirtError is True:
                    if "Domain not found" in str(e):
                        pass
704 705 706 707 708 709 710 711 712 713 714 715

    def __cleanup_after_destroy_vm(self, act):
        """Clean up the virtual machine's data after destroy.

        :param self: The virtual machine.

        :param act: Parent activity.
        """
        # Delete mem. dump if exists
        queue_name = self.mem_dump['datastore'].get_remote_queue_name(
            'storage')
        try:
716 717 718
            from storage.tasks.remote_tasks import delete_dump
            delete_dump.apply_async(args=[self.mem_dump['path']],
                                    queue=queue_name).get()
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
        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")

757
    def destroy(self, user=None, task_uuid=None):
Dudás Ádám committed
758 759 760 761 762 763 764 765 766 767 768
        """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
769
        """
770 771 772
        if self.destroyed:
            return  # already destroyed, nothing to do here

773 774 775
        def __on_commit(activity):
            activity.resultant_state = 'DESTROYED'

776
        with instance_activity(code_suffix='destroy', instance=self,
777 778
                               on_commit=__on_commit, task_uuid=task_uuid,
                               user=user) as act:
779

780
            if self.node:
781
                self.__destroy_vm(act)
782 783 784 785 786 787

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

788
            self.__cleanup_after_destroy_vm(act)
789

Dudás Ádám committed
790
            self.destroyed = timezone.now()
791
            self.save()
792 793

    def destroy_async(self, user=None):
Dudás Ádám committed
794
        """Execute destroy asynchronously.
795
        """
796 797
        return local_tasks.destroy.apply_async(args=[self, user],
                                               queue="localhost.man")
798 799 800 801

    def sleep(self, user=None, task_uuid=None):
        """Suspend virtual machine with memory dump.
        """
802 803 804
        if self.state not in ['RUNNING']:
            raise self.WrongStateError(self)

805
        def __on_abort(activity, error):
806
            if isinstance(error, TimeLimitExceeded):
807 808 809 810 811 812 813
                activity.resultant_state = None
            else:
                activity.resultant_state = 'ERROR'

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

814
        with instance_activity(code_suffix='sleep', instance=self,
815
                               on_abort=__on_abort, on_commit=__on_commit,
816
                               task_uuid=task_uuid, user=user):
817

818
            queue_name = self.get_remote_queue_name('vm')
819 820
            vm_tasks.sleep.apply_async(args=[self.vm_name,
                                             self.mem_dump['path']],
821
                                       queue=queue_name).get()
Guba Sándor committed
822

823
    def sleep_async(self, user=None):
Dudás Ádám committed
824
        """Execute sleep asynchronously.
825
        """
826 827
        return local_tasks.sleep.apply_async(args=[self, user],
                                             queue="localhost.man")
Guba Sándor committed
828

829
    def wake_up(self, user=None, task_uuid=None):
830 831 832
        if self.state not in ['SUSPENDED']:
            raise self.WrongStateError(self)

833 834 835 836 837 838
        def __on_abort(activity, error):
            activity.resultant_state = 'ERROR'

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

839
        with instance_activity(code_suffix='wake_up', instance=self,
840
                               on_abort=__on_abort, on_commit=__on_commit,
841
                               task_uuid=task_uuid, user=user):
842

843
            queue_name = self.get_remote_queue_name('vm')
844 845 846
            vm_tasks.wake_up.apply_async(args=[self.vm_name,
                                               self.mem_dump['path']],
                                         queue=queue_name).get()
847

848
    def wake_up_async(self, user=None):
Dudás Ádám committed
849
        """Execute wake_up asynchronously.
850
        """
851 852
        return local_tasks.wake_up.apply_async(args=[self, user],
                                               queue="localhost.man")
853

854 855 856
    def shutdown(self, user=None, task_uuid=None):
        """Shutdown virtual machine with ACPI signal.
        """
857
        def __on_abort(activity, error):
858
            if isinstance(error, TimeLimitExceeded):
859 860 861 862 863 864 865
                activity.resultant_state = None
            else:
                activity.resultant_state = 'ERROR'

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

866
        with instance_activity(code_suffix='shutdown', instance=self,
867
                               on_abort=__on_abort, on_commit=__on_commit,
868 869
                               task_uuid=task_uuid, user=user):
            queue_name = self.get_remote_queue_name('vm')
870 871
            logger.debug("RPC Shutdown at queue: %s, for vm: %s.",
                         self.vm_name, queue_name)
872
            vm_tasks.shutdown.apply_async(kwargs={'name': self.vm_name},
873
                                          queue=queue_name).get()
874 875 876
            self.node = None
            self.vnc_port = None
            self.save()
Guba Sándor committed
877

878 879
    def shutdown_async(self, user=None):
        """Execute shutdown asynchronously.
880
        """
881 882
        return local_tasks.shutdown.apply_async(args=[self, user],
                                                queue="localhost.man")
Guba Sándor committed
883

884 885 886
    def reset(self, user=None, task_uuid=None):
        """Reset virtual machine (reset button)
        """
887 888
        with instance_activity(code_suffix='reset', instance=self,
                               task_uuid=task_uuid, user=user):
889

890 891 892
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.restart.apply_async(args=[self.vm_name],
                                         queue=queue_name).get()
Guba Sándor committed
893

894 895
    def reset_async(self, user=None):
        """Execute reset asynchronously.
896
        """
897 898
        return local_tasks.restart.apply_async(args=[self, user],
                                               queue="localhost.man")
Guba Sándor committed
899

900
    def reboot(self, user=None, task_uuid=None):
Dudás Ádám committed
901
        """Reboot virtual machine with Ctrl+Alt+Del signal.
902
        """
903 904
        with instance_activity(code_suffix='reboot', instance=self,
                               task_uuid=task_uuid, user=user):
905

906 907 908
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.reboot.apply_async(args=[self.vm_name],
                                        queue=queue_name).get()
909

910
    def reboot_async(self, user=None):
911
        """Execute reboot asynchronously. """
912 913
        return local_tasks.reboot.apply_async(args=[self, user],
                                              queue="localhost.man")
914

915 916 917 918 919 920 921 922 923 924 925 926
    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")

    def migrate(self, to_node, user=None, task_uuid=None):
        """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():
927
                    net.destroy(delete_host=False)
928 929 930 931 932 933 934 935 936 937 938 939 940 941

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

942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957
    def save_as_template(self, name, **kwargs):
        # 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)
958 959

        def __try_save_disk(disk):
960
            try:
961
                return disk.save_as()
962
            except Disk.WrongDiskTypeError:
963
                return disk
964

965 966 967
        # copy disks
        disks = [__try_save_disk(disk) for disk in self.disks.all()]
        kwargs.setdefault('disks', disks)
968

969 970
        # create template and do additional setup
        tmpl = InstanceTemplate(**kwargs)
971

972 973 974 975 976 977 978 979 980 981 982
        # save template
        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
983 984 985 986 987

    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)