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

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

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

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

245

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

257 258
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
259
                                        self.instance.STATUS.PENDING,
260 261
                                        self.instance.STATUS.ERROR)

262 263 264
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

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

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

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

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

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

298 299 300 301 302
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass

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

309 310 311
        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
312 313


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

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

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

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

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

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


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

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

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

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

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

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


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

422
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
423
        self.instance.reboot_vm(timeout=timeout)
424 425 426
        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
427 428


429
@register_operation
430 431 432 433
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
434 435 436
    description = _("Remove the specified network interface and erase IP "
                    "address allocations, related firewall rules and "
                    "hostnames.")
437
    required_perms = ()
438
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
439

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

        interface.destroy()
        interface.delete()

452 453
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("remove %(vlan)s interface"),
454
                               vlan=kwargs['interface'].vlan)
455

456

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

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

480 481 482
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop('remove disk %(name)s'),
                               name=kwargs["disk"].name)
483 484


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

494
    def _operation(self, activity, timeout=5):
Dudás Ádám committed
495
        self.instance.reset_vm(timeout=timeout)
496 497 498
        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
499 500


501
@register_operation
502
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
503 504 505
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
506 507 508 509
    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.")
510
    abortable = True
511
    required_perms = ('vm.create_template', )
512
    accept_states = ('RUNNING', 'STOPPED')
513
    async_queue = "localhost.man.slow"
Dudás Ádám committed
514

515 516 517 518
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

519 520 521 522 523 524
    @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)
525
        else:
526 527
            v = 1
        return "%s v%d" % (name, v)
528

529
    def on_abort(self, activity, error):
530
        if hasattr(self, 'disks'):
531 532 533
            for disk in self.disks:
                disk.destroy()

534
    def _operation(self, activity, user, system, timeout=300, name=None,
535
                   with_shutdown=True, task=None, **kwargs):
536
        if with_shutdown:
537 538
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
539
                                                      user=user, task=task)
540 541 542
            except Instance.WrongStateError:
                pass

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

563 564
        from storage.models import Disk

Dudás Ádám committed
565 566
        def __try_save_disk(disk):
            try:
567
                return disk.save_as(task)
Dudás Ádám committed
568 569 570
            except Disk.WrongDiskTypeError:
                return disk

571
        self.disks = []
572 573 574 575 576 577 578
        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)
            ):
579 580
                self.disks.append(__try_save_disk(disk))

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


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

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

615 616 617 618 619 620 621 622 623 624 625
    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
626

627
@register_operation
628
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
629 630 631
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
632 633 634 635 636 637 638
    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.")
639
    required_perms = ()
640
    accept_states = ('RUNNING', )
641
    resultant_state = 'STOPPED'
Dudás Ádám committed
642

643
    def _operation(self, activity):
Dudás Ádám committed
644 645 646
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
647

Dudás Ádám committed
648 649 650 651 652
        # Delete virtual machine
        with activity.sub_activity('delete_vm'):
            self.instance.delete_vm()

        self.instance.yield_node()
Dudás Ádám committed
653 654


655
@register_operation
656
class SleepOperation(InstanceOperation):
Dudás Ádám committed
657 658 659
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
660 661 662 663 664 665 666 667
    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.")
668
    required_perms = ()
669
    accept_states = ('RUNNING', )
670
    resultant_state = 'SUSPENDED'
671
    async_queue = "localhost.man.slow"
Dudás Ádám committed
672

673 674 675 676
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
677 678 679 680 681 682
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

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

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

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


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

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

Dudás Ádám committed
714
    def on_abort(self, activity, error):
Bach Dániel committed
715 716 717 718
        if isinstance(error, SchedulerError):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'
Dudás Ádám committed
719

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

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

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

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


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

Őry Máté committed
756
    def _operation(self, activity, lease=None, force=False, save=False):
757 758 759 760 761 762 763 764 765 766 767 768 769
        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
770 771
        if save:
            self.instance.lease = lease
772
        self.instance.save()
773 774 775
        activity.result = create_readable(ugettext_noop(
            "Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
            suspend=suspend, delete=delete)
776 777


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

792
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
793
        activity.resultant_state = new_state
794 795 796 797 798 799 800
        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)
801 802


803
class NodeOperation(Operation):
804
    async_operation = abortable_async_node_operation
805
    host_cls = Node
806 807
    online_required = True
    superuser_required = True
808 809 810 811 812

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

813 814 815 816 817 818 819
    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())

820 821
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
822 823 824 825 826 827 828 829 830 831
        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.")

832 833
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
834 835
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
836 837
                                       node=self.node, user=user,
                                       readable_name=name)
838 839


840
@register_operation
841 842 843 844
class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
845
    description = _("Passivate node and move all instances to other ones.")
846
    required_perms = ()
847
    async_queue = "localhost.man.slow"
848

849
    def _operation(self, activity, user):
850 851 852
        if self.node.schedule_enabled:
            PassivateOperation(self.node).call(parent_activity=activity,
                                               user=user)
853
        for i in self.node.instance_set.all():
854 855 856 857
            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
858
                i.migrate(user=user)
859 860


861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 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 924 925 926
@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

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

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


929
@register_operation
930 931 932 933
class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
934 935 936
    description = _("Get a screenshot about the virtual machine's console. A "
                    "key will be pressed on the keyboard to stop "
                    "screensaver.")
937
    acl_level = "owner"
938
    required_perms = ()
939
    accept_states = ('RUNNING', )
940

Kálmán Viktor committed
941
    def _operation(self):
942 943 944
        return self.instance.get_screenshot(timeout=20)


945
@register_operation
Bach Dániel committed
946 947 948 949
class RecoverOperation(InstanceOperation):
    activity_code_suffix = 'recover'
    id = 'recover'
    name = _("recover")
950 951 952
    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
953 954
    acl_level = "owner"
    required_perms = ('vm.recover', )
955
    accept_states = ('DESTROYED', )
956
    resultant_state = 'PENDING'
Bach Dániel committed
957 958

    def check_precond(self):
959 960 961 962
        try:
            super(RecoverOperation, self).check_precond()
        except Instance.InstanceDestroyedError:
            pass
Bach Dániel committed
963 964 965 966 967 968 969 970 971 972

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


973
@register_operation
974 975 976 977
class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
978
    description = _("Change resources of a stopped virtual machine.")
979
    acl_level = "owner"
980
    required_perms = ('vm.change_resources', )
981
    accept_states = ('STOPPED', 'PENDING', )
982

983 984
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
985

986 987 988 989 990
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

991
        self.instance.full_clean()
992 993
        self.instance.save()

994 995 996 997 998 999
        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
        )

1000

Őry Máté committed
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
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)


1020
@register_operation
1021 1022
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
    activity_code_suffix = 'password_reset'
1023 1024
    id = 'password_reset'
    name = _("password reset")
1025 1026 1027 1028 1029
    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.")
1030 1031 1032 1033
    acl_level = "owner"
    required_perms = ()

    def _operation(self):
1034 1035 1036 1037 1038
        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()
1039 1040


1041
@register_operation
1042
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
1043 1044 1045 1046
    activity_code_suffix = 'mount_store'
    id = 'mount_store'
    name = _("mount store")
    description = _(
1047
        "This operation attaches your personal file store. Other users who "
Őry Máté committed
1048
        "have access to this machine can see these files as well."
1049
    )
1050 1051 1052
    acl_level = "owner"
    required_perms = ()

Kálmán Viktor committed
1053 1054 1055 1056 1057 1058 1059
    def check_auth(self, user):
        super(MountStoreOperation, self).check_auth(user)
        try:
            Store(user)
        except NoStoreException:
            raise PermissionDenied  # not show the button at all

1060
    def _operation(self, user):
1061 1062
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1063
        host = urlsplit(settings.STORE_URL).hostname
1064 1065
        username = Store(user).username
        password = user.profile.smb_password
1066 1067
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))