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

18
from __future__ import absolute_import, unicode_literals
Dudás Ádám committed
19
from logging import getLogger
20
from re import search
Őry Máté committed
21
from string import ascii_lowercase
Kálmán Viktor committed
22
from urlparse import urlsplit
Dudás Ádám committed
23

24
from django.core.exceptions import PermissionDenied
Dudás Ádám committed
25
from django.utils import timezone
26
from django.utils.translation import ugettext_lazy as _, ugettext_noop
Kálmán Viktor committed
27
from django.conf import settings
Dudás Ádám committed
28

29 30
from sizefield.utils import filesizeformat

Dudás Ádám committed
31
from celery.exceptions import TimeLimitExceeded
32

33 34 35
from common.models import (
    create_readable, humanize_exception, HumanReadableException
)
36
from common.operations import Operation, register_operation
37 38 39
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
40
from .models import (
41
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
42
    NodeActivity, pwgen
43
)
44
from .tasks import agent_tasks, local_agent_tasks
Dudás Ádám committed
45

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

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


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

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

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

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

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

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

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

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

117

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

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

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

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

162

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


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 208 209 210 211 212 213
register_operation(CreateDiskOperation)


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

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

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

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

247 248 249
register_operation(DownloadDiskOperation)


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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


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


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

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

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

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

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

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


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


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

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


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


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

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

        interface.destroy()
        interface.delete()

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

460

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


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

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

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

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


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

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

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


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

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

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

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

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

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

568 569
        from storage.models import Disk

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

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

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


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


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

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

623 624 625 626 627 628 629 630 631 632 633
    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
634

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


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

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

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


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


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

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

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

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

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

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


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


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

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

Dudás Ádám committed
730 731 732
    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

733
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
734
        # Schedule vm
Dudás Ádám committed
735 736
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
737 738

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

        # Estabilish network connection (vmdriver)
745 746 747
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
748
            self.instance.deploy_net()
Dudás Ádám committed
749

750 751 752 753
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
754 755


756
register_operation(WakeUpOperation)
757 758


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

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


register_operation(RenewOperation)


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

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


register_operation(ChangeStateOperation)


823
class NodeOperation(Operation):
824
    async_operation = abortable_async_node_operation
825
    host_cls = Node
826 827 828 829 830

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

831 832
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
833 834 835 836 837 838 839 840 841 842
        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.")

843 844
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
845 846
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
847 848
                                       node=self.node, user=user,
                                       readable_name=name)
849 850 851 852 853 854


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

860 861 862 863 864 865
    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)

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


877
register_operation(FlushOperation)
878 879 880 881 882 883


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

Kálmán Viktor committed
891
    def _operation(self):
892 893 894 895
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
Bach Dániel committed
896 897 898 899 900 901


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

    def check_precond(self):
911 912 913 914
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
915 916 917 918 919 920 921 922 923 924 925

    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)
926 927


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

937 938
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
939

940 941 942 943 944
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

945
        self.instance.full_clean()
946 947
        self.instance.save()

948 949 950 951 952 953
        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
        )

954 955

register_operation(ResourcesOperation)
956 957


Őry Máté committed
958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976
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)


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

    def _operation(self):
990 991 992 993 994
        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()
995 996 997


register_operation(PasswordResetOperation)
998 999


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

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

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


register_operation(MountStoreOperation)