operations.py 37 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
            self.instance.deploy_net()

302 303 304 305 306
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

Dudás Ádám committed
307
        # Resume vm
308 309 310
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
311
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
312

313 314 315
        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
316 317


318
register_operation(DeployOperation)
Dudás Ádám committed
319 320


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

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

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

        # Destroy disks
347 348 349
        with activity.sub_activity(
                'destroying_disks',
                readable_name=ugettext_noop("destroy disks")):
Dudás Ádám committed
350
            self.instance.destroy_disks()
Dudás Ádám committed
351

Dudás Ádám committed
352 353 354 355 356 357 358 359 360
        # 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
361 362 363 364 365

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


366
register_operation(DestroyOperation)
Dudás Ádám committed
367 368


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

380
    def rollback(self, activity):
381 382 383
        with activity.sub_activity(
            'rollback_net', readable_name=ugettext_noop(
                "redeploy network (rollback)")):
384 385
            self.instance.deploy_net()

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

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

404
        # Shutdown networks
405 406 407
        with activity.sub_activity(
            'shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
408 409
            self.instance.shutdown_net()

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


420
register_operation(MigrateOperation)
Dudás Ádám committed
421 422


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

432
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
433
        self.instance.reboot_vm(timeout=timeout)
434 435 436
        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
437 438


439
register_operation(RebootOperation)
Dudás Ádám committed
440 441


442 443 444 445
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
446 447 448
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
449
    required_perms = ()
450
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
451

452 453
    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
454 455 456 457
            with activity.sub_activity(
                'detach_network',
                readable_name=ugettext_noop("detach network")
            ):
458
                self.instance.detach_network(interface)
459 460 461 462 463
            interface.shutdown()

        interface.destroy()
        interface.delete()

464 465
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
466
                               vlan=kwargs['interface'].vlan)
467

468

Bach Dániel committed
469
register_operation(RemoveInterfaceOperation)
470 471


472 473 474 475
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
476 477
    description = _("Remove the specified disk from the virtual machine, and "
                    "destroy the data.")
478
    required_perms = ()
479
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
480 481

    def _operation(self, activity, user, system, disk):
482
        if self.instance.is_running and disk.type not in ["iso"]:
483 484 485 486
            with activity.sub_activity(
                'detach_disk',
                readable_name=ugettext_noop('detach disk')
            ):
487
                self.instance.detach_disk(disk)
488 489 490 491 492
        with activity.sub_activity(
            'destroy_disk',
            readable_name=ugettext_noop('destroy disk')
        ):
            return self.instance.disks.remove(disk)
493

494 495 496
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
497

Guba Sándor committed
498
register_operation(RemoveDiskOperation)
499 500


501
class ResetOperation(InstanceOperation):
Dudás Ádám committed
502 503 504
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
505
    description = _("Cold reboot virtual machine (power cycle).")
506
    required_perms = ()
507
    accept_states = ('RUNNING', )
Dudás Ádám committed
508

509
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
510
        self.instance.reset_vm(timeout=timeout)
511 512 513
        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
514

515
register_operation(ResetOperation)
Dudás Ádám committed
516 517


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

531 532 533 534
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

535 536 537 538 539 540
    @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)
541
        else:
542 543
            v = 1
        return "%s v%d" % (name, v)
544

545
    def on_abort(self, activity, error):
546
        if hasattr(self, 'disks'):
547 548 549
            for disk in self.disks:
                disk.destroy()

550
    def _operation(self, activity, user, system, timeout=300, name=None,
551
                   with_shutdown=True, task=None, **kwargs):
552
        if with_shutdown:
553 554
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
555
                                                      user=user, task=task)
556 557 558
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
559 560 561 562 563 564 565 566
        # 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,
567
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
568 569 570 571 572 573 574 575 576
            '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
577
        params.pop("parent_activity", None)
Dudás Ádám committed
578

579 580
        from storage.models import Disk

Dudás Ádám committed
581 582
        def __try_save_disk(disk):
            try:
583
                return disk.save_as(task)
Dudás Ádám committed
584 585 586
            except Disk.WrongDiskTypeError:
                return disk

587
        self.disks = []
588 589 590 591 592 593 594
        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)
            ):
595 596
                self.disks.append(__try_save_disk(disk))

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


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


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

629 630
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
631 632
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
633

634 635 636 637 638 639 640 641 642 643 644
    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
645

646
register_operation(ShutdownOperation)
Dudás Ádám committed
647 648


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

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

Dudás Ádám committed
669 670 671 672 673 674 675
        # 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
676 677


678
register_operation(ShutOffOperation)
Dudás Ádám committed
679 680


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

698 699 700 701
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

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

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

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

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


724
register_operation(SleepOperation)
Dudás Ádám committed
725 726


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

738
    def is_preferred(self):
739
        return self.instance.status == self.instance.STATUS.SUSPENDED
740

Dudás Ádám committed
741
    def on_abort(self, activity, error):
Bach Dániel committed
742 743 744 745
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
746

747
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
748
        # Schedule vm
Dudás Ádám committed
749 750
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
751 752

        # Resume vm
753 754 755
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
756
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
757 758

        # Estabilish network connection (vmdriver)
759 760 761
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
762
            self.instance.deploy_net()
Dudás Ádám committed
763

764 765 766 767
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
768 769


770
register_operation(WakeUpOperation)
771 772


773 774 775 776
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
777 778 779 780
    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.")
781
    acl_level = "operator"
782
    required_perms = ()
783
    concurrency_check = False
784

Őry Máté committed
785
    def _operation(self, activity, lease=None, force=False, save=False):
786 787 788 789 790 791 792 793 794 795 796 797 798
        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
799 800
        if save:
            self.instance.lease = lease
801
        self.instance.save()
802 803 804
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
805 806 807 808 809


register_operation(RenewOperation)


810
class ChangeStateOperation(InstanceOperation):
Guba Sándor committed
811 812
    activity_code_suffix = 'emergency_change_state'
    id = 'emergency_change_state'
813 814 815 816 817 818
    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.")
819
    acl_level = "owner"
Guba Sándor committed
820
    required_perms = ('vm.emergency_change_state', )
821
    concurrency_check = False
822

823
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
824
        activity.resultant_state = new_state
825 826 827 828 829 830 831
        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)
832 833 834 835 836


register_operation(ChangeStateOperation)


837
class NodeOperation(Operation):
838
    async_operation = abortable_async_node_operation
839
    host_cls = Node
840 841 842 843 844

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

845 846
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
847 848 849 850 851 852 853 854 855 856
        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.")

857 858
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
859 860
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
861 862
                                       node=self.node, user=user,
                                       readable_name=name)
863 864 865 866 867 868


class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
869
    description = _("Disable node and move all instances to other ones.")
870
    required_perms = ()
871
    superuser_required = True
872
    async_queue = "localhost.man.slow"
873

874 875 876 877 878 879
    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)

880
    def _operation(self, activity, user):
881
        self.node_enabled = self.node.enabled
882 883
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
884 885 886 887
            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
888
                i.migrate(user=user)
889 890


891
register_operation(FlushOperation)
892 893 894 895 896 897


class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
898 899 900
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
901
    acl_level = "owner"
902
    required_perms = ()
903
    accept_states = ('RUNNING', )
904

Kálmán Viktor committed
905
    def _operation(self):
906 907 908 909
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
Bach Dániel committed
910 911 912 913 914 915


class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
916 917 918
    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
919 920
    acl_level = "owner"
    required_perms = ('vm.recover', )
921
    accept_states = ('DESTROYED', )
922
    resultant_state = 'PENDING'
Bach Dániel committed
923 924

    def check_precond(self):
925 926 927 928
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
929 930 931 932 933 934 935 936 937 938 939

    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)
940 941


942 943 944 945
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
946
    description = _("Change resources of a stopped virtual machine.")
947
    acl_level = "owner"
948
    required_perms = ('vm.change_resources', )
949
    accept_states = ('STOPPED', 'PENDING', )
950

951 952
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
953

954 955 956 957 958
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

959
        self.instance.full_clean()
960 961
        self.instance.save()

962 963 964 965 966 967
        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
        )

968 969

register_operation(ResourcesOperation)
970 971


Őry Máté committed
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
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)


991 992
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
993 994
    id = 'password_reset'
    name = _("password reset")
995 996 997 998 999
    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.")
1000 1001 1002 1003
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
1004 1005 1006 1007 1008
        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()
1009 1010 1011


register_operation(PasswordResetOperation)
1012 1013


1014
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
1015 1016 1017 1018
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
1019
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
1020
        "have access to this machine can see these files as well."
1021
    )
1022 1023 1024
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
1025 1026 1027 1028 1029 1030 1031
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1032
    def _operation(self, user):
1033 1034
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1035
        host = urlsplit(settings.STORE_URL).hostname
1036 1037
        username = Store(user).username
        password = user.profile.smb_password
1038 1039 1040 1041 1042
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))


register_operation(MountStoreOperation)