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

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

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

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

164

165
@register_operation
166
class CreateDiskOperation(InstanceOperation):
167

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

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

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

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

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


207
@register_operation
208 209 210 211 212 213 214
class ResizeDiskOperation(InstanceOperation):

    activity_code_suffix = 'resize_disk'
    id = 'resize_disk'
    name = _("resize disk")
    description = _("Resize the virtual disk image. "
                    "Size must be greater value than the actual size.")
215
    required_perms = ('storage.resize_disk', )
216 217
    accept_states = ('RUNNING', )
    async_queue = "localhost.man.slow"
218 219

    def _operation(self, user, disk, size, activity):
220
        self.instance.resize_disk_live(disk, size)
221 222 223 224 225 226 227

    def get_activity_name(self, kwargs):
        return create_readable(
            ugettext_noop("resize disk %(name)s to %(size)s"),
            size=filesizeformat(kwargs['size']), name=kwargs['disk'].name)


228
@register_operation
229 230 231 232
class DownloadDiskOperation(InstanceOperation):
    activity_code_suffix = 'download_disk'
    id = 'download_disk'
    name = _("download disk")
233 234 235 236
    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.")
237
    abortable = True
238
    has_percentage = True
239
    required_perms = ('storage.download_disk', )
240
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
241
    async_queue = "localhost.man.slow"
242

243 244
    def _operation(self, user, url, task, activity, name=None):
        activity.result = url
Bach Dániel committed
245 246
        from storage.models import Disk

247
        disk = Disk.download(url=url, name=name, task=task)
248 249 250 251
        devnums = list(ascii_lowercase)
        for d in self.instance.disks.all():
            devnums.remove(d.dev_num)
        disk.dev_num = devnums.pop(0)
252
        disk.full_clean()
253
        disk.save()
254
        self.instance.disks.add(disk)
255 256
        activity.readable_name = create_readable(
            ugettext_noop("download %(name)s"), name=disk.name)
257

Őry Máté committed
258
        # TODO iso (cd) hot-plug is not supported by kvm/guests
259
        if self.instance.is_running and disk.type not in ["iso"]:
260 261 262 263
            with activity.sub_activity(
                'attach_disk',
                readable_name=ugettext_noop("attach disk")
            ):
264 265
                self.instance.attach_disk(disk)

266

267
@register_operation
268
class DeployOperation(InstanceOperation):
Dudás Ádám committed
269 270 271
    activity_code_suffix = 'deploy'
    id = 'deploy'
    name = _("deploy")
272 273
    description = _("Deploy and start the virtual machine (including storage "
                    "and network configuration).")
274
    required_perms = ()
275
    deny_states = ('SUSPENDED', 'RUNNING')
276
    resultant_state = 'RUNNING'
Dudás Ádám committed
277

278 279
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
280
                                        self.instance.STATUS.PENDING,
281 282
                                        self.instance.STATUS.ERROR)

283 284 285
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

Dudás Ádám committed
286 287
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'
288
        activity.result = create_readable(
Guba Sándor committed
289
            ugettext_noop("virtual machine successfully "
290 291
                          "deployed to node: %(node)s"),
            node=self.instance.node)
Dudás Ádám committed
292

293
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
294 295 296
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
297 298

        # Deploy virtual images
299 300 301
        with activity.sub_activity(
            'deploying_disks', readable_name=ugettext_noop(
                "deploy disks")):
Dudás Ádám committed
302 303 304
            self.instance.deploy_disks()

        # Deploy VM on remote machine
305
        if self.instance.state not in ['PAUSED']:
Guba Sándor committed
306 307 308
            rn = create_readable(ugettext_noop("deploy virtual machine"),
                                 ugettext_noop("deploy vm to %(node)s"),
                                 node=self.instance.node)
309
            with activity.sub_activity(
Guba Sándor committed
310
                    'deploying_vm', readable_name=rn) as deploy_act:
311
                deploy_act.result = self.instance.deploy_vm(timeout=timeout)
Dudás Ádám committed
312 313

        # Establish network connection (vmdriver)
314 315 316
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
317 318
            self.instance.deploy_net()

319 320 321 322 323
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

Dudás Ádám committed
324
        # Resume vm
325 326 327
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
328
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
329

330 331 332
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
333 334


335
@register_operation
336
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
337 338 339
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
340 341
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
342
    required_perms = ()
343
    resultant_state = 'DESTROYED'
Dudás Ádám committed
344

345
    def _operation(self, activity):
346
        # Destroy networks
347 348 349
        with activity.sub_activity(
                'destroying_net',
                readable_name=ugettext_noop("destroy network")):
350
            if self.instance.node:
351
                self.instance.shutdown_net()
352
            self.instance.destroy_net()
Dudás Ádám committed
353

354
        if self.instance.node:
Dudás Ádám committed
355
            # Delete virtual machine
356 357 358
            with activity.sub_activity(
                    'destroying_vm',
                    readable_name=ugettext_noop("destroy virtual machine")):
Dudás Ádám committed
359
                self.instance.delete_vm()
Dudás Ádám committed
360 361

        # Destroy disks
362 363 364
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
365
            self.instance.destroy_disks()
Dudás Ádám committed
366

Dudás Ádám committed
367 368 369 370 371 372 373 374 375
        # 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
376 377 378 379 380

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


381
@register_operation
382
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
383 384 385
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
386 387
    description = _("Move virtual machine to an other worker node with a few "
                    "seconds of interruption (live migration).")
388
    required_perms = ()
389
    superuser_required = True
390
    accept_states = ('RUNNING', )
391
    async_queue = "localhost.man.slow"
Dudás Ádám committed
392

393
    def rollback(self, activity):
394 395 396
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
397 398
            self.instance.deploy_net()

399
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
400
        if not to_node:
401 402 403
            with activity.sub_activity('scheduling',
                                       readable_name=ugettext_noop(
                                           "schedule")) as sa:
Dudás Ádám committed
404 405 406
                to_node = self.instance.select_node()
                sa.result = to_node

407
        try:
408 409 410
            with activity.sub_activity(
                'migrate_vm', readable_name=create_readable(
                    ugettext_noop("migrate to %(node)s"), node=to_node)):
411 412 413 414
                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
415
            raise
Dudás Ádám committed
416

417
        # Shutdown networks
418 419 420
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
421 422
            self.instance.shutdown_net()

Dudás Ádám committed
423 424 425 426
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
        # Estabilish network connection (vmdriver)
427 428 429
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
430
            self.instance.deploy_net()
Dudás Ádám committed
431 432


433
@register_operation
434
class RebootOperation(InstanceOperation):
Dudás Ádám committed
435 436 437
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
438 439
    description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del "
                    "signal to its console.")
440
    required_perms = ()
441
    accept_states = ('RUNNING', )
Dudás Ádám committed
442

443
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
444
        self.instance.reboot_vm(timeout=timeout)
445 446 447
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
448 449


450
@register_operation
451 452 453 454
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
455 456 457
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
458
    required_perms = ()
459
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
460

461 462
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
463 464 465 466
            with activity.sub_activity(
                'detach_network',
                readable_name=ugettext_noop("detach network")
            ):
467
                self.instance.detach_network(interface)
468 469 470 471 472
            interface.shutdown()

        interface.destroy()
        interface.delete()

473 474
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
475
                               vlan=kwargs['interface'].vlan)
476

477

478
@register_operation
479 480 481 482
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
483 484
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
485
    required_perms = ()
486
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
487 488

    def _operation(self, activity, user, system, disk):
489
        if self.instance.is_running and disk.type not in ["iso"]:
490 491 492 493
            with activity.sub_activity(
                'detach_disk',
                readable_name=ugettext_noop('detach disk')
            ):
494
                self.instance.detach_disk(disk)
495 496 497 498 499
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
            return self.instance.disks.remove(disk)
500

501 502 503
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
504 505


506
@register_operation
507
class ResetOperation(InstanceOperation):
Dudás Ádám committed
508 509 510
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
511
    description = _("Cold reboot virtual machine (power cycle).")
512
    required_perms = ()
513
    accept_states = ('RUNNING', )
Dudás Ádám committed
514

515
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
516
        self.instance.reset_vm(timeout=timeout)
517 518 519
        if self.instance.has_agent:
            activity.sub_activity('os_boot', readable_name=ugettext_noop(
                "wait operating system loading"), interruptible=True)
Dudás Ádám committed
520 521


522
@register_operation
523
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
524 525 526
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
527 528 529 530
    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.")
531
    has_percentage = True
532
    abortable = True
533
    required_perms = ('vm.create_template', )
534
    accept_states = ('RUNNING', 'STOPPED')
535
    async_queue = "localhost.man.slow"
Dudás Ádám committed
536

537 538 539 540
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

541 542 543 544 545 546
    @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)
547
        else:
548 549
            v = 1
        return "%s v%d" % (name, v)
550

551
    def on_abort(self, activity, error):
552
        if hasattr(self, 'disks'):
553 554 555
            for disk in self.disks:
                disk.destroy()

556
    def _operation(self, activity, user, system, timeout=300, name=None,
557
                   with_shutdown=True, task=None, **kwargs):
558
        if with_shutdown:
559 560
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
561
                                                      user=user, task=task)
562 563 564
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
565 566 567 568 569 570 571 572
        # 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,
573
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
574 575 576 577 578 579 580 581 582
            '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
583
        params.pop("parent_activity", None)
Dudás Ádám committed
584

585 586
        from storage.models import Disk

Dudás Ádám committed
587 588
        def __try_save_disk(disk):
            try:
589
                return disk.save_as(task)
Dudás Ádám committed
590 591 592
            except Disk.WrongDiskTypeError:
                return disk

593
        self.disks = []
594 595 596 597 598 599 600
        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)
            ):
601 602
                self.disks.append(__try_save_disk(disk))

Dudás Ádám committed
603 604 605 606 607
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
608
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
609 610 611 612 613 614 615 616 617 618
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


619
@register_operation
620
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
621 622 623
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
624 625 626 627
    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
628
    abortable = True
629
    required_perms = ()
630
    accept_states = ('RUNNING', )
631
    resultant_state = 'STOPPED'
Dudás Ádám committed
632

633 634
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
635
        self.instance.yield_node()
Dudás Ádám committed
636

637 638 639 640 641 642 643 644 645 646 647
    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
648

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

665
    def _operation(self, activity):
Dudás Ádám committed
666 667 668
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
669

Dudás Ádám committed
670 671 672 673 674
        # Delete virtual machine
        with activity.sub_activity('delete_vm'):
            self.instance.delete_vm()

        self.instance.yield_node()
Dudás Ádám committed
675 676


677
@register_operation
678
class SleepOperation(InstanceOperation):
Dudás Ádám committed
679 680 681
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
682 683 684 685 686 687 688 689
    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.")
690
    required_perms = ()
691
    accept_states = ('RUNNING', )
692
    resultant_state = 'SUSPENDED'
693
    async_queue = "localhost.man.slow"
Dudás Ádám committed
694

695 696 697 698
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
699 700 701 702 703 704
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

705
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
706
        # Destroy networks
707 708
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
709
            self.instance.shutdown_net()
Dudás Ádám committed
710 711

        # Suspend vm
712 713 714
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
715 716 717 718
            self.instance.suspend_vm(timeout=timeout)

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


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

733
    def is_preferred(self):
734
        return self.instance.status == self.instance.STATUS.SUSPENDED
735

Dudás Ádám committed
736
    def on_abort(self, activity, error):
Bach Dániel committed
737 738 739 740
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
741

742
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
743
        # Schedule vm
Dudás Ádám committed
744 745
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
746 747

        # Resume vm
748 749 750
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
751
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
752 753

        # Estabilish network connection (vmdriver)
754 755 756
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
757
            self.instance.deploy_net()
Dudás Ádám committed
758

759 760 761 762
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
763 764


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

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


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

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

825 826 827 828
        if reset_node:
            self.instance.node = None
            self.instance.save()

829

830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856
@register_operation
class RedeployOperation(InstanceOperation):
    activity_code_suffix = 'redeploy'
    id = 'redeploy'
    name = _("redeploy")
    description = _("Change the virtual machine state to NOSTATE "
                    "and redeploy the VM. This operation allows starting "
                    "machines formerly running on a failed node.")
    acl_level = "owner"
    required_perms = ('vm.redeploy', )
    concurrency_check = False

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

        self.instance._update_status()

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


857
class NodeOperation(Operation):
858
    async_operation = abortable_async_node_operation
859
    host_cls = Node
860 861
    online_required = True
    superuser_required = True
862 863 864 865 866

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

867 868 869 870 871 872 873
    def check_precond(self):
        super(NodeOperation, self).check_precond()
        if self.online_required and not self.node.online:
            raise humanize_exception(ugettext_noop(
                "You cannot call this operation on an offline node."),
                Exception())

874 875
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
876 877 878 879 880 881 882 883 884 885
        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.")

886 887
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
888 889
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
890 891
                                       node=self.node, user=user,
                                       readable_name=name)
892 893


894
@register_operation
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
class ResetNodeOperation(NodeOperation):
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
    description = _("Disable missing node and redeploy all instances "
                    "on other ones.")
    required_perms = ()
    online_required = False
    async_queue = "localhost.man.slow"

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

    def _operation(self, activity, user):
        if self.node.enabled:
            DisableOperation(self.node).call(parent_activity=activity,
                                             user=user)
        for i in self.node.instance_set.all():
            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):
                i.redeploy(user=user)


@register_operation
924 925 926 927
class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
928
    description = _("Passivate node and move all instances to other ones.")
929
    required_perms = ()
930
    async_queue = "localhost.man.slow"
931

932
    def _operation(self, activity, user):
933 934 935
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
936
        for i in self.node.instance_set.all():
937 938 939 940
            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
941
                i.migrate(user=user)
942 943


944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994
@register_operation
class ActivateOperation(NodeOperation):
    activity_code_suffix = 'activate'
    id = 'activate'
    name = _("activate")
    description = _("Make node active, i.e. scheduler is allowed to deploy "
                    "virtual machines to it.")
    required_perms = ()

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

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = True
        self.node.save()


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

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

    def _operation(self):
        self.node.enabled = True
        self.node.schedule_enabled = False
        self.node.save()


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

996 997 998 999 1000 1001 1002 1003 1004
    def check_precond(self):
        if not self.node.enabled:
            raise humanize_exception(ugettext_noop(
                "You cannot disable a disabled node."), Exception())
        if self.node.instance_set.exists():
            raise humanize_exception(ugettext_noop(
                "You cannot disable a node which is hosting instances."),
                Exception())
        super(DisableOperation, self).check_precond()
1005

1006 1007 1008 1009
    def _operation(self):
        self.node.enabled = False
        self.node.schedule_enabled = False
        self.node.save()
1010 1011


1012
@register_operation
1013 1014 1015 1016
class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
1017 1018 1019
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
1020
    acl_level = "owner"
1021
    required_perms = ()
1022
    accept_states = ('RUNNING', )
1023

Kálmán Viktor committed
1024
    def _operation(self):
1025 1026 1027
        return self.instance.get_screenshot(timeout=20)


1028
@register_operation
Bach Dániel committed
1029 1030 1031 1032
class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
1033 1034 1035
    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
1036 1037
    acl_level = "owner"
    required_perms = ('vm.recover', )
1038
    accept_states = ('DESTROYED', )
1039
    resultant_state = 'PENDING'
Bach Dániel committed
1040 1041

    def check_precond(self):
1042 1043 1044 1045
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
1046 1047 1048 1049 1050 1051 1052 1053 1054 1055

    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()


1056
@register_operation
1057 1058 1059 1060
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
1061
    description = _("Change resources of a stopped virtual machine.")
1062
    acl_level = "owner"
1063
    required_perms = ('vm.change_resources', )
1064
    accept_states = ('STOPPED', 'PENDING', )
1065

1066 1067
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
1068

1069 1070 1071 1072 1073
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

1074
        self.instance.full_clean()
1075 1076
        self.instance.save()

1077 1078 1079 1080 1081 1082
        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
        )

1083

Őry Máté committed
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
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)


1103
@register_operation
1104 1105
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
1106 1107
    id = 'password_reset'
    name = _("password reset")
1108 1109 1110 1111 1112
    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.")
1113 1114 1115 1116
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
1117 1118 1119 1120 1121
        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()
1122 1123


1124
@register_operation
1125
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
1126 1127 1128 1129
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
1130
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
1131
        "have access to this machine can see these files as well."
1132
    )
1133 1134 1135
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
1136 1137 1138 1139 1140 1141 1142
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1143
    def _operation(self, user):
1144 1145
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1146
        host = urlsplit(settings.STORE_URL).hostname
1147 1148
        username = Store(user).username
        password = user.profile.smb_password
1149 1150
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))