operations.py 21 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
class CreateDiskOperation(InstanceOperation):
    activity_code_suffix = 'create_disk'
    id = 'create_disk'
    name = _("create disk")
    description = _("Create empty disk for the VM.")
108
    required_perms = ('storage.create_empty_disk', )
109 110

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

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

120 121 122
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
123 124 125 126 127 128 129 130 131 132 133
        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
134
    has_percentage = True
135
    required_perms = ('storage.download_disk', )
136 137

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

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

148
        disk = Disk.download(url=url, name=name, task=task)
149 150 151 152 153
        self.instance.disks.add(disk)

register_operation(DownloadDiskOperation)


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

160 161 162 163 164
    def check_precond(self):
        super(DeployOperation, self).check_precond()
        if self.instance.status in ['RUNNING', 'SUSPENDED']:
            raise self.instance.WrongStateError(self.instance)

Dudás Ádám committed
165 166 167
    def on_commit(self, activity):
        activity.resultant_state = 'RUNNING'

168
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
169 170 171
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
172 173 174

        # Deploy virtual images
        with activity.sub_activity('deploying_disks'):
Dudás Ádám committed
175 176 177 178 179 180 181 182 183 184 185 186 187
            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
188

Dudás Ádám committed
189
        self.instance.renew(which='both', base_activity=activity)
Dudás Ádám committed
190 191


192
register_operation(DeployOperation)
Dudás Ádám committed
193 194


195
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
196 197 198
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
199
    description = _("Destroy virtual machine and its networks.")
Dudás Ádám committed
200 201 202 203

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

204
    def _operation(self, activity):
Dudás Ádám committed
205
        if self.instance.node:
Dudás Ádám committed
206 207
            # Destroy networks
            with activity.sub_activity('destroying_net'):
208
                self.instance.shutdown_net()
Dudás Ádám committed
209 210 211 212 213
                self.instance.destroy_net()

            # Delete virtual machine
            with activity.sub_activity('destroying_vm'):
                self.instance.delete_vm()
Dudás Ádám committed
214 215 216

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

Dudás Ádám committed
219 220 221 222 223 224 225 226 227
        # 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
228 229 230 231 232

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


233
register_operation(DestroyOperation)
Dudás Ádám committed
234 235


236
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
237 238 239
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
240
    description = _("Live migrate running VM to another node.")
Dudás Ádám committed
241

242 243 244 245
    def rollback(self, activity):
        with activity.sub_activity('rollback_net'):
            self.instance.deploy_net()

246 247 248 249 250
    def check_precond(self):
        super(MigrateOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

251 252 253 254 255 256
    def check_auth(self, user):
        if not user.is_superuser:
            raise PermissionDenied()

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

257
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
258 259 260 261 262
        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
263 264 265
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
266

267 268 269 270 271 272
        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
273
            raise
Dudás Ádám committed
274

Dudás Ádám committed
275 276 277 278 279
        # 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
280
            self.instance.deploy_net()
Dudás Ádám committed
281 282


283
register_operation(MigrateOperation)
Dudás Ádám committed
284 285


286
class RebootOperation(InstanceOperation):
Dudás Ádám committed
287 288 289
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
290
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
Dudás Ádám committed
291

292 293 294 295 296
    def check_precond(self):
        super(RebootOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

297
    def _operation(self, timeout=5):
Dudás Ádám committed
298
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
299 300


301
register_operation(RebootOperation)
Dudás Ádám committed
302 303


304 305 306 307 308 309 310 311 312 313 314 315 316 317
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
318
register_operation(RemoveInterfaceOperation)
319 320


321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
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
338
register_operation(RemoveDiskOperation)
339 340


341
class ResetOperation(InstanceOperation):
Dudás Ádám committed
342 343 344
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
345
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
346

347 348 349 350 351
    def check_precond(self):
        super(ResetOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

352
    def _operation(self, timeout=5):
Dudás Ádám committed
353
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
354

355
register_operation(ResetOperation)
Dudás Ádám committed
356 357


358
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
359 360 361 362 363 364 365 366
    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.
        """)
367
    abortable = True
368
    required_perms = ('vm.create_template', )
Dudás Ádám committed
369

370 371 372 373 374 375
    @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)
376
        else:
377 378
            v = 1
        return "%s v%d" % (name, v)
379

380 381 382 383 384
    def on_abort(self, activity, error):
        if getattr(self, 'disks'):
            for disk in self.disks:
                disk.destroy()

385 386 387 388 389
    def check_precond(self):
        super(SaveAsTemplateOperation, self).check_precond()
        if self.instance.status not in ['RUNNING', 'PENDING', 'STOPPED']:
            raise self.instance.WrongStateError(self.instance)

390
    def _operation(self, activity, user, system, timeout=300, name=None,
391
                   with_shutdown=True, task=None, **kwargs):
392
        if with_shutdown:
393 394
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
395
                                                      user=user, task=task)
396 397 398
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
399 400 401 402 403 404 405 406
        # 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,
407
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
408 409 410 411 412 413 414 415 416
            '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
417
        params.pop("parent_activity", None)
Dudás Ádám committed
418

419 420
        from storage.models import Disk

Dudás Ádám committed
421 422
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
423
                return disk.save_as()
Dudás Ádám committed
424 425 426
            except Disk.WrongDiskTypeError:
                return disk

427
        self.disks = []
428
        with activity.sub_activity('saving_disks'):
429 430 431 432 433
            for disk in self.instance.disks.all():
                self.disks.append(__try_save_disk(disk))

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

Dudás Ádám committed
435 436 437 438 439
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
440
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
441 442 443 444 445 446 447 448 449 450
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


451
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
452 453


454
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
455 456 457
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
458
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
459
    abortable = True
Dudás Ádám committed
460

461 462 463 464 465
    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
466 467 468
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

469 470
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
471 472
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
473 474


475
register_operation(ShutdownOperation)
Dudás Ádám committed
476 477


478
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
479 480 481
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
482
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
483

484 485 486 487 488
    def check_precond(self):
        super(ShutOffOperation, self).check_precond()
        if self.instance.status not in ['RUNNING']:
            raise self.instance.WrongStateError(self.instance)

489
    def on_commit(self, activity):
Dudás Ádám committed
490 491
        activity.resultant_state = 'STOPPED'

492
    def _operation(self, activity):
Dudás Ádám committed
493 494 495
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
496

Dudás Ádám committed
497 498 499 500 501 502 503
        # 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
504 505


506
register_operation(ShutOffOperation)
Dudás Ádám committed
507 508


509
class SleepOperation(InstanceOperation):
Dudás Ádám committed
510 511 512
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
513
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
514 515

    def check_precond(self):
516
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
517 518 519 520 521 522 523 524 525 526 527 528
        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'

529
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
530
        # Destroy networks
Dudás Ádám committed
531 532
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
533 534 535

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
536 537 538 539
            self.instance.suspend_vm(timeout=timeout)

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


542
register_operation(SleepOperation)
Dudás Ádám committed
543 544


545
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
546 547 548 549 550 551 552 553 554
    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):
555
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
556 557 558 559 560 561 562 563 564
        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'

565
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
566
        # Schedule vm
Dudás Ádám committed
567 568
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
569 570 571

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
572
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
573 574 575

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
576
            self.instance.deploy_net()
Dudás Ádám committed
577 578 579 580 581

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


582
register_operation(WakeUpOperation)
583 584 585


class NodeOperation(Operation):
586
    async_operation = abortable_async_node_operation
587
    host_cls = Node
588 589 590 591 592

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

593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
    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)
608 609 610 611 612 613


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

616
    def _operation(self, activity, user):
617 618 619
        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
620
                i.migrate(user=user)
621 622


623
register_operation(FlushOperation)
624 625 626 627 628 629 630 631 632 633 634 635 636 637


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)

Kálmán Viktor committed
638
    def _operation(self):
639 640 641 642
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)
643 644 645 646 647 648 649 650 651


class ResourcesOperation(InstanceOperation):
    activity_code_suffix = 'Resources change'
    id = 'resources_change'
    name = _("resources change")
    description = _("Change resources")
    acl_level = "owner"
    concurrency_check = False
652
    required_perms = ('vm.change_resources', )
653 654 655

    def check_precond(self):
        super(ResourcesOperation, self).check_precond()
656
        if self.instance.status not in ["STOPPED", "PENDING"]:
657 658
            raise self.instance.WrongStateError(self.instance)

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

661 662 663 664 665 666 667 668 669
        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)