operations.py 16.8 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
from .tasks.local_tasks import async_instance_operation, async_node_operation
from .models import (
31 32
    Instance, InstanceActivity, InstanceTemplate, Interface, Node,
    NodeActivity,
33
)
Dudás Ádám committed
34 35 36


logger = getLogger(__name__)
37 38


39
class InstanceOperation(Operation):
40
    acl_level = 'owner'
41
    async_operation = async_instance_operation
42
    host_cls = Instance
Dudás Ádám committed
43

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

    def check_precond(self):
49 50
        if self.instance.destroyed_at:
            raise self.instance.InstanceDestroyedError(self.instance)
51 52

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

57
        super(InstanceOperation, self).check_auth(user=user)
58

59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
    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)
75

76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
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
97
register_operation(AddInterfaceOperation)
98 99


100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
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
117
register_operation(AddDiskOperation)
118 119


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

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

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

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

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


153
register_operation(DeployOperation)
Dudás Ádám committed
154 155


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

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

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

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

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

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

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


194
register_operation(DestroyOperation)
Dudás Ádám committed
195 196


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

203
    def _operation(self, activity, user, system, to_node=None, timeout=120):
Dudás Ádám committed
204 205 206 207 208
        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
209 210 211
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
212 213

        with activity.sub_activity('migrate_vm'):
Dudás Ádám committed
214 215
            self.instance.migrate_vm(to_node=to_node, timeout=timeout)

Dudás Ádám committed
216 217 218 219 220
        # 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
221
            self.instance.deploy_net()
Dudás Ádám committed
222 223


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


227
class RebootOperation(InstanceOperation):
Dudás Ádám committed
228 229 230
    activity_code_suffix = 'reboot'
    id = 'reboot'
    name = _("reboot")
231
    description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
Dudás Ádám committed
232

233
    def _operation(self, activity, user, system, timeout=5):
Dudás Ádám committed
234
        self.instance.reboot_vm(timeout=timeout)
Dudás Ádám committed
235 236


237
register_operation(RebootOperation)
Dudás Ádám committed
238 239


240 241 242 243 244 245 246 247 248 249 250 251 252 253
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
254
register_operation(RemoveInterfaceOperation)
255 256


257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
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
274
register_operation(RemoveDiskOperation)
275 276


277
class ResetOperation(InstanceOperation):
Dudás Ádám committed
278 279 280
    activity_code_suffix = 'reset'
    id = 'reset'
    name = _("reset")
281
    description = _("Reset virtual machine (reset button).")
Dudás Ádám committed
282

283
    def _operation(self, activity, user, system, timeout=5):
Dudás Ádám committed
284
        self.instance.reset_vm(timeout=timeout)
Dudás Ádám committed
285

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


289
class SaveAsTemplateOperation(InstanceOperation):
Dudás Ádám committed
290 291 292 293 294 295 296 297 298
    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.
        """)

299 300 301 302 303 304
    @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)
305
        else:
306 307
            v = 1
        return "%s v%d" % (name, v)
308

309
    def _operation(self, activity, user, system, timeout=300, name=None,
310 311
                   with_shutdown=True, **kwargs):
        if with_shutdown:
312 313 314 315 316 317
            try:
                ShutdownOperation(self.instance).call(parent_activity=activity,
                                                      user=user)
            except Instance.WrongStateError:
                pass

Dudás Ádám committed
318 319 320 321 322 323 324 325
        # 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,
326
            'name': name or self._rename(self.instance.name),
Dudás Ádám committed
327 328 329 330 331 332 333 334 335 336
            '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)

337 338
        from storage.models import Disk

Dudás Ádám committed
339 340
        def __try_save_disk(disk):
            try:
Dudás Ádám committed
341
                return disk.save_as()
Dudás Ádám committed
342 343 344
            except Disk.WrongDiskTypeError:
                return disk

345 346 347 348
        with activity.sub_activity('saving_disks'):
            disks = [__try_save_disk(disk)
                     for disk in self.instance.disks.all()]

Dudás Ádám committed
349 350 351 352 353
        # create template and do additional setup
        tmpl = InstanceTemplate(**params)
        tmpl.full_clean()  # Avoiding database errors.
        tmpl.save()
        try:
354
            tmpl.disks.add(*disks)
Dudás Ádám committed
355 356 357 358 359 360 361 362 363 364
            # create interface templates
            for i in self.instance.interface_set.all():
                i.save_as_template(tmpl)
        except:
            tmpl.delete()
            raise
        else:
            return tmpl


365
register_operation(SaveAsTemplateOperation)
Dudás Ádám committed
366 367


368
class ShutdownOperation(InstanceOperation):
Dudás Ádám committed
369 370 371
    activity_code_suffix = 'shutdown'
    id = 'shutdown'
    name = _("shutdown")
372
    description = _("Shutdown virtual machine with ACPI signal.")
Dudás Ádám committed
373

374 375 376 377 378
    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
379 380 381 382 383 384 385 386 387
    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 = 'STOPPED'

388
    def _operation(self, activity, user, system, timeout=120):
Dudás Ádám committed
389 390 391
        self.instance.shutdown_vm(timeout=timeout)
        self.instance.yield_node()
        self.instance.yield_vnc_port()
Dudás Ádám committed
392 393


394
register_operation(ShutdownOperation)
Dudás Ádám committed
395 396


397
class ShutOffOperation(InstanceOperation):
Dudás Ádám committed
398 399 400
    activity_code_suffix = 'shut_off'
    id = 'shut_off'
    name = _("shut off")
401
    description = _("Shut off VM (plug-out).")
Dudás Ádám committed
402

403
    def on_commit(self, activity):
Dudás Ádám committed
404 405
        activity.resultant_state = 'STOPPED'

406
    def _operation(self, activity, user, system):
Dudás Ádám committed
407 408 409
        # Shutdown networks
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
410

Dudás Ádám committed
411 412 413 414 415 416 417
        # 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
418 419


420
register_operation(ShutOffOperation)
Dudás Ádám committed
421 422


423
class SleepOperation(InstanceOperation):
Dudás Ádám committed
424 425 426
    activity_code_suffix = 'sleep'
    id = 'sleep'
    name = _("sleep")
427
    description = _("Suspend virtual machine with memory dump.")
Dudás Ádám committed
428 429

    def check_precond(self):
430
        super(SleepOperation, self).check_precond()
Dudás Ádám committed
431 432 433 434 435 436 437 438 439 440 441 442
        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'

443
    def _operation(self, activity, user, system, timeout=60):
Dudás Ádám committed
444
        # Destroy networks
Dudás Ádám committed
445 446
        with activity.sub_activity('shutdown_net'):
            self.instance.shutdown_net()
Dudás Ádám committed
447 448 449

        # Suspend vm
        with activity.sub_activity('suspending'):
Dudás Ádám committed
450 451 452 453
            self.instance.suspend_vm(timeout=timeout)

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


456
register_operation(SleepOperation)
Dudás Ádám committed
457 458


459
class WakeUpOperation(InstanceOperation):
Dudás Ádám committed
460 461 462 463 464 465 466 467 468
    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):
469
        super(WakeUpOperation, self).check_precond()
Dudás Ádám committed
470 471 472 473 474 475 476 477 478
        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'

479
    def _operation(self, activity, user, system, timeout=60):
Dudás Ádám committed
480
        # Schedule vm
Dudás Ádám committed
481 482
        self.instance.allocate_vnc_port()
        self.instance.allocate_node()
Dudás Ádám committed
483 484 485

        # Resume vm
        with activity.sub_activity('resuming'):
Dudás Ádám committed
486
            self.instance.wake_up_vm(timeout=timeout)
Dudás Ádám committed
487 488 489

        # Estabilish network connection (vmdriver)
        with activity.sub_activity('deploying_net'):
Dudás Ádám committed
490
            self.instance.deploy_net()
Dudás Ádám committed
491 492 493 494 495

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


496
register_operation(WakeUpOperation)
497 498 499 500


class NodeOperation(Operation):
    async_operation = async_node_operation
501
    host_cls = Node
502 503 504 505 506

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

507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    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)
522 523 524 525 526 527


class FlushOperation(NodeOperation):
    activity_code_suffix = 'flush'
    id = 'flush'
    name = _("flush")
528
    description = _("Disable node and move all instances to other ones.")
529 530 531 532 533 534 535 536

    def _operation(self, activity, user, system):
        self.node.disable(user, activity)
        for i in self.node.instance_set.all():
            with activity.sub_activity('migrate_instance_%d' % i.pk):
                i.migrate()


537
register_operation(FlushOperation)