operations.py 60.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
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
Dudás Ádám committed
31
from django.utils import timezone
32
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
33
from django.conf import settings
Bach Dániel committed
34
from django.db.models import Q
Dudás Ádám committed
35

36 37
from sizefield.utils import filesizeformat

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

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

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

Dudás Ádám committed
60
logger = getLogger(__name__)
61 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
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
137 138 139 140 141 142 143 144 145 146
        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.")

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

158 159 160 161 162
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

163

164 165 166 167 168 169 170 171 172 173 174
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
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
class EnsureAgentMixin(object):
    accept_states = ('RUNNING', )

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

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

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


class RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation):
    remote_queue = ('agent', )
    concurrency_check = False


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

208 209 210 211 212 213 214
    def rollback(self, net, activity):
        with activity.sub_activity(
            'destroying_net',
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

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

238 239 240 241
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

242

243
@register_operation
244
class CreateDiskOperation(InstanceOperation):
245

246 247
    id = 'create_disk'
    name = _("create disk")
248
    description = _("Create and attach empty disk to the virtual machine.")
249
    required_perms = ('storage.create_empty_disk', )
250
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
251

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

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

266
        if self.instance.is_running:
267 268 269 270
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
271
                disk.deploy()
272
            self.instance._attach_disk(parent_activity=activity, disk=disk)
273

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


280
@register_operation
281
class ResizeDiskOperation(RemoteInstanceOperation):
282 283 284 285 286

    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
287
    required_perms = ('storage.resize_disk', )
288 289
    accept_states = ('RUNNING', )
    async_queue = "localhost.man.slow"
290 291
    remote_queue = ('vm', 'slow')
    task = vm_tasks.resize_disk
292

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

    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)

302 303 304 305 306
    def _operation(self, disk, size):
        super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
        disk.size = size
        disk.save()

307

308
@register_operation
309 310 311
class DownloadDiskOperation(InstanceOperation):
    id = 'download_disk'
    name = _("download disk")
312 313 314 315
    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.")
316
    abortable = True
317
    has_percentage = True
318
    required_perms = ('storage.download_disk', )
319
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
320
    async_queue = "localhost.man.slow"
321

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

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

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

344

345
@register_operation
346
class DeployOperation(InstanceOperation):
Dudás Ádám committed
347 348
    id = 'deploy'
    name = _("deploy")
349 350
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
351
    required_perms = ()
352
    deny_states = ('SUSPENDED', 'RUNNING')
353
    resultant_state = 'RUNNING'
Dudás Ádám committed
354

355 356
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
357
                                        self.instance.STATUS.PENDING,
358 359
                                        self.instance.STATUS.ERROR)

360 361 362
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
363 364
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
365
        activity.result = create_readable(
Guba Sándor committed
366
            ugettext_noop("virtual machine successfully "
367 368
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
369

370
    def _operation(self, activity, node=None):
Dudás Ádám committed
371 372
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
373 374 375 376 377
        if node is not None:
            self.instance.node = node
            self.instance.save()
        else:
            self.instance.allocate_node()
Dudás Ádám committed
378 379

        # Deploy virtual images
380 381 382 383 384 385
        try:
            self.instance._deploy_disks(parent_activity=activity)
        except:
            self.instance.yield_node()
            self.instance.yield_vnc_port()
            raise
Dudás Ádám committed
386 387

        # Deploy VM on remote machine
388
        if self.instance.state not in ['PAUSED']:
389
            self.instance._deploy_vm(parent_activity=activity)
Dudás Ádám committed
390 391

        # Establish network connection (vmdriver)
392 393 394
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
395 396
            self.instance.deploy_net()

397 398 399 400 401
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

404 405 406
        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
407

408
    @register_operation
Őry Máté committed
409
    class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation):
410 411
        id = "_deploy_vm"
        name = _("deploy vm")
Őry Máté committed
412
        description = _("Deploy virtual machine.")
413 414 415
        remote_queue = ("vm", "slow")
        task = vm_tasks.deploy

Őry Máté committed
416
        def _get_remote_args(self, **kwargs):
417 418 419 420 421 422 423 424 425
            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
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
    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()

443
    @register_operation
Őry Máté committed
444
    class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation):
445 446 447 448 449
        id = "_resume_vm"
        name = _("boot virtual machine")
        remote_queue = ("vm", "slow")
        task = vm_tasks.resume

Dudás Ádám committed
450

451
@register_operation
452
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
453 454
    id = 'destroy'
    name = _("destroy")
455 456
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
457
    required_perms = ()
458
    resultant_state = 'DESTROYED'
Dudás Ádám committed
459

460 461 462
    def on_abort(self, activity, error):
        activity.resultant_state = None

463
    def _operation(self, activity, system):
464
        # Destroy networks
465 466 467
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
468
            if self.instance.node:
469
                self.instance.shutdown_net()
470
            self.instance.destroy_net()
Dudás Ádám committed
471

472
        if self.instance.node:
473
            self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
474 475

        # Destroy disks
476 477 478
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
479
            self.instance.destroy_disks()
Dudás Ádám committed
480

Dudás Ádám committed
481 482
        # Delete mem. dump if exists
        try:
483
            self.instance._delete_mem_dump(parent_activity=activity)
Dudás Ádám committed
484 485 486 487 488 489
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
490 491 492 493

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

494
    @register_operation
495
    class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation):
496 497 498 499 500
        id = "_delete_vm"
        name = _("destroy virtual machine")
        task = vm_tasks.destroy
        # if e.libvirtError and "Domain not found" in str(e):

501 502 503 504 505 506 507 508 509 510 511 512 513 514
    @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
515

516
@register_operation
517
class MigrateOperation(RemoteInstanceOperation):
Dudás Ádám committed
518 519
    id = 'migrate'
    name = _("migrate")
520 521
    description = _("Move a running virtual machine to an other worker node "
                    "keeping its full state.")
522
    required_perms = ()
523
    superuser_required = True
524
    accept_states = ('RUNNING', )
525
    async_queue = "localhost.man.slow"
526 527
    task = vm_tasks.migrate
    remote_queue = ("vm", "slow")
528
    remote_timeout = 1000
529

530
    def _get_remote_args(self, to_node, live_migration, **kwargs):
531
        return (super(MigrateOperation, self)._get_remote_args(**kwargs)
532
                + [to_node.host.hostname, live_migration])
Dudás Ádám committed
533

534
    def rollback(self, activity):
535 536 537
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
538 539
            self.instance.deploy_net()

540
    def _operation(self, activity, to_node=None, live_migration=True):
Dudás Ádám committed
541
        if not to_node:
Bach Dániel committed
542 543 544 545 546 547
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

548
        try:
549 550 551
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
552 553
                super(MigrateOperation, self)._operation(
                    to_node=to_node, live_migration=live_migration)
554 555 556
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
557
            raise
Dudás Ádám committed
558

559
        # Shutdown networks
560 561 562
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
563 564
            self.instance.shutdown_net()

Dudás Ádám committed
565 566 567
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
568

Dudás Ádám committed
569
        # Estabilish network connection (vmdriver)
570 571 572
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
573
            self.instance.deploy_net()
Dudás Ádám committed
574 575


576
@register_operation
577
class RebootOperation(RemoteInstanceOperation):
Dudás Ádám committed
578 579
    id = 'reboot'
    name = _("reboot")
580 581
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
582
    required_perms = ()
583
    accept_states = ('RUNNING', )
584
    task = vm_tasks.reboot
Dudás Ádám committed
585

586 587
    def _operation(self, activity):
        super(RebootOperation, self)._operation()
588 589 590
        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
591 592


593
@register_operation
594 595 596
class RemoveInterfaceOperation(InstanceOperation):
    id = 'remove_interface'
    name = _("remove interface")
597 598 599
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
600
    required_perms = ()
601
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
602

603 604
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
605 606
            self.instance._detach_network(interface=interface,
                                          parent_activity=activity)
607 608 609 610 611
            interface.shutdown()

        interface.destroy()
        interface.delete()

612 613
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
614
                               vlan=kwargs['interface'].vlan)
615

616

617
@register_operation
618 619 620 621 622
class RemovePortOperation(InstanceOperation):
    id = 'remove_port'
    name = _("close port")
    description = _("Close the specified port.")
    concurrency_check = False
623
    required_perms = ('vm.config_ports', )
624 625 626 627

    def _operation(self, activity, rule):
        interface = rule.host.interface_set.get()
        if interface.instance != self.instance:
628
            raise SuspiciousOperation()
629 630 631 632 633 634 635
        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
636 637 638 639 640
class AddPortOperation(InstanceOperation):
    id = 'add_port'
    name = _("open port")
    description = _("Open the specified port.")
    concurrency_check = False
641
    required_perms = ('vm.config_ports', )
642 643 644

    def _operation(self, activity, host, proto, port):
        if host.interface_set.get().instance != self.instance:
645
            raise SuspiciousOperation()
646 647 648 649 650 651 652
        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
653 654 655
class RemoveDiskOperation(InstanceOperation):
    id = 'remove_disk'
    name = _("remove disk")
656 657
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
658
    required_perms = ()
659
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
660 661

    def _operation(self, activity, user, system, disk):
662
        if self.instance.is_running and disk.type not in ["iso"]:
663
            self.instance._detach_disk(disk=disk, parent_activity=activity)
664 665 666 667
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
668
            disk.destroy()
669
            return self.instance.disks.remove(disk)
670

671 672 673
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
674 675


676
@register_operation
677
class ResetOperation(RemoteInstanceOperation):
Dudás Ádám committed
678 679
    id = 'reset'
    name = _("reset")
680
    description = _("Cold reboot virtual machine (power cycle).")
681
    required_perms = ()
682
    accept_states = ('RUNNING', )
683
    task = vm_tasks.reset
Dudás Ádám committed
684

685 686
    def _operation(self, activity):
        super(ResetOperation, self)._operation()
687 688 689
        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
690 691


692
@register_operation
693
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
694 695
    id = 'save_as_template'
    name = _("save as template")
696 697 698 699
    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.")
700
    has_percentage = True
701
    abortable = True
702
    required_perms = ('vm.create_template', )
703
    accept_states = ('RUNNING', 'STOPPED')
704
    async_queue = "localhost.man.slow"
Dudás Ádám committed
705

706 707 708 709
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

710 711 712 713 714 715
    @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)
716
        else:
717 718
            v = 1
        return "%s v%d" % (name, v)
719

720
    def on_abort(self, activity, error):
721
        if hasattr(self, 'disks'):
722 723 724
            for disk in self.disks:
                disk.destroy()

725
    def _operation(self, activity, user, system, name=None,
726
                   with_shutdown=True, clone=False, task=None, **kwargs):
727 728
        try:
            self.instance._cleanup(parent_activity=activity, user=user)
729
        except:
730 731
            pass

732
        if with_shutdown:
733
            try:
734 735
                self.instance.shutdown(parent_activity=activity,
                                       user=user, task=task)
736 737 738
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
739 740 741 742 743 744 745 746
        # 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,
747
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
748 749
            'num_cores': self.instance.num_cores,
            'owner': user,
750
            'parent': self.instance.template or None,  # Can be problem
Dudás Ádám committed
751 752 753 754 755 756
            '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
757
        params.pop("parent_activity", None)
Dudás Ádám committed
758

759 760
        from storage.models import Disk

Dudás Ádám committed
761 762
        def __try_save_disk(disk):
            try:
763
                return disk.save_as(task)
Dudás Ádám committed
764 765 766
            except Disk.WrongDiskTypeError:
                return disk

767
        self.disks = []
768 769 770 771 772 773 774
        for disk in self.instance.disks.all():
            with activity.sub_activity(
                'saving_disk',
                readable_name=create_readable(
                    ugettext_noop("saving disk %(name)s"),
                    name=disk.name)
            ):
775 776
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
777 778 779 780
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
781
        # Copy traits from the VM instance
782
        tmpl.req_traits.add(*self.instance.req_traits.all())
783 784
        if clone:
            tmpl.clone_acl(self.instance.template)
Guba Sándor committed
785
            # Add permission for the original owner of the template
786 787
            tmpl.set_level(self.instance.template.owner, 'owner')
            tmpl.set_level(user, 'owner')
Dudás Ádám committed
788
        try:
789
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
790 791 792 793 794 795 796 797 798 799
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


800
@register_operation
801 802
class ShutdownOperation(AbortableRemoteOperationMixin,
                        RemoteInstanceOperation):
Dudás Ádám committed
803 804
    id = 'shutdown'
    name = _("shutdown")
805 806 807 808
    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
809
    abortable = True
810
    required_perms = ()
811
    accept_states = ('RUNNING', )
812
    resultant_state = 'STOPPED'
813 814
    task = vm_tasks.shutdown
    remote_queue = ("vm", "slow")
815
    remote_timeout = 180
Dudás Ádám committed
816

817 818
    def _operation(self, task):
        super(ShutdownOperation, self)._operation(task=task)
Dudás Ádám committed
819
        self.instance.yield_node()
Dudás Ádám committed
820

821 822 823 824 825 826 827 828 829 830 831
    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
832

833
@register_operation
834
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
835 836
    id = 'shut_off'
    name = _("shut off")
837 838 839 840 841 842 843
    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.")
844
    required_perms = ()
845
    accept_states = ('RUNNING', 'PAUSED')
846
    resultant_state = 'STOPPED'
Dudás Ádám committed
847

848
    def _operation(self, activity):
Dudás Ádám committed
849 850 851
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
852

853
        self.instance._delete_vm(parent_activity=activity)
Dudás Ádám committed
854
        self.instance.yield_node()
Dudás Ádám committed
855 856


857
@register_operation
858
class SleepOperation(InstanceOperation):
Dudás Ádám committed
859 860
    id = 'sleep'
    name = _("sleep")
861 862 863 864 865 866 867 868
    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.")
869
    required_perms = ()
870
    accept_states = ('RUNNING', )
871
    resultant_state = 'SUSPENDED'
872
    async_queue = "localhost.man.slow"
Dudás Ádám committed
873

874 875 876 877
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
878 879 880 881 882 883
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

884
    def _operation(self, activity, system):
885 886 887
        with activity.sub_activity('shutdown_net',
                                   readable_name=ugettext_noop(
                                       "shutdown network")):
Dudás Ádám committed
888
            self.instance.shutdown_net()
889
        self.instance._suspend_vm(parent_activity=activity)
890
        self.instance.yield_node()
Dudás Ádám committed
891

892 893 894 895
    @register_operation
    class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_suspend_vm"
        name = _("suspend virtual machine")
896
        task = vm_tasks.sleep
897
        remote_queue = ("vm", "slow")
898
        remote_timeout = 1000
Dudás Ádám committed
899

900 901 902 903
        def _get_remote_args(self, **kwargs):
            return (super(SleepOperation.SuspendVmOperation, self)
                    ._get_remote_args(**kwargs)
                    + [self.instance.mem_dump['path']])
Dudás Ádám committed
904 905


906
@register_operation
907
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
908 909
    id = 'wake_up'
    name = _("wake up")
910 911 912
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
913
    required_perms = ()
914
    accept_states = ('SUSPENDED', )
915
    resultant_state = 'RUNNING'
916
    async_queue = "localhost.man.slow"
Dudás Ádám committed
917

918
    def is_preferred(self):
919
        return self.instance.status == self.instance.STATUS.SUSPENDED
920

Dudás Ádám committed
921
    def on_abort(self, activity, error):
Bach Dániel committed
922 923 924 925
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
926

927
    def _operation(self, activity):
Dudás Ádám committed
928
        # Schedule vm
Dudás Ádám committed
929
        self.instance.allocate_vnc_port()
930
        self.instance.allocate_node()
Dudás Ádám committed
931 932

        # Resume vm
933
        self.instance._wake_up_vm(parent_activity=activity)
Dudás Ádám committed
934 935

        # Estabilish network connection (vmdriver)
936 937 938
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
939
            self.instance.deploy_net()
Dudás Ádám committed
940

941 942 943 944
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
945

946 947 948 949 950 951
    @register_operation
    class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation):
        id = "_wake_up_vm"
        name = _("resume virtual machine")
        task = vm_tasks.wake_up
        remote_queue = ("vm", "slow")
952
        remote_timeout = 1000
953 954 955 956 957 958

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

Dudás Ádám committed
959

960
@register_operation
961 962 963
class RenewOperation(InstanceOperation):
    id = 'renew'
    name = _("renew")
964 965 966 967
    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.")
968
    acl_level = "operator"
969
    required_perms = ()
970
    concurrency_check = False
971

Őry Máté committed
972
    def _operation(self, activity, lease=None, force=False, save=False):
973 974 975 976 977 978 979 980 981 982 983 984 985
        suspend, delete = self.instance.get_renew_times(lease)
        if (not force and suspend and self.instance.time_of_suspend and
                suspend < self.instance.time_of_suspend):
            raise HumanReadableException.create(ugettext_noop(
                "Renewing the machine with the selected lease would result "
                "in its suspension time get earlier than before."))
        if (not force and delete and self.instance.time_of_delete and
                delete < self.instance.time_of_delete):
            raise HumanReadableException.create(ugettext_noop(
                "Renewing the machine with the selected lease would result "
                "in its delete time get earlier than before."))
        self.instance.time_of_suspend = suspend
        self.instance.time_of_delete = delete
Őry Máté committed
986 987
        if save:
            self.instance.lease = lease
988
        self.instance.save()
989 990 991
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
992 993


994
@register_operation
995
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
996
    id = 'emergency_change_state'
997 998 999 1000 1001 1002
    name = _("emergency state change")
    description = _("Change the virtual machine state to NOSTATE. This "
                    "should only be used if manual intervention was needed in "
                    "the virtualization layer, and the machine has to be "
                    "redeployed without losing its storage and network "
                    "resources.")
1003
    acl_level = "owner"
Guba Sándor committed
1004
    required_perms = ('vm.emergency_change_state', )
1005
    concurrency_check = False
1006

1007 1008
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
                   reset_node=False):
1009
        activity.resultant_state = new_state
1010 1011 1012 1013 1014 1015 1016
        if interrupt:
            msg_txt = ugettext_noop("Activity is forcibly interrupted.")
            message = create_readable(msg_txt, msg_txt)
            for i in InstanceActivity.objects.filter(
                    finished__isnull=True, instance=self.instance):
                i.finish(False, result=message)
                logger.error('Forced finishing activity %s', i)
1017

1018 1019 1020 1021
        if reset_node:
            self.instance.node = None
            self.instance.save()

1022

1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
@register_operation
class RedeployOperation(InstanceOperation):
    id = 'redeploy'
    name = _("redeploy")
    description = _("Change the virtual machine state to NOSTATE "
                    "and redeploy the VM. This operation allows starting "
                    "machines formerly running on a failed node.")
    acl_level = "owner"
    required_perms = ('vm.redeploy', )
    concurrency_check = False

    def _operation(self, user, activity, with_emergency_change_state=True):
        if with_emergency_change_state:
            ChangeStateOperation(self.instance).call(
                parent_activity=activity, user=user,
                new_state='NOSTATE', interrupt=False, reset_node=True)
        else:
            ShutOffOperation(self.instance).call(
                parent_activity=activity, user=user)

        self.instance._update_status()

        DeployOperation(self.instance).call(
            parent_activity=activity, user=user)

1048

1049
class NodeOperation(Operation):
1050
    async_operation = abortable_async_node_operation
1051
    host_cls = Node
1052 1053
    online_required = True
    superuser_required = True
1054 1055 1056 1057 1058

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

1059 1060 1061 1062 1063 1064 1065
    def check_precond(self):
        super(NodeOperation, self).check_precond()
        if self.online_required and not self.node.online:
            raise humanize_exception(ugettext_noop(
                "You cannot call this operation on an offline node."),
                Exception())

1066 1067
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
        if parent:
            if parent.node != self.node:
                raise ValueError("The node associated with the specified "
                                 "parent activity does not match the node "
                                 "bound to the operation.")
            if parent.user != user:
                raise ValueError("The user associated with the specified "
                                 "parent activity does not match the user "
                                 "provided as parameter.")

1078 1079 1080
            return parent.create_sub(
                code_suffix=self.get_activity_code_suffix(),
                readable_name=name)
1081
        else:
1082 1083 1084
            return NodeActivity.create(
                code_suffix=self.get_activity_code_suffix(), node=self.node,
                user=user, readable_name=name)
1085 1086


1087
@register_operation
1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
class ResetNodeOperation(NodeOperation):
    id = 'reset'
    name = _("reset")
    description = _("Disable missing node and redeploy all instances "
                    "on other ones.")
    required_perms = ()
    online_required = False
    async_queue = "localhost.man.slow"

    def check_precond(self):
        super(ResetNodeOperation, self).check_precond()
        if not self.node.enabled or self.node.online:
            raise humanize_exception(ugettext_noop(
                "You cannot reset a disabled or online node."), Exception())

    def _operation(self, activity, user):
        for i in self.node.instance_set.all():
            name = create_readable(ugettext_noop(
1106
                "redeploy %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
1107 1108 1109 1110
            with activity.sub_activity('migrate_instance_%d' % i.pk,
                                       readable_name=name):
                i.redeploy(user=user)

1111 1112 1113 1114
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()

1115 1116

@register_operation
1117 1118 1119
class FlushOperation(NodeOperation):
    id = 'flush'
    name = _("flush")
1120
    description = _("Passivate node and move all instances to other ones.")
1121
    required_perms = ()
1122
    async_queue = "localhost.man.slow"
1123

1124
    def _operation(self, activity, user):
1125 1126 1127
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
1128
        for i in self.node.instance_set.all():
1129 1130 1131 1132
            name = create_readable(ugettext_noop(
                "migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
            with activity.sub_activity('migrate_instance_%d' % i.pk,
                                       readable_name=name):
Bach Dániel committed
1133
                i.migrate(user=user)
1134 1135


1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
@register_operation
class ActivateOperation(NodeOperation):
    id = 'activate'
    name = _("activate")
    description = _("Make node active, i.e. scheduler is allowed to deploy "
                    "virtual machines to it.")
    required_perms = ()

    def check_precond(self):
        super(ActivateOperation, self).check_precond()
        if self.node.enabled and self.node.schedule_enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot activate an active node."), Exception())

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = True
1153
        self.node.get_info(invalidate_cache=True)
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174
        self.node.save()


@register_operation
class PassivateOperation(NodeOperation):
    id = 'passivate'
    name = _("passivate")
    description = _("Make node passive, i.e. scheduler is denied to deploy "
                    "virtual machines to it, but remaining instances and "
                    "the ones manually migrated will continue running.")
    required_perms = ()

    def check_precond(self):
        if self.node.enabled and not self.node.schedule_enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot passivate a passive node."), Exception())
        super(PassivateOperation, self).check_precond()

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = False
1175
        self.node.get_info(invalidate_cache=True)
1176 1177 1178 1179 1180 1181 1182 1183 1184 1185
        self.node.save()


@register_operation
class DisableOperation(NodeOperation):
    id = 'disable'
    name = _("disable")
    description = _("Disable node.")
    required_perms = ()
    online_required = False
1186

1187 1188 1189 1190 1191 1192 1193 1194 1195
    def check_precond(self):
        if not self.node.enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot disable a disabled node."), Exception())
        if self.node.instance_set.exists():
            raise humanize_exception(ugettext_noop(
                "You cannot disable a node which is hosting instances."),
                Exception())
        super(DisableOperation, self).check_precond()
1196

1197 1198 1199 1200
    def _operation(self):
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()
1201 1202


1203
@register_operation
1204 1205 1206
class UpdateNodeOperation(NodeOperation):
    id = 'update_node'
    name = _("update node")
1207 1208
    description = _("Upgrade or install node software (vmdriver, agentdriver, "
                    "monitor-client) with Salt.")
1209 1210 1211 1212 1213
    required_perms = ()
    online_required = False
    async_queue = "localhost.man.slow"

    def minion_cmd(self, module, params, timeout=3600):
1214 1215
        # see https://git.ik.bme.hu/circle/cloud/issues/377
        from salt.client import LocalClient
1216 1217 1218
        name = self.node.host.hostname
        client = LocalClient()
        data = client.cmd(
1219 1220
            name, module, params, timeout=timeout)

1221
        try:
1222
            data = data[name]
1223 1224
        except KeyError:
            raise HumanReadableException.create(ugettext_noop(
1225 1226
                "No minions matched the target (%(target)s). "
                "Data: (%(data)s)"), target=name, data=data)
1227

1228 1229 1230 1231 1232 1233
        if not isinstance(data, dict):
            raise HumanReadableException.create(ugettext_noop(
                "Unhandled exception: %(msg)s"), msg=unicode(data))

        return data

1234 1235 1236 1237 1238
    def _operation(self, activity):
        with activity.sub_activity(
                'upgrade_packages',
                readable_name=ugettext_noop('upgrade packages')) as sa:
            data = self.minion_cmd('pkg.upgrade', [])
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250
            if not data.get('result'):
                raise HumanReadableException.create(ugettext_noop(
                    "Unhandled exception: %(msg)s"), msg=unicode(data))

            # data = {'vim': {'new': '1.2.7', 'old': '1.3.7'}}
            data = [v for v in data.values() if isinstance(v, dict)]
            upgraded = len([x for x in data
                            if x.get('old') and x.get('new')])
            installed = len([x for x in data
                             if not x.get('old') and x.get('new')])
            removed = len([x for x in data
                           if x.get('old') and not x.get('new')])
1251 1252 1253 1254 1255
            sa.result = create_readable(ugettext_noop(
                "Upgraded: %(upgraded)s, Installed: %(installed)s, "
                "Removed: %(removed)s"), upgraded=upgraded,
                installed=installed, removed=removed)

1256
        data = self.minion_cmd('state.sls', ['node'])
1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
        failed = 0
        for k, v in data.iteritems():
            logger.debug('salt state %s %s', k, v)
            act_name = ': '.join(k.split('_|-')[:2])
            if not v["result"] or v["changes"]:
                act = activity.create_sub(
                    act_name[:70], readable_name=act_name)
                act.result = create_readable(ugettext_noop(
                    "Changes: %(changes)s Comment: %(comment)s"),
                    changes=v["changes"], comment=v["comment"])
                act.finish(v["result"])
                if not v["result"]:
                    failed += 1

        if failed:
            raise HumanReadableException.create(ugettext_noop(
                "Failed: %(failed)s"), failed=failed)


@register_operation
1277
class ScreenshotOperation(RemoteInstanceOperation):
1278 1279
    id = 'screenshot'
    name = _("screenshot")
1280 1281 1282
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
1283
    acl_level = "owner"
1284
    required_perms = ()
1285
    accept_states = ('RUNNING', )
1286
    task = vm_tasks.screenshot
1287 1288


1289
@register_operation
Bach Dániel committed
1290 1291 1292
class RecoverOperation(InstanceOperation):
    id = 'recover'
    name = _("recover")
1293 1294 1295
    description = _("Try to recover virtual machine disks from destroyed "
                    "state. Network resources (allocations) are already lost, "
                    "so you will have to manually add interfaces afterwards.")
Bach Dániel committed
1296 1297
    acl_level = "owner"
    required_perms = ('vm.recover', )
1298
    accept_states = ('DESTROYED', )
1299
    resultant_state = 'PENDING'
Bach Dániel committed
1300 1301

    def check_precond(self):
1302 1303 1304 1305
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
1306

1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
    def _operation(self, user, activity):
        with activity.sub_activity(
            'recover_instance',
                readable_name=ugettext_noop("recover instance")):
            self.instance.destroyed_at = None
            for disk in self.instance.disks.all():
                disk.destroyed = None
                disk.restore()
                disk.save()
            self.instance.status = 'PENDING'
            self.instance.save()

        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

        if self.instance.template:
            for net in self.instance.template.interface_set.all():
                self.instance.add_interface(
                    parent_activity=activity, user=user, vlan=net.vlan)
Bach Dániel committed
1328 1329


1330
@register_operation
1331 1332 1333
class ResourcesOperation(InstanceOperation):
    id = 'resources_change'
    name = _("resources change")
1334
    description = _("Change resources of a stopped virtual machine.")
1335
    acl_level = "owner"
1336
    required_perms = ('vm.change_resources', )
1337
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
1338

1339
    def _operation(self, user, activity,
1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
                   num_cores, ram_size, max_ram_size, priority,
                   with_shutdown=False, task=None):
        if self.instance.status == 'RUNNING' and not with_shutdown:
            raise Instance.WrongStateError(self.instance)

        try:
            self.instance.shutdown(parent_activity=activity, task=task)
        except Instance.WrongStateError:
            pass

        self.instance._update_status()

1352 1353 1354 1355 1356
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

1357
        self.instance.full_clean()
1358 1359
        self.instance.save()

1360 1361 1362 1363 1364 1365
        activity.result = create_readable(ugettext_noop(
            "Priority: %(priority)s, Num cores: %(num_cores)s, "
            "Ram size: %(ram_size)s"), priority=priority, num_cores=num_cores,
            ram_size=ram_size
        )

1366

1367
@register_operation
Bach Dániel committed
1368
class PasswordResetOperation(RemoteAgentOperation):
1369 1370
    id = 'password_reset'
    name = _("password reset")
1371 1372 1373 1374 1375
    description = _("Generate and set a new login password on the virtual "
                    "machine. This operation requires the agent running. "
                    "Resetting the password is not warranted to allow you "
                    "logging in as other settings are possible to prevent "
                    "it.")
1376
    acl_level = "owner"
Bach Dániel committed
1377
    task = agent_tasks.change_password
1378 1379
    required_perms = ()

Bach Dániel committed
1380 1381 1382 1383 1384 1385 1386 1387 1388
    def _get_remote_args(self, password, **kwargs):
        return (super(PasswordResetOperation, self)._get_remote_args(**kwargs)
                + [password])

    def _operation(self, password=None):
        if not password:
            password = pwgen()
        super(PasswordResetOperation, self)._operation(password=password)
        self.instance.pw = password
1389
        self.instance.save()
1390 1391


1392
@register_operation
Bach Dániel committed
1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606
class AgentStartedOperation(InstanceOperation):
    id = 'agent_started'
    name = _("agent")
    acl_level = "owner"
    required_perms = ()
    concurrency_check = False

    @classmethod
    def get_activity_code_suffix(cls):
        return 'agent'

    @property
    def initialized(self):
        return self.instance.activity_log.filter(
            activity_code='vm.Instance.agent._cleanup').exists()

    def measure_boot_time(self):
        if not self.instance.template:
            return

        deploy_time = InstanceActivity.objects.filter(
            instance=self.instance, activity_code="vm.Instance.deploy"
        ).latest("finished").finished

        total_boot_time = (timezone.now() - deploy_time).total_seconds()

        Client().send([
            "template.%(pk)d.boot_time %(val)f %(time)s" % {
                'pk': self.instance.template.pk,
                'val': total_boot_time,
                'time': time.time(),
            }
        ])

    def finish_agent_wait(self):
        for i in InstanceActivity.objects.filter(
                (Q(activity_code__endswith='.os_boot') |
                 Q(activity_code__endswith='.agent_wait')),
                instance=self.instance, finished__isnull=True):
            i.finish(True)

    def _operation(self, user, activity, old_version=None, agent_system=None):
        with activity.sub_activity('starting', concurrency_check=False,
                                   readable_name=ugettext_noop('starting')):
            pass

        self.finish_agent_wait()

        self.instance._change_ip(parent_activity=activity)
        self.instance._restart_networking(parent_activity=activity)

        new_version = settings.AGENT_VERSION
        if new_version and old_version and new_version != old_version:
            try:
                self.instance.update_agent(
                    parent_activity=activity, agent_system=agent_system)
            except TimeoutError:
                pass
            else:
                activity.sub_activity(
                    'agent_wait', readable_name=ugettext_noop(
                        "wait agent restarting"), interruptible=True)
                return  # agent is going to restart

        if not self.initialized:
            try:
                self.measure_boot_time()
            except:
                logger.exception('Unhandled error in measure_boot_time()')
            self.instance._cleanup(parent_activity=activity)
            self.instance.password_reset(
                parent_activity=activity, password=self.instance.pw)
            self.instance._set_time(parent_activity=activity)
            self.instance._set_hostname(parent_activity=activity)

    @register_operation
    class CleanupOperation(SubOperationMixin, RemoteAgentOperation):
        id = '_cleanup'
        name = _("cleanup")
        task = agent_tasks.cleanup

    @register_operation
    class SetTimeOperation(SubOperationMixin, RemoteAgentOperation):
        id = '_set_time'
        name = _("set time")
        task = agent_tasks.set_time

        def _get_remote_args(self, **kwargs):
            cls = AgentStartedOperation.SetTimeOperation
            return (super(cls, self)._get_remote_args(**kwargs)
                    + [time.time()])

    @register_operation
    class SetHostnameOperation(SubOperationMixin, RemoteAgentOperation):
        id = '_set_hostname'
        name = _("set hostname")
        task = agent_tasks.set_hostname

        def _get_remote_args(self, **kwargs):
            cls = AgentStartedOperation.SetHostnameOperation
            return (super(cls, self)._get_remote_args(**kwargs)
                    + [self.instance.short_hostname])

    @register_operation
    class RestartNetworkingOperation(SubOperationMixin, RemoteAgentOperation):
        id = '_restart_networking'
        name = _("restart networking")
        task = agent_tasks.restart_networking

    @register_operation
    class ChangeIpOperation(SubOperationMixin, RemoteAgentOperation):
        id = '_change_ip'
        name = _("change ip")
        task = agent_tasks.change_ip

        def _get_remote_args(self, **kwargs):
            hosts = Host.objects.filter(interface__instance=self.instance)
            interfaces = {str(host.mac): host.get_network_config()
                          for host in hosts}
            cls = AgentStartedOperation.ChangeIpOperation
            return (super(cls, self)._get_remote_args(**kwargs)
                    + [interfaces, settings.FIREWALL_SETTINGS['rdns_ip']])


@register_operation
class UpdateAgentOperation(RemoteAgentOperation):
    id = 'update_agent'
    name = _("update agent")
    acl_level = "owner"
    required_perms = ()

    def get_activity_name(self, kwargs):
        return create_readable(
            ugettext_noop('update agent to %(version)s'),
            version=settings.AGENT_VERSION)

    @staticmethod
    def create_linux_tar():
        def exclude(tarinfo):
            ignored = ('./.', './misc', './windows')
            if any(tarinfo.name.startswith(x) for x in ignored):
                return None
            else:
                return tarinfo

        f = StringIO()

        with TarFile.open(fileobj=f, mode='w:gz') as tar:
            agent_path = os.path.join(settings.AGENT_DIR, "agent-linux")
            tar.add(agent_path, arcname='.', filter=exclude)

            version_fileobj = StringIO(settings.AGENT_VERSION)
            version_info = TarInfo(name='version.txt')
            version_info.size = len(version_fileobj.buf)
            tar.addfile(version_info, version_fileobj)

        return encodestring(f.getvalue()).replace('\n', '')

    @staticmethod
    def create_windows_tar():
        f = StringIO()

        agent_path = os.path.join(settings.AGENT_DIR, "agent-win")
        with TarFile.open(fileobj=f, mode='w|gz') as tar:
            tar.add(agent_path, arcname='.')

            version_fileobj = StringIO(settings.AGENT_VERSION)
            version_info = TarInfo(name='version.txt')
            version_info.size = len(version_fileobj.buf)
            tar.addfile(version_info, version_fileobj)

        return encodestring(f.getvalue()).replace('\n', '')

    def _operation(self, user, activity, agent_system):
        queue = self._get_remote_queue()
        instance = self.instance
        if agent_system == "Windows":
            executable = os.listdir(
                os.path.join(settings.AGENT_DIR, "agent-win"))[0]
            data = self.create_windows_tar()
        elif agent_system == "Linux":
            executable = ""
            data = self.create_linux_tar()
        else:
            # Legacy update method
            executable = ""
            return agent_tasks.update_legacy.apply_async(
                queue=queue,
                args=(instance.vm_name, self.create_linux_tar())
            ).get(timeout=60)

        checksum = md5(data).hexdigest()
        chunk_size = 1024 * 1024
        chunk_number = 0
        index = 0
        filename = settings.AGENT_VERSION + ".tar"
        while True:
            chunk = data[index:index+chunk_size]
            if chunk:
                agent_tasks.append.apply_async(
                    queue=queue,
                    args=(instance.vm_name, chunk,
                          filename, chunk_number)).get(timeout=60)
                index = index + chunk_size
                chunk_number = chunk_number + 1
            else:
                agent_tasks.update.apply_async(
                    queue=queue,
                    args=(instance.vm_name, filename, executable, checksum)
                ).get(timeout=60)
                break


@register_operation
1607
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
1608 1609 1610
    id = 'mount_store'
    name = _("mount store")
    description = _(
1611
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
1612
        "have access to this machine can see these files as well."
1613
    )
1614 1615 1616
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
1617 1618 1619 1620 1621 1622 1623
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1624
    def _operation(self, user):
1625 1626
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1627
        host = urlsplit(settings.STORE_URL).hostname
1628 1629
        username = Store(user).username
        password = user.profile.smb_password
1630 1631
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))
1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689


class AbstractDiskOperation(SubOperationMixin, RemoteInstanceOperation):
    required_perms = ()

    def _get_remote_args(self, disk, **kwargs):
        return (super(AbstractDiskOperation, self)._get_remote_args(**kwargs)
                + [disk.get_vmdisk_desc()])


@register_operation
class AttachDisk(AbstractDiskOperation):
    id = "_attach_disk"
    name = _("attach disk")
    task = vm_tasks.attach_disk


class DetachMixin(object):
    def _operation(self, activity, **kwargs):
        try:
            super(DetachMixin, self)._operation(**kwargs)
        except Exception as e:
            if hasattr(e, "libvirtError") and "not found" in unicode(e):
                activity.result = create_readable(
                    ugettext_noop("Resource was not found."),
                    ugettext_noop("Resource was not found. %(exception)s"),
                    exception=unicode(e))
            else:
                raise


@register_operation
class DetachDisk(DetachMixin, AbstractDiskOperation):
    id = "_detach_disk"
    name = _("detach disk")
    task = vm_tasks.detach_disk


class AbstractNetworkOperation(SubOperationMixin, RemoteInstanceOperation):
    required_perms = ()

    def _get_remote_args(self, interface, **kwargs):
        return (super(AbstractNetworkOperation, self)
                ._get_remote_args(**kwargs) + [interface.get_vmnetwork_desc()])


@register_operation
class AttachNetwork(AbstractNetworkOperation):
    id = "_attach_network"
    name = _("attach network")
    task = vm_tasks.attach_network


@register_operation
class DetachNetwork(DetachMixin, AbstractNetworkOperation):
    id = "_detach_network"
    name = _("detach network")
    task = vm_tasks.detach_network