operations.py 62.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE.  If not, see <http://www.gnu.org/licenses/>.

18
from __future__ import absolute_import, unicode_literals
Bach Dániel committed
19 20
from base64 import encodestring
from hashlib import md5
Dudás Ádám committed
21
from logging import getLogger
Bach Dániel committed
22
import os
23
from re import search
Őry Máté committed
24
from string import ascii_lowercase
Bach Dániel committed
25 26 27
from StringIO import StringIO
from tarfile import TarFile, TarInfo
import time
Kálmán Viktor committed
28
from urlparse import urlsplit
Dudás Ádám committed
29

30
from django.core.exceptions import PermissionDenied, SuspiciousOperation
31
from django.core.urlresolvers import reverse
Dudás Ádám committed
32
from django.utils import timezone
33
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
34
from django.conf import settings
Bach Dániel committed
35
from django.db.models import Q
Dudás Ádám committed
36

37 38
from sizefield.utils import filesizeformat

39 40
from celery.contrib.abortable import AbortableAsyncResult
from celery.exceptions import TimeLimitExceeded, TimeoutError
41

42 43 44
from common.models import (
    create_readable, humanize_exception, HumanReadableException
)
45
from common.operations import Operation, register_operation, SubOperationMixin
Bach Dániel committed
46
from manager.scheduler import SchedulerError
47 48 49
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
50
from .models import (
51
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
52
    NodeActivity, pwgen
53
)
Bach Dániel committed
54
from .tasks import agent_tasks, vm_tasks
Dudás Ádám committed
55

Kálmán Viktor committed
56
from dashboard.store_api import Store, NoStoreException
Bach Dániel committed
57 58
from firewall.models import Host
from monitor.client import Client
59
from storage.tasks import storage_tasks
Kálmán Viktor committed
60

Dudás Ádám committed
61
logger = getLogger(__name__)
62 63


64 65 66 67 68
class RemoteOperationMixin(object):

    remote_timeout = 30

    def _operation(self, **kwargs):
Őry Máté committed
69
        args = self._get_remote_args(**kwargs)
70 71 72 73 74 75 76 77 78
        return self.task.apply_async(
            args=args, queue=self._get_remote_queue()
        ).get(timeout=self.remote_timeout)

    def check_precond(self):
        super(RemoteOperationMixin, self).check_precond()
        self._get_remote_queue()


79 80 81 82 83 84 85
class AbortableRemoteOperationMixin(object):
    remote_step = property(lambda self: self.remote_timeout / 10)

    def _operation(self, task, **kwargs):
        args = self._get_remote_args(**kwargs),
        remote = self.task.apply_async(
            args=args, queue=self._get_remote_queue())
86
        for i in xrange(0, self.remote_timeout, self.remote_step):
87 88 89 90 91 92 93
            try:
                return remote.get(timeout=self.remote_step)
            except TimeoutError as e:
                if task is not None and task.is_aborted():
                    AbortableAsyncResult(remote.id).abort()
                    raise humanize_exception(ugettext_noop(
                        "Operation aborted by user."), e)
94
        raise TimeLimitExceeded()
95 96


97
class InstanceOperation(Operation):
98
    acl_level = 'owner'
99
    async_operation = abortable_async_instance_operation
100
    host_cls = Instance
101
    concurrency_check = True
102 103
    accept_states = None
    deny_states = None
104
    resultant_state = None
Dudás Ádám committed
105

106
    def __init__(self, instance):
107
        super(InstanceOperation, self).__init__(subject=instance)
108 109 110
        self.instance = instance

    def check_precond(self):
111 112
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
113 114 115 116 117 118 119 120 121 122 123 124 125 126
        if self.accept_states:
            if self.instance.status not in self.accept_states:
                logger.debug("precond failed for %s: %s not in %s",
                             unicode(self.__class__),
                             unicode(self.instance.status),
                             unicode(self.accept_states))
                raise self.instance.WrongStateError(self.instance)
        if self.deny_states:
            if self.instance.status in self.deny_states:
                logger.debug("precond failed for %s: %s in %s",
                             unicode(self.__class__),
                             unicode(self.instance.status),
                             unicode(self.accept_states))
                raise self.instance.WrongStateError(self.instance)
127 128

    def check_auth(self, user):
129
        if not self.instance.has_level(user, self.acl_level):
130 131 132
            raise humanize_exception(ugettext_noop(
                "%(acl_level)s level is required for this operation."),
                PermissionDenied(), acl_level=self.acl_level)
133

134
        super(InstanceOperation, self).check_auth(user=user)
135

136 137 138 139
        if (self.instance.node and not self.instance.node.online
                and not user.is_superuser):
            raise self.instance.WrongStateError(self.instance)

140 141
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
142 143 144 145 146 147 148 149 150 151
        if parent:
            if parent.instance != self.instance:
                raise ValueError("The instance associated with the specified "
                                 "parent activity does not match the instance "
                                 "bound to the operation.")
            if parent.user != user:
                raise ValueError("The user associated with the specified "
                                 "parent activity does not match the user "
                                 "provided as parameter.")

152 153 154
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name, resultant_state=self.resultant_state)
155 156
        else:
            return InstanceActivity.create(
157 158
                code_suffix=self.get_activity_code_suffix(),
                instance=self.instance,
159
                readable_name=name, user=user,
160 161
                concurrency_check=self.concurrency_check,
                resultant_state=self.resultant_state)
162

163 164 165 166 167
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

168

169 170 171 172 173 174 175 176 177 178 179
class RemoteInstanceOperation(RemoteOperationMixin, InstanceOperation):

    remote_queue = ('vm', 'fast')

    def _get_remote_queue(self):
        return self.instance.get_remote_queue_name(*self.remote_queue)

    def _get_remote_args(self, **kwargs):
        return [self.instance.vm_name]


Bach Dániel committed
180 181 182 183 184 185 186 187 188 189 190 191 192 193
class EnsureAgentMixin(object):
    accept_states = ('RUNNING', )

    def check_precond(self):
        super(EnsureAgentMixin, self).check_precond()

        last_boot_time = self.instance.activity_log.filter(
            succeeded=True, activity_code__in=(
                "vm.Instance.deploy", "vm.Instance.reset",
                "vm.Instance.reboot")).latest("finished").finished

        try:
            InstanceActivity.objects.filter(
                activity_code="vm.Instance.agent.starting",
194 195
                started__gt=last_boot_time, instance=self.instance
            ).latest("started")
Bach Dániel committed
196 197 198 199 200 201 202 203 204
        except InstanceActivity.DoesNotExist:  # no agent since last boot
            raise self.instance.NoAgentError(self.instance)


class RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation):
    remote_queue = ('agent', )
    concurrency_check = False


205
@register_operation
206 207 208 209 210
class AddInterfaceOperation(InstanceOperation):
    id = 'add_interface'
    name = _("add interface")
    description = _("Add a new network interface for the specified VLAN to "
                    "the VM.")
211
    required_perms = ()
212
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
213

214 215 216 217 218 219 220
    def rollback(self, net, activity):
        with activity.sub_activity(
            'destroying_net',
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

221
    def _operation(self, activity, user, system, vlan, managed=None):
222
        if not vlan.has_level(user, 'user'):
223 224 225
            raise humanize_exception(ugettext_noop(
                "User acces to vlan %(vlan)s is required."),
                PermissionDenied(), vlan=vlan)
226 227 228 229 230 231 232
        if managed is None:
            managed = vlan.managed

        net = Interface.create(base_activity=activity, instance=self.instance,
                               managed=managed, owner=user, vlan=vlan)

        if self.instance.is_running:
233
            try:
234 235
                self.instance._attach_network(
                    interface=net, parent_activity=activity)
236 237 238 239
            except Exception as e:
                if hasattr(e, 'libvirtError'):
                    self.rollback(net, activity)
                raise
240
            net.deploy()
Bach Dániel committed
241 242
            self.instance._change_ip(parent_activity=activity)
            self.instance._restart_networking(parent_activity=activity)
243

244 245 246 247
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

248

249
@register_operation
250
class CreateDiskOperation(InstanceOperation):
251

252 253
    id = 'create_disk'
    name = _("create disk")
254
    description = _("Create and attach empty disk to the virtual machine.")
255
    required_perms = ('storage.create_empty_disk', )
256
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
257

258
    def _operation(self, user, size, activity, name=None):
Bach Dániel committed
259 260
        from storage.models import Disk

261 262 263
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
264
        disk.full_clean()
265 266 267 268
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
269
        disk.save()
270 271
        self.instance.disks.add(disk)

272
        if self.instance.is_running:
273 274 275 276
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
277
                disk.deploy()
278
            self.instance._attach_disk(parent_activity=activity, disk=disk)
279

280
    def get_activity_name(self, kwargs):
281 282 283
        return create_readable(
            ugettext_noop("create disk %(name)s (%(size)s)"),
            size=filesizeformat(kwargs['size']), name=kwargs['name'])
284 285


286
@register_operation
287
class ResizeDiskOperation(RemoteInstanceOperation):
288 289 290 291 292

    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
293
    required_perms = ('storage.resize_disk', )
294 295
    accept_states = ('RUNNING', )
    async_queue = "localhost.man.slow"
296 297
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
298

299 300 301
    def _get_remote_args(self, disk, size, **kwargs):
        return (super(ResizeDiskOperation, self)
                ._get_remote_args(**kwargs) + [disk.path, size])
302 303 304 305 306 307

    def get_activity_name(self, kwargs):
        return create_readable(
            ugettext_noop("resize disk %(name)s to %(size)s"),
            size=filesizeformat(kwargs['size']), name=kwargs['disk'].name)

308 309 310 311 312
    def _operation(self, disk, size):
        super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
        disk.size = size
        disk.save()

313

314
@register_operation
315 316 317
class DownloadDiskOperation(InstanceOperation):
    id = 'download_disk'
    name = _("download disk")
318 319 320 321
    description = _("Download and attach disk image (ISO file) for the "
                    "virtual machine. Most operating systems do not detect a "
                    "new optical drive, so you may have to reboot the "
                    "machine.")
322
    abortable = True
323
    has_percentage = True
324
    required_perms = ('storage.download_disk', )
325
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
326
    async_queue = "localhost.man.slow"
327

328
    def _operation(self, user, url, task, activity, name=None):
Bach Dániel committed
329 330
        from storage.models import Disk

331
        disk = Disk.download(url=url, name=name, task=task)
332 333 334 335
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
336
        disk.full_clean()
337
        disk.save()
338
        self.instance.disks.add(disk)
339 340
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
341

342 343 344 345
        activity.result = create_readable(ugettext_noop(
            "Downloading %(url)s is finished. The file md5sum "
            "is: '%(checksum)s'."),
            url=url, checksum=disk.checksum)
Őry Máté committed
346
        # TODO iso (cd) hot-plug is not supported by kvm/guests
347
        if self.instance.is_running and disk.type not in ["iso"]:
348
            self.instance._attach_disk(parent_activity=activity, disk=disk)
349

350

351
@register_operation
352
class DeployOperation(InstanceOperation):
Dudás Ádám committed
353 354
    id = 'deploy'
    name = _("deploy")
355 356
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
357
    required_perms = ()
358
    deny_states = ('SUSPENDED', 'RUNNING')
359
    resultant_state = 'RUNNING'
Dudás Ádám committed
360

361 362
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
363
                                        self.instance.STATUS.PENDING,
364 365
                                        self.instance.STATUS.ERROR)

366 367 368
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
369 370
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
371
        activity.result = create_readable(
Guba Sándor committed
372
            ugettext_noop("virtual machine successfully "
373 374
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
375

376
    def _operation(self, activity, node=None):
Dudás Ádám committed
377 378
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
379 380 381 382 383
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
384 385

        # Deploy virtual images
386 387 388 389 390 391
        try:
            self.instance._deploy_disks(parent_activity=activity)
        except:
            self.instance.yield_node()
            self.instance.yield_vnc_port()
            raise
Dudás Ádám committed
392 393

        # Deploy VM on remote machine
394
        if self.instance.state not in ['PAUSED']:
395
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
396 397

        # Establish network connection (vmdriver)
398 399 400
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
401 402
            self.instance.deploy_net()

403 404 405 406 407
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

408
        self.instance._resume_vm(parent_activity=activity)
Dudás Ádám committed
409

410 411 412
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
413

414
    @register_operation
Őry Máté committed
415
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
416 417
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
418
        description = _("Deploy virtual machine.")
419 420 421
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy

Őry Máté committed
422
        def _get_remote_args(self, **kwargs):
423 424 425 426 427 428 429 430 431
            return [self.instance.get_vm_desc()]
            # intentionally not calling super

        def get_activity_name(self, kwargs):
            return create_readable(ugettext_noop("deploy virtual machine"),
                                   ugettext_noop("deploy vm to %(node)s"),
                                   node=self.instance.node)

    @register_operation
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    class DeployDisksOperation(SubOperationMixin, InstanceOperation):
        id = "_deploy_disks"
        name = _("deploy disks")
        description = _("Deploy all associated disks.")

        def _operation(self):
            devnums = list(ascii_lowercase)  # a-z
            for disk in self.instance.disks.all():
                # 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
                disk.deploy()

449
    @register_operation
Őry Máté committed
450
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
451 452 453 454 455
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
456

457
@register_operation
458
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
459 460
    id = 'destroy'
    name = _("destroy")
461 462
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
463
    required_perms = ()
464
    resultant_state = 'DESTROYED'
Dudás Ádám committed
465

466 467 468
    def on_abort(self, activity, error):
        activity.resultant_state = None

469
    def _operation(self, activity, system):
470
        # Destroy networks
471 472 473
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
474
            if self.instance.node:
475
                self.instance.shutdown_net()
476
            self.instance.destroy_net()
Dudás Ádám committed
477

478
        if self.instance.node:
479
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
480 481

        # Destroy disks
482 483 484
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
485
            self.instance.destroy_disks()
Dudás Ádám committed
486

Dudás Ádám committed
487 488
        # Delete mem. dump if exists
        try:
489
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
490 491 492 493 494 495
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
496 497 498 499

        self.instance.destroyed_at = timezone.now()
        self.instance.save()

500
    @register_operation
501
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
502 503 504 505 506
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

507 508 509 510 511 512 513 514 515 516 517 518 519 520
    @register_operation
    class DeleteMemDumpOperation(RemoteOperationMixin, SubOperationMixin,
                                 InstanceOperation):
        id = "_delete_mem_dump"
        name = _("removing memory dump")
        task = storage_tasks.delete_dump

        def _get_remote_queue(self):
            return self.instance.mem_dump['datastore'].get_remote_queue_name(
                "storage", "fast")

        def _get_remote_args(self, **kwargs):
            return [self.instance.mem_dump['path']]

Dudás Ádám committed
521

522
@register_operation
523
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
524 525
    id = 'migrate'
    name = _("migrate")
526 527
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
528
    required_perms = ()
529
    superuser_required = True
530
    accept_states = ('RUNNING', )
531
    async_queue = "localhost.man.slow"
532 533
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
534
    remote_timeout = 1000
535

536
    def _get_remote_args(self, to_node, live_migration, **kwargs):
537
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
538
                + [to_node.host.hostname, live_migration])
Dudás Ádám committed
539

540
    def rollback(self, activity):
541 542 543
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
544 545
            self.instance.deploy_net()

546
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
547
        if not to_node:
Bach Dániel committed
548 549 550 551 552 553
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

554
        try:
555 556 557
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
558 559
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
560 561 562
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
563
            raise
Dudás Ádám committed
564

565
        # Shutdown networks
566 567 568
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
569 570
            self.instance.shutdown_net()

Dudás Ádám committed
571 572 573
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
574

Dudás Ádám committed
575
        # Estabilish network connection (vmdriver)
576 577 578
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
579
            self.instance.deploy_net()
Dudás Ádám committed
580 581


582
@register_operation
583
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
584 585
    id = 'reboot'
    name = _("reboot")
586 587
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
588
    required_perms = ()
589
    accept_states = ('RUNNING', )
590
    task = vm_tasks.reboot
Dudás Ádám committed
591

592 593
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
594 595 596
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
597 598


599
@register_operation
600 601 602
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
603 604 605
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
606
    required_perms = ()
607
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
608

609 610
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
611 612
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
613 614 615 616 617
            interface.shutdown()

        interface.destroy()
        interface.delete()

618 619
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
620
                               vlan=kwargs['interface'].vlan)
621

622

623
@register_operation
624 625 626 627 628
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
629
    required_perms = ('vm.config_ports', )
630 631 632 633

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
634
            raise SuspiciousOperation()
635 636 637 638 639 640 641
        activity.readable_name = create_readable(
            ugettext_noop("close %(proto)s/%(port)d on %(host)s"),
            proto=rule.proto, port=rule.dport, host=rule.host)
        rule.delete()


@register_operation
642 643 644 645 646
class AddPortOperation(InstanceOperation):
    id = 'add_port'
    name = _("open port")
    description = _("Open the specified port.")
    concurrency_check = False
647
    required_perms = ('vm.config_ports', )
648 649 650

    def _operation(self, activity, host, proto, port):
        if host.interface_set.get().instance != self.instance:
651
            raise SuspiciousOperation()
652 653 654 655 656 657 658
        host.add_port(proto, private=port)
        activity.readable_name = create_readable(
            ugettext_noop("open %(proto)s/%(port)d on %(host)s"),
            proto=proto, port=port, host=host)


@register_operation
659 660 661
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
662 663
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
664
    required_perms = ()
665
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
666 667

    def _operation(self, activity, user, system, disk):
668
        if self.instance.is_running and disk.type not in ["iso"]:
669
            self.instance._detach_disk(disk=disk, parent_activity=activity)
670 671 672 673
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
674
            disk.destroy()
675
            return self.instance.disks.remove(disk)
676

677 678 679
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
680 681


682
@register_operation
683
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
684 685
    id = 'reset'
    name = _("reset")
686
    description = _("Cold reboot virtual machine (power cycle).")
687
    required_perms = ()
688
    accept_states = ('RUNNING', )
689
    task = vm_tasks.reset
Dudás Ádám committed
690

691 692
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
693 694 695
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
696 697


698
@register_operation
699
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
700 701
    id = 'save_as_template'
    name = _("save as template")
702 703 704 705
    description = _("Save virtual machine as a template so they can be shared "
                    "with users and groups.  Anyone who has access to a "
                    "template (and to the networks it uses) will be able to "
                    "start an instance of it.")
706
    has_percentage = True
707
    abortable = True
708
    required_perms = ('vm.create_template', )
709
    accept_states = ('RUNNING', 'STOPPED')
710
    async_queue = "localhost.man.slow"
Dudás Ádám committed
711

712 713 714 715
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

716 717 718 719 720 721
    @staticmethod
    def _rename(name):
        m = search(r" v(\d+)$", name)
        if m:
            v = int(m.group(1)) + 1
            name = search(r"^(.*) v(\d+)$", name).group(1)
722
        else:
723 724
            v = 1
        return "%s v%d" % (name, v)
725

726
    def on_abort(self, activity, error):
727
        if hasattr(self, 'disks'):
728 729 730
            for disk in self.disks:
                disk.destroy()

731
    def _operation(self, activity, user, system, name=None,
732
                   with_shutdown=True, clone=False, task=None, **kwargs):
733 734
        try:
            self.instance._cleanup(parent_activity=activity, user=user)
735
        except:
736 737
            pass

738
        if with_shutdown:
739
            try:
740 741
                self.instance.shutdown(parent_activity=activity,
                                       user=user, task=task)
742 743 744
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
745 746 747 748 749 750 751 752
        # prepare parameters
        params = {
            'access_method': self.instance.access_method,
            'arch': self.instance.arch,
            'boot_menu': self.instance.boot_menu,
            'description': self.instance.description,
            'lease': self.instance.lease,  # Can be problem in new VM
            'max_ram_size': self.instance.max_ram_size,
753
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
754 755
            'num_cores': self.instance.num_cores,
            'owner': user,
756
            'parent': self.instance.template or None,  # Can be problem
Dudás Ádám committed
757 758 759 760 761 762
            'priority': self.instance.priority,
            'ram_size': self.instance.ram_size,
            'raw_data': self.instance.raw_data,
            'system': self.instance.system,
        }
        params.update(kwargs)
Bach Dániel committed
763
        params.pop("parent_activity", None)
Dudás Ádám committed
764

765 766
        from storage.models import Disk

Dudás Ádám committed
767 768
        def __try_save_disk(disk):
            try:
769
                return disk.save_as(task)
Dudás Ádám committed
770 771 772
            except Disk.WrongDiskTypeError:
                return disk

773
        self.disks = []
774 775 776 777 778 779 780
        for disk in self.instance.disks.all():
            with activity.sub_activity(
                'saving_disk',
                readable_name=create_readable(
                    ugettext_noop("saving disk %(name)s"),
                    name=disk.name)
            ):
781 782
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
783 784 785 786
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
787
        # Copy traits from the VM instance
788
        tmpl.req_traits.add(*self.instance.req_traits.all())
789 790
        if clone:
            tmpl.clone_acl(self.instance.template)
Guba Sándor committed
791
            # Add permission for the original owner of the template
792 793
            tmpl.set_level(self.instance.template.owner, 'owner')
            tmpl.set_level(user, 'owner')
Dudás Ádám committed
794
        try:
795
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
796 797 798 799 800 801 802
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
803 804 805 806
            return create_readable(
                ugettext_noop("New template: %(template)s"),
                template=reverse('dashboard.views.template-detail',
                                 kwargs={'pk': tmpl.pk}))
Dudás Ádám committed
807 808


809
@register_operation
810 811
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
812 813
    id = 'shutdown'
    name = _("shutdown")
814 815 816 817
    description = _("Try to halt virtual machine by a standard ACPI signal, "
                    "allowing the operating system to keep a consistent "
                    "state. The operation will fail if the machine does not "
                    "turn itself off in a period.")
Kálmán Viktor committed
818
    abortable = True
819
    required_perms = ()
820
    accept_states = ('RUNNING', )
821
    resultant_state = 'STOPPED'
822 823
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
824
    remote_timeout = 180
Dudás Ádám committed
825

826 827
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
828
        self.instance.yield_node()
Dudás Ádám committed
829

830 831 832 833 834 835 836 837 838 839 840
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.result = humanize_exception(ugettext_noop(
                "The virtual machine did not switch off in the provided time "
                "limit. Most of the time this is caused by incorrect ACPI "
                "settings. You can also try to power off the machine from the "
                "operating system manually."), error)
            activity.resultant_state = None
        else:
            super(ShutdownOperation, self).on_abort(activity, error)

Dudás Ádám committed
841

842
@register_operation
843
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
844 845
    id = 'shut_off'
    name = _("shut off")
846 847 848 849 850 851 852
    description = _("Forcibly halt a virtual machine without notifying the "
                    "operating system. This operation will even work in cases "
                    "when shutdown does not, but the operating system and the "
                    "file systems are likely to be in an inconsistent state,  "
                    "so data loss is also possible. The effect of this "
                    "operation is the same as interrupting the power supply "
                    "of a physical machine.")
853
    required_perms = ()
854
    accept_states = ('RUNNING', 'PAUSED')
855
    resultant_state = 'STOPPED'
Dudás Ádám committed
856

857
    def _operation(self, activity):
Dudás Ádám committed
858 859 860
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
861

862
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
863
        self.instance.yield_node()
Dudás Ádám committed
864 865


866
@register_operation
867
class SleepOperation(InstanceOperation):
Dudás Ádám committed
868 869
    id = 'sleep'
    name = _("sleep")
870 871 872 873 874 875 876 877
    description = _("Suspend virtual machine. This means the machine is "
                    "stopped and its memory is saved to disk, so if the "
                    "machine is waked up, all the applications will keep "
                    "running. Most of the applications will be able to "
                    "continue even after a long suspension, but those which "
                    "need a continous network connection may fail when "
                    "resumed. In the meantime, the machine will only use "
                    "storage resources, and keep network resources allocated.")
878
    required_perms = ()
879
    accept_states = ('RUNNING', )
880
    resultant_state = 'SUSPENDED'
881
    async_queue = "localhost.man.slow"
Dudás Ádám committed
882

883 884 885 886
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
887 888 889 890 891 892
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

893
    def _operation(self, activity, system):
894 895 896
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
897
            self.instance.shutdown_net()
898
        self.instance._suspend_vm(parent_activity=activity)
899
        self.instance.yield_node()
Dudás Ádám committed
900

901 902 903 904
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
905
        task = vm_tasks.sleep
906
        remote_queue = ("vm", "slow")
907
        remote_timeout = 1000
Dudás Ádám committed
908

909 910 911 912
        def _get_remote_args(self, **kwargs):
            return (super(SleepOperation.SuspendVmOperation, self)
                    ._get_remote_args(**kwargs)
                    + [self.instance.mem_dump['path']])
Dudás Ádám committed
913 914


915
@register_operation
916
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
917 918
    id = 'wake_up'
    name = _("wake up")
919 920 921
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
922
    required_perms = ()
923
    accept_states = ('SUSPENDED', )
924
    resultant_state = 'RUNNING'
925
    async_queue = "localhost.man.slow"
Dudás Ádám committed
926

927
    def is_preferred(self):
928
        return self.instance.status == self.instance.STATUS.SUSPENDED
929

Dudás Ádám committed
930
    def on_abort(self, activity, error):
Bach Dániel committed
931 932 933 934
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
935

936
    def _operation(self, activity):
Dudás Ádám committed
937
        # Schedule vm
Dudás Ádám committed
938
        self.instance.allocate_vnc_port()
939
        self.instance.allocate_node()
Dudás Ádám committed
940 941

        # Resume vm
942
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
943 944

        # Estabilish network connection (vmdriver)
945 946 947
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
948
            self.instance.deploy_net()
Dudás Ádám committed
949

950 951 952 953
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
954

955 956 957 958 959 960
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
961
        remote_timeout = 1000
962 963 964 965 966 967

        def _get_remote_args(self, **kwargs):
            return (super(WakeUpOperation.WakeUpVmOperation, self)
                    ._get_remote_args(**kwargs)
                    + [self.instance.mem_dump['path']])

Dudás Ádám committed
968

969
@register_operation
970 971 972
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
973 974 975 976
    description = _("Virtual machines are suspended and destroyed after they "
                    "expire. This operation renews expiration times according "
                    "to the lease type. If the machine is close to the "
                    "expiration, its owner will be notified.")
977
    acl_level = "operator"
978
    required_perms = ()
979
    concurrency_check = False
980

981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002
    def set_time_of_suspend(self, activity, suspend, force):
        with activity.sub_activity(
            'renew_suspend', concurrency_check=False,
                readable_name=ugettext_noop('set time of suspend')):
            if (not force and suspend and self.instance.time_of_suspend and
                    suspend < self.instance.time_of_suspend):
                raise HumanReadableException.create(ugettext_noop(
                    "Renewing the machine with the selected lease would "
                    "result in its suspension time get earlier than before."))
            self.instance.time_of_suspend = suspend

    def set_time_of_delete(self, activity, delete, force):
        with activity.sub_activity(
            'renew_delete', concurrency_check=False,
                readable_name=ugettext_noop('set time of delete')):
            if (not force and delete and self.instance.time_of_delete and
                    delete < self.instance.time_of_delete):
                raise HumanReadableException.create(ugettext_noop(
                    "Renewing the machine with the selected lease would "
                    "result in its delete time get earlier than before."))
            self.instance.time_of_delete = delete

Őry Máté committed
1003
    def _operation(self, activity, lease=None, force=False, save=False):
1004
        suspend, delete = self.instance.get_renew_times(lease)
1005 1006 1007 1008 1009 1010 1011 1012 1013
        try:
            self.set_time_of_suspend(activity, suspend, force)
        except HumanReadableException:
            pass
        try:
            self.set_time_of_delete(activity, delete, force)
        except HumanReadableException:
            pass

Őry Máté committed
1014 1015
        if save:
            self.instance.lease = lease
1016

1017
        self.instance.save()
1018

1019
        return create_readable(ugettext_noop(
1020
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
1021 1022
            suspend=self.instance.time_of_suspend,
            delete=self.instance.time_of_suspend)
1023 1024


1025
@register_operation
1026
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
1027
    id = 'emergency_change_state'
1028 1029 1030 1031 1032 1033
    name = _("emergency state change")
    description = _("Change the virtual machine state to NOSTATE. This "
                    "should only be used if manual intervention was needed in "
                    "the virtualization layer, and the machine has to be "
                    "redeployed without losing its storage and network "
                    "resources.")
1034
    acl_level = "owner"
Guba Sándor committed
1035
    required_perms = ('vm.emergency_change_state', )
1036
    concurrency_check = False
1037

1038 1039
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
1040
        activity.resultant_state = new_state
1041 1042 1043 1044 1045 1046 1047
        if interrupt:
            msg_txt = ugettext_noop("Activity is forcibly interrupted.")
            message = create_readable(msg_txt, msg_txt)
            for i in InstanceActivity.objects.filter(
                    finished__isnull=True, instance=self.instance):
                i.finish(False, result=message)
                logger.error('Forced finishing activity %s', i)
1048

1049 1050 1051 1052
        if reset_node:
            self.instance.node = None
            self.instance.save()

1053

1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
@register_operation
class RedeployOperation(InstanceOperation):
    id = 'redeploy'
    name = _("redeploy")
    description = _("Change the virtual machine state to NOSTATE "
                    "and redeploy the VM. This operation allows starting "
                    "machines formerly running on a failed node.")
    acl_level = "owner"
    required_perms = ('vm.redeploy', )
    concurrency_check = False

    def _operation(self, user, activity, with_emergency_change_state=True):
        if with_emergency_change_state:
            ChangeStateOperation(self.instance).call(
                parent_activity=activity, user=user,
                new_state='NOSTATE', interrupt=False, reset_node=True)
        else:
            ShutOffOperation(self.instance).call(
                parent_activity=activity, user=user)

        self.instance._update_status()

        DeployOperation(self.instance).call(
            parent_activity=activity, user=user)

1079

1080
class NodeOperation(Operation):
1081
    async_operation = abortable_async_node_operation
1082
    host_cls = Node
1083 1084
    online_required = True
    superuser_required = True
1085 1086 1087 1088 1089

    def __init__(self, node):
        super(NodeOperation, self).__init__(subject=node)
        self.node = node

1090 1091 1092 1093 1094 1095 1096
    def check_precond(self):
        super(NodeOperation, self).check_precond()
        if self.online_required and not self.node.online:
            raise humanize_exception(ugettext_noop(
                "You cannot call this operation on an offline node."),
                Exception())

1097 1098
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
1099 1100 1101 1102 1103 1104 1105 1106 1107 1108
        if parent:
            if parent.node != self.node:
                raise ValueError("The node associated with the specified "
                                 "parent activity does not match the node "
                                 "bound to the operation.")
            if parent.user != user:
                raise ValueError("The user associated with the specified "
                                 "parent activity does not match the user "
                                 "provided as parameter.")

1109 1110 1111
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
1112
        else:
1113 1114 1115
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
1116 1117