operations.py 35.9 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
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
Dudás Ádám committed
58

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

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

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

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

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

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

109 110 111 112 113
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

114

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

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

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

154 155 156 157
    def get_activity_name(self, kwargs):
        return create_readable(ugettext_noop("add %(vlan)s interface"),
                               vlan=kwargs['vlan'])

158

Bach Dániel committed
159
register_operation(AddInterfaceOperation)
160 161


162
class CreateDiskOperation(InstanceOperation):
163

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

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

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

185
        if self.instance.is_running:
186 187 188 189
            with activity.sub_activity(
                'deploying_disk',
                readable_name=ugettext_noop("deploying disk")
            ):
190
                disk.deploy()
191 192 193 194
            with activity.sub_activity(
                'attach_disk',
                readable_name=ugettext_noop("attach disk")
            ):
195 196
                self.instance.attach_disk(disk)

197
    def get_activity_name(self, kwargs):
198 199 200
        return create_readable(
            ugettext_noop("create disk %(name)s (%(size)s)"),
            size=filesizeformat(kwargs['size']), name=kwargs['name'])
201 202


203 204 205 206 207 208 209
register_operation(CreateDiskOperation)


class DownloadDiskOperation(InstanceOperation):
    activity_code_suffix = 'download_disk'
    id = 'download_disk'
    name = _("download disk")
210 211 212 213
    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.")
214
    abortable = True
215
    has_percentage = True
216
    required_perms = ('storage.download_disk', )
217
    accept_states = ('STOPPED', 'PENDING', 'RUNNING')
218

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

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

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

242 243 244
register_operation(DownloadDiskOperation)


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

254 255
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
256
                                        self.instance.STATUS.PENDING,
257 258
                                        self.instance.STATUS.ERROR)

259 260 261
    def on_abort(self, activity, error):
        activity.resultant_state = 'STOPPED'

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

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

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

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

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

        # Resume vm
296 297 298
        with activity.sub_activity(
            'booting', readable_name=ugettext_noop(
                "boot virtual machine")):
Dudás Ádám committed
299
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
300

301 302 303 304
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
305 306


307
register_operation(DeployOperation)
Dudás Ádám committed
308 309


310
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
311 312 313
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
314 315
    description = _("Permanently destroy virtual machine, its network "
                    "settings and disks.")
316
    required_perms = ()
Dudás Ádám committed
317 318 319 320

    def on_commit(self, activity):
        activity.resultant_state = 'DESTROYED'

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

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

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

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

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


357
register_operation(DestroyOperation)
Dudás Ádám committed
358 359


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

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

375 376 377 378 379 380
    def check_auth(self, user):
        if not user.is_superuser:
            raise PermissionDenied()

        super(MigrateOperation, self).check_auth(user=user)

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')
Dudás Ádám committed
518

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

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

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

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

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

567 568
        from storage.models import Disk

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

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

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


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


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

Dudás Ádám committed
616 617 618
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

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


625
register_operation(ShutdownOperation)
Dudás Ádám committed
626 627


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', )
Dudás Ádám committed
641

642
    def on_commit(self, activity):
Dudás Ádám committed
643 644
        activity.resultant_state = 'STOPPED'

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

Dudás Ádám committed
650 651 652 653 654 655 656
        # 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
657 658


659
register_operation(ShutOffOperation)
Dudás Ádám committed
660 661


662
class SleepOperation(InstanceOperation):
Dudás Ádám committed
663 664 665
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
666 667 668 669 670 671 672 673
    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.")
674
    required_perms = ()
675
    accept_states = ('RUNNING', )
Dudás Ádám committed
676

677 678 679 680
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
681 682 683 684 685 686 687 688 689
    def on_abort(self, activity, error):
        if isinstance(error, TimeLimitExceeded):
            activity.resultant_state = None
        else:
            activity.resultant_state = 'ERROR'

    def on_commit(self, activity):
        activity.resultant_state = 'SUSPENDED'

690
    def _operation(self, activity, timeout=240):
Dudás Ádám committed
691
        # Destroy networks
692 693
        with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
                "shutdown network")):
Dudás Ádám committed
694
            self.instance.shutdown_net()
Dudás Ádám committed
695 696

        # Suspend vm
697 698 699
        with activity.sub_activity('suspending',
                                   readable_name=ugettext_noop(
                                       "suspend virtual machine")):
Dudás Ádám committed
700 701 702 703
            self.instance.suspend_vm(timeout=timeout)

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


706
register_operation(SleepOperation)
Dudás Ádám committed
707 708


709
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
710 711 712
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
713 714 715
    description = _("Wake up sleeping (suspended) virtual machine. This will "
                    "load the saved memory of the system and start the "
                    "virtual machine from this state.")
716
    required_perms = ()
717
    accept_states = ('SUSPENDED', )
Dudás Ádám committed
718

719
    def is_preferred(self):
720
        return self.instance.status == self.instance.STATUS.SUSPENDED
721

Dudás Ádám committed
722 723 724 725 726 727
    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'

728
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
729
        # Schedule vm
Dudás Ádám committed
730 731
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
732 733

        # Resume vm
734 735 736
        with activity.sub_activity(
            'resuming', readable_name=ugettext_noop(
                "resume virtual machine")):
Dudás Ádám committed
737
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
738 739

        # Estabilish network connection (vmdriver)
740 741 742
        with activity.sub_activity(
            'deploying_net', readable_name=ugettext_noop(
                "deploy network")):
Dudás Ádám committed
743
            self.instance.deploy_net()
Dudás Ádám committed
744

745 746 747 748
        try:
            self.instance.renew(parent_activity=activity)
        except:
            pass
Dudás Ádám committed
749 750


751
register_operation(WakeUpOperation)
752 753


754 755 756 757
class RenewOperation(InstanceOperation):
    activity_code_suffix = 'renew'
    id = 'renew'
    name = _("renew")
758 759 760 761
    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.")
762
    acl_level = "operator"
763
    required_perms = ()
764
    concurrency_check = False
765

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


register_operation(RenewOperation)


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

804
    def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
805
        activity.resultant_state = new_state
806 807 808 809 810 811 812
        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)
813 814 815 816 817


register_operation(ChangeStateOperation)


818
class NodeOperation(Operation):
819
    async_operation = abortable_async_node_operation
820
    host_cls = Node
821 822 823 824 825

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

826 827
    def create_activity(self, parent, user, kwargs):
        name = self.get_activity_name(kwargs)
828 829 830 831 832 833 834 835 836 837
        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.")

838 839
            return parent.create_sub(code_suffix=self.activity_code_suffix,
                                     readable_name=name)
840 841
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
842 843
                                       node=self.node, user=user,
                                       readable_name=name)
844 845 846 847 848 849


class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
850
    description = _("Disable node and move all instances to other ones.")
851
    required_perms = ()
852

853 854 855 856 857 858
    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)

859 860
    def check_auth(self, user):
        if not user.is_superuser:
861 862
            raise humanize_exception(ugettext_noop(
                "Superuser privileges are required."), PermissionDenied())
863 864 865

        super(FlushOperation, self).check_auth(user=user)

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', )
Bach Dániel committed
908 909

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

    def on_commit(self, activity):
        activity.resultant_state = 'PENDING'

    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)
928 929


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

939 940
    def _operation(self, user, activity,
                   num_cores, ram_size, max_ram_size, priority):
941

942 943 944 945 946
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

947
        self.instance.full_clean()
948 949
        self.instance.save()

950 951 952 953 954 955
        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
        )

956 957

register_operation(ResourcesOperation)
958 959


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


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

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


register_operation(PasswordResetOperation)
1000 1001


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

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

1020 1021 1022
    def _operation(self):
        inst = self.instance
        queue = self.instance.get_remote_queue_name("agent")
1023
        host = urlsplit(settings.STORE_URL).hostname
Kálmán Viktor committed
1024 1025
        username = Store(inst.owner).username
        password = inst.owner.profile.smb_password
1026 1027 1028 1029 1030
        agent_tasks.mount_store.apply_async(
            queue=queue, args=(inst.vm_name, host, username, password))


register_operation(MountStoreOperation)