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

18
from __future__ import absolute_import, unicode_literals
Dudás Ádám committed
19
from logging import getLogger
20
from re import search
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
from storage.models import Disk
30 31 32
from .tasks.local_tasks import (
    abortable_async_instance_operation, abortable_async_node_operation,
)
33
from .models import (
34 35
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
    NodeActivity,
36
)
Dudás Ádám committed
37 38 39


logger = getLogger(__name__)
40 41


42
class InstanceOperation(Operation):
43
    acl_level = 'owner'
44
    async_operation = abortable_async_instance_operation
45
    host_cls = Instance
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 77
    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,
                user=user)
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 112 113 114
        # TODO remove check when hot-attach is implemented
        if self.instance.status not in ['STOPPED']:
            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
117 118 119
        if not name:
            name = "new disk"
        disk = Disk.create(size=size, name=name, type="qcow2-norm")
120 121 122 123 124 125 126 127 128 129 130
        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
131
    has_percentage = True
132 133

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

139
    def _operation(self, user, url, task, name=None):
140
        # TODO implement with hot-attach when it'll be available
141
        disk = Disk.download(url=url, name=name, task=task)
142 143 144 145 146
        self.instance.disks.add(disk)

register_operation(DownloadDiskOperation)


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

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

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

        # Deploy virtual images
        with activity.sub_activity('deploying_disks'):
Dudás Ádám committed
163 164 165 166 167 168 169 170 171 172 173 174 175
            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
176

Dudás Ádám committed
177
        self.instance.renew(which='both', base_activity=activity)
Dudás Ádám committed
178 179


180
register_operation(DeployOperation)
Dudás Ádám committed
181 182


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

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

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

            # Delete virtual machine
            with activity.sub_activity('destroying_vm'):
                self.instance.delete_vm()
Dudás Ádám committed
202 203 204

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

Dudás Ádám committed
207 208 209 210 211 212 213 214 215
        # 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
216 217 218 219 220

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


221
register_operation(DestroyOperation)
Dudás Ádám committed
222 223


224
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
225 226 227
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
228
    description = _("Live migrate running VM to another node.")
Dudás Ádám committed
229

230 231 232 233
    def rollback(self, activity):
        with activity.sub_activity('rollback_net'):
            self.instance.deploy_net()

234
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
235 236 237 238 239
        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
240 241 242
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
243

244 245 246 247 248 249
        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
250
            raise
Dudás Ádám committed
251

Dudás Ádám committed
252 253 254 255 256
        # 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
257
            self.instance.deploy_net()
Dudás Ádám committed
258 259


260
register_operation(MigrateOperation)
Dudás Ádám committed
261 262


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

269
    def _operation(self, timeout=5):
Dudás Ádám committed
270
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
271 272


273
register_operation(RebootOperation)
Dudás Ádám committed
274 275


276 277 278 279 280 281 282 283 284 285 286 287 288 289
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
290
register_operation(RemoveInterfaceOperation)
291 292


293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
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
310
register_operation(RemoveDiskOperation)
311 312


313
class ResetOperation(InstanceOperation):
Dudás Ádám committed
314 315 316
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
317
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
318

319
    def _operation(self, timeout=5):
Dudás Ádám committed
320
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
321

322
register_operation(ResetOperation)
Dudás Ádám committed
323 324


325
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
326 327 328 329 330 331 332 333
    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.
        """)
334
    abortable = True
Dudás Ádám committed
335

336 337 338 339 340 341
    @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)
342
        else:
343 344
            v = 1
        return "%s v%d" % (name, v)
345

346
    def _operation(self, activity, user, system, timeout=300, name=None,
347
                   with_shutdown=True, task=None, **kwargs):
348
        if with_shutdown:
349 350
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
351
                                                      user=user, task=task)
352 353 354
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
355 356 357 358 359 360 361 362
        # 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,
363
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
364 365 366 367 368 369 370 371 372
            '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
373
        params.pop("parent_activity", None)
Dudás Ádám committed
374

375 376
        from storage.models import Disk

Dudás Ádám committed
377 378
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
379
                return disk.save_as()
Dudás Ádám committed
380 381 382
            except Disk.WrongDiskTypeError:
                return disk

383 384 385 386
        with activity.sub_activity('saving_disks'):
            disks = [__try_save_disk(disk)
                     for disk in self.instance.disks.all()]

Dudás Ádám committed
387 388 389 390 391
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
392
            tmpl.disks.add(*disks)
Dudás Ádám committed
393 394 395 396 397 398 399 400 401 402
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


403
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
404 405


406
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
407 408 409
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
410
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
411
    abortable = True
Dudás Ádám committed
412

413 414 415 416 417
    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
418 419 420
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

421 422
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
423 424
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
425 426


427
register_operation(ShutdownOperation)
Dudás Ádám committed
428 429


430
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
431 432 433
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
434
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
435

436
    def on_commit(self, activity):
Dudás Ádám committed
437 438
        activity.resultant_state = 'STOPPED'

439
    def _operation(self, activity):
Dudás Ádám committed
440 441 442
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
443

Dudás Ádám committed
444 445 446 447 448 449 450
        # 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
451 452


453
register_operation(ShutOffOperation)
Dudás Ádám committed
454 455


456
class SleepOperation(InstanceOperation):
Dudás Ádám committed
457 458 459
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
460
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
461 462

    def check_precond(self):
463
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
464 465 466 467 468 469 470 471 472 473 474 475
        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'

476
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
477
        # Destroy networks
Dudás Ádám committed
478 479
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
480 481 482

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
483 484 485 486
            self.instance.suspend_vm(timeout=timeout)

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


489
register_operation(SleepOperation)
Dudás Ádám committed
490 491


492
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
493 494 495 496 497 498 499 500 501
    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):
502
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
503 504 505 506 507 508 509 510 511
        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'

512
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
513
        # Schedule vm
Dudás Ádám committed
514 515
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
516 517 518

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
519
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
520 521 522

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
523
            self.instance.deploy_net()
Dudás Ádám committed
524 525 526 527 528

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


529
register_operation(WakeUpOperation)
530 531 532


class NodeOperation(Operation):
533
    async_operation = abortable_async_node_operation
534
    host_cls = Node
535 536 537 538 539

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

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
    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)
555 556 557 558 559 560


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

563
    def _operation(self, activity, user):
564 565 566
        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
567
                i.migrate(user=user)
568 569


570
register_operation(FlushOperation)
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589


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)