operations.py 65.3 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
19 20

from StringIO import StringIO
Bach Dániel committed
21 22
from base64 import encodestring
from hashlib import md5
Dudás Ádám committed
23
from logging import getLogger
Őry Máté committed
24
from string import ascii_lowercase
Bach Dániel committed
25
from tarfile import TarFile, TarInfo
Kálmán Viktor committed
26
from urlparse import urlsplit
Dudás Ádám committed
27

28 29 30 31 32
import os
import time
from celery.contrib.abortable import AbortableAsyncResult
from celery.exceptions import TimeLimitExceeded, TimeoutError
from django.conf import settings
33
from django.core.exceptions import PermissionDenied, SuspiciousOperation
34
from django.core.urlresolvers import reverse
35
from django.db.models import Q
Dudás Ádám committed
36
from django.utils import timezone
37
from django.utils.translation import ugettext_lazy as _, ugettext_noop
38
from re import search
39 40
from sizefield.utils import filesizeformat

41 42 43
from common.models import (
    create_readable, humanize_exception, HumanReadableException
)
44
from common.operations import Operation, register_operation, SubOperationMixin
45 46
from dashboard.store_api import Store, NoStoreException
from firewall.models import Host
Bach Dániel committed
47
from manager.scheduler import SchedulerError
48 49 50
from monitor.client import Client
from storage.models import Disk
from storage.tasks import storage_tasks
51
from .models import (
52
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
53
    NodeActivity, pwgen
54
)
Bach Dániel committed
55
from .tasks import agent_tasks, vm_tasks
56 57 58
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
Kálmán Viktor committed
59

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


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

    def _operation(self, **kwargs):
Őry Máté committed
67
        args = self._get_remote_args(**kwargs)
68 69 70 71 72 73 74 75 76
        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()


77 78 79 80 81 82 83
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())
84
        for i in xrange(0, self.remote_timeout, self.remote_step):
85 86 87 88 89 90 91
            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)
92
        raise TimeLimitExceeded()
93 94


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

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

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

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

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

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

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

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

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

166

167 168 169 170 171 172 173 174 175 176
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
177
class EnsureAgentMixin(object):
Bálint Máhonfai committed
178
    accept_states = ('RUNNING',)
Bach Dániel committed
179 180 181 182 183 184 185 186 187 188 189 190

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


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


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

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

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

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

245

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

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

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

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

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


282
@register_operation
283
class ResizeDiskOperation(RemoteInstanceOperation):
284 285 286 287
    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
288 289
    required_perms = ('storage.resize_disk',)
    accept_states = ('RUNNING',)
290
    async_queue = "localhost.man.slow"
291 292
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
293

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

    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)

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

311

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

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

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

346

347
@register_operation
348 349 350 351 352 353
class ImportDiskOperation(InstanceOperation):
    id = 'import_disk'
    name = _('import disk')
    description = _('Import and attach a disk image to the virtual machine '
                    'from the user store. The disk image has to be in the '
                    'root directory of the store.')
354 355
    abortable = True
    has_percentage = True
356 357 358 359 360 361 362 363 364 365 366
    required_perms = ('storage.import_disk',)
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
    async_queue = 'localhost.man.slow'

    def check_auth(self, user):
        super(ImportDiskOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied

367
    def _operation(self, user, name, disk_path, task):
368 369
        store = Store(user)
        download_link = store.request_download(disk_path)
370
        disk = Disk.import_disk(user, name, download_link, task)
371 372 373 374
        self.instance.disks.add(disk)


@register_operation
375 376 377
class ExportDiskOperation(InstanceOperation):
    id = 'export_disk'
    name = _('export disk')
378
    description = _('Export disk to the selected format.')
379 380
    abortable = True
    has_percentage = True
381 382
    required_perms = ('storage.export_disk',)
    accept_states = ('STOPPED',)
383
    async_queue = 'localhost.man.slow'
384

385 386 387 388 389 390 391
    def check_auth(self, user):
        super(ExportDiskOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied

392
    def _operation(self, user, disk, exported_name, disk_format, task):
393
        store = Store(user)
394
        upload_link = store.request_upload('/')
395
        disk.export(exported_name, disk_format, upload_link, task)
396 397 398


@register_operation
399
class DeployOperation(InstanceOperation):
Dudás Ádám committed
400 401
    id = 'deploy'
    name = _("deploy")
402 403
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
404
    required_perms = ()
405
    deny_states = ('SUSPENDED', 'RUNNING')
406
    resultant_state = 'RUNNING'
Dudás Ádám committed
407

408 409
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
410
                                        self.instance.STATUS.PENDING,
411 412
                                        self.instance.STATUS.ERROR)

413 414 415
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
416 417
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
418
        activity.result = create_readable(
Guba Sándor committed
419
            ugettext_noop("virtual machine successfully "
420 421
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
422

423
    def _operation(self, activity, node=None):
Dudás Ádám committed
424 425
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
426 427 428 429 430
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
431 432

        # Deploy virtual images
433 434 435 436 437 438
        try:
            self.instance._deploy_disks(parent_activity=activity)
        except:
            self.instance.yield_node()
            self.instance.yield_vnc_port()
            raise
Dudás Ádám committed
439 440

        # Deploy VM on remote machine
441
        if self.instance.state not in ['PAUSED']:
442
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
443 444

        # Establish network connection (vmdriver)
445
        with activity.sub_activity(
Bálint Máhonfai committed
446 447
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
448 449
            self.instance.deploy_net()

450 451 452 453 454
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

457 458 459
        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
460

461
    @register_operation
Őry Máté committed
462
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
463 464
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
465
        description = _("Deploy virtual machine.")
466 467
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy
Bálint Máhonfai committed
468 469
        remote_timeout = 120

Őry Máté committed
470
        def _get_remote_args(self, **kwargs):
471 472 473 474 475 476 477 478 479
            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
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
    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()

497
    @register_operation
Őry Máté committed
498
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
499 500 501 502 503
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
504

505
@register_operation
506
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
507 508
    id = 'destroy'
    name = _("destroy")
509 510
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
511
    required_perms = ()
512
    resultant_state = 'DESTROYED'
Dudás Ádám committed
513

514 515 516
    def on_abort(self, activity, error):
        activity.resultant_state = None

517
    def _operation(self, activity, system):
518
        # Destroy networks
519 520 521
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
522
            if self.instance.node:
523
                self.instance.shutdown_net()
524
            self.instance.destroy_net()
Dudás Ádám committed
525

526
        if self.instance.node:
527
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
528 529

        # Destroy disks
530 531 532
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
533
            self.instance.destroy_disks()
Dudás Ádám committed
534

Dudás Ádám committed
535 536
        # Delete mem. dump if exists
        try:
537
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
538 539 540 541 542 543
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
544 545 546 547

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

548
    @register_operation
549
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
550 551 552
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
553
        remote_timeout = 120
554 555
        # if e.libvirtError and "Domain not found" in str(e):

556 557 558 559 560 561 562 563 564 565 566 567 568 569
    @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
570

571
@register_operation
572
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
573 574
    id = 'migrate'
    name = _("migrate")
575 576
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
577
    required_perms = ()
578
    superuser_required = True
Bálint Máhonfai committed
579
    accept_states = ('RUNNING',)
580
    async_queue = "localhost.man.slow"
581 582
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
583
    remote_timeout = 1000
584

585
    def _get_remote_args(self, to_node, live_migration, **kwargs):
586 587
        return (super(MigrateOperation, self)._get_remote_args(**kwargs) +
                [to_node.host.hostname, live_migration])
Dudás Ádám committed
588

589
    def rollback(self, activity):
590
        with activity.sub_activity(
Bálint Máhonfai committed
591 592
                'rollback_net', readable_name=ugettext_noop(
                    "redeploy network (rollback)")):
593 594
            self.instance.deploy_net()

595
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
596
        if not to_node:
Bach Dániel committed
597 598 599 600 601 602
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

603
        try:
604
            with activity.sub_activity(
Bálint Máhonfai committed
605 606
                    'migrate_vm', readable_name=create_readable(
                        ugettext_noop("migrate to %(node)s"), node=to_node)):
607 608
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
609 610 611
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
612
            raise
Dudás Ádám committed
613

614
        # Shutdown networks
615
        with activity.sub_activity(
Bálint Máhonfai committed
616 617
                'shutdown_net', readable_name=ugettext_noop(
                    "shutdown network")):
618 619
            self.instance.shutdown_net()

Dudás Ádám committed
620 621 622
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
623

Dudás Ádám committed
624
        # Estabilish network connection (vmdriver)
625
        with activity.sub_activity(
Bálint Máhonfai committed
626 627
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
628
            self.instance.deploy_net()
Dudás Ádám committed
629 630


631
@register_operation
632
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
633 634
    id = 'reboot'
    name = _("reboot")
635 636
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
637
    required_perms = ()
Bálint Máhonfai committed
638
    accept_states = ('RUNNING',)
639
    task = vm_tasks.reboot
Dudás Ádám committed
640

641 642
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
643 644 645
        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
646 647


648
@register_operation
649 650 651
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
652 653 654
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
655
    required_perms = ()
656
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
657

658 659
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
660 661
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
662 663 664 665 666
            interface.shutdown()

        interface.destroy()
        interface.delete()

667 668
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
669
                               vlan=kwargs['interface'].vlan)
670

671

672
@register_operation
673 674 675 676 677
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
678
    acl_level = "operator"
Bálint Máhonfai committed
679
    required_perms = ('vm.config_ports',)
680 681 682 683

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
684
            raise SuspiciousOperation()
685 686 687 688 689 690 691
        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
692 693 694 695 696
class AddPortOperation(InstanceOperation):
    id = 'add_port'
    name = _("open port")
    description = _("Open the specified port.")
    concurrency_check = False
697
    acl_level = "operator"
Bálint Máhonfai committed
698
    required_perms = ('vm.config_ports',)
699 700 701

    def _operation(self, activity, host, proto, port):
        if host.interface_set.get().instance != self.instance:
702
            raise SuspiciousOperation()
703 704 705 706 707 708 709
        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
710 711 712
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
713 714
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
715
    required_perms = ()
716
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
717 718

    def _operation(self, activity, user, system, disk):
719
        if self.instance.is_running and disk.type not in ["iso"]:
720
            self.instance._detach_disk(disk=disk, parent_activity=activity)
721
        with activity.sub_activity(
Bálint Máhonfai committed
722 723
                'destroy_disk',
                readable_name=ugettext_noop('destroy disk')
724
        ):
725
            disk.destroy()
726
            return self.instance.disks.remove(disk)
727

728 729 730
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
731 732


733
@register_operation
734
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
735 736
    id = 'reset'
    name = _("reset")
737
    description = _("Cold reboot virtual machine (power cycle).")
738
    required_perms = ()
Bálint Máhonfai committed
739
    accept_states = ('RUNNING',)
740
    task = vm_tasks.reset
Dudás Ádám committed
741

742 743
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
744 745 746
        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
747 748


749
@register_operation
750
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
751 752
    id = 'save_as_template'
    name = _("save as template")
753 754 755 756
    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.")
757
    has_percentage = True
758
    abortable = True
Bálint Máhonfai committed
759
    required_perms = ('vm.create_template',)
760
    accept_states = ('RUNNING', 'STOPPED')
761
    async_queue = "localhost.man.slow"
Dudás Ádám committed
762

763 764 765 766
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

767 768 769 770 771 772
    @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)
773
        else:
774 775
            v = 1
        return "%s v%d" % (name, v)
776

777
    def on_abort(self, activity, error):
778
        if hasattr(self, 'disks'):
779 780 781
            for disk in self.disks:
                disk.destroy()

782
    def _operation(self, activity, user, system, name=None,
783
                   with_shutdown=True, clone=False, task=None, **kwargs):
784 785
        try:
            self.instance._cleanup(parent_activity=activity, user=user)
786
        except:
787 788
            pass

789
        if with_shutdown:
790
            try:
791 792
                self.instance.shutdown(parent_activity=activity,
                                       user=user, task=task)
793 794 795
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
796 797 798 799 800 801 802 803
        # 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,
804
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
805 806
            'num_cores': self.instance.num_cores,
            'owner': user,
807
            'parent': self.instance.template or None,  # Can be problem
Dudás Ádám committed
808 809 810 811 812 813
            '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
814
        params.pop("parent_activity", None)
Dudás Ádám committed
815

816 817
        from storage.models import Disk

Dudás Ádám committed
818 819
        def __try_save_disk(disk):
            try:
820
                return disk.save_as(task)
Dudás Ádám committed
821 822 823
            except Disk.WrongDiskTypeError:
                return disk

824
        self.disks = []
825 826
        for disk in self.instance.disks.all():
            with activity.sub_activity(
Bálint Máhonfai committed
827 828 829 830
                    'saving_disk',
                    readable_name=create_readable(
                        ugettext_noop("saving disk %(name)s"),
                        name=disk.name)
831
            ):
832 833
                self.disks.append(__try_save_disk(disk))

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


860
@register_operation
861 862
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
863 864
    id = 'shutdown'
    name = _("shutdown")
865 866 867 868
    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
869
    abortable = True
870
    required_perms = ()
Bálint Máhonfai committed
871
    accept_states = ('RUNNING',)
872
    resultant_state = 'STOPPED'
873 874
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
875
    remote_timeout = 180
Dudás Ádám committed
876

877 878
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
879
        self.instance.yield_node()
Dudás Ádám committed
880

881 882 883 884 885 886 887 888 889 890 891
    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
892

893
@register_operation
894
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
895 896
    id = 'shut_off'
    name = _("shut off")
897 898 899 900 901 902 903
    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.")
904
    required_perms = ()
905
    accept_states = ('RUNNING', 'PAUSED')
906
    resultant_state = 'STOPPED'
Dudás Ádám committed
907

908
    def _operation(self, activity):
Dudás Ádám committed
909
        # Shutdown networks
910 911 912
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
913
            self.instance.shutdown_net()
Dudás Ádám committed
914

915
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
916
        self.instance.yield_node()
Dudás Ádám committed
917 918


919
@register_operation
920
class SleepOperation(InstanceOperation):
Dudás Ádám committed
921 922
    id = 'sleep'
    name = _("sleep")
923 924 925 926 927 928 929 930
    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.")
931
    required_perms = ()
Bálint Máhonfai committed
932
    accept_states = ('RUNNING',)
933
    resultant_state = 'SUSPENDED'
934
    async_queue = "localhost.man.slow"
Dudás Ádám committed
935

936 937 938 939
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
940 941 942 943 944 945
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

946
    def _operation(self, activity, system):
947 948 949
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
950
            self.instance.shutdown_net()
951
        self.instance._suspend_vm(parent_activity=activity)
952
        self.instance.yield_node()
Dudás Ádám committed
953

954 955 956 957
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
958
        task = vm_tasks.sleep
959
        remote_queue = ("vm", "slow")
960
        remote_timeout = 1000
Dudás Ádám committed
961

962 963
        def _get_remote_args(self, **kwargs):
            return (super(SleepOperation.SuspendVmOperation, self)
964 965
                    ._get_remote_args(**kwargs) +
                    [self.instance.mem_dump['path']])
Dudás Ádám committed
966 967


968
@register_operation
969
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
970 971
    id = 'wake_up'
    name = _("wake up")
972 973 974
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
975
    required_perms = ()
Bálint Máhonfai committed
976
    accept_states = ('SUSPENDED',)
977
    resultant_state = 'RUNNING'
978
    async_queue = "localhost.man.slow"
Dudás Ádám committed
979

980
    def is_preferred(self):
981
        return self.instance.status == self.instance.STATUS.SUSPENDED
982

Dudás Ádám committed
983
    def on_abort(self, activity, error):
Bach Dániel committed
984 985 986 987
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
988

989
    def _operation(self, activity):
Dudás Ádám committed
990
        # Schedule vm
Dudás Ádám committed
991
        self.instance.allocate_vnc_port()
992
        self.instance.allocate_node()
Dudás Ádám committed
993 994

        # Resume vm
995
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
996 997

        # Estabilish network connection (vmdriver)
998
        with activity.sub_activity(
Bálint Máhonfai committed
999 1000
                'deploying_net', readable_name=ugettext_noop(
                    "deploy network")):
Dudás Ádám committed
1001
            self.instance.deploy_net()
Dudás Ádám committed
1002

1003 1004 1005 1006
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
1007

1008 1009 1010 1011 1012 1013
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
1014
        remote_timeout = 1000
1015 1016 1017

        def _get_remote_args(self, **kwargs):
            return (super(WakeUpOperation.WakeUpVmOperation, self)
1018 1019
                    ._get_remote_args(**kwargs) +
                    [self.instance.mem_dump['path']])
1020

Dudás Ádám committed
1021

1022
@register_operation
1023 1024 1025
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
1026 1027 1028 1029
    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.")
1030
    acl_level = "operator"
1031
    required_perms = ()
1032
    concurrency_check = False
1033

1034 1035
    def set_time_of_suspend(self, activity, suspend, force):
        with activity.sub_activity(
Bálint Máhonfai committed
1036
                'renew_suspend', concurrency_check=False,
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046
                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
1047
                'renew_delete', concurrency_check=False,
1048 1049 1050 1051 1052 1053 1054 1055
                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
1056
    def _operation(self, activity, lease=None, force=False, save=False):
1057
        suspend, delete = self.instance.get_renew_times(lease)
1058 1059 1060 1061 1062 1063 1064 1065 1066
        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
1067 1068
        if save:
            self.instance.lease = lease
1069

1070
        self.instance.save()