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

18
from __future__ import absolute_import, unicode_literals
Bach Dániel committed
19 20
from base64 import encodestring
from hashlib import md5
Dudás Ádám committed
21
from logging import getLogger
Bach Dániel committed
22
import os
23
from re import search
Őry Máté committed
24
from string import ascii_lowercase
Bach Dániel committed
25 26 27
from StringIO import StringIO
from tarfile import TarFile, TarInfo
import time
Kálmán Viktor committed
28
from urlparse import urlsplit
Dudás Ádám committed
29

30
from django.core.exceptions import PermissionDenied, SuspiciousOperation
31
from django.core.urlresolvers import reverse
Dudás Ádám committed
32
from django.utils import timezone
33
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
34
from django.conf import settings
Bach Dániel committed
35
from django.db.models import Q
Dudás Ádám committed
36

37 38
from sizefield.utils import filesizeformat

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

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

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

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


64 65 66 67
class RemoteOperationMixin(object):
    remote_timeout = 30

    def _operation(self, **kwargs):
Őry Máté committed
68
        args = self._get_remote_args(**kwargs)
69 70 71 72 73 74 75 76 77
        return self.task.apply_async(
            args=args, queue=self._get_remote_queue()
        ).get(timeout=self.remote_timeout)

    def check_precond(self):
        super(RemoteOperationMixin, self).check_precond()
        self._get_remote_queue()


78 79 80 81 82 83 84
class AbortableRemoteOperationMixin(object):
    remote_step = property(lambda self: self.remote_timeout / 10)

    def _operation(self, task, **kwargs):
        args = self._get_remote_args(**kwargs),
        remote = self.task.apply_async(
            args=args, queue=self._get_remote_queue())
85
        for i in xrange(0, self.remote_timeout, self.remote_step):
86 87 88 89 90 91 92
            try:
                return remote.get(timeout=self.remote_step)
            except TimeoutError as e:
                if task is not None and task.is_aborted():
                    AbortableAsyncResult(remote.id).abort()
                    raise humanize_exception(ugettext_noop(
                        "Operation aborted by user."), e)
93
        raise TimeLimitExceeded()
94 95


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

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

    def check_precond(self):
110 111
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
112 113 114 115 116 117 118 119 120 121 122 123 124 125
        if self.accept_states:
            if self.instance.status not in self.accept_states:
                logger.debug("precond failed for %s: %s not in %s",
                             unicode(self.__class__),
                             unicode(self.instance.status),
                             unicode(self.accept_states))
                raise self.instance.WrongStateError(self.instance)
        if self.deny_states:
            if self.instance.status in self.deny_states:
                logger.debug("precond failed for %s: %s in %s",
                             unicode(self.__class__),
                             unicode(self.instance.status),
                             unicode(self.accept_states))
                raise self.instance.WrongStateError(self.instance)
126 127

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

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

135 136
        if (self.instance.node and not self.instance.node.online and
                not user.is_superuser):
137 138
            raise self.instance.WrongStateError(self.instance)

139 140
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
141 142 143 144 145 146 147 148 149 150
        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.")

151 152 153
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name, resultant_state=self.resultant_state)
154 155
        else:
            return InstanceActivity.create(
156 157
                code_suffix=self.get_activity_code_suffix(),
                instance=self.instance,
158
                readable_name=name, user=user,
159 160
                concurrency_check=self.concurrency_check,
                resultant_state=self.resultant_state)
161

162 163 164 165 166
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

167

168 169 170 171 172 173 174 175 176 177
class RemoteInstanceOperation(RemoteOperationMixin, InstanceOperation):
    remote_queue = ('vm', 'fast')

    def _get_remote_queue(self):
        return self.instance.get_remote_queue_name(*self.remote_queue)

    def _get_remote_args(self, **kwargs):
        return [self.instance.vm_name]


Bach Dániel committed
178
class EnsureAgentMixin(object):
Bálint Máhonfai committed
179
    accept_states = ('RUNNING',)
Bach Dániel committed
180 181 182 183 184 185 186 187 188 189 190 191

    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",
192 193
                started__gt=last_boot_time, instance=self.instance
            ).latest("started")
Bach Dániel committed
194 195 196 197 198
        except InstanceActivity.DoesNotExist:  # no agent since last boot
            raise self.instance.NoAgentError(self.instance)


class RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation):
Bálint Máhonfai committed
199
    remote_queue = ('agent',)
Bach Dániel committed
200 201 202
    concurrency_check = False


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

212 213
    def rollback(self, net, activity):
        with activity.sub_activity(
Bálint Máhonfai committed
214
                'destroying_net',
215 216 217 218
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

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

242 243 244 245
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

246

247
@register_operation
248 249 250
class CreateDiskOperation(InstanceOperation):
    id = 'create_disk'
    name = _("create disk")
251
    description = _("Create and attach empty disk to the virtual machine.")
Bálint Máhonfai committed
252
    required_perms = ('storage.create_empty_disk',)
253
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
254

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

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

269
        if self.instance.is_running:
270
            with activity.sub_activity(
Bálint Máhonfai committed
271 272
                    'deploying_disk',
                    readable_name=ugettext_noop("deploying disk")
273
            ):
274
                disk.deploy()
275
            self.instance._attach_disk(parent_activity=activity, disk=disk)
276

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


283
@register_operation
284
class ResizeDiskOperation(RemoteInstanceOperation):
285 286 287 288
    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
Bálint Máhonfai committed
289 290
    required_perms = ('storage.resize_disk',)
    accept_states = ('RUNNING',)
291
    async_queue = "localhost.man.slow"
292 293
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
294

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

    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)

304
    def _operation(self, disk, size):
305 306 307
        if not disk.is_resizable:
            raise HumanReadableException.create(ugettext_noop(
                'Disk type "%(type)s" is not resizable.'), type=disk.type)
308 309 310 311
        super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
        disk.size = size
        disk.save()

312

313
@register_operation
314 315 316
class DownloadDiskOperation(InstanceOperation):
    id = 'download_disk'
    name = _("download disk")
317 318 319 320
    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.")
321
    abortable = True
322
    has_percentage = True
Bálint Máhonfai committed
323
    required_perms = ('storage.download_disk',)
324
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
325
    async_queue = "localhost.man.slow"
326

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

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

341 342 343 344
        activity.result = create_readable(ugettext_noop(
            "Downloading %(url)s is finished. The file md5sum "
            "is: '%(checksum)s'."),
            url=url, checksum=disk.checksum)
Őry Máté committed
345
        # TODO iso (cd) hot-plug is not supported by kvm/guests
346
        if self.instance.is_running and disk.type not in ["iso"]:
347
            self.instance._attach_disk(parent_activity=activity, disk=disk)
348

349

350
@register_operation
351 352 353
class ExportDiskOperation(InstanceOperation):
    id = 'export_disk'
    name = _('export disk')
354
    description = _('Export disk to the selected format.')
355 356
    required_perms = ('storage.export_disk',)
    accept_states = ('STOPPED',)
357
    async_queue = 'localhost.man.slow'
358

359 360 361 362 363 364 365 366 367 368 369 370
    def check_auth(self, user):
        super(ExportDiskOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied

    def _operation(self, user, disk, format):
        store = Store(user)
        store.new_folder('/export')
        upload_link = store.request_upload('/export')
        disk.export(format, upload_link)
371 372 373


@register_operation
374 375 376 377 378 379 380 381 382 383
class ImportDiskOperation(InstanceOperation):
    id = 'import_disk'
    name = _('import disk')
    description = _('Import and attach a disk image to the virtual machine.')
    abortable = True
    has_percentage = True
    required_perms = ('storage.import_disk',)
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
    async_queue = 'localhost.man.slow'

384 385
    def _operation(self, activity):
        pass
386 387 388


@register_operation
389
class DeployOperation(InstanceOperation):
Dudás Ádám committed
390 391
    id = 'deploy'
    name = _("deploy")
392 393
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
394
    required_perms = ()
395
    deny_states = ('SUSPENDED', 'RUNNING')
396
    resultant_state = 'RUNNING'
Dudás Ádám committed
397

398 399
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
400
                                        self.instance.STATUS.PENDING,
401 402
                                        self.instance.STATUS.ERROR)

403 404 405
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
406 407
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
408
        activity.result = create_readable(
Guba Sándor committed
409
            ugettext_noop("virtual machine successfully "
410 411
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
412

413
    def _operation(self, activity, node=None):
Dudás Ádám committed
414 415
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
416 417 418 419 420
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
421 422

        # Deploy virtual images
423 424 425 426 427 428
        try:
            self.instance._deploy_disks(parent_activity=activity)
        except:
            self.instance.yield_node()
            self.instance.yield_vnc_port()
            raise
Dudás Ádám committed
429 430

        # Deploy VM on remote machine
431
        if self.instance.state not in ['PAUSED']:
432
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
433 434

        # Establish network connection (vmdriver)
435
        with activity.sub_activity(
Bálint Máhonfai committed
436 437
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
438 439
            self.instance.deploy_net()

440 441 442 443 444
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

447 448 449
        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
450

451
    @register_operation
Őry Máté committed
452
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
453 454
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
455
        description = _("Deploy virtual machine.")
456 457
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy
Bálint Máhonfai committed
458 459
        remote_timeout = 120

Őry Máté committed
460
        def _get_remote_args(self, **kwargs):
461 462 463 464 465 466 467 468 469
            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
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
    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()

487
    @register_operation
Őry Máté committed
488
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
489 490 491 492 493
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
494

495
@register_operation
496
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
497 498
    id = 'destroy'
    name = _("destroy")
499 500
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
501
    required_perms = ()
502
    resultant_state = 'DESTROYED'
Dudás Ádám committed
503

504 505 506
    def on_abort(self, activity, error):
        activity.resultant_state = None

507
    def _operation(self, activity, system):
508
        # Destroy networks
509 510 511
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
512
            if self.instance.node:
513
                self.instance.shutdown_net()
514
            self.instance.destroy_net()
Dudás Ádám committed
515

516
        if self.instance.node:
517
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
518 519

        # Destroy disks
520 521 522
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
523
            self.instance.destroy_disks()
Dudás Ádám committed
524

Dudás Ádám committed
525 526
        # Delete mem. dump if exists
        try:
527
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
528 529 530 531 532 533
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
534 535 536 537

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

538
    @register_operation
539
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
540 541 542 543 544
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

545 546 547 548 549 550 551 552 553 554 555 556 557 558
    @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
559

560
@register_operation
561
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
562 563
    id = 'migrate'
    name = _("migrate")
564 565
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
566
    required_perms = ()
567
    superuser_required = True
Bálint Máhonfai committed
568
    accept_states = ('RUNNING',)
569
    async_queue = "localhost.man.slow"
570 571
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
572
    remote_timeout = 1000
573

574
    def _get_remote_args(self, to_node, live_migration, **kwargs):
575 576
        return (super(MigrateOperation, self)._get_remote_args(**kwargs) +
                [to_node.host.hostname, live_migration])
Dudás Ádám committed
577

578
    def rollback(self, activity):
579
        with activity.sub_activity(
Bálint Máhonfai committed
580 581
                'rollback_net', readable_name=ugettext_noop(
                    "redeploy network (rollback)")):
582 583
            self.instance.deploy_net()

584
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
585
        if not to_node:
Bach Dániel committed
586 587 588 589 590 591
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

592
        try:
593
            with activity.sub_activity(
Bálint Máhonfai committed
594 595
                    'migrate_vm', readable_name=create_readable(
                        ugettext_noop("migrate to %(node)s"), node=to_node)):
596 597
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
598 599 600
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
601
            raise
Dudás Ádám committed
602

603
        # Shutdown networks
604
        with activity.sub_activity(
Bálint Máhonfai committed
605 606
                'shutdown_net', readable_name=ugettext_noop(
                    "shutdown network")):
607 608
            self.instance.shutdown_net()

Dudás Ádám committed
609 610 611
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
612

Dudás Ádám committed
613
        # Estabilish network connection (vmdriver)
614
        with activity.sub_activity(
Bálint Máhonfai committed
615 616
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
617
            self.instance.deploy_net()
Dudás Ádám committed
618 619


620
@register_operation
621
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
622 623
    id = 'reboot'
    name = _("reboot")
624 625
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
626
    required_perms = ()
Bálint Máhonfai committed
627
    accept_states = ('RUNNING',)
628
    task = vm_tasks.reboot
Dudás Ádám committed
629

630 631
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
632 633 634
        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
635 636


637
@register_operation
638 639 640
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
641 642 643
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
644
    required_perms = ()
645
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
646

647 648
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
649 650
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
651 652 653 654 655
            interface.shutdown()

        interface.destroy()
        interface.delete()

656 657
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
658
                               vlan=kwargs['interface'].vlan)
659

660

661
@register_operation
662 663 664 665 666
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
667
    acl_level = "operator"
Bálint Máhonfai committed
668
    required_perms = ('vm.config_ports',)
669 670 671 672

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
673
            raise SuspiciousOperation()
674 675 676 677 678 679 680
        activity.readable_name = create_readable(
            ugettext_noop("close %(proto)s/%(port)d on %(host)s"),
            proto=rule.proto, port=rule.dport, host=rule.host)
        rule.delete()


@register_operation
681 682 683 684 685
class AddPortOperation(InstanceOperation):
    id = 'add_port'
    name = _("open port")
    description = _("Open the specified port.")
    concurrency_check = False
686
    acl_level = "operator"
Bálint Máhonfai committed
687
    required_perms = ('vm.config_ports',)
688 689 690

    def _operation(self, activity, host, proto, port):
        if host.interface_set.get().instance != self.instance:
691
            raise SuspiciousOperation()
692 693 694 695 696 697 698
        host.add_port(proto, private=port)
        activity.readable_name = create_readable(
            ugettext_noop("open %(proto)s/%(port)d on %(host)s"),
            proto=proto, port=port, host=host)


@register_operation
699 700 701
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
702 703
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
704
    required_perms = ()
705
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
706 707

    def _operation(self, activity, user, system, disk):
708
        if self.instance.is_running and disk.type not in ["iso"]:
709
            self.instance._detach_disk(disk=disk, parent_activity=activity)
710
        with activity.sub_activity(
Bálint Máhonfai committed
711 712
                'destroy_disk',
                readable_name=ugettext_noop('destroy disk')
713
        ):
714
            disk.destroy()
715
            return self.instance.disks.remove(disk)
716

717 718 719
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
720 721


722
@register_operation
723
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
724 725
    id = 'reset'
    name = _("reset")
726
    description = _("Cold reboot virtual machine (power cycle).")
727
    required_perms = ()
Bálint Máhonfai committed
728
    accept_states = ('RUNNING',)
729
    task = vm_tasks.reset
Dudás Ádám committed
730

731 732
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
733 734 735
        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
736 737


738
@register_operation
739
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
740 741
    id = 'save_as_template'
    name = _("save as template")
742 743 744 745
    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.")
746
    has_percentage = True
747
    abortable = True
Bálint Máhonfai committed
748
    required_perms = ('vm.create_template',)
749
    accept_states = ('RUNNING', 'STOPPED')
750
    async_queue = "localhost.man.slow"
Dudás Ádám committed
751

752 753 754 755
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

756 757 758 759 760 761
    @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)
762
        else:
763 764
            v = 1
        return "%s v%d" % (name, v)
765

766
    def on_abort(self, activity, error):
767
        if hasattr(self, 'disks'):
768 769 770
            for disk in self.disks:
                disk.destroy()

771
    def _operation(self, activity, user, system, name=None,
772
                   with_shutdown=True, clone=False, task=None, **kwargs):
773 774
        try:
            self.instance._cleanup(parent_activity=activity, user=user)
775
        except:
776 777
            pass

778
        if with_shutdown:
779
            try:
780 781
                self.instance.shutdown(parent_activity=activity,
                                       user=user, task=task)
782 783 784
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
785 786 787 788 789 790 791 792
        # 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,
793
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
794 795
            'num_cores': self.instance.num_cores,
            'owner': user,
796
            'parent': self.instance.template or None,  # Can be problem
Dudás Ádám committed
797 798 799 800 801 802
            '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
803
        params.pop("parent_activity", None)
Dudás Ádám committed
804

805 806
        from storage.models import Disk

Dudás Ádám committed
807 808
        def __try_save_disk(disk):
            try:
809
                return disk.save_as(task)
Dudás Ádám committed
810 811 812
            except Disk.WrongDiskTypeError:
                return disk

813
        self.disks = []
814 815
        for disk in self.instance.disks.all():
            with activity.sub_activity(
Bálint Máhonfai committed
816 817 818 819
                    'saving_disk',
                    readable_name=create_readable(
                        ugettext_noop("saving disk %(name)s"),
                        name=disk.name)
820
            ):
821 822
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
823 824 825 826
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
827
        # Copy traits from the VM instance
828
        tmpl.req_traits.add(*self.instance.req_traits.all())
829 830
        if clone:
            tmpl.clone_acl(self.instance.template)
Guba Sándor committed
831
            # Add permission for the original owner of the template
832 833
            tmpl.set_level(self.instance.template.owner, 'owner')
            tmpl.set_level(user, 'owner')
Dudás Ádám committed
834
        try:
835
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
836 837 838 839 840 841 842
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
843 844 845 846
            return create_readable(
                ugettext_noop("New template: %(template)s"),
                template=reverse('dashboard.views.template-detail',
                                 kwargs={'pk': tmpl.pk}))
Dudás Ádám committed
847 848


849
@register_operation
850 851
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
852 853
    id = 'shutdown'
    name = _("shutdown")
854 855 856 857
    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
858
    abortable = True
859
    required_perms = ()
Bálint Máhonfai committed
860
    accept_states = ('RUNNING',)
861
    resultant_state = 'STOPPED'
862 863
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
864
    remote_timeout = 180
Dudás Ádám committed
865

866 867
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
868
        self.instance.yield_node()
Dudás Ádám committed
869

870 871 872 873 874 875 876 877 878 879 880
    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
881

882
@register_operation
883
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
884 885
    id = 'shut_off'
    name = _("shut off")
886 887 888 889 890 891 892
    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.")
893
    required_perms = ()
894
    accept_states = ('RUNNING', 'PAUSED')
895
    resultant_state = 'STOPPED'
Dudás Ádám committed
896

897
    def _operation(self, activity):
Dudás Ádám committed
898
        # Shutdown networks
899 900 901
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
902
            self.instance.shutdown_net()
Dudás Ádám committed
903

904
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
905
        self.instance.yield_node()
Dudás Ádám committed
906 907


908
@register_operation
909
class SleepOperation(InstanceOperation):
Dudás Ádám committed
910 911
    id = 'sleep'
    name = _("sleep")
912 913 914 915 916 917 918 919
    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.")
920
    required_perms = ()
Bálint Máhonfai committed
921
    accept_states = ('RUNNING',)
922
    resultant_state = 'SUSPENDED'
923
    async_queue = "localhost.man.slow"
Dudás Ádám committed
924

925 926 927 928
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
929 930 931 932 933 934
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

935
    def _operation(self, activity, system):
936 937 938
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
939
            self.instance.shutdown_net()
940
        self.instance._suspend_vm(parent_activity=activity)
941
        self.instance.yield_node()
Dudás Ádám committed
942

943 944 945 946
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
947
        task = vm_tasks.sleep
948
        remote_queue = ("vm", "slow")
949
        remote_timeout = 1000
Dudás Ádám committed
950

951 952
        def _get_remote_args(self, **kwargs):
            return (super(SleepOperation.SuspendVmOperation, self)
953 954
                    ._get_remote_args(**kwargs) +
                    [self.instance.mem_dump['path']])
Dudás Ádám committed
955 956


957
@register_operation
958
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
959 960
    id = 'wake_up'
    name = _("wake up")
961 962 963
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
964
    required_perms = ()
Bálint Máhonfai committed
965
    accept_states = ('SUSPENDED',)
966
    resultant_state = 'RUNNING'
967
    async_queue = "localhost.man.slow"
Dudás Ádám committed
968

969
    def is_preferred(self):
970
        return self.instance.status == self.instance.STATUS.SUSPENDED
971

Dudás Ádám committed
972
    def on_abort(self, activity, error):
Bach Dániel committed
973 974 975 976
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
977

978
    def _operation(self, activity):
Dudás Ádám committed
979
        # Schedule vm
Dudás Ádám committed
980
        self.instance.allocate_vnc_port()
981
        self.instance.allocate_node()
Dudás Ádám committed
982 983

        # Resume vm
984
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
985 986

        # Estabilish network connection (vmdriver)
987
        with activity.sub_activity(
Bálint Máhonfai committed
988 989
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
990
            self.instance.deploy_net()
Dudás Ádám committed
991

992 993 994 995
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
996

997 998 999 1000 1001 1002
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
1003
        remote_timeout = 1000
1004 1005 1006

        def _get_remote_args(self, **kwargs):
            return (super(WakeUpOperation.WakeUpVmOperation, self)
1007 1008
                    ._get_remote_args(**kwargs) +
                    [self.instance.mem_dump['path']])
1009

Dudás Ádám committed
1010

1011
@register_operation
1012 1013 1014
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
1015 1016 1017 1018
    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.")
1019
    acl_level = "operator"
1020
    required_perms = ()
1021
    concurrency_check = False
1022

1023 1024
    def set_time_of_suspend(self, activity, suspend, force):
        with activity.sub_activity(
Bálint Máhonfai committed
1025
                'renew_suspend', concurrency_check=False,
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
                readable_name=ugettext_noop('set time of suspend')):
            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."))
            self.instance.time_of_suspend = suspend

    def set_time_of_delete(self, activity, delete, force):
        with activity.sub_activity(
Bálint Máhonfai committed
1036
                'renew_delete', concurrency_check=False,
1037 1038 1039 1040 1041 1042 1043 1044
                readable_name=ugettext_noop('set time of delete')):
            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_delete = delete

Őry Máté committed
1045
    def _operation(self, activity, lease=None, force=False, save=False):
1046
        suspend, delete = self.instance.get_renew_times(lease)
1047 1048 1049 1050 1051 1052 1053 1054 1055
        try:
            self.set_time_of_suspend(activity, suspend, force)
        except HumanReadableException:
            pass
        try:
            self.set_time_of_delete(activity, delete, force)
        except HumanReadableException:
            pass

Őry Máté committed
1056 1057
        if save:
            self.instance.lease = lease
1058

1059
        self.instance.save()
1060

1061
        return create_readable(ugettext_noop(
1062
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
1063 1064
            suspend=self.instance.time_of_suspend,
            delete=self.instance.time_of_suspend)
1065 1066


1067
@register_operation
1068
class ChangeStateOperation(InstanceOperation):