operations.py 59.8 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
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
            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)
93
        raise TimeLimitExceeded()
94 95


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

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

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

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

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

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

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

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

163

164 165 166 167 168 169 170 171 172 173 174
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
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
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


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

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

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

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

242

243
@register_operation
244
class CreateDiskOperation(InstanceOperation):
245

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

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

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

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

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


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

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

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

    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)

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

307

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

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

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

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

344

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Dudás Ádám committed
445

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

        interface.destroy()
        interface.delete()

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

608

609
@register_operation
610 611 612 613 614
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
615
    required_perms = ('vm.config_ports', )
616 617 618 619

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
620
            raise SuspiciousOperation()
621 622 623 624 625 626 627
        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

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

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

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


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

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


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

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

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

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

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

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

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

751 752
        from storage.models import Disk

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

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

Dudás Ádám committed
769 770 771 772
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
773
        # Copy traits from the VM instance
774
        tmpl.req_traits.add(*self.instance.req_traits.all())
775 776
        if clone:
            tmpl.clone_acl(self.instance.template)
Guba Sándor committed
777
            # Add permission for the original owner of the template
778 779
            tmpl.set_level(self.instance.template.owner, 'owner')
            tmpl.set_level(user, 'owner')
Dudás Ádám committed
780
        try:
781
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
782 783 784 785 786 787 788 789 790 791
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


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

809 810
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
811
        self.instance.yield_node()
Dudás Ádám committed
812

813 814 815 816 817 818 819 820 821 822 823
    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
824

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

840
    def _operation(self, activity):
Dudás Ádám committed
841 842 843
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
844

845
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
846
        self.instance.yield_node()
Dudás Ádám committed
847 848


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

866 867 868 869
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
870 871 872 873 874 875
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

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

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

892 893 894 895
        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
896 897


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

910
    def is_preferred(self):
911
        return self.instance.status == self.instance.STATUS.SUSPENDED
912

Dudás Ádám committed
913
    def on_abort(self, activity, error):
Bach Dániel committed
914 915 916 917
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
918

919
    def _operation(self, activity):
Dudás Ádám committed
920
        # Schedule vm
Dudás Ádám committed
921
        self.instance.allocate_vnc_port()
922
        self.instance.allocate_node()
Dudás Ádám committed
923 924

        # Resume vm
925
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
926 927

        # Estabilish network connection (vmdriver)
928 929 930
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
931
            self.instance.deploy_net()
Dudás Ádám committed
932

933 934 935 936
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
937

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

        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
951

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

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


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

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

1010 1011 1012 1013
        if reset_node:
            self.instance.node = None
            self.instance.save()

1014

1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
@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)

1040

1041
class NodeOperation(Operation):
1042
    async_operation = abortable_async_node_operation
1043
    host_cls = Node
1044 1045
    online_required = True
    superuser_required = True
1046 1047 1048 1049 1050

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

1051 1052 1053 1054 1055 1056 1057
    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())

1058 1059
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
        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.")

1070 1071 1072
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
1073
        else:
1074 1075 1076
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
1077 1078


1079
@register_operation
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
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
1108 1109 1110
class FlushOperation(NodeOperation):
    id = 'flush'
    name = _("flush")
1111
    description = _("Passivate node and move all instances to other ones.")
1112
    required_perms = ()
1113
    async_queue = "localhost.man.slow"
1114

1115
    def _operation(self, activity, user):
1116 1117 1118
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1119
        for i in self.node.instance_set.all():
1120 1121 1122 1123
            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
1124
                i.migrate(user=user)
1125 1126