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

18
from __future__ import absolute_import, unicode_literals
Dudás Ádám committed
19
from logging import getLogger
20
from re import search
Dudás Ádám committed
21

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

from celery.exceptions import TimeLimitExceeded
27

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


logger = getLogger(__name__)
39 40


41
class InstanceOperation(Operation):
42
    acl_level = 'owner'
43
    async_operation = abortable_async_instance_operation
44
    host_cls = Instance
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
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
99
register_operation(AddInterfaceOperation)
100 101


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

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

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

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

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

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

register_operation(DownloadDiskOperation)


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

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

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

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

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


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


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

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

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

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

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

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

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


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


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

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

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

247 248 249 250 251 252
        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
253
            raise
Dudás Ádám committed
254

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


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


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

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


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


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


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


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

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

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


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

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

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

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

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

383 384
        from storage.models import Disk

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

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

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

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


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


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

425 426 427 428 429
    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
430 431 432
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

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


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


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

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

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

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


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


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

    def check_precond(self):
475
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
476 477 478 479 480 481 482 483 484 485 486 487
        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'

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

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

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


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


504
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
505 506 507 508 509 510 511 512 513
    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):
514
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
515 516 517 518 519 520 521 522 523
        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'

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

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

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

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


541
register_operation(WakeUpOperation)
542 543 544


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

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

552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
    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)
567 568 569 570 571 572


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

575
    def _operation(self, activity, user):
576 577 578
        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
579
                i.migrate(user=user)
580 581


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


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)