operations.py 46 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 76 77 78 79 80 81 82 83 84 85 86
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())
        for i in xrange(int(self.remote_timeout / self.remote_step)):
            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)
        return remote.get(timeout=self.remote_step)


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

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

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

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

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

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

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

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

154

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


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

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

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

206 207 208 209
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

210

211
@register_operation
212
class CreateDiskOperation(InstanceOperation):
213

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

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

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

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

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


248
@register_operation
249
class ResizeDiskOperation(RemoteInstanceOperation):
250 251 252 253 254

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

261 262 263
    def _get_remote_args(self, disk, size, **kwargs):
        return (super(ResizeDiskOperation, self)
                ._get_remote_args(**kwargs) + [disk.path, size])
264 265 266 267 268 269 270

    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)


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

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

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

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

304

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

315 316
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
317
                                        self.instance.STATUS.PENDING,
318 319
                                        self.instance.STATUS.ERROR)

320 321 322
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

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

330
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
331 332
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
333
        self.instance.allocate_node()
Dudás Ádám committed
334 335

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

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

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

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

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

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

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

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

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

Dudás Ádám committed
400

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

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

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

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

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

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

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

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

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

463
@register_operation
464
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
465 466
    id = 'migrate'
    name = _("migrate")
467 468
    description = _("Move virtual machine to an other worker node with a few "
                    "seconds of interruption (live migration).")
469
    required_perms = ()
470
    superuser_required = True
471
    accept_states = ('RUNNING', )
472
    async_queue = "localhost.man.slow"
473 474 475 476 477 478 479 480
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
    timeout = 600

    def _get_remote_args(self, to_node, **kwargs):
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
                + [to_node.host.hostname, True])
        # TODO handle non-live migration
Dudás Ádám committed
481

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

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

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

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

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

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


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

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


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

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

        interface.destroy()
        interface.delete()

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

563

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

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

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


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

596 597
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
598 599 600
        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
601 602


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

617 618 619 620
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

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

631
    def on_abort(self, activity, error):
632
        if hasattr(self, 'disks'):
633 634 635
            for disk in self.disks:
                disk.destroy()

636
    def _operation(self, activity, user, system, timeout=300, name=None,
637
                   with_shutdown=True, task=None, **kwargs):
638
        if with_shutdown:
639 640
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
641
                                                      user=user, task=task)
642 643 644
            except Instance.WrongStateError:
                pass

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

665 666
        from storage.models import Disk

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

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

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


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

716 717
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
718
        self.instance.yield_node()
Dudás Ádám committed
719

720 721 722 723 724 725 726 727 728 729 730
    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
731

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

747
    def _operation(self, activity):
Dudás Ádám committed
748 749 750
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
751

752
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
753
        self.instance.yield_node()
Dudás Ádám committed
754 755


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

773 774 775 776
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
777 778 779 780 781 782
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

783 784 785 786
    def _operation(self, activity):
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
787
            self.instance.shutdown_net()
788 789
        self.instance._suspend_vm(parent_activity=activity)
        self.instance.yield_node()
Dudás Ádám committed
790

791 792 793 794 795 796 797
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
        task = vm_tasks.suspend
        remote_queue = ("vm", "slow")
        timeout = 600
Dudás Ádám committed
798

799 800 801 802
        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
803 804


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

816
    def is_preferred(self):
817
        return self.instance.status == self.instance.STATUS.SUSPENDED
818

Dudás Ádám committed
819
    def on_abort(self, activity, error):
Bach Dániel committed
820 821 822 823
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
824

825
    def _operation(self, activity):
Dudás Ádám committed
826
        # Schedule vm
Dudás Ádám committed
827
        self.instance.allocate_vnc_port()
828
        self.instance.allocate_node()
Dudás Ádám committed
829 830

        # Resume vm
831
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
832 833

        # Estabilish network connection (vmdriver)
834 835 836
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
837
            self.instance.deploy_net()
Dudás Ádám committed
838

839 840 841 842
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
843

844 845 846 847 848 849 850 851 852 853 854 855 856
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
        timeout = 600

        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
857

858
@register_operation
859 860 861
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
862 863 864 865
    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.")
866
    acl_level = "operator"
867
    required_perms = ()
868
    concurrency_check = False
869

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


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

905 906
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
907
        activity.resultant_state = new_state
908 909 910 911 912 913 914
        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)
915

916 917 918 919
        if reset_node:
            self.instance.node = None
            self.instance.save()

920

921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
@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)

946

947
class NodeOperation(Operation):
948
    async_operation = abortable_async_node_operation
949
    host_cls = Node
950 951
    online_required = True
    superuser_required = True
952 953 954 955 956

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

957 958 959 960 961 962 963
    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())

964 965
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
966 967 968 969 970 971 972 973 974 975
        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.")

976 977 978
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
979
        else:
980 981 982
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
983 984


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

1021
    def _operation(self, activity, user):
1022 1023 1024
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1025
        for i in self.node.instance_set.all():
1026 1027 1028 1029
            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
1030
                i.migrate(user=user)
1031 1032


1033 1034 1035 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
@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
1081

1082 1083 1084 1085 1086 1087 1088 1089 1090
    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()
1091

1092 1093 1094 1095
    def _operation(self):
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()
1096 1097


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


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

    def check_precond(self):
1124 1125 1126 1127
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
1128 1129 1130 1131 1132 1133 1134 1135 1136 1137

    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()


1138
@register_operation
1139 1140 1141
class ResourcesOperation(InstanceOperation):
    id = 'resources_change'
    name = _("resources change")
1142
    description = _("Change resources of a stopped virtual machine.")
1143
    acl_level = "owner"
1144
    required_perms = ('vm.change_resources', )
1145
    accept_states = ('STOPPED', 'PENDING', )
1146

1147 1148
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
1149

1150 1151 1152 1153 1154
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

1155
        self.instance.full_clean()
1156 1157
        self.instance.save()

1158 1159 1160 1161 1162 1163
        activity.result = create_readable(ugettext_noop(
            "Priority: %(priority)s, Num cores: %(num_cores)s, "
            "Ram size: %(ram_size)s"), priority=priority, num_cores=num_cores,
            ram_size=ram_size
        )

1164

Őry Máté committed
1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
class EnsureAgentMixin(object):
    accept_states = ('RUNNING', )

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

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

        try:
            InstanceActivity.objects.filter(
                activity_code="vm.Instance.agent.starting",
                started__gt=last_boot_time).latest("started")
        except InstanceActivity.DoesNotExist:  # no agent since last boot
            raise self.instance.NoAgentError(self.instance)


1184
@register_operation
1185
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):