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

22
from django.core.exceptions import PermissionDenied
Dudás Ádám committed
23 24 25 26
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from celery.exceptions import TimeLimitExceeded
27

28
from common.operations import Operation, register_operation
29 30 31
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
32
from .models import (
33 34
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
    NodeActivity,
35
)
Dudás Ádám committed
36 37 38


logger = getLogger(__name__)
39 40


41
class InstanceOperation(Operation):
42
    acl_level = 'owner'
43
    async_operation = abortable_async_instance_operation
44
    host_cls = Instance
45
    concurrency_check = True
Dudás Ádám committed
46

47
    def __init__(self, instance):
48
        super(InstanceOperation, self).__init__(subject=instance)
49 50 51
        self.instance = instance

    def check_precond(self):
52 53
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
54 55

    def check_auth(self, user):
56 57 58 59
        if not self.instance.has_level(user, self.acl_level):
            raise PermissionDenied("%s doesn't have the required ACL level." %
                                   user)

60
        super(InstanceOperation, self).check_auth(user=user)
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
    def create_activity(self, parent, user):
        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.")

            return parent.create_sub(code_suffix=self.activity_code_suffix)
        else:
            return InstanceActivity.create(
                code_suffix=self.activity_code_suffix, instance=self.instance,
77
                user=user, concurrency_check=self.concurrency_check)
78

79

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
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.")

    def _operation(self, activity, user, system, vlan, managed=None):
        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:
            net.deploy()

        return net


Bach Dániel committed
100
register_operation(AddInterfaceOperation)
101 102


103 104 105 106 107 108 109
class CreateDiskOperation(InstanceOperation):
    activity_code_suffix = 'create_disk'
    id = 'create_disk'
    name = _("create disk")
    description = _("Create empty disk for the VM.")

    def check_precond(self):
110
        super(CreateDiskOperation, self).check_precond()
111
        # TODO remove check when hot-attach is implemented
112
        if self.instance.status not in ['STOPPED', 'PENDING']:
113 114
            raise self.instance.WrongStateError(self.instance)

115
    def _operation(self, user, size, name=None):
116
        # TODO implement with hot-attach when it'll be available
Bach Dániel committed
117 118
        from storage.models import Disk

119 120 121
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
122 123 124 125 126 127 128 129 130 131 132
        self.instance.disks.add(disk)

register_operation(CreateDiskOperation)


class DownloadDiskOperation(InstanceOperation):
    activity_code_suffix = 'download_disk'
    id = 'download_disk'
    name = _("download disk")
    description = _("Download disk for the VM.")
    abortable = True
133
    has_percentage = True
134 135

    def check_precond(self):
136
        super(DownloadDiskOperation, self).check_precond()
137
        # TODO remove check when hot-attach is implemented
138
        if self.instance.status not in ['STOPPED', 'PENDING']:
139 140
            raise self.instance.WrongStateError(self.instance)

141
    def _operation(self, user, url, task, name=None):
142
        # TODO implement with hot-attach when it'll be available
Bach Dániel committed
143 144
        from storage.models import Disk

145
        disk = Disk.download(url=url, name=name, task=task)
146 147 148 149 150
        self.instance.disks.add(disk)

register_operation(DownloadDiskOperation)


151
class DeployOperation(InstanceOperation):
Dudás Ádám committed
152 153 154
    activity_code_suffix = 'deploy'
    id = 'deploy'
    name = _("deploy")
155
    description = _("Deploy new virtual machine with network.")
Dudás Ádám committed
156 157 158 159

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

160
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
161 162 163
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
164 165 166

        # Deploy virtual images
        with activity.sub_activity('deploying_disks'):
Dudás Ádám committed
167 168 169 170 171 172 173 174 175 176 177 178 179
            self.instance.deploy_disks()

        # Deploy VM on remote machine
        with activity.sub_activity('deploying_vm') as deploy_act:
            deploy_act.result = self.instance.deploy_vm(timeout=timeout)

        # Establish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
            self.instance.deploy_net()

        # Resume vm
        with activity.sub_activity('booting'):
            self.instance.resume_vm(timeout=timeout)
Dudás Ádám committed
180

Dudás Ádám committed
181
        self.instance.renew(which='both', base_activity=activity)
Dudás Ádám committed
182 183


184
register_operation(DeployOperation)
Dudás Ádám committed
185 186


187
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
188 189 190
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
191
    description = _("Destroy virtual machine and its networks.")
Dudás Ádám committed
192 193 194 195

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

196
    def _operation(self, activity):
Dudás Ádám committed
197
        if self.instance.node:
Dudás Ádám committed
198 199
            # Destroy networks
            with activity.sub_activity('destroying_net'):
200
                self.instance.shutdown_net()
Dudás Ádám committed
201 202 203 204 205
                self.instance.destroy_net()

            # Delete virtual machine
            with activity.sub_activity('destroying_vm'):
                self.instance.delete_vm()
Dudás Ádám committed
206 207 208

        # Destroy disks
        with activity.sub_activity('destroying_disks'):
Dudás Ádám committed
209
            self.instance.destroy_disks()
Dudás Ádám committed
210

Dudás Ádám committed
211 212 213 214 215 216 217 218 219
        # 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
220 221 222 223 224

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


225
register_operation(DestroyOperation)
Dudás Ádám committed
226 227


228
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
229 230 231
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
232
    description = _("Live migrate running VM to another node.")
Dudás Ádám committed
233

234 235 236 237
    def rollback(self, activity):
        with activity.sub_activity('rollback_net'):
            self.instance.deploy_net()

238
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
239 240 241 242 243
        if not to_node:
            with activity.sub_activity('scheduling') as sa:
                to_node = self.instance.select_node()
                sa.result = to_node

Dudás Ádám committed
244 245 246
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
247

248 249 250 251 252 253
        try:
            with activity.sub_activity('migrate_vm'):
                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
254
            raise
Dudás Ádám committed
255

Dudás Ádám committed
256 257 258 259 260
        # Refresh node information
        self.instance.node = to_node
        self.instance.save()
        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
261
            self.instance.deploy_net()
Dudás Ádám committed
262 263


264
register_operation(MigrateOperation)
Dudás Ádám committed
265 266


267
class RebootOperation(InstanceOperation):
Dudás Ádám committed
268 269 270
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
271
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
Dudás Ádám committed
272

273
    def _operation(self, timeout=5):
Dudás Ádám committed
274
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
275 276


277
register_operation(RebootOperation)
Dudás Ádám committed
278 279


280 281 282 283 284 285 286 287 288 289 290 291 292 293
class RemoveInterfaceOperation(InstanceOperation):
    activity_code_suffix = 'remove_interface'
    id = 'remove_interface'
    name = _("remove interface")
    description = _("Remove the specified network interface from the VM.")

    def _operation(self, activity, user, system, interface):
        if self.instance.is_running:
            interface.shutdown()

        interface.destroy()
        interface.delete()


Bach Dániel committed
294
register_operation(RemoveInterfaceOperation)
295 296


297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
class RemoveDiskOperation(InstanceOperation):
    activity_code_suffix = 'remove_disk'
    id = 'remove_disk'
    name = _("remove disk")
    description = _("Remove the specified disk from the VM.")

    def check_precond(self):
        super(RemoveDiskOperation, self).check_precond()
        # TODO remove check when hot-detach is implemented
        if self.instance.status not in ['STOPPED']:
            raise self.instance.WrongStateError(self.instance)

    def _operation(self, activity, user, system, disk):
        # TODO implement with hot-detach when it'll be available
        return self.instance.disks.remove(disk)


Guba Sándor committed
314
register_operation(RemoveDiskOperation)
315 316


317
class ResetOperation(InstanceOperation):
Dudás Ádám committed
318 319 320
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
321
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
322

323
    def _operation(self, timeout=5):
Dudás Ádám committed
324
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
325

326
register_operation(ResetOperation)
Dudás Ádám committed
327 328


329
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
330 331 332 333 334 335 336 337
    activity_code_suffix = 'save_as_template'
    id = 'save_as_template'
    name = _("save as template")
    description = _("""Save Virtual Machine as a Template.

        Template can be shared with groups and users.
        Users can instantiate Virtual Machines from Templates.
        """)
338
    abortable = True
Dudás Ádám committed
339

340 341 342 343 344 345
    @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)
346
        else:
347 348
            v = 1
        return "%s v%d" % (name, v)
349

350 351 352 353 354
    def on_abort(self, activity, error):
        if getattr(self, 'disks'):
            for disk in self.disks:
                disk.destroy()

355
    def _operation(self, activity, user, system, timeout=300, name=None,
356
                   with_shutdown=True, task=None, **kwargs):
357
        if with_shutdown:
358 359
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
360
                                                      user=user, task=task)
361 362 363
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
364 365 366 367 368 369 370 371
        # 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,
372
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
373 374 375 376 377 378 379 380 381
            '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
382
        params.pop("parent_activity", None)
Dudás Ádám committed
383

384 385
        from storage.models import Disk

Dudás Ádám committed
386 387
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
388
                return disk.save_as()
Dudás Ádám committed
389 390 391
            except Disk.WrongDiskTypeError:
                return disk

392
        self.disks = []
393
        with activity.sub_activity('saving_disks'):
394 395 396 397 398
            for disk in self.instance.disks.all():
                self.disks.append(__try_save_disk(disk))

        for disk in self.disks:
            disk.set_level(user, 'owner')
399

Dudás Ádám committed
400 401 402 403 404
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
405
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
406 407 408 409 410 411 412 413 414 415
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


416
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
417 418


419
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
420 421 422
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
423
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
424
    abortable = True
Dudás Ádám committed
425

426 427 428 429 430
    def check_precond(self):
        super(ShutdownOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

Dudás Ádám committed
431 432 433
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

434 435
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
436 437
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
438 439


440
register_operation(ShutdownOperation)
Dudás Ádám committed
441 442


443
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
444 445 446
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
447
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
448

449
    def on_commit(self, activity):
Dudás Ádám committed
450 451
        activity.resultant_state = 'STOPPED'

452
    def _operation(self, activity):
Dudás Ádám committed
453 454 455
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
456

Dudás Ádám committed
457 458 459 460 461 462 463
        # 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
464 465


466
register_operation(ShutOffOperation)
Dudás Ádám committed
467 468


469
class SleepOperation(InstanceOperation):
Dudás Ádám committed
470 471 472
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
473
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
474 475

    def check_precond(self):
476
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
477 478 479 480 481 482 483 484 485 486 487 488
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

    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'

489
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
490
        # Destroy networks
Dudás Ádám committed
491 492
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
493 494 495

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
496 497 498 499
            self.instance.suspend_vm(timeout=timeout)

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


502
register_operation(SleepOperation)
Dudás Ádám committed
503 504


505
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
506 507 508 509 510 511 512 513 514
    activity_code_suffix = 'wake_up'
    id = 'wake_up'
    name = _("wake up")
    description = _("""Wake up Virtual Machine from SUSPENDED state.

        Power on Virtual Machine and load its memory from dump.
        """)

    def check_precond(self):
515
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
516 517 518 519 520 521 522 523 524
        if self.instance.status not in ['SUSPENDED']:
            raise self.instance.WrongStateError(self.instance)

    def on_abort(self, activity, error):
        activity.resultant_state = 'ERROR'

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

525
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
526
        # Schedule vm
Dudás Ádám committed
527 528
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
529 530 531

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
532
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
533 534 535

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
536
            self.instance.deploy_net()
Dudás Ádám committed
537 538 539 540 541

        # Renew vm
        self.instance.renew(which='both', base_activity=activity)


542
register_operation(WakeUpOperation)
543 544 545


class NodeOperation(Operation):
546
    async_operation = abortable_async_node_operation
547
    host_cls = Node
548 549 550 551 552

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

553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
    def create_activity(self, parent, user):
        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.")

            return parent.create_sub(code_suffix=self.activity_code_suffix)
        else:
            return NodeActivity.create(code_suffix=self.activity_code_suffix,
                                       node=self.node, user=user)
568 569 570 571 572 573


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

576
    def _operation(self, activity, user):
577 578 579
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
            with activity.sub_activity('migrate_instance_%d' % i.pk):
Bach Dániel committed
580
                i.migrate(user=user)
581 582


583
register_operation(FlushOperation)
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602


class ScreenshotOperation(InstanceOperation):
    activity_code_suffix = 'screenshot'
    id = 'screenshot'
    name = _("screenshot")
    description = _("Get screenshot")
    acl_level = "owner"

    def check_precond(self):
        super(ScreenshotOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

    def _operation(self, instance, user):
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617


class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
    description = _("Change resources")
    acl_level = "owner"
    concurrency_check = False

    def check_precond(self):
        super(ResourcesOperation, self).check_precond()
        if self.instance.status in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

618
    def check_auth(self, user):
619 620 621
        if not user.has_perm('vm.change_resources'):
            raise PermissionDenied()

622 623 624 625
        super(InstanceOperation, self).check_auth(user=user)

    def _operation(self, user, num_cores, ram_size, max_ram_size, priority):

626 627 628 629 630 631 632 633 634
        self.instance.num_cores = num_cores
        self.instance.ram_size = ram_size
        self.instance.max_ram_size = max_ram_size
        self.instance.priority = priority

        self.instance.save()


register_operation(ResourcesOperation)