operations.py 35 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
    activity_code_suffix = 'create_disk'
    id = 'create_disk'
    name = _("create disk")
167
    description = _("Create and attach empty disk to the virtual machine.")
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
register_operation(CreateDiskOperation)


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

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

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

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

242 243 244
register_operation(DownloadDiskOperation)


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

254 255
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
256
                                        self.instance.STATUS.PENDING,
257 258
                                        self.instance.STATUS.ERROR)

Dudás Ádám committed
259 260 261
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'

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

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

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

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

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

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


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


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

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

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

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

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

Dudás Ádám committed
334 335 336 337 338 339 340 341 342
        # 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
343 344 345 346 347

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


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


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

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

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

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

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

380
        try:
381 382 383
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
384 385 386 387
                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
388
            raise
Dudás Ádám committed
389

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

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


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


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

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


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


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

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

        interface.destroy()
        interface.delete()

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

451

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


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

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

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

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


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

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

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


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

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

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

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

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

Dudás Ádám committed
538 539 540 541 542 543 544 545
        # 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,
546
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
547 548 549 550 551 552 553 554 555
            '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
556
        params.pop("parent_activity", None)
Dudás Ádám committed
557

558 559
        from storage.models import Disk

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

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

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


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


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

Dudás Ádám committed
607 608 609
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

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


616
register_operation(ShutdownOperation)
Dudás Ádám committed
617 618


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

633
    def on_commit(self, activity):
Dudás Ádám committed
634 635
        activity.resultant_state = 'STOPPED'

636
    def _operation(self, activity):
Dudás Ádám committed
637 638 639
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
640

Dudás Ádám committed
641 642 643 644 645 646 647
        # 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
648 649


650
register_operation(ShutOffOperation)
Dudás Ádám committed
651 652


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

668 669 670 671
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
672 673 674 675 676 677 678 679 680
    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'

681
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
682
        # Destroy networks
683 684
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
685
            self.instance.shutdown_net()
Dudás Ádám committed
686 687

        # Suspend vm
688 689 690
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
691 692 693 694
            self.instance.suspend_vm(timeout=timeout)

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


697
register_operation(SleepOperation)
Dudás Ádám committed
698 699


700
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
701 702 703
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
704 705 706
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
707
    required_perms = ()
708
    accept_states = ('SUSPENDED', )
Dudás Ádám committed
709

710
    def is_preferred(self):
711
        return self.instance.status == self.instance.STATUS.SUSPENDED
712

Dudás Ádám committed
713 714 715 716 717 718
    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

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

719
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
720
        # Schedule vm
Dudás Ádám committed
721 722
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
723 724

        # Resume vm
725 726 727
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
728
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
729 730

        # Estabilish network connection (vmdriver)
731 732 733
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
734
            self.instance.deploy_net()
Dudás Ádám committed
735

736 737 738 739
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
740 741


742
register_operation(WakeUpOperation)
743 744


745 746 747 748
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
749 750 751 752
    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.")
753
    acl_level = "operator"
754
    required_perms = ()
755
    concurrency_check = False
756

757 758 759 760 761 762 763 764 765 766 767 768 769 770
    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
771
        self.instance.save()
772 773 774
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
775 776 777 778 779


register_operation(RenewOperation)


780
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
781 782
    activity_code_suffix = 'emergency_change_state'
    id = 'emergency_change_state'
783 784 785 786 787 788
    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.")
789
    acl_level = "owner"
Guba Sándor committed
790
    required_perms = ('vm.emergency_change_state', )
791

Guba Sándor committed
792
    def _operation(self, user, activity, new_state="NOSTATE"):
793 794 795 796 797 798
        activity.resultant_state = new_state


register_operation(ChangeStateOperation)


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

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

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

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


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

834 835 836 837 838 839
    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)

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

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

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


858
register_operation(FlushOperation)
859 860 861 862 863 864


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

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


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


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

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

    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)
909 910


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

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

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

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

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

937 938

register_operation(ResourcesOperation)
939 940


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


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

    def _operation(self):
973 974 975 976 977
        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()
978 979 980


register_operation(PasswordResetOperation)
981 982


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

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

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


register_operation(MountStoreOperation)