operations.py 18.2 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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
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):
        super(AddDiskOperation, self).check_precond()
        # TODO remove check when hot-attach is implemented
        if self.instance.status not in ['STOPPED']:
            raise self.instance.WrongStateError(self.instance)

    def _operation(self, user, size):
        # TODO implement with hot-attach when it'll be available
        disk = Disk.create(owner=user, size=size)
        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

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

    def _operation(self, user, url):
        # TODO implement with hot-attach when it'll be available
        disk = Disk.download(owner=user, url=url)
        self.instance.disks.add(disk)

register_operation(DownloadDiskOperation)


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

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

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

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

Dudás Ádám committed
174
        self.instance.renew(which='both', base_activity=activity)
Dudás Ádám committed
175 176


177
register_operation(DeployOperation)
Dudás Ádám committed
178 179


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

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

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

            # Delete virtual machine
            with activity.sub_activity('destroying_vm'):
                self.instance.delete_vm()
Dudás Ádám committed
199 200 201

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

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

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


218
register_operation(DestroyOperation)
Dudás Ádám committed
219 220


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

227 228 229 230
    def rollback(self, activity):
        with activity.sub_activity('rollback_net'):
            self.instance.deploy_net()

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

241 242 243 244 245 246
        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
247
            raise
Dudás Ádám committed
248

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


257
register_operation(MigrateOperation)
Dudás Ádám committed
258 259


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

266
    def _operation(self, timeout=5):
Dudás Ádám committed
267
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
268 269


270
register_operation(RebootOperation)
Dudás Ádám committed
271 272


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


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


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

316
    def _operation(self, timeout=5):
Dudás Ádám committed
317
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
318

319
register_operation(ResetOperation)
Dudás Ádám committed
320 321


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

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

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

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

372 373
        from storage.models import Disk

Dudás Ádám committed
374 375
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
376
                return disk.save_as()
Dudás Ádám committed
377 378 379
            except Disk.WrongDiskTypeError:
                return disk

380 381 382 383
        with activity.sub_activity('saving_disks'):
            disks = [__try_save_disk(disk)
                     for disk in self.instance.disks.all()]

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


400
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
401 402


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

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

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


424
register_operation(ShutdownOperation)
Dudás Ádám committed
425 426


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

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

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

Dudás Ádám committed
441 442 443 444 445 446 447
        # 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
448 449


450
register_operation(ShutOffOperation)
Dudás Ádám committed
451 452


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

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

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

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

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


486
register_operation(SleepOperation)
Dudás Ádám committed
487 488


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

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

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

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

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


526
register_operation(WakeUpOperation)
527 528 529


class NodeOperation(Operation):
530
    async_operation = abortable_async_node_operation
531
    host_cls = Node
532 533 534 535 536

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

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


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

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


567
register_operation(FlushOperation)
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586


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)