operations.py 16.9 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 109 110 111 112 113 114 115 116 117 118
class AddDiskOperation(InstanceOperation):
    activity_code_suffix = 'add_disk'
    id = 'add_disk'
    name = _("add disk")
    description = _("Add the specified disk to 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, activity, user, system, disk):
        # TODO implement with hot-attach when it'll be available
        return self.instance.disks.add(disk)


Guba Sándor committed
119
register_operation(AddDiskOperation)
120 121


122
class DeployOperation(InstanceOperation):
Dudás Ádám committed
123 124 125
    activity_code_suffix = 'deploy'
    id = 'deploy'
    name = _("deploy")
126
    description = _("Deploy new virtual machine with network.")
Dudás Ádám committed
127 128 129 130

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

131
    def _operation(self, activity, timeout=15):
Dudás Ádám committed
132 133 134
        # Allocate VNC port and host node
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
135 136 137

        # Deploy virtual images
        with activity.sub_activity('deploying_disks'):
Dudás Ádám committed
138 139 140 141 142 143 144 145 146 147 148 149 150
            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
151

Dudás Ádám committed
152
        self.instance.renew(which='both', base_activity=activity)
Dudás Ádám committed
153 154


155
register_operation(DeployOperation)
Dudás Ádám committed
156 157


158
class DestroyOperation(InstanceOperation):
Dudás Ádám committed
159 160 161
    activity_code_suffix = 'destroy'
    id = 'destroy'
    name = _("destroy")
162
    description = _("Destroy virtual machine and its networks.")
Dudás Ádám committed
163 164 165 166

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

167
    def _operation(self, activity):
Dudás Ádám committed
168
        if self.instance.node:
Dudás Ádám committed
169 170
            # Destroy networks
            with activity.sub_activity('destroying_net'):
171
                self.instance.shutdown_net()
Dudás Ádám committed
172 173 174 175 176
                self.instance.destroy_net()

            # Delete virtual machine
            with activity.sub_activity('destroying_vm'):
                self.instance.delete_vm()
Dudás Ádám committed
177 178 179

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

Dudás Ádám committed
182 183 184 185 186 187 188 189 190
        # 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
191 192 193 194 195

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


196
register_operation(DestroyOperation)
Dudás Ádám committed
197 198


199
class MigrateOperation(InstanceOperation):
Dudás Ádám committed
200 201 202
    activity_code_suffix = 'migrate'
    id = 'migrate'
    name = _("migrate")
203
    description = _("Live migrate running VM to another node.")
Dudás Ádám committed
204

205 206 207 208
    def rollback(self, activity):
        with activity.sub_activity('rollback_net'):
            self.instance.deploy_net()

209
    def _operation(self, activity, to_node=None, timeout=120):
Dudás Ádám committed
210 211 212 213 214
        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
215 216 217
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
218

219 220 221 222 223 224
        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
225
            raise
Dudás Ádám committed
226

Dudás Ádám committed
227 228 229 230 231
        # 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
232
            self.instance.deploy_net()
Dudás Ádám committed
233 234


235
register_operation(MigrateOperation)
Dudás Ádám committed
236 237


238
class RebootOperation(InstanceOperation):
Dudás Ádám committed
239 240 241
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
242
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
Dudás Ádám committed
243

244
    def _operation(self, timeout=5):
Dudás Ádám committed
245
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
246 247


248
register_operation(RebootOperation)
Dudás Ádám committed
249 250


251 252 253 254 255 256 257 258 259 260 261 262 263 264
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
265
register_operation(RemoveInterfaceOperation)
266 267


268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
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
285
register_operation(RemoveDiskOperation)
286 287


288
class ResetOperation(InstanceOperation):
Dudás Ádám committed
289 290 291
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
292
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
293

294
    def _operation(self, timeout=5):
Dudás Ádám committed
295
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
296

297
register_operation(ResetOperation)
Dudás Ádám committed
298 299


300
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
301 302 303 304 305 306 307 308
    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.
        """)
309
    abortable = True
Dudás Ádám committed
310

311 312 313 314 315 316
    @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)
317
        else:
318 319
            v = 1
        return "%s v%d" % (name, v)
320

321
    def _operation(self, activity, user, system, timeout=300, name=None,
322
                   with_shutdown=True, task=None, **kwargs):
323
        if with_shutdown:
324 325
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
326
                                                      user=user, task=task)
327 328 329
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
330 331 332 333 334 335 336 337
        # 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,
338
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
339 340 341 342 343 344 345 346 347
            '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
348
        params.pop("parent_activity", None)
Dudás Ádám committed
349

350 351
        from storage.models import Disk

Dudás Ádám committed
352 353
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
354
                return disk.save_as()
Dudás Ádám committed
355 356 357
            except Disk.WrongDiskTypeError:
                return disk

358 359 360 361
        with activity.sub_activity('saving_disks'):
            disks = [__try_save_disk(disk)
                     for disk in self.instance.disks.all()]

Dudás Ádám committed
362 363 364 365 366
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
367
            tmpl.disks.add(*disks)
Dudás Ádám committed
368 369 370 371 372 373 374 375 376 377
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


378
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
379 380


381
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
382 383 384
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
385
    description = _("Shutdown virtual machine with ACPI signal.")
Kálmán Viktor committed
386
    abortable = True
Dudás Ádám committed
387

388 389 390 391 392
    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
393 394 395
    def on_commit(self, activity):
        activity.resultant_state = 'STOPPED'

396 397
    def _operation(self, task=None):
        self.instance.shutdown_vm(task=task)
Dudás Ádám committed
398 399
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
400 401


402
register_operation(ShutdownOperation)
Dudás Ádám committed
403 404


405
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
406 407 408
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
409
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
410

411
    def on_commit(self, activity):
Dudás Ádám committed
412 413
        activity.resultant_state = 'STOPPED'

414
    def _operation(self, activity):
Dudás Ádám committed
415 416 417
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
418

Dudás Ádám committed
419 420 421 422 423 424 425
        # 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
426 427


428
register_operation(ShutOffOperation)
Dudás Ádám committed
429 430


431
class SleepOperation(InstanceOperation):
Dudás Ádám committed
432 433 434
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
435
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
436 437

    def check_precond(self):
438
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
439 440 441 442 443 444 445 446 447 448 449 450
        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'

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

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
458 459 460 461
            self.instance.suspend_vm(timeout=timeout)

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


464
register_operation(SleepOperation)
Dudás Ádám committed
465 466


467
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
468 469 470 471 472 473 474 475 476
    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):
477
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
478 479 480 481 482 483 484 485 486
        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'

487
    def _operation(self, activity, timeout=60):
Dudás Ádám committed
488
        # Schedule vm
Dudás Ádám committed
489 490
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
491 492 493

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
494
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
495 496 497

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
498
            self.instance.deploy_net()
Dudás Ádám committed
499 500 501 502 503

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


504
register_operation(WakeUpOperation)
505 506 507


class NodeOperation(Operation):
508
    async_operation = abortable_async_node_operation
509
    host_cls = Node
510 511 512 513 514

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

515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    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)
530 531 532 533 534 535


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

538
    def _operation(self, activity, user):
539 540 541
        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
542
                i.migrate(user=user)
543 544


545
register_operation(FlushOperation)