operations.py 56 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
Dudás Ádám committed
31
from django.utils import timezone
32
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
33
from django.conf import settings
Bach Dániel committed
34
from django.db.models import Q
Dudás Ádám committed
35

36 37
from sizefield.utils import filesizeformat

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

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

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

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


63 64 65 66 67
class RemoteOperationMixin(object):

    remote_timeout = 30

    def _operation(self, **kwargs):
Őry Máté committed
68
        args = self._get_remote_args(**kwargs)
69 70 71 72 73 74 75 76 77
        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()


78 79 80 81 82 83 84
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())
85
        for i in xrange(0, self.remote_timeout, self.remote_step):
86 87 88 89 90 91 92 93 94
            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)


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

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

    def check_precond(self):
109 110
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
111 112 113 114 115 116 117 118 119 120 121 122 123 124
        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)
125 126

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

132
        super(InstanceOperation, self).check_auth(user=user)
133

134 135
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
136 137 138 139 140 141 142 143 144 145
        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.")

146 147 148
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name, resultant_state=self.resultant_state)
149 150
        else:
            return InstanceActivity.create(
151 152
                code_suffix=self.get_activity_code_suffix(),
                instance=self.instance,
153
                readable_name=name, user=user,
154 155
                concurrency_check=self.concurrency_check,
                resultant_state=self.resultant_state)
156

157 158 159 160 161
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

162

163 164 165 166 167 168 169 170 171 172 173
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
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
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",
                started__gt=last_boot_time).latest("started")
        except InstanceActivity.DoesNotExist:  # no agent since last boot
            raise self.instance.NoAgentError(self.instance)


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


198
@register_operation
199 200 201 202 203
class AddInterfaceOperation(InstanceOperation):
    id = 'add_interface'
    name = _("add interface")
    description = _("Add a new network interface for the specified VLAN to "
                    "the VM.")
204
    required_perms = ()
205
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
206

207 208 209 210 211 212 213
    def rollback(self, net, activity):
        with activity.sub_activity(
            'destroying_net',
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

214
    def _operation(self, activity, user, system, vlan, managed=None):
215
        if not vlan.has_level(user, 'user'):
216 217 218
            raise humanize_exception(ugettext_noop(
                "User acces to vlan %(vlan)s is required."),
                PermissionDenied(), vlan=vlan)
219 220 221 222 223 224 225
        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:
226
            try:
227 228
                self.instance._attach_network(
                    interface=net, parent_activity=activity)
229 230 231 232
            except Exception as e:
                if hasattr(e, 'libvirtError'):
                    self.rollback(net, activity)
                raise
233
            net.deploy()
Bach Dániel committed
234 235
            self.instance._change_ip(parent_activity=activity)
            self.instance._restart_networking(parent_activity=activity)
236

237 238 239 240
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

241

242
@register_operation
243
class CreateDiskOperation(InstanceOperation):
244

245 246
    id = 'create_disk'
    name = _("create disk")
247
    description = _("Create and attach empty disk to the virtual machine.")
248
    required_perms = ('storage.create_empty_disk', )
249
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
250

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

254 255 256
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
257
        disk.full_clean()
258 259 260 261
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
262
        disk.save()
263 264
        self.instance.disks.add(disk)

265
        if self.instance.is_running:
266 267 268 269
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
270
                disk.deploy()
271
            self.instance._attach_disk(parent_activity=activity, disk=disk)
272

273
    def get_activity_name(self, kwargs):
274 275 276
        return create_readable(
            ugettext_noop("create disk %(name)s (%(size)s)"),
            size=filesizeformat(kwargs['size']), name=kwargs['name'])
277 278


279
@register_operation
280
class ResizeDiskOperation(RemoteInstanceOperation):
281 282 283 284 285

    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
286
    required_perms = ('storage.resize_disk', )
287 288
    accept_states = ('RUNNING', )
    async_queue = "localhost.man.slow"
289 290
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
291

292 293 294
    def _get_remote_args(self, disk, size, **kwargs):
        return (super(ResizeDiskOperation, self)
                ._get_remote_args(**kwargs) + [disk.path, size])
295 296 297 298 299 300

    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)

301 302 303 304 305
    def _operation(self, disk, size):
        super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
        disk.size = size
        disk.save()

306

307
@register_operation
308 309 310
class DownloadDiskOperation(InstanceOperation):
    id = 'download_disk'
    name = _("download disk")
311 312 313 314
    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.")
315
    abortable = True
316
    has_percentage = True
317
    required_perms = ('storage.download_disk', )
318
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
319
    async_queue = "localhost.man.slow"
320

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

324
        disk = Disk.download(url=url, name=name, task=task)
325 326 327 328
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
329
        disk.full_clean()
330
        disk.save()
331
        self.instance.disks.add(disk)
332 333
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
334

335 336 337 338
        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
339
        # TODO iso (cd) hot-plug is not supported by kvm/guests
340
        if self.instance.is_running and disk.type not in ["iso"]:
341
            self.instance._attach_disk(parent_activity=activity, disk=disk)
342

343

344
@register_operation
345
class DeployOperation(InstanceOperation):
Dudás Ádám committed
346 347
    id = 'deploy'
    name = _("deploy")
348 349
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
350
    required_perms = ()
351
    deny_states = ('SUSPENDED', 'RUNNING')
352
    resultant_state = 'RUNNING'
Dudás Ádám committed
353

354 355
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
356
                                        self.instance.STATUS.PENDING,
357 358
                                        self.instance.STATUS.ERROR)

359 360 361
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
362 363
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
364
        activity.result = create_readable(
Guba Sándor committed
365
            ugettext_noop("virtual machine successfully "
366 367
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
368

369
    def _operation(self, activity, node=None):
Dudás Ádám committed
370 371
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
372 373 374 375 376
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
377 378

        # Deploy virtual images
379
        self.instance._deploy_disks(parent_activity=activity)
Dudás Ádám committed
380 381

        # Deploy VM on remote machine
382
        if self.instance.state not in ['PAUSED']:
383
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
384 385

        # Establish network connection (vmdriver)
386 387 388
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
389 390
            self.instance.deploy_net()

391 392 393 394 395
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

398 399 400
        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
401

402
    @register_operation
Őry Máté committed
403
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
404 405
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
406
        description = _("Deploy virtual machine.")
407 408 409
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy

Őry Máté committed
410
        def _get_remote_args(self, **kwargs):
411 412 413 414 415 416 417 418 419
            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
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
    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()

437
    @register_operation
Őry Máté committed
438
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
439 440 441 442 443
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
444

445
@register_operation
446
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
447 448
    id = 'destroy'
    name = _("destroy")
449 450
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
451
    required_perms = ()
452
    resultant_state = 'DESTROYED'
Dudás Ádám committed
453

454
    def _operation(self, activity, system):
455
        # Destroy networks
456 457 458
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
459
            if self.instance.node:
460
                self.instance.shutdown_net()
461
            self.instance.destroy_net()
Dudás Ádám committed
462

463
        if self.instance.node:
464
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
465 466

        # Destroy disks
467 468 469
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
470
            self.instance.destroy_disks()
Dudás Ádám committed
471

Dudás Ádám committed
472 473
        # Delete mem. dump if exists
        try:
474
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
475 476 477 478 479 480
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
481 482 483 484

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

485
    @register_operation
486
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
487 488 489 490 491
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

492 493 494 495 496 497 498 499 500 501 502 503 504 505
    @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
506

507
@register_operation
508
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
509 510
    id = 'migrate'
    name = _("migrate")
511 512
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
513
    required_perms = ()
514
    superuser_required = True
515
    accept_states = ('RUNNING', )
516
    async_queue = "localhost.man.slow"
517 518
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
519
    remote_timeout = 1000
520

521
    def _get_remote_args(self, to_node, live_migration, **kwargs):
522
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
523
                + [to_node.host.hostname, live_migration])
Dudás Ádám committed
524

525
    def rollback(self, activity):
526 527 528
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
529 530
            self.instance.deploy_net()

531
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
532
        if not to_node:
Bach Dániel committed
533 534 535 536 537 538
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

539
        try:
540 541 542
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
543 544
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
545 546 547
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
548
            raise
Dudás Ádám committed
549

550
        # Shutdown networks
551 552 553
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
554 555
            self.instance.shutdown_net()

Dudás Ádám committed
556 557 558
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
559

Dudás Ádám committed
560
        # Estabilish network connection (vmdriver)
561 562 563
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
564
            self.instance.deploy_net()
Dudás Ádám committed
565 566


567
@register_operation
568
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
569 570
    id = 'reboot'
    name = _("reboot")
571 572
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
573
    required_perms = ()
574
    accept_states = ('RUNNING', )
575
    task = vm_tasks.reboot
Dudás Ádám committed
576

577 578
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
579 580 581
        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
582 583


584
@register_operation
585 586 587
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
588 589 590
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
591
    required_perms = ()
592
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
593

594 595
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
596 597
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
598 599 600 601 602
            interface.shutdown()

        interface.destroy()
        interface.delete()

603 604
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
605
                               vlan=kwargs['interface'].vlan)
606

607

608
@register_operation
609 610 611 612 613
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
614
    required_perms = ('vm.config_ports', )
615 616 617 618 619 620 621 622 623 624 625 626 627
    accept_states = ()

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
            raise PermissionDenied()
        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
628 629 630 631 632
class AddPortOperation(InstanceOperation):
    id = 'add_port'
    name = _("open port")
    description = _("Open the specified port.")
    concurrency_check = False
633
    required_perms = ('vm.config_ports', )
634 635 636 637 638 639 640 641 642 643 644 645
    accept_states = ()

    def _operation(self, activity, host, proto, port):
        if host.interface_set.get().instance != self.instance:
            raise PermissionDenied()
        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
646 647 648
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
649 650
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
651
    required_perms = ()
652
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
653 654

    def _operation(self, activity, user, system, disk):
655
        if self.instance.is_running and disk.type not in ["iso"]:
656
            self.instance._detach_disk(disk=disk, parent_activity=activity)
657 658 659 660
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
661
            disk.destroy()
662
            return self.instance.disks.remove(disk)
663

664 665 666
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
667 668


669
@register_operation
670
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
671 672
    id = 'reset'
    name = _("reset")
673
    description = _("Cold reboot virtual machine (power cycle).")
674
    required_perms = ()
675
    accept_states = ('RUNNING', )
676
    task = vm_tasks.reset
Dudás Ádám committed
677

678 679
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
680 681 682
        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
683 684


685
@register_operation
686
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
687 688
    id = 'save_as_template'
    name = _("save as template")
689 690 691 692
    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.")
693
    has_percentage = True
694
    abortable = True
695
    required_perms = ('vm.create_template', )
696
    accept_states = ('RUNNING', 'STOPPED')
697
    async_queue = "localhost.man.slow"
Dudás Ádám committed
698

699 700 701 702
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

703 704 705 706 707 708
    @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)
709
        else:
710 711
            v = 1
        return "%s v%d" % (name, v)
712

713
    def on_abort(self, activity, error):
714
        if hasattr(self, 'disks'):
715 716 717
            for disk in self.disks:
                disk.destroy()

718
    def _operation(self, activity, user, system, name=None,
719
                   with_shutdown=True, clone=False, task=None, **kwargs):
720 721 722 723 724
        try:
            self.instance._cleanup(parent_activity=activity, user=user)
        except Instance.WrongStateError:
            pass

725
        if with_shutdown:
726 727
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
728
                                                      user=user, task=task)
729 730 731
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
732 733 734 735 736 737 738 739
        # 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,
740
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
741 742 743 744 745 746 747 748 749
            'num_cores': self.instance.num_cores,
            'owner': user,
            'parent': self.instance.template,  # Can be problem
            '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
750
        params.pop("parent_activity", None)
Dudás Ádám committed
751

752 753
        from storage.models import Disk

Dudás Ádám committed
754 755
        def __try_save_disk(disk):
            try:
756
                return disk.save_as(task)
Dudás Ádám committed
757 758 759
            except Disk.WrongDiskTypeError:
                return disk

760
        self.disks = []
761 762 763 764 765 766 767
        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)
            ):
768 769
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
770 771 772 773
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
774 775
        if clone:
            tmpl.clone_acl(self.instance.template)
Dudás Ádám committed
776
        try:
777
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
778 779 780 781 782 783 784 785 786 787
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


788
@register_operation
789 790
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
791 792
    id = 'shutdown'
    name = _("shutdown")
793 794 795 796
    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
797
    abortable = True
798
    required_perms = ()
799
    accept_states = ('RUNNING', )
800
    resultant_state = 'STOPPED'
801 802
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
803
    remote_timeout = 120
Dudás Ádám committed
804

805 806
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
807
        self.instance.yield_node()
Dudás Ádám committed
808

809 810 811 812 813 814 815 816 817 818 819
    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
820

821
@register_operation
822
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
823 824
    id = 'shut_off'
    name = _("shut off")
825 826 827 828 829 830 831
    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.")
832
    required_perms = ()
833
    accept_states = ('RUNNING', 'PAUSED')
834
    resultant_state = 'STOPPED'
Dudás Ádám committed
835

836
    def _operation(self, activity):
Dudás Ádám committed
837 838 839
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
840

841
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
842
        self.instance.yield_node()
Dudás Ádám committed
843 844


845
@register_operation
846
class SleepOperation(InstanceOperation):
Dudás Ádám committed
847 848
    id = 'sleep'
    name = _("sleep")
849 850 851 852 853 854 855 856
    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.")
857
    required_perms = ()
858
    accept_states = ('RUNNING', )
859
    resultant_state = 'SUSPENDED'
860
    async_queue = "localhost.man.slow"
Dudás Ádám committed
861

862 863 864 865
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
866 867 868 869 870 871
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

872
    def _operation(self, activity, system):
873 874 875
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
876
            self.instance.shutdown_net()
877
        self.instance._suspend_vm(parent_activity=activity)
878
        self.instance.yield_node()
Dudás Ádám committed
879

880 881 882 883
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
884
        task = vm_tasks.sleep
885
        remote_queue = ("vm", "slow")
886
        remote_timeout = 1000
Dudás Ádám committed
887

888 889 890 891
        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
892 893


894
@register_operation
895
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
896 897
    id = 'wake_up'
    name = _("wake up")
898 899 900
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
901
    required_perms = ()
902
    accept_states = ('SUSPENDED', )
903
    resultant_state = 'RUNNING'
904
    async_queue = "localhost.man.slow"
Dudás Ádám committed
905

906
    def is_preferred(self):
907
        return self.instance.status == self.instance.STATUS.SUSPENDED
908

Dudás Ádám committed
909
    def on_abort(self, activity, error):
Bach Dániel committed
910 911 912 913
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
914

915
    def _operation(self, activity):
Dudás Ádám committed
916
        # Schedule vm
Dudás Ádám committed
917
        self.instance.allocate_vnc_port()
918
        self.instance.allocate_node()
Dudás Ádám committed
919 920

        # Resume vm
921
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
922 923

        # Estabilish network connection (vmdriver)
924 925 926
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
927
            self.instance.deploy_net()
Dudás Ádám committed
928

929 930 931 932
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
933

934 935 936 937 938 939
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
940
        remote_timeout = 1000
941 942 943 944 945 946

        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
947

948
@register_operation
949 950 951
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
952 953 954 955
    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.")
956
    acl_level = "operator"
957
    required_perms = ()
958
    concurrency_check = False
959

Őry Máté committed
960
    def _operation(self, activity, lease=None, force=False, save=False):
961 962 963 964 965 966 967 968 969 970 971 972 973
        suspend, delete = self.instance.get_renew_times(lease)
        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."))
        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_suspend = suspend
        self.instance.time_of_delete = delete
Őry Máté committed
974 975
        if save:
            self.instance.lease = lease
976
        self.instance.save()
977 978 979
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
980 981


982
@register_operation
983
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
984
    id = 'emergency_change_state'
985 986 987 988 989 990
    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.")
991
    acl_level = "owner"
Guba Sándor committed
992
    required_perms = ('vm.emergency_change_state', )
993
    concurrency_check = False
994

995 996
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
997
        activity.resultant_state = new_state
998 999 1000 1001 1002 1003 1004
        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)
1005

1006 1007 1008 1009
        if reset_node:
            self.instance.node = None
            self.instance.save()

1010

1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
@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)

1036

1037
class NodeOperation(Operation):
1038
    async_operation = abortable_async_node_operation
1039
    host_cls = Node
1040 1041
    online_required = True
    superuser_required = True
1042 1043 1044 1045 1046

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

1047 1048 1049 1050 1051 1052 1053
    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())

1054 1055
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065
        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.")

1066 1067 1068
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
1069
        else:
1070 1071 1072
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
1073 1074


1075
@register_operation
1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103
class ResetNodeOperation(NodeOperation):
    id = 'reset'
    name = _("reset")
    description = _("Disable missing node and redeploy all instances "
                    "on other ones.")
    required_perms = ()
    online_required = False
    async_queue = "localhost.man.slow"

    def check_precond(self):
        super(ResetNodeOperation, self).check_precond()
        if not self.node.enabled or self.node.online:
            raise humanize_exception(ugettext_noop(
                "You cannot reset a disabled or online node."), Exception())

    def _operation(self, activity, user):
        if self.node.enabled:
            DisableOperation(self.node).call(parent_activity=activity,
                                             user=user)
        for i in self.node.instance_set.all():
            name = create_readable(ugettext_noop(
                "migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
            with activity.sub_activity('migrate_instance_%d' % i.pk,
                                       readable_name=name):
                i.redeploy(user=user)


@register_operation
1104 1105 1106
class FlushOperation(NodeOperation):
    id = 'flush'
    name = _("flush")
1107
    description = _("Passivate node and move all instances to other ones.")
1108
    required_perms = ()
1109
    async_queue = "localhost.man.slow"
1110

1111
    def _operation(self, activity, user):
1112 1113 1114
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1115
        for i in self.node.instance_set.all():
1116 1117 1118 1119
            name = create_readable(ugettext_noop(
                "migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
            with activity.sub_activity('migrate_instance_%d' % i.pk,
                                       readable_name=name):
Bach Dániel committed
1120
                i.migrate(user=user)
1121 1122


1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
@register_operation
class ActivateOperation(NodeOperation):
    id = 'activate'
    name = _("activate")
    description = _("Make node active, i.e. scheduler is allowed to deploy "
                    "virtual machines to it.")
    required_perms = ()

    def check_precond(self):
        super(ActivateOperation, self).check_precond()
        if self.node.enabled and self.node.schedule_enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot activate an active node."), Exception())

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = True
        self.node.save()


@register_operation
class PassivateOperation(NodeOperation):
    id = 'passivate'
    name = _("passivate")
    description = _("Make node passive, i.e. scheduler is denied to deploy "
                    "virtual machines to it, but remaining instances and "
                    "the ones manually migrated will continue running.")
    required_perms = ()

    def check_precond(self):
        if self.node.enabled and not self.node.schedule_enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot passivate a passive node."), Exception())
        super(PassivateOperation, self).check_precond()

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = False
        self.node.save()


@register_operation
class DisableOperation(NodeOperation):
    id = 'disable'
    name = _("disable")
    description = _("Disable node.")
    required_perms = ()
    online_required = False
1171

1172 1173 1174 1175 1176 1177 1178 1179 1180