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

18
from __future__ import absolute_import, unicode_literals
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
Dudás Ádám committed
58

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

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

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

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

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

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

109 110 111 112 113
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

114

115 116 117 118 119 120
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.")
121
    required_perms = ()
122
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
123

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

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

154 155 156 157
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

158

Bach Dániel committed
159
register_operation(AddInterfaceOperation)
160 161


162
class CreateDiskOperation(InstanceOperation):
163

164 165 166 167
    activity_code_suffix = 'create_disk'
    id = 'create_disk'
    name = _("create disk")
    description = _("Create empty disk for the VM.")
168
    required_perms = ('storage.create_empty_disk', )
169
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
170

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

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

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

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


203 204 205 206 207 208 209 210 211
register_operation(CreateDiskOperation)


class DownloadDiskOperation(InstanceOperation):
    activity_code_suffix = 'download_disk'
    id = 'download_disk'
    name = _("download disk")
    description = _("Download disk for the VM.")
    abortable = True
212
    has_percentage = True
213
    required_perms = ('storage.download_disk', )
214
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
215

216 217
    def _operation(self, user, url, task, activity, name=None):
        activity.result = url
Bach Dániel committed
218 219
        from storage.models import Disk

220
        disk = Disk.download(url=url, name=name, task=task)
221 222 223 224
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
225
        disk.full_clean()
226
        disk.save()
227
        self.instance.disks.add(disk)
228 229
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
230

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

239 240 241
register_operation(DownloadDiskOperation)


242
class DeployOperation(InstanceOperation):
Dudás Ádám committed
243 244 245
    activity_code_suffix = 'deploy'
    id = 'deploy'
    name = _("deploy")
246
    description = _("Deploy new virtual machine with network.")
247
    required_perms = ()
248
    deny_states = ('SUSPENDED', 'RUNNING')
Dudás Ádám committed
249

250 251
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
252
                                        self.instance.STATUS.PENDING,
253 254
                                        self.instance.STATUS.ERROR)

Dudás Ádám committed
255 256 257
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'

258
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
259 260 261
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
262 263

        # Deploy virtual images
264 265 266
        with activity.sub_activity(
            'deploying_disks', readable_name=ugettext_noop(
                "deploy disks")):
Dudás Ádám committed
267 268 269
            self.instance.deploy_disks()

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

        # Establish network connection (vmdriver)
277 278 279
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
280 281 282
            self.instance.deploy_net()

        # Resume vm
283 284 285
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
286
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
287

288 289 290 291
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
292 293


294
register_operation(DeployOperation)
Dudás Ádám committed
295 296


297
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
298 299 300
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
301
    description = _("Destroy virtual machine and its networks.")
302
    required_perms = ()
Dudás Ádám committed
303 304 305 306

    def on_commit(self, activity):
        activity.resultant_state = 'DESTROYED'

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

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

        # Destroy disks
324 325 326
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
327
            self.instance.destroy_disks()
Dudás Ádám committed
328

Dudás Ádám committed
329 330 331 332 333 334 335 336 337
        # 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
338 339 340 341 342

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


343
register_operation(DestroyOperation)
Dudás Ádám committed
344 345


346
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
347 348 349
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
350
    description = _("Live migrate running VM to another node.")
351
    required_perms = ()
352
    accept_states = ('RUNNING', )
Dudás Ádám committed
353

354
    def rollback(self, activity):
355 356 357
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
358 359
            self.instance.deploy_net()

360 361 362 363 364 365
    def check_auth(self, user):
        if not user.is_superuser:
            raise PermissionDenied()

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

366
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
367
        if not to_node:
368 369 370
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
Dudás Ádám committed
371 372 373
                to_node = self.instance.select_node()
                sa.result = to_node

374
        try:
375 376 377
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
378 379 380 381
                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
382
            raise
Dudás Ádám committed
383

384
        # Shutdown networks
385 386 387
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
388 389
            self.instance.shutdown_net()

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


400
register_operation(MigrateOperation)
Dudás Ádám committed
401 402


403
class RebootOperation(InstanceOperation):
Dudás Ádám committed
404 405 406
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
407
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
408
    required_perms = ()
409
    accept_states = ('RUNNING', )
Dudás Ádám committed
410

411
    def _operation(self, timeout=5):
Dudás Ádám committed
412
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
413 414


415
register_operation(RebootOperation)
Dudás Ádám committed
416 417


418 419 420 421 422
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
    description = _("Remove the specified network interface from the VM.")
423
    required_perms = ()
424
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
425

426 427
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
428 429 430 431
            with activity.sub_activity(
                'detach_network',
                readable_name=ugettext_noop("detach network")
            ):
432
                self.instance.detach_network(interface)
433 434 435 436 437
            interface.shutdown()

        interface.destroy()
        interface.delete()

438 439
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
440
                               vlan=kwargs['interface'].vlan)
441

442

Bach Dániel committed
443
register_operation(RemoveInterfaceOperation)
444 445


446 447 448 449 450
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
    description = _("Remove the specified disk from the VM.")
451
    required_perms = ()
452
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
453 454

    def _operation(self, activity, user, system, disk):
455
        if self.instance.is_running and disk.type not in ["iso"]:
456 457 458 459
            with activity.sub_activity(
                'detach_disk',
                readable_name=ugettext_noop('detach disk')
            ):
460
                self.instance.detach_disk(disk)
461 462 463 464 465
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
            return self.instance.disks.remove(disk)
466

467 468 469
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
470

Guba Sándor committed
471
register_operation(RemoveDiskOperation)
472 473


474
class ResetOperation(InstanceOperation):
Dudás Ádám committed
475 476 477
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
478
    description = _("Reset virtual machine (reset button).")
479
    required_perms = ()
480
    accept_states = ('RUNNING', )
Dudás Ádám committed
481

482
    def _operation(self, timeout=5):
Dudás Ádám committed
483
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
484

485
register_operation(ResetOperation)
Dudás Ádám committed
486 487


488
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
489 490 491 492 493 494 495 496
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
    description = _("""Save Virtual Machine as a Template.

        Template can be shared with groups and users.
        Users can instantiate Virtual Machines from Templates.
        """)
497
    abortable = True
498
    required_perms = ('vm.create_template', )
499
    accept_states = ('RUNNING', 'PENDING', 'STOPPED')
Dudás Ádám committed
500

501 502 503 504
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

505 506 507 508 509 510
    @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)
511
        else:
512 513
            v = 1
        return "%s v%d" % (name, v)
514

515
    def on_abort(self, activity, error):
516
        if hasattr(self, 'disks'):
517 518 519
            for disk in self.disks:
                disk.destroy()

520
    def _operation(self, activity, user, system, timeout=300, name=None,
521
                   with_shutdown=True, task=None, **kwargs):
522
        if with_shutdown:
523 524
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
525
                                                      user=user, task=task)
526 527 528
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
529 530 531 532 533 534 535 536
        # 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,
537
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
538 539 540 541 542 543 544 545 546
            '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
547
        params.pop("parent_activity", None)
Dudás Ádám committed
548

549 550
        from storage.models import Disk

Dudás Ádám committed
551 552
        def __try_save_disk(disk):
            try:
553
                return disk.save_as(task)
Dudás Ádám committed
554 555 556
            except Disk.WrongDiskTypeError:
                return disk

557
        self.disks = []
558 559 560 561 562 563 564
        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)
            ):
565 566
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
567 568 569 570 571
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
572
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
573 574 575 576 577 578 579 580 581 582
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


583
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
584 585


586
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
587 588 589
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
590
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
591
    abortable = True
592
    required_perms = ()
593
    accept_states = ('RUNNING', )
594

Dudás Ádám committed
595 596 597
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

598 599
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
600 601
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
602 603


604
register_operation(ShutdownOperation)
Dudás Ádám committed
605 606


607
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
608 609 610
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
611
    description = _("Shut off VM (plug-out).")
612
    required_perms = ()
613
    accept_states = ('RUNNING', )
Dudás Ádám committed
614

615
    def on_commit(self, activity):
Dudás Ádám committed
616 617
        activity.resultant_state = 'STOPPED'

618
    def _operation(self, activity):
Dudás Ádám committed
619 620 621
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
622

Dudás Ádám committed
623 624 625 626 627 628 629
        # 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
630 631


632
register_operation(ShutOffOperation)
Dudás Ádám committed
633 634


635
class SleepOperation(InstanceOperation):
Dudás Ádám committed
636 637 638
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
639
    description = _("Suspend virtual machine with memory dump.")
640
    required_perms = ()
641
    accept_states = ('RUNNING', )
Dudás Ádám committed
642

643 644 645 646
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
647 648 649 650 651 652 653 654 655
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

    def on_commit(self, activity):
        activity.resultant_state = 'SUSPENDED'

656
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
657
        # Destroy networks
658 659
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
660
            self.instance.shutdown_net()
Dudás Ádám committed
661 662

        # Suspend vm
663 664 665
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
666 667 668 669
            self.instance.suspend_vm(timeout=timeout)

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


672
register_operation(SleepOperation)
Dudás Ádám committed
673 674


675
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
676 677 678 679 680 681 682
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
    description = _("""Wake up Virtual Machine from SUSPENDED state.

        Power on Virtual Machine and load its memory from dump.
        """)
683
    required_perms = ()
684
    accept_states = ('SUSPENDED', )
Dudás Ádám committed
685

686
    def is_preferred(self):
687
        return self.instance.status == self.instance.STATUS.SUSPENDED
688

Dudás Ádám committed
689 690 691 692 693 694
    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'

695
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
696
        # Schedule vm
Dudás Ádám committed
697 698
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
699 700

        # Resume vm
701 702 703
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
704
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
705 706

        # Estabilish network connection (vmdriver)
707 708 709
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
710
            self.instance.deploy_net()
Dudás Ádám committed
711

712 713 714 715
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
716 717


718
register_operation(WakeUpOperation)
719 720


721 722 723 724 725 726
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
    description = _("Renew expiration times")
    acl_level = "operator"
727
    required_perms = ()
728
    concurrency_check = False
729

730 731 732 733 734 735 736 737 738 739 740 741 742 743
    def _operation(self, activity, lease=None, force=False):
        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
744
        self.instance.save()
745 746 747
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
748 749 750 751 752


register_operation(RenewOperation)


753
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
754 755 756
    activity_code_suffix = 'emergency_change_state'
    id = 'emergency_change_state'
    name = _("emergency change state")
757 758
    description = _("Change the virtual machine state to NOSTATE")
    acl_level = "owner"
Guba Sándor committed
759
    required_perms = ('vm.emergency_change_state', )
760

Guba Sándor committed
761
    def _operation(self, user, activity, new_state="NOSTATE"):
762 763 764 765 766 767
        activity.resultant_state = new_state


register_operation(ChangeStateOperation)


768
class NodeOperation(Operation):
769
    async_operation = abortable_async_node_operation
770
    host_cls = Node
771 772 773 774 775

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

776 777
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
778 779 780 781 782 783 784 785 786 787
        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.")

788 789
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
790 791
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
792 793
                                       node=self.node, user=user,
                                       readable_name=name)
794 795 796 797 798 799


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

803 804 805 806 807 808
    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)

809 810
    def check_auth(self, user):
        if not user.is_superuser:
811 812
            raise humanize_exception(ugettext_noop(
                "Superuser privileges are required."), PermissionDenied())
813 814 815

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

816
    def _operation(self, activity, user):
817
        self.node_enabled = self.node.enabled
818 819
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
820 821 822 823
            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
824
                i.migrate(user=user)
825 826


827
register_operation(FlushOperation)
828 829 830 831 832 833 834 835


class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
    description = _("Get screenshot")
    acl_level = "owner"
836
    required_perms = ()
837
    accept_states = ('RUNNING', )
838

Kálmán Viktor committed
839
    def _operation(self):
840 841 842 843
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
Bach Dániel committed
844 845 846 847 848 849 850 851 852


class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
    description = _("Recover virtual machine from destroyed state.")
    acl_level = "owner"
    required_perms = ('vm.recover', )
853
    accept_states = ('DESTROYED', )
Bach Dániel committed
854 855

    def check_precond(self):
856 857 858 859
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
860 861 862 863 864 865 866 867 868 869 870 871 872 873

    def on_commit(self, activity):
        activity.resultant_state = 'PENDING'

    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)
874 875


876 877 878 879 880 881
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
    description = _("Change resources")
    acl_level = "owner"
882
    required_perms = ('vm.change_resources', )
883
    accept_states = ('STOPPED', 'PENDING', )
884

885 886
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
887

888 889 890 891 892
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

893
        self.instance.full_clean()
894 895
        self.instance.save()

896 897 898 899 900 901
        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
        )

902 903

register_operation(ResourcesOperation)
904 905


Őry Máté committed
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
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)


925 926
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
927 928 929 930 931 932 933
    id = 'password_reset'
    name = _("password reset")
    description = _("Password reset")
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
934 935 936 937 938
        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()
939 940 941


register_operation(PasswordResetOperation)
942 943 944 945 946 947 948


class MountStoreOperation(InstanceOperation):
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
949
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
950
        "have access to this machine can see these files as well."
951
    )
952 953 954 955 956 957 958 959
    acl_level = "owner"
    required_perms = ()

    def check_precond(self):
        super(MountStoreOperation, self).check_precond()
        if self.instance.status not in ["RUNNING"]:
            raise self.instance.WrongStateError(self.instance)

Kálmán Viktor committed
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
        try:
            latest_deploy = InstanceActivity.objects.filter(
                instance=self.instance, activity_code="vm.Instance.deploy"
            ).latest("finished").finished
        except InstanceActivity.DoesNotExist:  # no deploy no agent
            raise self.instance.WrongStateError(self.instance)

        try:
            InstanceActivity.objects.filter(
                activity_code="vm.Instance.agent.starting",
                started__gt=latest_deploy).latest("started")
        except InstanceActivity.DoesNotExist:  # no agent no mount
            raise self.instance.WrongStateError(self.instance)

    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

981 982 983
    def _operation(self):
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
Kálmán Viktor committed
984 985 986
        host = urlsplit(settings.STORE_URL).netloc
        username = Store(inst.owner).username
        password = inst.owner.profile.smb_password
987 988 989 990 991
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))


register_operation(MountStoreOperation)