operations.py 46.2 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 267

    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)


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

282 283
    def _operation(self, user, url, task, activity, name=None):
        activity.result = url
Bach Dániel committed
284 285
        from storage.models import Disk

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

Őry Máté committed
297
        # TODO iso (cd) hot-plug is not supported by kvm/guests
298
        if self.instance.is_running and disk.type not in ["iso"]:
299
            self.instance._attach_disk(parent_activity=activity, disk=disk)
300

301

302
@register_operation
303
class DeployOperation(InstanceOperation):
Dudás Ádám committed
304 305
    id = 'deploy'
    name = _("deploy")
306 307
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
308
    required_perms = ()
309
    deny_states = ('SUSPENDED', 'RUNNING')
310
    resultant_state = 'RUNNING'
Dudás Ádám committed
311

312 313
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
314
                                        self.instance.STATUS.PENDING,
315 316
                                        self.instance.STATUS.ERROR)

317 318 319
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
320 321
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
322
        activity.result = create_readable(
Guba Sándor committed
323
            ugettext_noop("virtual machine successfully "
324 325
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
326

327
    def _operation(self, activity, node=None):
Dudás Ádám committed
328 329
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
330 331 332 333 334
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
335 336

        # Deploy virtual images
337
        self.instance._deploy_disks(parent_activity=activity)
Dudás Ádám committed
338 339

        # Deploy VM on remote machine
340
        if self.instance.state not in ['PAUSED']:
341
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
342 343

        # Establish network connection (vmdriver)
344 345 346
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
347 348
            self.instance.deploy_net()

349 350 351 352 353
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

356 357 358
        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
359

360
    @register_operation
Őry Máté committed
361
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
362 363
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
364
        description = _("Deploy virtual machine.")
365 366 367
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy

Őry Máté committed
368
        def _get_remote_args(self, **kwargs):
369 370 371 372 373 374 375 376 377
            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
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
    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()

395
    @register_operation
Őry Máté committed
396
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
397 398 399 400 401
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
402

403
@register_operation
404
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
405 406
    id = 'destroy'
    name = _("destroy")
407 408
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
409
    required_perms = ()
410
    resultant_state = 'DESTROYED'
Dudás Ádám committed
411

412
    def _operation(self, activity, system):
413
        # Destroy networks
414 415 416
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
417
            if self.instance.node:
418
                self.instance.shutdown_net()
419
            self.instance.destroy_net()
Dudás Ádám committed
420

421
        if self.instance.node:
422
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
423 424

        # Destroy disks
425 426 427
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
428
            self.instance.destroy_disks()
Dudás Ádám committed
429

Dudás Ádám committed
430 431
        # Delete mem. dump if exists
        try:
432
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
433 434 435 436 437 438
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
439 440 441 442

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

443
    @register_operation
444
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
445 446 447 448 449
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

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

465
@register_operation
466
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
467 468
    id = 'migrate'
    name = _("migrate")
469 470
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
471
    required_perms = ()
472
    superuser_required = True
473
    accept_states = ('RUNNING', )
474
    async_queue = "localhost.man.slow"
475 476
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
477
    remote_timeout = 1000
478

479
    def _get_remote_args(self, to_node, live_migration, **kwargs):
480
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
481
                + [to_node.host.hostname, live_migration])
Dudás Ádám committed
482

483
    def rollback(self, activity):
484 485 486
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
487 488
            self.instance.deploy_net()

489
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
490
        if not to_node:
Bach Dániel committed
491 492 493 494 495 496
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

497
        try:
498 499 500
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
501 502
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
503 504 505
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
506
            raise
Dudás Ádám committed
507

508
        # Shutdown networks
509 510 511
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
512 513
            self.instance.shutdown_net()

Dudás Ádám committed
514 515 516
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
517

Dudás Ádám committed
518
        # Estabilish network connection (vmdriver)
519 520 521
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
522
            self.instance.deploy_net()
Dudás Ádám committed
523 524


525
@register_operation
526
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
527 528
    id = 'reboot'
    name = _("reboot")
529 530
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
531
    required_perms = ()
532
    accept_states = ('RUNNING', )
533
    task = vm_tasks.reboot
Dudás Ádám committed
534

535 536
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
537 538 539
        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
540 541


542
@register_operation
543 544 545
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
546 547 548
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
549
    required_perms = ()
550
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
551

552 553
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
554 555
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
556 557 558 559 560
            interface.shutdown()

        interface.destroy()
        interface.delete()

561 562
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
563
                               vlan=kwargs['interface'].vlan)
564

565

566
@register_operation
567 568 569
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
570 571
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
572
    required_perms = ()
573
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
574 575

    def _operation(self, activity, user, system, disk):
576
        if self.instance.is_running and disk.type not in ["iso"]:
577
            self.instance._detach_disk(disk=disk, parent_activity=activity)
578 579 580 581
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
582
            disk.destroy()
583
            return self.instance.disks.remove(disk)
584

585 586 587
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
588 589


590
@register_operation
591
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
592 593
    id = 'reset'
    name = _("reset")
594
    description = _("Cold reboot virtual machine (power cycle).")
595
    required_perms = ()
596
    accept_states = ('RUNNING', )
597
    task = vm_tasks.reset
Dudás Ádám committed
598

599 600
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
601 602 603
        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
604 605


606
@register_operation
607
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
608 609
    id = 'save_as_template'
    name = _("save as template")
610 611 612 613
    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.")
614
    has_percentage = True
615
    abortable = True
616
    required_perms = ('vm.create_template', )
617
    accept_states = ('RUNNING', 'STOPPED')
618
    async_queue = "localhost.man.slow"
Dudás Ádám committed
619

620 621 622 623
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

624 625 626 627 628 629
    @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)
630
        else:
631 632
            v = 1
        return "%s v%d" % (name, v)
633

634
    def on_abort(self, activity, error):
635
        if hasattr(self, 'disks'):
636 637 638
            for disk in self.disks:
                disk.destroy()

639
    def _operation(self, activity, user, system, name=None,
640
                   with_shutdown=True, clone=False, task=None, **kwargs):
641
        if with_shutdown:
642 643
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
644
                                                      user=user, task=task)
645 646 647
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
648 649 650 651 652 653 654 655
        # 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,
656
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
657 658 659 660 661 662 663 664 665
            '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
666
        params.pop("parent_activity", None)
Dudás Ádám committed
667

668 669
        from storage.models import Disk

Dudás Ádám committed
670 671
        def __try_save_disk(disk):
            try:
672
                return disk.save_as(task)
Dudás Ádám committed
673 674 675
            except Disk.WrongDiskTypeError:
                return disk

676
        self.disks = []
677 678 679 680 681 682 683
        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)
            ):
684 685
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
686 687 688 689
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
690 691
        if clone:
            tmpl.clone_acl(self.instance.template)
Dudás Ádám committed
692
        try:
693
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
694 695 696 697 698 699 700 701 702 703
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


704
@register_operation
705 706
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
707 708
    id = 'shutdown'
    name = _("shutdown")
709 710 711 712
    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
713
    abortable = True
714
    required_perms = ()
715
    accept_states = ('RUNNING', )
716
    resultant_state = 'STOPPED'
717 718
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
719
    remote_timeout = 120
Dudás Ádám committed
720

721 722
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
723
        self.instance.yield_node()
Dudás Ádám committed
724

725 726 727 728 729 730 731 732 733 734 735
    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
736

737
@register_operation
738
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
739 740
    id = 'shut_off'
    name = _("shut off")
741 742 743 744 745 746 747
    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.")
748
    required_perms = ()
749
    accept_states = ('RUNNING', )
750
    resultant_state = 'STOPPED'
Dudás Ádám committed
751

752
    def _operation(self, activity):
Dudás Ádám committed
753 754 755
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
756

757
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
758
        self.instance.yield_node()
Dudás Ádám committed
759 760


761
@register_operation
762
class SleepOperation(InstanceOperation):
Dudás Ádám committed
763 764
    id = 'sleep'
    name = _("sleep")
765 766 767 768 769 770 771 772
    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.")
773
    required_perms = ()
774
    accept_states = ('RUNNING', )
775
    resultant_state = 'SUSPENDED'
776
    async_queue = "localhost.man.slow"
Dudás Ádám committed
777

778 779 780 781
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
782 783 784 785 786 787
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

788
    def _operation(self, activity, system):
789 790 791
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
792
            self.instance.shutdown_net()
793
        self.instance._suspend_vm(parent_activity=activity)
794
        self.instance.yield_node()
Dudás Ádám committed
795

796 797 798 799
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
800
        task = vm_tasks.sleep
801
        remote_queue = ("vm", "slow")
802
        remote_timeout = 1000
Dudás Ádám committed
803

804 805 806 807
        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
808 809


810
@register_operation
811
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
812 813
    id = 'wake_up'
    name = _("wake up")
814 815 816
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
817
    required_perms = ()
818
    accept_states = ('SUSPENDED', )
819
    resultant_state = 'RUNNING'
Dudás Ádám committed
820

821
    def is_preferred(self):
822
        return self.instance.status == self.instance.STATUS.SUSPENDED
823

Dudás Ádám committed
824
    def on_abort(self, activity, error):
Bach Dániel committed
825 826 827 828
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
829

830
    def _operation(self, activity):
Dudás Ádám committed
831
        # Schedule vm
Dudás Ádám committed
832
        self.instance.allocate_vnc_port()
833
        self.instance.allocate_node()
Dudás Ádám committed
834 835

        # Resume vm
836
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
837 838

        # Estabilish network connection (vmdriver)
839 840 841
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
842
            self.instance.deploy_net()
Dudás Ádám committed
843

844 845 846 847
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
848

849 850 851 852 853 854
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
855
        remote_timeout = 1000
856 857 858 859 860 861

        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
862

863
@register_operation
864 865 866
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
867 868 869 870
    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.")
871
    acl_level = "operator"
872
    required_perms = ()
873
    concurrency_check = False
874

Őry Máté committed
875
    def _operation(self, activity, lease=None, force=False, save=False):
876 877 878 879 880 881 882 883 884 885 886 887 888
        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
889 890
        if save:
            self.instance.lease = lease
891
        self.instance.save()
892 893 894
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
895 896


897
@register_operation
898
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
899
    id = 'emergency_change_state'
900 901 902 903 904 905
    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.")
906
    acl_level = "owner"
Guba Sándor committed
907
    required_perms = ('vm.emergency_change_state', )
908
    concurrency_check = False
909

910 911
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
912
        activity.resultant_state = new_state
913 914 915 916 917 918 919
        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)
920

921 922 923 924
        if reset_node:
            self.instance.node = None
            self.instance.save()

925

926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950
@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)

951

952
class NodeOperation(Operation):
953
    async_operation = abortable_async_node_operation
954
    host_cls = Node
955 956
    online_required = True
    superuser_required = True
957 958 959 960 961

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

962 963 964 965 966 967 968
    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())

969 970
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
971 972 973 974 975 976 977 978 979 980
        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.")

981 982 983
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
984
        else:
985 986 987
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
988 989


990
@register_operation
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
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
1019 1020 1021
class FlushOperation(NodeOperation):
    id = 'flush'
    name = _("flush")
1022
    description = _("Passivate node and move all instances to other ones.")
1023
    required_perms = ()
1024
    async_queue = "localhost.man.slow"
1025

1026
    def _operation(self, activity, user):
1027 1028 1029
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1030
        for i in self.node.instance_set.all():
1031 1032 1033 1034
            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
1035
                i.migrate(user=user)
1036 1037


1038 1039 1040 1041 1042 1043 1044 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
@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
1086

1087 1088 1089 1090 1091 1092 1093 1094 1095
    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()
1096

1097 1098 1099 1100
    def _operation(self):
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()
1101 1102


1103
@register_operation
1104
class ScreenshotOperation(RemoteInstanceOperation):
1105 1106
    id = 'screenshot'
    name = _("screenshot")
1107 1108 1109
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
1110
    acl_level = "owner"
1111
    required_perms = ()
1112
    accept_states = ('RUNNING', )
1113
    task = vm_tasks.screenshot
1114 1115


1116
@register_operation
Bach Dániel committed
1117 1118 1119
class RecoverOperation(InstanceOperation):
    id = 'recover'
    name = _("recover")
1120 1121 1122
    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
1123 1124
    acl_level = "owner"
    required_perms = ('vm.recover', )
1125
    accept_states = ('DESTROYED', )
1126
    resultant_state = 'PENDING'
Bach Dániel committed
1127 1128

    def check_precond(self):
1129 1130 1131 1132
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142

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


1143
@register_operation
1144 1145 1146
class ResourcesOperation(InstanceOperation):
    id = 'resources_change'
    name = _("resources change")
1147
    description = _("Change resources of a stopped virtual machine.")
1148
    acl_level = "owner"
1149