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

18
from __future__ import absolute_import, unicode_literals
Dudás Ádám committed
19
from logging import getLogger
20
from re import search
Őry Máté committed
21
from string import ascii_lowercase
Kálmán Viktor committed
22
from urlparse import urlsplit
Dudás Ádám committed
23

24
from django.core.exceptions import PermissionDenied
Dudás Ádám committed
25
from django.utils import timezone
26
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
27
from django.conf import settings
Dudás Ádám committed
28

29 30
from sizefield.utils import filesizeformat

31 32
from celery.contrib.abortable import AbortableAsyncResult
from celery.exceptions import TimeLimitExceeded, TimeoutError
33

34 35 36
from common.models import (
    create_readable, humanize_exception, HumanReadableException
)
37
from common.operations import Operation, register_operation, SubOperationMixin
Bach Dániel committed
38
from manager.scheduler import SchedulerError
39 40 41
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
42
from .models import (
43
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
44
    NodeActivity, pwgen
45
)
46
from .tasks import agent_tasks, local_agent_tasks, vm_tasks
Dudás Ádám committed
47

Kálmán Viktor committed
48
from dashboard.store_api import Store, NoStoreException
49
from storage.tasks import storage_tasks
Kálmán Viktor committed
50

Dudás Ádám committed
51
logger = getLogger(__name__)
52 53


54 55 56 57 58
class RemoteOperationMixin(object):

    remote_timeout = 30

    def _operation(self, **kwargs):
Őry Máté committed
59
        args = self._get_remote_args(**kwargs)
60 61 62 63 64 65 66 67 68
        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()


69 70 71 72 73 74 75
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())
76
        for i in xrange(0, self.remote_timeout, self.remote_step):
77 78 79 80 81 82 83 84 85
            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)


86
class InstanceOperation(Operation):
87
    acl_level = 'owner'
88
    async_operation = abortable_async_instance_operation
89
    host_cls = Instance
90
    concurrency_check = True
91 92
    accept_states = None
    deny_states = None
93
    resultant_state = None
Dudás Ádám committed
94

95
    def __init__(self, instance):
96
        super(InstanceOperation, self).__init__(subject=instance)
97 98 99
        self.instance = instance

    def check_precond(self):
100 101
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
102 103 104 105 106 107 108 109 110 111 112 113 114 115
        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)
116 117

    def check_auth(self, user):
118
        if not self.instance.has_level(user, self.acl_level):
119 120 121
            raise humanize_exception(ugettext_noop(
                "%(acl_level)s level is required for this operation."),
                PermissionDenied(), acl_level=self.acl_level)
122

123
        super(InstanceOperation, self).check_auth(user=user)
124

125 126
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
127 128 129 130 131 132 133 134 135 136
        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.")

137 138 139
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name, resultant_state=self.resultant_state)
140 141
        else:
            return InstanceActivity.create(
142 143
                code_suffix=self.get_activity_code_suffix(),
                instance=self.instance,
144
                readable_name=name, user=user,
145 146
                concurrency_check=self.concurrency_check,
                resultant_state=self.resultant_state)
147

148 149 150 151 152
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

153

154 155 156 157 158 159 160 161 162 163 164
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]


165
@register_operation
166 167 168 169 170
class AddInterfaceOperation(InstanceOperation):
    id = 'add_interface'
    name = _("add interface")
    description = _("Add a new network interface for the specified VLAN to "
                    "the VM.")
171
    required_perms = ()
172
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
173

174 175 176 177 178 179 180
    def rollback(self, net, activity):
        with activity.sub_activity(
            'destroying_net',
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

181
    def _operation(self, activity, user, system, vlan, managed=None):
182
        if not vlan.has_level(user, 'user'):
183 184 185
            raise humanize_exception(ugettext_noop(
                "User acces to vlan %(vlan)s is required."),
                PermissionDenied(), vlan=vlan)
186 187 188 189 190 191 192
        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:
193
            try:
194 195
                self.instance._attach_network(
                    interface=net, parent_activity=activity)
196 197 198 199
            except Exception as e:
                if hasattr(e, 'libvirtError'):
                    self.rollback(net, activity)
                raise
200
            net.deploy()
201
            local_agent_tasks.send_networking_commands(self.instance, activity)
202

203 204 205 206
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

207

208
@register_operation
209
class CreateDiskOperation(InstanceOperation):
210

211 212
    id = 'create_disk'
    name = _("create disk")
213
    description = _("Create and attach empty disk to the virtual machine.")
214
    required_perms = ('storage.create_empty_disk', )
215
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
216

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

220 221 222
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
223
        disk.full_clean()
224 225 226 227
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
228
        disk.save()
229 230
        self.instance.disks.add(disk)

231
        if self.instance.is_running:
232 233 234 235
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
236
                disk.deploy()
237
            self.instance._attach_disk(parent_activity=activity, disk=disk)
238

239
    def get_activity_name(self, kwargs):
240 241 242
        return create_readable(
            ugettext_noop("create disk %(name)s (%(size)s)"),
            size=filesizeformat(kwargs['size']), name=kwargs['name'])
243 244


245
@register_operation
246
class ResizeDiskOperation(RemoteInstanceOperation):
247 248 249 250 251

    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
252
    required_perms = ('storage.resize_disk', )
253 254
    accept_states = ('RUNNING', )
    async_queue = "localhost.man.slow"
255 256
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
257

258 259 260
    def _get_remote_args(self, disk, size, **kwargs):
        return (super(ResizeDiskOperation, self)
                ._get_remote_args(**kwargs) + [disk.path, size])
261 262 263 264 265 266

    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)

267 268 269 270 271
    def _operation(self, disk, size):
        super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
        disk.size = size
        disk.save()

272

273
@register_operation
274 275 276
class DownloadDiskOperation(InstanceOperation):
    id = 'download_disk'
    name = _("download disk")
277 278 279 280
    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.")
281
    abortable = True
282
    has_percentage = True
283
    required_perms = ('storage.download_disk', )
284
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
285
    async_queue = "localhost.man.slow"
286

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

290
        disk = Disk.download(url=url, name=name, task=task)
291 292 293 294
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
295
        disk.full_clean()
296
        disk.save()
297
        self.instance.disks.add(disk)
298 299
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
300

301 302 303 304
        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
305
        # TODO iso (cd) hot-plug is not supported by kvm/guests
306
        if self.instance.is_running and disk.type not in ["iso"]:
307
            self.instance._attach_disk(parent_activity=activity, disk=disk)
308

309

310
@register_operation
311
class DeployOperation(InstanceOperation):
Dudás Ádám committed
312 313
    id = 'deploy'
    name = _("deploy")
314 315
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
316
    required_perms = ()
317
    deny_states = ('SUSPENDED', 'RUNNING')
318
    resultant_state = 'RUNNING'
Dudás Ádám committed
319

320 321
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
322
                                        self.instance.STATUS.PENDING,
323 324
                                        self.instance.STATUS.ERROR)

325 326 327
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
328 329
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
330
        activity.result = create_readable(
Guba Sándor committed
331
            ugettext_noop("virtual machine successfully "
332 333
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
334

335
    def _operation(self, activity, node=None):
Dudás Ádám committed
336 337
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
338 339 340 341 342
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
343 344

        # Deploy virtual images
345
        self.instance._deploy_disks(parent_activity=activity)
Dudás Ádám committed
346 347

        # Deploy VM on remote machine
348
        if self.instance.state not in ['PAUSED']:
349
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
350 351

        # Establish network connection (vmdriver)
352 353 354
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
355 356
            self.instance.deploy_net()

357 358 359 360 361
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

364 365 366
        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
367

368
    @register_operation
Őry Máté committed
369
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
370 371
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
372
        description = _("Deploy virtual machine.")
373 374 375
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy

Őry Máté committed
376
        def _get_remote_args(self, **kwargs):
377 378 379 380 381 382 383 384 385
            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
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
    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()

403
    @register_operation
Őry Máté committed
404
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
405 406 407 408 409
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
410

411
@register_operation
412
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
413 414
    id = 'destroy'
    name = _("destroy")
415 416
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
417
    required_perms = ()
418
    resultant_state = 'DESTROYED'
Dudás Ádám committed
419

420
    def _operation(self, activity, system):
421
        # Destroy networks
422 423 424
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
425
            if self.instance.node:
426
                self.instance.shutdown_net()
427
            self.instance.destroy_net()
Dudás Ádám committed
428

429
        if self.instance.node:
430
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
431 432

        # Destroy disks
433 434 435
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
436
            self.instance.destroy_disks()
Dudás Ádám committed
437

Dudás Ádám committed
438 439
        # Delete mem. dump if exists
        try:
440
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
441 442 443 444 445 446
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
447 448 449 450

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

451
    @register_operation
452
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
453 454 455 456 457
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

458 459 460 461 462 463 464 465 466 467 468 469 470 471
    @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
472

473
@register_operation
474
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
475 476
    id = 'migrate'
    name = _("migrate")
477 478
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
479
    required_perms = ()
480
    superuser_required = True
481
    accept_states = ('RUNNING', )
482
    async_queue = "localhost.man.slow"
483 484
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
485
    remote_timeout = 1000
486

487
    def _get_remote_args(self, to_node, live_migration, **kwargs):
488
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
489
                + [to_node.host.hostname, live_migration])
Dudás Ádám committed
490

491
    def rollback(self, activity):
492 493 494
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
495 496
            self.instance.deploy_net()

497
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
498
        if not to_node:
Bach Dániel committed
499 500 501 502 503 504
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

505
        try:
506 507 508
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
509 510
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
511 512 513
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
514
            raise
Dudás Ádám committed
515

516
        # Shutdown networks
517 518 519
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
520 521
            self.instance.shutdown_net()

Dudás Ádám committed
522 523 524
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
525

Dudás Ádám committed
526
        # Estabilish network connection (vmdriver)
527 528 529
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
530
            self.instance.deploy_net()
Dudás Ádám committed
531 532


533
@register_operation
534
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
535 536
    id = 'reboot'
    name = _("reboot")
537 538
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
539
    required_perms = ()
540
    accept_states = ('RUNNING', )
541
    task = vm_tasks.reboot
Dudás Ádám committed
542

543 544
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
545 546 547
        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
548 549


550
@register_operation
551 552 553
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
554 555 556
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
557
    required_perms = ()
558
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
559

560 561
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
562 563
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
564 565 566 567 568
            interface.shutdown()

        interface.destroy()
        interface.delete()

569 570
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
571
                               vlan=kwargs['interface'].vlan)
572

573

574
@register_operation
575 576 577
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
578 579
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
580
    required_perms = ()
581
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
582 583

    def _operation(self, activity, user, system, disk):
584
        if self.instance.is_running and disk.type not in ["iso"]:
585
            self.instance._detach_disk(disk=disk, parent_activity=activity)
586 587 588 589
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
590
            disk.destroy()
591
            return self.instance.disks.remove(disk)
592

593 594 595
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
596 597


598
@register_operation
599
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
600 601
    id = 'reset'
    name = _("reset")
602
    description = _("Cold reboot virtual machine (power cycle).")
603
    required_perms = ()
604
    accept_states = ('RUNNING', )
605
    task = vm_tasks.reset
Dudás Ádám committed
606

607 608
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
609 610 611
        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
612 613


614
@register_operation
615
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
616 617
    id = 'save_as_template'
    name = _("save as template")
618 619 620 621
    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.")
622
    has_percentage = True
623
    abortable = True
624
    required_perms = ('vm.create_template', )
625
    accept_states = ('RUNNING', 'STOPPED')
626
    async_queue = "localhost.man.slow"
Dudás Ádám committed
627

628 629 630 631
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

632 633 634 635 636 637
    @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)
638
        else:
639 640
            v = 1
        return "%s v%d" % (name, v)
641

642
    def on_abort(self, activity, error):
643
        if hasattr(self, 'disks'):
644 645 646
            for disk in self.disks:
                disk.destroy()

647
    def _operation(self, activity, user, system, name=None,
648
                   with_shutdown=True, clone=False, task=None, **kwargs):
649
        if with_shutdown:
650 651
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
652
                                                      user=user, task=task)
653 654 655
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
656 657 658 659 660 661 662 663
        # 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,
664
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
665 666 667 668 669 670 671 672 673
            '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
674
        params.pop("parent_activity", None)
Dudás Ádám committed
675

676 677
        from storage.models import Disk

Dudás Ádám committed
678 679
        def __try_save_disk(disk):
            try:
680
                return disk.save_as(task)
Dudás Ádám committed
681 682 683
            except Disk.WrongDiskTypeError:
                return disk

684
        self.disks = []
685 686 687 688 689 690 691
        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)
            ):
692 693
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
694 695 696 697
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
698 699
        if clone:
            tmpl.clone_acl(self.instance.template)
Dudás Ádám committed
700
        try:
701
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
702 703 704 705 706 707 708 709 710 711
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


712
@register_operation
713 714
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
715 716
    id = 'shutdown'
    name = _("shutdown")
717 718 719 720
    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
721
    abortable = True
722
    required_perms = ()
723
    accept_states = ('RUNNING', )
724
    resultant_state = 'STOPPED'
725 726
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
727
    remote_timeout = 120
Dudás Ádám committed
728

729 730
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
731
        self.instance.yield_node()
Dudás Ádám committed
732

733 734 735 736 737 738 739 740 741 742 743
    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
744

745
@register_operation
746
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
747 748
    id = 'shut_off'
    name = _("shut off")
749 750 751 752 753 754 755
    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.")
756
    required_perms = ()
757
    accept_states = ('RUNNING', 'PAUSED')
758
    resultant_state = 'STOPPED'
Dudás Ádám committed
759

760
    def _operation(self, activity):
Dudás Ádám committed
761 762 763
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
764

765
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
766
        self.instance.yield_node()
Dudás Ádám committed
767 768


769
@register_operation
770
class SleepOperation(InstanceOperation):
Dudás Ádám committed
771 772
    id = 'sleep'
    name = _("sleep")
773 774 775 776 777 778 779 780
    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.")
781
    required_perms = ()
782
    accept_states = ('RUNNING', )
783
    resultant_state = 'SUSPENDED'
784
    async_queue = "localhost.man.slow"
Dudás Ádám committed
785

786 787 788 789
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
790 791 792 793 794 795
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

796
    def _operation(self, activity, system):
797 798 799
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
800
            self.instance.shutdown_net()
801
        self.instance._suspend_vm(parent_activity=activity)
802
        self.instance.yield_node()
Dudás Ádám committed
803

804 805 806 807
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
808
        task = vm_tasks.sleep
809
        remote_queue = ("vm", "slow")
810
        remote_timeout = 1000
Dudás Ádám committed
811

812 813 814 815
        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
816 817


818
@register_operation
819
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
820 821
    id = 'wake_up'
    name = _("wake up")
822 823 824
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
825
    required_perms = ()
826
    accept_states = ('SUSPENDED', )
827
    resultant_state = 'RUNNING'
828
    async_queue = "localhost.man.slow"
Dudás Ádám committed
829

830
    def is_preferred(self):
831
        return self.instance.status == self.instance.STATUS.SUSPENDED
832

Dudás Ádám committed
833
    def on_abort(self, activity, error):
Bach Dániel committed
834 835 836 837
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
838

839
    def _operation(self, activity):
Dudás Ádám committed
840
        # Schedule vm
Dudás Ádám committed
841
        self.instance.allocate_vnc_port()
842
        self.instance.allocate_node()
Dudás Ádám committed
843 844

        # Resume vm
845
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
846 847

        # Estabilish network connection (vmdriver)
848 849 850
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
851
            self.instance.deploy_net()
Dudás Ádám committed
852

853 854 855 856
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
857

858 859 860 861 862 863
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
864
        remote_timeout = 1000
865 866 867 868 869 870

        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
871

872
@register_operation
873 874 875
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
876 877 878 879
    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.")
880
    acl_level = "operator"
881
    required_perms = ()
882
    concurrency_check = False
883

Őry Máté committed
884
    def _operation(self, activity, lease=None, force=False, save=False):
885 886 887 888 889 890 891 892 893 894 895 896 897
        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
898 899
        if save:
            self.instance.lease = lease
900
        self.instance.save()
901 902 903
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
904 905


906
@register_operation
907
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
908
    id = 'emergency_change_state'
909 910 911 912 913 914
    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.")
915
    acl_level = "owner"
Guba Sándor committed
916
    required_perms = ('vm.emergency_change_state', )
917
    concurrency_check = False
918

919 920
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
921
        activity.resultant_state = new_state
922 923 924 925 926 927 928
        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)
929

930 931 932 933
        if reset_node:
            self.instance.node = None
            self.instance.save()

934

935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
@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)

960

961
class NodeOperation(Operation):
962
    async_operation = abortable_async_node_operation
963
    host_cls = Node
964 965
    online_required = True
    superuser_required = True
966 967 968 969 970

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

971 972 973 974 975 976 977
    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())

978 979
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
980 981 982 983 984 985 986 987 988 989
        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.")

990 991 992
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
993
        else:
994 995 996
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
997 998


999
@register_operation
1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
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
1028 1029 1030
class FlushOperation(NodeOperation):
    id = 'flush'
    name = _("flush")
1031
    description = _("Passivate node and move all instances to other ones.")
1032
    required_perms = ()
1033
    async_queue = "localhost.man.slow"
1034

1035
    def _operation(self, activity, user):
1036 1037 1038
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1039
        for i in self.node.instance_set.all():
1040 1041 1042 1043
            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
1044
                i.migrate(user=user)
1045 1046


1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
@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
1095

1096 1097 1098 1099 1100 1101 1102 1103 1104
    def check_precond(self):
        if not self.node.enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot disable a disabled node."), Exception())
        if self.node.instance_set.exists():
            raise humanize_exception(ugettext_noop(
                "You cannot disable a node which is hosting instances."),
                Exception())
        super(DisableOperation, self).check_precond()
1105

1106 1107 1108 1109
    def _operation(self):
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()
1110 1111


1112
@register_operation
1113
class ScreenshotOperation(RemoteInstanceOperation):
1114 1115
    id = 'screenshot'
    name = _("screenshot")
1116 1117 1118
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
1119
    acl_level = "owner"
1120
    required_perms = ()
1121
    accept_states = ('RUNNING', )
1122
    task = vm_tasks.screenshot
1123 1124


1125
@register_operation
Bach Dániel committed
1126 1127 1128
class RecoverOperation(InstanceOperation):
    id = 'recover'
    name = _("recover")
1129 1130 1131
    description = _("Try to recover virtual machine disks from destroyed "
                    "state. Network resources (allocations) are already lost, "
                    "so you will have to manually add interfaces afterwards.")
Bach Dániel committed
1132 1133
    acl_level = "owner"
    required_perms = ('vm.recover', )
1134
    accept_states = ('DESTROYED', )
1135
    resultant_state = 'PENDING'
Bach Dániel committed
1136 1137

    def check_precond(self):
1138 1139 1140 1141
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
1142 1143 1144 1145 1146 1147 1148 1149 1150 1151

    def _operation(self):
        for disk in self.instance.disks.all():
            disk.destroyed = None
            disk.restore()
            disk.save()
        self.instance.destroyed_at = None
        self.instance.sav