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

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

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

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

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

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,
                user=user)
77

78 79 80 81 82
    def is_preferred(self):
        """If this is the recommended op in the current state of the instance.
        """
        return False

83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
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
104
register_operation(AddInterfaceOperation)
105 106


107 108 109 110 111 112 113
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):
114
        super(CreateDiskOperation, self).check_precond()
115
        # TODO remove check when hot-attach is implemented
116
        if self.instance.status not in ['STOPPED', 'PENDING']:
117 118
            raise self.instance.WrongStateError(self.instance)

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

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

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

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

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

register_operation(DownloadDiskOperation)


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

162 163 164 165
    def is_preferred(self):
        return self.instance.status in (self.instance.STATUS.STOPPED,
                                        self.instance.STATUS.ERROR)

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

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

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

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


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


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

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

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

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

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

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

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


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


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

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

247
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
248 249 250 251 252
        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
253 254 255
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
256

257 258 259 260 261 262
        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
263
            raise
Dudás Ádám committed
264

Dudás Ádám committed
265 266 267 268 269
        # 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
270
            self.instance.deploy_net()
Dudás Ádám committed
271 272


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


276
class RebootOperation(InstanceOperation):
Dudás Ádám committed
277 278 279
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
280
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
Dudás Ádám committed
281

282
    def _operation(self, timeout=5):
Dudás Ádám committed
283
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
284 285


286
register_operation(RebootOperation)
Dudás Ádám committed
287 288


289 290 291 292 293 294 295 296 297 298 299 300 301 302
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
303
register_operation(RemoveInterfaceOperation)
304 305


306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
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
323
register_operation(RemoveDiskOperation)
324 325


326
class ResetOperation(InstanceOperation):
Dudás Ádám committed
327 328 329
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
330
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
331

332
    def _operation(self, timeout=5):
Dudás Ádám committed
333
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
334

335
register_operation(ResetOperation)
Dudás Ádám committed
336 337


338
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
339 340 341 342 343 344 345 346
    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.
        """)
347
    abortable = True
Dudás Ádám committed
348

349 350 351 352
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

353 354 355 356 357 358
    @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)
359
        else:
360 361
            v = 1
        return "%s v%d" % (name, v)
362

363 364 365 366 367
    def on_abort(self, activity, error):
        if getattr(self, 'disks'):
            for disk in self.disks:
                disk.destroy()

368
    def _operation(self, activity, user, system, timeout=300, name=None,
369
                   with_shutdown=True, task=None, **kwargs):
370
        if with_shutdown:
371 372
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
373
                                                      user=user, task=task)
374 375 376
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
377 378 379 380 381 382 383 384
        # 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,
385
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
386 387 388 389 390 391 392 393 394
            '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
395
        params.pop("parent_activity", None)
Dudás Ádám committed
396

397 398
        from storage.models import Disk

Dudás Ádám committed
399 400
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
401
                return disk.save_as()
Dudás Ádám committed
402 403 404
            except Disk.WrongDiskTypeError:
                return disk

405
        self.disks = []
406
        with activity.sub_activity('saving_disks'):
407 408 409 410 411
            for disk in self.instance.disks.all():
                self.disks.append(__try_save_disk(disk))

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

Dudás Ádám committed
413 414 415 416 417
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
418
            tmpl.disks.add(*self.disks)
Dudás Ádám committed
419 420 421 422 423 424 425 426 427 428
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


429
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
430 431


432
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
433 434 435
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
436
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
437
    abortable = True
Dudás Ádám committed
438

439 440 441 442 443
    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
444 445 446
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

447 448
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
449 450
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
451 452


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


456
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
457 458 459
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
460
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
461

462
    def on_commit(self, activity):
Dudás Ádám committed
463 464
        activity.resultant_state = 'STOPPED'

465
    def _operation(self, activity):
Dudás Ádám committed
466 467 468
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
469

Dudás Ádám committed
470 471 472 473 474 475 476
        # 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
477 478


479
register_operation(ShutOffOperation)
Dudás Ádám committed
480 481


482
class SleepOperation(InstanceOperation):
Dudás Ádám committed
483 484 485
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
486
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
487

488 489 490 491
    def is_preferred(self):
        return (not self.instance.is_base and
                self.instance.status == self.instance.STATUS.RUNNING)

Dudás Ádám committed
492
    def check_precond(self):
493
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
494 495 496 497 498 499 500 501 502 503 504 505
        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'

506
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
507
        # Destroy networks
Dudás Ádám committed
508 509
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
510 511 512

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
513 514 515 516
            self.instance.suspend_vm(timeout=timeout)

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


519
register_operation(SleepOperation)
Dudás Ádám committed
520 521


522
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
523 524 525 526 527 528 529 530
    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.
        """)

531 532 533 534
    def is_preferred(self):
        return (self.instance.is_base and
                self.instance.status == self.instance.STATUS.SUSPENDED)

Dudás Ádám committed
535
    def check_precond(self):
536
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
537 538 539 540 541 542 543 544 545
        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'

546
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
547
        # Schedule vm
Dudás Ádám committed
548 549
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
550 551 552

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
553
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
554 555 556

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
557
            self.instance.deploy_net()
Dudás Ádám committed
558 559 560 561 562

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


563
register_operation(WakeUpOperation)
564 565 566


class NodeOperation(Operation):
567
    async_operation = abortable_async_node_operation
568
    host_cls = Node
569 570 571 572 573

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

574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
    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)
589 590 591 592 593 594


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

597
    def _operation(self, activity, user):
598 599 600
        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
601
                i.migrate(user=user)
602 603


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


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
619
    def _operation(self):
620 621 622 623
        return self.instance.get_screenshot(timeout=20)


register_operation(ScreenshotOperation)