operations.py 36.5 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
Bach Dániel committed
37
from manager.scheduler import SchedulerError
38 39 40
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
41
from .models import (
42
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
43
    NodeActivity, pwgen
44
)
45
from .tasks import agent_tasks, local_agent_tasks
Dudás Ádám committed
46

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

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


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

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

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

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

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

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

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

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

118

119 120 121 122 123 124
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.")
125
    required_perms = ()
126
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
127

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

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

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

163

Bach Dániel committed
164
register_operation(AddInterfaceOperation)
165 166


167
class CreateDiskOperation(InstanceOperation):
168

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

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

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

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

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


208 209 210 211 212 213 214
register_operation(CreateDiskOperation)


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

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

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

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

248 249 250
register_operation(DownloadDiskOperation)


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

261 262
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
263
                                        self.instance.STATUS.PENDING,
264 265
                                        self.instance.STATUS.ERROR)

266 267 268
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
269 270
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
271
        activity.result = create_readable(
Guba Sándor committed
272
            ugettext_noop("virtual machine successfully "
273 274
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
275

276
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
277 278 279
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
280 281

        # Deploy virtual images
282 283 284
        with activity.sub_activity(
            'deploying_disks', readable_name=ugettext_noop(
                "deploy disks")):
Dudás Ádám committed
285 286 287
            self.instance.deploy_disks()

        # Deploy VM on remote machine
288
        if self.instance.state not in ['PAUSED']:
Guba Sándor committed
289 290 291
            rn = create_readable(ugettext_noop("deploy virtual machine"),
                                 ugettext_noop("deploy vm to %(node)s"),
                                 node=self.instance.node)
292
            with activity.sub_activity(
Guba Sándor committed
293
                    'deploying_vm', readable_name=rn) as deploy_act:
294
                deploy_act.result = self.instance.deploy_vm(timeout=timeout)
Dudás Ádám committed
295 296

        # Establish network connection (vmdriver)
297 298 299
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
300 301 302
            self.instance.deploy_net()

        # Resume vm
303 304 305
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
306
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
307

308 309 310 311
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
312 313


314
register_operation(DeployOperation)
Dudás Ádám committed
315 316


317
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
318 319 320
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
321 322
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
323
    required_perms = ()
324
    resultant_state = 'DESTROYED'
Dudás Ádám committed
325

326
    def _operation(self, activity):
327
        # Destroy networks
328 329 330
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
331
            if self.instance.node:
332
                self.instance.shutdown_net()
333
            self.instance.destroy_net()
Dudás Ádám committed
334

335
        if self.instance.node:
Dudás Ádám committed
336
            # Delete virtual machine
337 338 339
            with activity.sub_activity(
                    'destroying_vm',
                    readable_name=ugettext_noop("destroy virtual machine")):
Dudás Ádám committed
340
                self.instance.delete_vm()
Dudás Ádám committed
341 342

        # Destroy disks
343 344 345
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
346
            self.instance.destroy_disks()
Dudás Ádám committed
347

Dudás Ádám committed
348 349 350 351 352 353 354 355 356
        # 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
357 358 359 360 361

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


362
register_operation(DestroyOperation)
Dudás Ádám committed
363 364


365
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
366 367 368
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
369 370
    description = _("Move virtual machine to an other worker node with a few "
                    "seconds of interruption (live migration).")
371
    required_perms = ()
372
    superuser_required = True
373
    accept_states = ('RUNNING', )
374
    async_queue = "localhost.man.slow"
Dudás Ádám committed
375

376
    def rollback(self, activity):
377 378 379
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
380 381
            self.instance.deploy_net()

382
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
383
        if not to_node:
384 385 386
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
Dudás Ádám committed
387 388 389
                to_node = self.instance.select_node()
                sa.result = to_node

390
        try:
391 392 393
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
394 395 396 397
                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
398
            raise
Dudás Ádám committed
399

400
        # Shutdown networks
401 402 403
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
404 405
            self.instance.shutdown_net()

Dudás Ádám committed
406 407 408 409
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
        # Estabilish network connection (vmdriver)
410 411 412
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
413
            self.instance.deploy_net()
Dudás Ádám committed
414 415


416
register_operation(MigrateOperation)
Dudás Ádám committed
417 418


419
class RebootOperation(InstanceOperation):
Dudás Ádám committed
420 421 422
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
423 424
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
425
    required_perms = ()
426
    accept_states = ('RUNNING', )
Dudás Ádám committed
427

428
    def _operation(self, timeout=5):
Dudás Ádám committed
429
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
430 431


432
register_operation(RebootOperation)
Dudás Ádám committed
433 434


435 436 437 438
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
439 440 441
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
442
    required_perms = ()
443
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
444

445 446
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
447 448 449 450
            with activity.sub_activity(
                'detach_network',
                readable_name=ugettext_noop("detach network")
            ):
451
                self.instance.detach_network(interface)
452 453 454 455 456
            interface.shutdown()

        interface.destroy()
        interface.delete()

457 458
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
459
                               vlan=kwargs['interface'].vlan)
460

461

Bach Dániel committed
462
register_operation(RemoveInterfaceOperation)
463 464


465 466 467 468
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
469 470
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
471
    required_perms = ()
472
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
473 474

    def _operation(self, activity, user, system, disk):
475
        if self.instance.is_running and disk.type not in ["iso"]:
476 477 478 479
            with activity.sub_activity(
                'detach_disk',
                readable_name=ugettext_noop('detach disk')
            ):
480
                self.instance.detach_disk(disk)
481 482 483 484 485
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
            return self.instance.disks.remove(disk)
486

487 488 489
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
490

Guba Sándor committed
491
register_operation(RemoveDiskOperation)
492 493


494
class ResetOperation(InstanceOperation):
Dudás Ádám committed
495 496 497
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
498
    description = _("Cold reboot virtual machine (power cycle).")
499
    required_perms = ()
500
    accept_states = ('RUNNING', )
Dudás Ádám committed
501

502
    def _operation(self, timeout=5):
Dudás Ádám committed
503
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
504

505
register_operation(ResetOperation)
Dudás Ádám committed
506 507


508
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
509 510 511
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
512 513 514 515
    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.")
516
    abortable = True
517
    required_perms = ('vm.create_template', )
518
    accept_states = ('RUNNING', 'PENDING', 'STOPPED')
519
    async_queue = "localhost.man.slow"
Dudás Ádám committed
520

521 522 523 524
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

525 526 527 528 529 530
    @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)
531
        else:
532 533
            v = 1
        return "%s v%d" % (name, v)
534

535
    def on_abort(self, activity, error):
536
        if hasattr(self, 'disks'):
537 538 539
            for disk in self.disks:
                disk.destroy()

540
    def _operation(self, activity, user, system, timeout=300, name=None,
541
                   with_shutdown=True, task=None, **kwargs):
542
        if with_shutdown:
543 544
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
545
                                                      user=user, task=task)
546 547 548
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
549 550 551 552 553 554 555 556
        # 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,
557
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
558 559 560 561 562 563 564 565 566
            '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
567
        params.pop("parent_activity", None)
Dudás Ádám committed
568

569 570
        from storage.models import Disk

Dudás Ádám committed
571 572
        def __try_save_disk(disk):
            try:
573
                return disk.save_as(task)
Dudás Ádám committed
574 575 576
            except Disk.WrongDiskTypeError:
                return disk

577
        self.disks = []
578 579 580 581 582 583 584
        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)
            ):
585 586
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
587 588 589 590 591
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
592
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
593 594 595 596 597 598 599 600 601 602
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


603
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
604 605


606
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
607 608 609
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
610 611 612 613
    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
614
    abortable = True
615
    required_perms = ()
616
    accept_states = ('RUNNING', )
617
    resultant_state = 'STOPPED'
Dudás Ádám committed
618

619 620
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
621 622
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
623

624 625 626 627 628 629 630 631 632 633 634
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.result = humanize_exception(ugettext_noop(
                "The virtual machine did not switch off in the provided time "
                "limit. Most of the time this is caused by incorrect ACPI "
                "settings. You can also try to power off the machine from the "
                "operating system manually."), error)
            activity.resultant_state = None
        else:
            super(ShutdownOperation, self).on_abort(activity, error)

Dudás Ádám committed
635

636
register_operation(ShutdownOperation)
Dudás Ádám committed
637 638


639
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
640 641 642
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
643 644 645 646 647 648 649
    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.")
650
    required_perms = ()
651
    accept_states = ('RUNNING', )
652
    resultant_state = 'STOPPED'
Dudás Ádám committed
653

654
    def _operation(self, activity):
Dudás Ádám committed
655 656 657
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
658

Dudás Ádám committed
659 660 661 662 663 664 665
        # 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
666 667


668
register_operation(ShutOffOperation)
Dudás Ádám committed
669 670


671
class SleepOperation(InstanceOperation):
Dudás Ádám committed
672 673 674
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
675 676 677 678 679 680 681 682
    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.")
683
    required_perms = ()
684
    accept_states = ('RUNNING', )
685
    resultant_state = 'SUSPENDED'
686
    async_queue = "localhost.man.slow"
Dudás Ádám committed
687

688 689 690 691
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
692 693 694 695 696 697
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

698
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
699
        # Destroy networks
700 701
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
702
            self.instance.shutdown_net()
Dudás Ádám committed
703 704

        # Suspend vm
705 706 707
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
708 709 710 711
            self.instance.suspend_vm(timeout=timeout)

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


714
register_operation(SleepOperation)
Dudás Ádám committed
715 716


717
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
718 719 720
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
721 722 723
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
724
    required_perms = ()
725
    accept_states = ('SUSPENDED', )
726
    resultant_state = 'RUNNING'
Dudás Ádám committed
727

728
    def is_preferred(self):
729
        return self.instance.status == self.instance.STATUS.SUSPENDED
730

Dudás Ádám committed
731
    def on_abort(self, activity, error):
Bach Dániel committed
732 733 734 735
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
736

737
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
738
        # Schedule vm
Dudás Ádám committed
739 740
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
741 742

        # Resume vm
743 744 745
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
746
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
747 748

        # Estabilish network connection (vmdriver)
749 750 751
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
752
            self.instance.deploy_net()
Dudás Ádám committed
753

754 755 756 757
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
758 759


760
register_operation(WakeUpOperation)
761 762


763 764 765 766
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
767 768 769 770
    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.")
771
    acl_level = "operator"
772
    required_perms = ()
773
    concurrency_check = False
774

Őry Máté committed
775
    def _operation(self, activity, lease=None, force=False, save=False):
776 777 778 779 780 781 782 783 784 785 786 787 788
        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
789 790
        if save:
            self.instance.lease = lease
791
        self.instance.save()
792 793 794
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
795 796 797 798 799


register_operation(RenewOperation)


800
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
801 802
    activity_code_suffix = 'emergency_change_state'
    id = 'emergency_change_state'
803 804 805 806 807 808
    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.")
809
    acl_level = "owner"
Guba Sándor committed
810
    required_perms = ('vm.emergency_change_state', )
811
    concurrency_check = False
812

813
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
814
        activity.resultant_state = new_state
815 816 817 818 819 820 821
        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)
822 823 824 825 826


register_operation(ChangeStateOperation)


827
class NodeOperation(Operation):
828
    async_operation = abortable_async_node_operation
829
    host_cls = Node
830 831 832 833 834

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

835 836
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
837 838 839 840 841 842 843 844 845 846
        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.")

847 848
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
849 850
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
851 852
                                       node=self.node, user=user,
                                       readable_name=name)
853 854 855 856 857 858


class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
859
    description = _("Disable node and move all instances to other ones.")
860
    required_perms = ()
861
    superuser_required = True
862
    async_queue = "localhost.man.slow"
863

864 865 866 867 868 869
    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)

870
    def _operation(self, activity, user):
871
        self.node_enabled = self.node.enabled
872 873
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
874 875 876 877
            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
878
                i.migrate(user=user)
879 880


881
register_operation(FlushOperation)
882 883 884 885 886 887


class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
888 889 890
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
891
    acl_level = "owner"
892
    required_perms = ()
893
    accept_states = ('RUNNING', )
894

Kálmán Viktor committed
895
    def _operation(self):
896 897 898 899
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
Bach Dániel committed
900 901 902 903 904 905


class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
906 907 908
    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
909 910
    acl_level = "owner"
    required_perms = ('vm.recover', )
911
    accept_states = ('DESTROYED', )
912
    resultant_state = 'PENDING'
Bach Dániel committed
913 914

    def check_precond(self):
915 916 917 918
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
919 920 921 922 923 924 925 926 927 928 929

    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)
930 931


932 933 934 935
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
936
    description = _("Change resources of a stopped virtual machine.")
937
    acl_level = "owner"
938
    required_perms = ('vm.change_resources', )
939
    accept_states = ('STOPPED', 'PENDING', )
940

941 942
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
943

944 945 946 947 948
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

949
        self.instance.full_clean()
950 951
        self.instance.save()

952 953 954 955 956 957
        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
        )

958 959

register_operation(ResourcesOperation)
960 961


Őry Máté committed
962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
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)


981 982
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
983 984
    id = 'password_reset'
    name = _("password reset")
985 986 987 988 989
    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.")
990 991 992 993
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
994 995 996 997 998
        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()
999 1000 1001


register_operation(PasswordResetOperation)
1002 1003


1004
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
1005 1006 1007 1008
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
1009
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
1010
        "have access to this machine can see these files as well."
1011
    )
1012 1013 1014
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
1015 1016 1017 1018 1019 1020 1021
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1022
    def _operation(self, user):
1023 1024
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1025
        host = urlsplit(settings.STORE_URL).hostname
1026 1027
        username = Store(user).username
        password = user.profile.smb_password
1028 1029 1030 1031 1032
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))


register_operation(MountStoreOperation)