operations.py 35.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
Dudás Ádám committed
19
from logging import getLogger
20
from re import search
Őry Máté committed
21
from string import ascii_lowercase
Kálmán Viktor committed
22
from urlparse import urlsplit
Dudás Ádám committed
23

24
from django.core.exceptions import PermissionDenied
Dudás Ádám committed
25
from django.utils import timezone
26
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
27
from django.conf import settings
Dudás Ádám committed
28

29 30
from sizefield.utils import filesizeformat

Dudás Ádám committed
31
from celery.exceptions import TimeLimitExceeded
32

33 34 35
from common.models import (
    create_readable, humanize_exception, HumanReadableException
)
36
from common.operations import Operation, register_operation
37 38 39
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
40
from .models import (
41
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
42
    NodeActivity, pwgen
43
)
44
from .tasks import agent_tasks
Dudás Ádám committed
45

Kálmán Viktor committed
46 47
from dashboard.store_api import Store, NoStoreException

Dudás Ádám committed
48
logger = getLogger(__name__)
49 50


51
class InstanceOperation(Operation):
52
    acl_level = 'owner'
53
    async_operation = abortable_async_instance_operation
54
    host_cls = Instance
55
    concurrency_check = True
56 57
    accept_states = None
    deny_states = None
58
    resultant_state = None
Dudás Ádám committed
59

60
    def __init__(self, instance):
61
        super(InstanceOperation, self).__init__(subject=instance)
62 63 64
        self.instance = instance

    def check_precond(self):
65 66
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
67 68 69 70 71 72 73 74 75 76 77 78 79 80
        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)
81 82

    def check_auth(self, user):
83
        if not self.instance.has_level(user, self.acl_level):
84 85 86
            raise humanize_exception(ugettext_noop(
                "%(acl_level)s level is required for this operation."),
                PermissionDenied(), acl_level=self.acl_level)
87

88
        super(InstanceOperation, self).check_auth(user=user)
89

90 91
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
92 93 94 95 96 97 98 99 100 101
        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.")

102
            return parent.create_sub(code_suffix=self.activity_code_suffix,
103 104
                                     readable_name=name,
                                     resultant_state=self.resultant_state)
105 106 107
        else:
            return InstanceActivity.create(
                code_suffix=self.activity_code_suffix, instance=self.instance,
108
                readable_name=name, user=user,
109 110
                concurrency_check=self.concurrency_check,
                resultant_state=self.resultant_state)
111

112 113 114 115 116
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

117

118 119 120 121 122 123
class AddInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'add_interface'
    id = 'add_interface'
    name = _("add interface")
    description = _("Add a new network interface for the specified VLAN to "
                    "the VM.")
124
    required_perms = ()
125
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
126

127 128 129 130 131 132 133
    def rollback(self, net, activity):
        with activity.sub_activity(
            'destroying_net',
                readable_name=ugettext_noop("destroy network (rollback)")):
            net.destroy()
            net.delete()

134
    def _operation(self, activity, user, system, vlan, managed=None):
135
        if not vlan.has_level(user, 'user'):
136 137 138
            raise humanize_exception(ugettext_noop(
                "User acces to vlan %(vlan)s is required."),
                PermissionDenied(), vlan=vlan)
139 140 141 142 143 144 145
        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:
146
            try:
147 148 149
                with activity.sub_activity(
                    'attach_network',
                        readable_name=ugettext_noop("attach network")):
150 151 152 153 154
                    self.instance.attach_network(net)
            except Exception as e:
                if hasattr(e, 'libvirtError'):
                    self.rollback(net, activity)
                raise
155 156
            net.deploy()

157 158 159 160
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

161

Bach Dániel committed
162
register_operation(AddInterfaceOperation)
163 164


165
class CreateDiskOperation(InstanceOperation):
166

167 168 169
    activity_code_suffix = 'create_disk'
    id = 'create_disk'
    name = _("create disk")
170
    description = _("Create and attach empty disk to the virtual machine.")
171
    required_perms = ('storage.create_empty_disk', )
172
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
173

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

177 178 179
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
180
        disk.full_clean()
181 182 183 184
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
185
        disk.save()
186 187
        self.instance.disks.add(disk)

188
        if self.instance.is_running:
189 190 191 192
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
193
                disk.deploy()
194 195 196 197
            with activity.sub_activity(
                'attach_disk',
                readable_name=ugettext_noop("attach disk")
            ):
198 199
                self.instance.attach_disk(disk)

200
    def get_activity_name(self, kwargs):
201 202 203
        return create_readable(
            ugettext_noop("create disk %(name)s (%(size)s)"),
            size=filesizeformat(kwargs['size']), name=kwargs['name'])
204 205


206 207 208 209 210 211 212
register_operation(CreateDiskOperation)


class DownloadDiskOperation(InstanceOperation):
    activity_code_suffix = 'download_disk'
    id = 'download_disk'
    name = _("download disk")
213 214 215 216
    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.")
217
    abortable = True
218
    has_percentage = True
219
    required_perms = ('storage.download_disk', )
220
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
221

222 223
    def _operation(self, user, url, task, activity, name=None):
        activity.result = url
Bach Dániel committed
224 225
        from storage.models import Disk

226
        disk = Disk.download(url=url, name=name, task=task)
227 228 229 230
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
231
        disk.full_clean()
232
        disk.save()
233
        self.instance.disks.add(disk)
234 235
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
236

Őry Máté committed
237
        # TODO iso (cd) hot-plug is not supported by kvm/guests
238
        if self.instance.is_running and disk.type not in ["iso"]:
239 240 241 242
            with activity.sub_activity(
                'attach_disk',
                readable_name=ugettext_noop("attach disk")
            ):
243 244
                self.instance.attach_disk(disk)

245 246 247
register_operation(DownloadDiskOperation)


248
class DeployOperation(InstanceOperation):
Dudás Ádám committed
249 250 251
    activity_code_suffix = 'deploy'
    id = 'deploy'
    name = _("deploy")
252 253
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
254
    required_perms = ()
255
    deny_states = ('SUSPENDED', 'RUNNING')
256
    resultant_state = 'RUNNING'
Dudás Ádám committed
257

258 259
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
260
                                        self.instance.STATUS.PENDING,
261 262
                                        self.instance.STATUS.ERROR)

263
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
264 265 266
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
267 268

        # Deploy virtual images
269 270 271
        with activity.sub_activity(
            'deploying_disks', readable_name=ugettext_noop(
                "deploy disks")):
Dudás Ádám committed
272 273 274
            self.instance.deploy_disks()

        # Deploy VM on remote machine
275
        if self.instance.state not in ['PAUSED']:
276 277 278
            with activity.sub_activity(
                'deploying_vm', readable_name=ugettext_noop(
                    "deploy virtual machine")) as deploy_act:
279
                deploy_act.result = self.instance.deploy_vm(timeout=timeout)
Dudás Ádám committed
280 281

        # Establish network connection (vmdriver)
282 283 284
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
285 286 287
            self.instance.deploy_net()

        # Resume vm
288 289 290
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
291
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
292

293 294 295 296
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
297 298


299
register_operation(DeployOperation)
Dudás Ádám committed
300 301


302
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
303 304 305
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
306 307
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
308
    required_perms = ()
309
    resultant_state = 'DESTROYED'
Dudás Ádám committed
310

311
    def _operation(self, activity):
312
        # Destroy networks
313 314 315
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
316
            if self.instance.node:
317
                self.instance.shutdown_net()
318
            self.instance.destroy_net()
Dudás Ádám committed
319

320
        if self.instance.node:
Dudás Ádám committed
321
            # Delete virtual machine
322 323 324
            with activity.sub_activity(
                    'destroying_vm',
                    readable_name=ugettext_noop("destroy virtual machine")):
Dudás Ádám committed
325
                self.instance.delete_vm()
Dudás Ádám committed
326 327

        # Destroy disks
328 329 330
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
331
            self.instance.destroy_disks()
Dudás Ádám committed
332

Dudás Ádám committed
333 334 335 336 337 338 339 340 341
        # Delete mem. dump if exists
        try:
            self.instance.delete_mem_dump()
        except:
            pass

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
342 343 344 345 346

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


347
register_operation(DestroyOperation)
Dudás Ádám committed
348 349


350
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
351 352 353
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
354 355
    description = _("Move virtual machine to an other worker node with a few "
                    "seconds of interruption (live migration).")
356
    required_perms = ()
357
    accept_states = ('RUNNING', )
Dudás Ádám committed
358

359
    def rollback(self, activity):
360 361 362
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
363 364
            self.instance.deploy_net()

365 366 367 368 369 370
    def check_auth(self, user):
        if not user.is_superuser:
            raise PermissionDenied()

        super(MigrateOperation, self).check_auth(user=user)

371
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
372
        if not to_node:
373 374 375
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
Dudás Ádám committed
376 377 378
                to_node = self.instance.select_node()
                sa.result = to_node

379
        try:
380 381 382
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
383 384 385 386
                self.instance.migrate_vm(to_node=to_node, timeout=timeout)
        except Exception as e:
            if hasattr(e, 'libvirtError'):
                self.rollback(activity)
Bach Dániel committed
387
            raise
Dudás Ádám committed
388

389
        # Shutdown networks
390 391 392
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
393 394
            self.instance.shutdown_net()

Dudás Ádám committed
395 396 397 398
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
        # Estabilish network connection (vmdriver)
399 400 401
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
402
            self.instance.deploy_net()
Dudás Ádám committed
403 404


405
register_operation(MigrateOperation)
Dudás Ádám committed
406 407


408
class RebootOperation(InstanceOperation):
Dudás Ádám committed
409 410 411
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
412 413
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
414
    required_perms = ()
415
    accept_states = ('RUNNING', )
Dudás Ádám committed
416

417
    def _operation(self, timeout=5):
Dudás Ádám committed
418
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
419 420


421
register_operation(RebootOperation)
Dudás Ádám committed
422 423


424 425 426 427
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
428 429 430
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
431
    required_perms = ()
432
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
433

434 435
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
436 437 438 439
            with activity.sub_activity(
                'detach_network',
                readable_name=ugettext_noop("detach network")
            ):
440
                self.instance.detach_network(interface)
441 442 443 444 445
            interface.shutdown()

        interface.destroy()
        interface.delete()

446 447
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
448
                               vlan=kwargs['interface'].vlan)
449

450

Bach Dániel committed
451
register_operation(RemoveInterfaceOperation)
452 453


454 455 456 457
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
458 459
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
460
    required_perms = ()
461
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
462 463

    def _operation(self, activity, user, system, disk):
464
        if self.instance.is_running and disk.type not in ["iso"]:
465 466 467 468
            with activity.sub_activity(
                'detach_disk',
                readable_name=ugettext_noop('detach disk')
            ):
469
                self.instance.detach_disk(disk)
470 471 472 473 474
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
            return self.instance.disks.remove(disk)
475

476 477 478
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
479

Guba Sándor committed
480
register_operation(RemoveDiskOperation)
481 482


483
class ResetOperation(InstanceOperation):
Dudás Ádám committed
484 485 486
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
487
    description = _("Cold reboot virtual machine (power cycle).")
488
    required_perms = ()
489
    accept_states = ('RUNNING', )
Dudás Ádám committed
490

491
    def _operation(self, timeout=5):
Dudás Ádám committed
492
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
493

494
register_operation(ResetOperation)
Dudás Ádám committed
495 496


497
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
498 499 500
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
501 502 503 504
    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.")
505
    abortable = True
506
    required_perms = ('vm.create_template', )
507
    accept_states = ('RUNNING', 'PENDING', 'STOPPED')
Dudás Ádám committed
508

509 510 511 512
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

513 514 515 516 517 518
    @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)
519
        else:
520 521
            v = 1
        return "%s v%d" % (name, v)
522

523
    def on_abort(self, activity, error):
524
        if hasattr(self, 'disks'):
525 526 527
            for disk in self.disks:
                disk.destroy()

528
    def _operation(self, activity, user, system, timeout=300, name=None,
529
                   with_shutdown=True, task=None, **kwargs):
530
        if with_shutdown:
531 532
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
533
                                                      user=user, task=task)
534 535 536
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
537 538 539 540 541 542 543 544
        # 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,
545
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
546 547 548 549 550 551 552 553 554
            'num_cores': self.instance.num_cores,
            'owner': user,
            'parent': self.instance.template,  # Can be problem
            'priority': self.instance.priority,
            'ram_size': self.instance.ram_size,
            'raw_data': self.instance.raw_data,
            'system': self.instance.system,
        }
        params.update(kwargs)
Bach Dániel committed
555
        params.pop("parent_activity", None)
Dudás Ádám committed
556

557 558
        from storage.models import Disk

Dudás Ádám committed
559 560
        def __try_save_disk(disk):
            try:
561
                return disk.save_as(task)
Dudás Ádám committed
562 563 564
            except Disk.WrongDiskTypeError:
                return disk

565
        self.disks = []
566 567 568 569 570 571 572
        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)
            ):
573 574
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
575 576 577 578 579
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
580
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
581 582 583 584 585 586 587 588 589 590
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


591
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
592 593


594
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
595 596 597
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
598 599 600 601
    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
602
    abortable = True
603
    required_perms = ()
604
    accept_states = ('RUNNING', )
605
    resultant_state = 'STOPPED'
Dudás Ádám committed
606

607 608
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
609 610
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
611 612


613
register_operation(ShutdownOperation)
Dudás Ádám committed
614 615


616
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
617 618 619
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
620 621 622 623 624 625 626
    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.")
627
    required_perms = ()
628
    accept_states = ('RUNNING', )
629
    resultant_state = 'STOPPED'
Dudás Ádám committed
630

631
    def _operation(self, activity):
Dudás Ádám committed
632 633 634
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
635

Dudás Ádám committed
636 637 638 639 640 641 642
        # Delete virtual machine
        with activity.sub_activity('delete_vm'):
            self.instance.delete_vm()

        # Clear node and VNC port association
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
643 644


645
register_operation(ShutOffOperation)
Dudás Ádám committed
646 647


648
class SleepOperation(InstanceOperation):
Dudás Ádám committed
649 650 651
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
652 653 654 655 656 657 658 659
    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.")
660
    required_perms = ()
661
    accept_states = ('RUNNING', )
662
    resultant_state = 'SUSPENDED'
Dudás Ádám committed
663

664 665 666 667
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
668 669 670 671 672 673
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

674
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
675
        # Destroy networks
676 677
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
678
            self.instance.shutdown_net()
Dudás Ádám committed
679 680

        # Suspend vm
681 682 683
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
684 685 686 687
            self.instance.suspend_vm(timeout=timeout)

        self.instance.yield_node()
        # VNC port needs to be kept
Dudás Ádám committed
688 689


690
register_operation(SleepOperation)
Dudás Ádám committed
691 692


693
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
694 695 696
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
697 698 699
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
700
    required_perms = ()
701
    accept_states = ('SUSPENDED', )
702
    resultant_state = 'RUNNING'
Dudás Ádám committed
703

704
    def is_preferred(self):
705
        return self.instance.status == self.instance.STATUS.SUSPENDED
706

Dudás Ádám committed
707 708 709
    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

710
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
711
        # Schedule vm
Dudás Ádám committed
712 713
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
714 715

        # Resume vm
716 717 718
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
719
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
720 721

        # Estabilish network connection (vmdriver)
722 723 724
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
725
            self.instance.deploy_net()
Dudás Ádám committed
726

727 728 729 730
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
731 732


733
register_operation(WakeUpOperation)
734 735


736 737 738 739
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
740 741 742 743
    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.")
744
    acl_level = "operator"
745
    required_perms = ()
746
    concurrency_check = False
747

Őry Máté committed
748
    def _operation(self, activity, lease=None, force=False, save=False):
749 750 751 752 753 754 755 756 757 758 759 760 761
        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
762 763
        if save:
            self.instance.lease = lease
764
        self.instance.save()
765 766 767
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
768 769 770 771 772


register_operation(RenewOperation)


773
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
774 775
    activity_code_suffix = 'emergency_change_state'
    id = 'emergency_change_state'
776 777 778 779 780 781
    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.")
782
    acl_level = "owner"
Guba Sándor committed
783
    required_perms = ('vm.emergency_change_state', )
784
    concurrency_check = False
785

786
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
787
        activity.resultant_state = new_state
788 789 790 791 792 793 794
        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)
795 796 797 798 799


register_operation(ChangeStateOperation)


800
class NodeOperation(Operation):
801
    async_operation = abortable_async_node_operation
802
    host_cls = Node
803 804 805 806 807

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

808 809
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
810 811 812 813 814 815 816 817 818 819
        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.")

820 821
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
822 823
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
824 825
                                       node=self.node, user=user,
                                       readable_name=name)
826 827 828 829 830 831


class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
832
    description = _("Disable node and move all instances to other ones.")
833
    required_perms = ()
834

835 836 837 838 839 840
    def on_abort(self, activity, error):
        from manager.scheduler import TraitsUnsatisfiableException
        if isinstance(error, TraitsUnsatisfiableException):
            if self.node_enabled:
                self.node.enable(activity.user, activity)

841 842
    def check_auth(self, user):
        if not user.is_superuser:
843 844
            raise humanize_exception(ugettext_noop(
                "Superuser privileges are required."), PermissionDenied())
845 846 847

        super(FlushOperation, self).check_auth(user=user)

848
    def _operation(self, activity, user):
849
        self.node_enabled = self.node.enabled
850 851
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
852 853 854 855
            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
856
                i.migrate(user=user)
857 858


859
register_operation(FlushOperation)
860 861 862 863 864 865


class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
866 867 868
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
869
    acl_level = "owner"
870
    required_perms = ()
871
    accept_states = ('RUNNING', )
872

Kálmán Viktor committed
873
    def _operation(self):
874 875 876 877
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
Bach Dániel committed
878 879 880 881 882 883


class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
884 885 886
    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
887 888
    acl_level = "owner"
    required_perms = ('vm.recover', )
889
    accept_states = ('DESTROYED', )
890
    resultant_state = 'PENDING'
Bach Dániel committed
891 892

    def check_precond(self):
893 894 895 896
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
897 898 899 900 901 902 903 904 905 906 907

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


register_operation(RecoverOperation)
908 909


910 911 912 913
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
914
    description = _("Change resources of a stopped virtual machine.")
915
    acl_level = "owner"
916
    required_perms = ('vm.change_resources', )
917
    accept_states = ('STOPPED', 'PENDING', )
918

919 920
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
921

922 923 924 925 926
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

927
        self.instance.full_clean()
928 929
        self.instance.save()

930 931 932 933 934 935
        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
        )

936 937

register_operation(ResourcesOperation)
938 939


Őry Máté committed
940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958
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)


959 960
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
961 962
    id = 'password_reset'
    name = _("password reset")
963 964 965 966 967
    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.")
968 969 970 971
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
972 973 974 975 976
        self.instance.pw = pwgen()
        queue = self.instance.get_remote_queue_name("agent")
        agent_tasks.change_password.apply_async(
            queue=queue, args=(self.instance.vm_name, self.instance.pw))
        self.instance.save()
977 978 979


register_operation(PasswordResetOperation)
980 981


982
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
983 984 985 986
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
987
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
988
        "have access to this machine can see these files as well."
989
    )
990 991 992
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
993 994 995 996 997 998 999
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1000 1001 1002
    def _operation(self):
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1003
        host = urlsplit(settings.STORE_URL).hostname
Kálmán Viktor committed
1004 1005
        username = Store(inst.owner).username
        password = inst.owner.profile.smb_password
1006 1007 1008 1009 1010
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))


register_operation(MountStoreOperation)