models.py 32.6 KB
Newer Older
1
from contextlib import contextmanager
2
from datetime import timedelta
3
from importlib import import_module
Őry Máté committed
4
import logging
Dudás Ádám committed
5
from netaddr import EUI, mac_unix
6

7
import django.conf
8
from django.contrib.auth.models import User
Dudás Ádám committed
9
from django.core import signing
Őry Máté committed
10 11 12
from django.db.models import (Model, ForeignKey, ManyToManyField, IntegerField,
                              DateTimeField, BooleanField, TextField,
                              CharField, permalink)
13 14
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
Dudás Ádám committed
15

16
from model_utils.models import TimeStampedModel
17
from taggit.managers import TaggableManager
18

19
from .tasks import local_tasks, vm_tasks, net_tasks
20 21
from firewall.models import Vlan, Host
from storage.models import Disk
22
from common.models import ActivityModel, activitycontextimpl
23
from acl.models import AclBase
24 25 26

logger = logging.getLogger(__name__)
pwgen = User.objects.make_random_password
27
scheduler = import_module(name=django.conf.settings.VM_SCHEDULER)
28
ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS
Őry Máté committed
29 30 31 32
ACCESS_METHODS = [(key, name) for key, (name, port, transport)
                  in ACCESS_PROTOCOLS.iteritems()]
ARCHITECTURES = (('x86_64', 'x86-64 (64 bit)'),
                 ('i686', 'x86 (32 bit)'))
33
VNC_PORT_RANGE = (2000, 65536)  # inclusive start, exclusive end
34 35


Őry Máté committed
36
class BaseResourceConfigModel(Model):
tarokkk committed
37

38
    """Abstract base for models with base resource configuration parameters.
39
    """
Őry Máté committed
40 41 42 43 44 45 46 47 48 49 50 51
    num_cores = IntegerField(verbose_name=_('number of cores'),
                             help_text=_('Number of virtual CPU cores '
                                         'available to the virtual machine.'))
    ram_size = IntegerField(verbose_name=_('RAM size'),
                            help_text=_('Mebibytes of memory.'))
    max_ram_size = IntegerField(verbose_name=_('maximal RAM size'),
                                help_text=_('Upper memory size limit '
                                            'for balloning.'))
    arch = CharField(max_length=10, verbose_name=_('architecture'),
                     choices=ARCHITECTURES)
    priority = IntegerField(verbose_name=_('priority'),
                            help_text=_('CPU priority.'))
52 53 54 55 56 57

    class Meta:
        abstract = True


class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):
tarokkk committed
58

59 60
    """Pre-created, named base resource configurations.
    """
Őry Máté committed
61 62 63
    name = CharField(max_length=50, unique=True,
                     verbose_name=_('name'), help_text=
                     _('Name of base resource configuration.'))
64 65 66 67 68

    def __unicode__(self):
        return self.name


69 70 71 72 73 74 75 76 77 78 79
class VirtualMachineDescModel(BaseResourceConfigModel):
    """Abstract base for virtual machine describing models.
    """
    access_method = CharField(max_length=10, choices=ACCESS_METHODS,
                              verbose_name=_('access method'),
                              help_text=_('Primary remote access method.'))
    boot_menu = BooleanField(verbose_name=_('boot menu'), default=False,
                             help_text=_(
                                 'Show boot device selection menu on boot.'))
    raw_data = TextField(verbose_name=_('raw_data'), blank=True, help_text=_(
        'Additional libvirt domain parameters in XML format.'))
80
    tags = TaggableManager()
81 82 83 84 85

    class Meta:
        abstract = True


86
class Node(TimeStampedModel):
tarokkk committed
87

Őry Máté committed
88
    """A VM host machine, a hypervisor.
89
    """
Őry Máté committed
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    name = CharField(max_length=50, unique=True,
                     verbose_name=_('name'),
                     help_text=_('Human readable name of node.'))
    num_cores = IntegerField(verbose_name=_('number of cores'),
                             help_text=_('Number of CPU threads '
                                         'available to the virtual machines.'))
    ram_size = IntegerField(verbose_name=_('RAM size'),
                            help_text=_('Mebibytes of memory.'))
    priority = IntegerField(verbose_name=_('priority'),
                            help_text=_('Node usage priority.'))
    host = ForeignKey(Host, verbose_name=_('host'),
                      help_text=_('Host in firewall.'))
    enabled = BooleanField(verbose_name=_('enabled'), default=False,
                           help_text=_('Indicates whether the node can '
                                       'be used for hosting.'))
105
    tags = TaggableManager()
106 107 108 109 110 111 112 113 114 115

    class Meta:
        permissions = ()

    @property
    def online(self):
        """Indicates whether the node is connected and functional.
        """
        pass  # TODO implement check

116 117 118
    def __unicode__(self):
        return self.name

119

120 121 122 123
class NodeActivity(ActivityModel):
    node = ForeignKey(Node, related_name='activity_log',
                      help_text=_('Node this activity works on.'),
                      verbose_name=_('node'))
124

125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
    @classmethod
    def create(cls, code_suffix, node, task_uuid=None, user=None):
        act = cls(activity_code='vm.Node.' + code_suffix,
                  node=node, parent=None, started=timezone.now(),
                  task_uuid=task_uuid, user=user)
        act.save()
        return act

    def create_sub(self, code_suffix, task_uuid=None):
        act = NodeActivity(
            activity_code=self.activity_code + '.' + code_suffix,
            node=self.node, parent=self, started=timezone.now(),
            task_uuid=task_uuid, user=self.user)
        act.save()
        return act

    @contextmanager
    def sub_activity(self, code_suffix, task_uuid=None):
        act = self.create_sub(code_suffix, task_uuid)
144
        return activitycontextimpl(act)
145 146 147 148 149


@contextmanager
def node_activity(code_suffix, node, task_uuid=None, user=None):
    act = InstanceActivity.create(code_suffix, node, task_uuid, user)
150
    return activitycontextimpl(act)
151

Őry Máté committed
152 153

class Lease(Model):
tarokkk committed
154

155 156 157 158 159
    """Lease times for VM instances.

    Specifies a time duration until suspension and deletion of a VM
    instance.
    """
Őry Máté committed
160 161 162 163
    name = CharField(max_length=100, unique=True,
                     verbose_name=_('name'))
    suspend_interval_seconds = IntegerField(verbose_name=_('suspend interval'))
    delete_interval_seconds = IntegerField(verbose_name=_('delete interval'))
164

165 166 167
    class Meta:
        ordering = ['name', ]

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    @property
    def suspend_interval(self):
        return timedelta(seconds=self.suspend_interval_seconds)

    @suspend_interval.setter
    def suspend_interval(self, value):
        self.suspend_interval_seconds = value.seconds

    @property
    def delete_interval(self):
        return timedelta(seconds=self.delete_interval_seconds)

    @delete_interval.setter
    def delete_interval(self, value):
        self.delete_interval_seconds = value.seconds

184 185 186
    def __unicode__(self):
        return self.name

187

188
class InstanceTemplate(VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203
    """Virtual machine template.

    Every template has:
      * a name and a description
      * an optional parent template
      * state of the template
      * an OS name/description
      * a method of access to the system
      * default values of base resource configuration
      * list of attached images
      * set of interfaces
      * lease times (suspension & deletion)
      * time of creation and last modification
    """
Őry Máté committed
204
    STATES = [('NEW', _('new')),        # template has just been created
205
              ('SAVING', _('saving')),  # changes are being saved
Őry Máté committed
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
              ('READY', _('ready'))]    # template is ready for instantiation
    name = CharField(max_length=100, unique=True,
                     verbose_name=_('name'),
                     help_text=_('Human readable name of template.'))
    description = TextField(verbose_name=_('description'), blank=True)
    parent = ForeignKey('self', null=True, blank=True,
                        verbose_name=_('parent template'),
                        help_text=_('Template which this one is derived of.'))
    system = TextField(verbose_name=_('operating system'),
                       blank=True,
                       help_text=(_('Name of operating system in '
                                    'format like "%s".') %
                                  'Ubuntu 12.04 LTS Desktop amd64'))
    state = CharField(max_length=10, choices=STATES, default='NEW')
    disks = ManyToManyField(Disk, verbose_name=_('disks'),
                            related_name='template_set',
                            help_text=_('Disks which are to be mounted.'))
    lease = ForeignKey(Lease, related_name='template_set',
                       verbose_name=_('lease'),
                       help_text=_('Expiration times.'))
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245

    class Meta:
        ordering = ['name', ]
        permissions = ()
        verbose_name = _('template')
        verbose_name_plural = _('templates')

    def __unicode__(self):
        return self.name

    def running_instances(self):
        """Returns the number of running instances of the template.
        """
        return self.instance_set.filter(state='RUNNING').count()

    @property
    def os_type(self):
        """Get the type of the template's operating system.
        """
        if self.access_method == 'rdp':
246
            return 'win'
247
        else:
248
            return 'linux'
249 250


Őry Máté committed
251
class InterfaceTemplate(Model):
tarokkk committed
252

253 254 255 256
    """Network interface template for an instance template.

    If the interface is managed, a host will be created for it.
    """
Őry Máté committed
257 258 259 260 261 262 263
    vlan = ForeignKey(Vlan, verbose_name=_('vlan'),
                      help_text=_('Network the interface belongs to.'))
    managed = BooleanField(verbose_name=_('managed'), default=True,
                           help_text=_('If a firewall host (i.e. IP address '
                                       'association) should be generated.'))
    template = ForeignKey(InstanceTemplate, verbose_name=_('template'),
                          related_name='interface_set',
264 265
                          help_text=_('Template the interface '
                                      'template belongs to.'))
266 267 268 269 270 271 272

    class Meta:
        permissions = ()
        verbose_name = _('interface template')
        verbose_name_plural = _('interface templates')


273
class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
tarokkk committed
274

275 276 277 278 279 280 281 282 283 284 285
    """Virtual machine instance.

    Every instance has:
      * a name and a description
      * an optional parent template
      * associated share
      * a generated password for login authentication
      * time of deletion and time of suspension
      * lease times (suspension & deletion)
      * last boot timestamp
      * host node
286
      * current state (libvirt domain state)
287 288
      * time of creation and last modification
      * base resource configuration values
289
      * owner and privilege information
290 291 292 293 294 295 296 297 298
    """
    STATES = [('NOSTATE', _('nostate')),
              ('RUNNING', _('running')),
              ('BLOCKED', _('blocked')),
              ('PAUSED', _('paused')),
              ('SHUTDOWN', _('shutdown')),
              ('SHUTOFF', _('shutoff')),
              ('CRASHED', _('crashed')),
              ('PMSUSPENDED', _('pmsuspended'))]  # libvirt domain states
299 300 301 302 303
    ACL_LEVELS = (
        ('user', _('user')),          # see all details
        ('operator', _('operator')),  # console, networking, change state
        ('owner', _('owner')),        # superuser, can delete, delegate perms
    )
Őry Máté committed
304
    name = CharField(blank=True, max_length=100, verbose_name=_('name'),
305
                     help_text=_("Human readable name of instance."))
Őry Máté committed
306 307 308
    description = TextField(blank=True, verbose_name=_('description'))
    template = ForeignKey(InstanceTemplate, blank=True, null=True,
                          related_name='instance_set',
309
                          help_text=_("Template the instance derives from."),
Őry Máté committed
310
                          verbose_name=_('template'))
311
    pw = CharField(help_text=_("Original password of the instance."),
Őry Máté committed
312 313 314
                   max_length=20, verbose_name=_('password'))
    time_of_suspend = DateTimeField(blank=True, default=None, null=True,
                                    verbose_name=_('time of suspend'),
315 316
                                    help_text=_("Proposed time of automatic "
                                                "suspension."))
Őry Máté committed
317 318
    time_of_delete = DateTimeField(blank=True, default=None, null=True,
                                   verbose_name=_('time of delete'),
319 320
                                   help_text=_("Proposed time of automatic "
                                               "deletion."))
Őry Máté committed
321
    active_since = DateTimeField(blank=True, null=True,
322 323
                                 help_text=_("Time stamp of successful "
                                             "boot report."),
Őry Máté committed
324 325 326
                                 verbose_name=_('active since'))
    node = ForeignKey(Node, blank=True, null=True,
                      related_name='instance_set',
327
                      help_text=_("Current hypervisor of this instance."),
Őry Máté committed
328 329 330
                      verbose_name=_('host node'))
    state = CharField(choices=STATES, default='NOSTATE', max_length=20)
    disks = ManyToManyField(Disk, related_name='instance_set',
331
                            help_text=_("Set of mounted disks."),
Őry Máté committed
332
                            verbose_name=_('disks'))
333
    lease = ForeignKey(Lease, help_text=_("Preferred expiration periods."))
334 335 336
    vnc_port = IntegerField(blank=True, default=None, null=True,
                            help_text=_("TCP port where VNC console listens."),
                            unique=True, verbose_name=_('vnc_port'))
Őry Máté committed
337
    owner = ForeignKey(User)
338 339 340
    destoryed = DateTimeField(blank=True, null=True,
                              help_text=_("The virtual machine's time of "
                                          "destruction."))
341 342 343 344 345 346 347

    class Meta:
        ordering = ['pk', ]
        verbose_name = _('instance')
        verbose_name_plural = _('instances')

    def __unicode__(self):
348 349
        parts = [self.name, "(" + str(self.id) + ")"]
        return " ".join([s for s in parts if s != ""])
350 351

    @classmethod
352 353
    def create_from_template(cls, template, owner, disks=None, networks=None,
                             **kwargs):
354 355 356 357 358
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
359
        disks = template.disks.all() if disks is None else disks
360

361 362 363
        networks = (template.interface_set.all() if networks is None
                    else networks)

364 365
        # prepare parameters
        kwargs['template'] = template
366
        kwargs['owner'] = owner
367
        kwargs.setdefault('pw', pwgen())
368 369 370 371 372
        ca = ['name', 'description', 'num_cores', 'ram_size', 'max_ram_size',
              'arch', 'priority', 'boot_menu', 'raw_data', 'lease',
              'access_method']
        for attr in ca:
            kwargs.setdefault(attr, getattr(template, attr))
373 374 375
        # create instance and do additional setup
        inst = cls(**kwargs)
        # save instance
376
        inst.clean()
377
        inst.save()
378
        # create related entities
379
        for disk in disks:
380
            inst.disks.add(disk.get_exclusive())
381

382 383 384 385
        for net in networks:
            i = Interface.create(instance=inst, vlan=net.vlan, owner=owner,
                                 managed=net.managed)

386 387
            if i.host:
                i.host.enable_net()
388 389 390
                port, proto = ACCESS_PROTOCOLS[i.instance.access_method][1:3]
                # TODO fix this port fw
                i.host.add_port(proto, private=port)
391 392 393

        return inst

Őry Máté committed
394
    @permalink
395
    def get_absolute_url(self):
396
        return ('dashboard.views.detail', None, {'pk': self.id})
397 398

    @property
399 400 401 402 403 404 405 406 407
    def vm_name(self):
        """Name of the VM instance.

        This is a unique identifier as opposed to the 'name' attribute, which
        is just for display.
        """
        return 'cloud-' + str(self.id)

    @property
408 409 410 411 412 413
    def mem_dump(self):
        """Return the path for the memory dump.

        It is always on the first hard drive storage named cloud-<id>.dump
        """
        path = self.disks.all()[0].datastore.path
Dudás Ádám committed
414
        return path + '/' + self.vm_name + '.dump'
415 416

    @property
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
    def primary_host(self):
        interfaces = self.interface_set.select_related('host')
        hosts = [i.host for i in interfaces if i.host]
        if not hosts:
            return None
        hs = [h for h in hosts if h.ipv6]
        if hs:
            return hs[0]
        hs = [h for h in hosts if not h.shared_ip]
        if hs:
            return hs[0]
        return hosts[0]

    @property
    def ipv4(self):
432 433
        """Primary IPv4 address of the instance.
        """
434 435 436 437
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
438 439
        """Primary IPv6 address of the instance.
        """
440 441 442 443
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
444 445
        """Primary MAC address of the instance.
        """
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
        return self.primary_host.mac if self.primary_host else None

    @property
    def uptime(self):
        """Uptime of the instance.
        """
        if self.active_since:
            return timezone.now() - self.active_since
        else:
            return timedelta()  # zero

    def get_age(self):
        """Deprecated. Use uptime instead.

        Get age of VM in seconds.
        """
        return self.uptime.seconds

    @property
    def waiting(self):
        """Indicates whether the instance's waiting for an operation to finish.
        """
468
        return self.activity_log.filter(finished__isnull=True).exists()
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497

    def get_connect_port(self, use_ipv6=False):
        """Get public port number for default access method.
        """
        port, proto = ACCESS_PROTOCOLS[self.access_method][1:3]
        if self.primary_host:
            endpoints = self.primary_host.get_public_endpoints(port, proto)
            endpoint = endpoints['ipv6'] if use_ipv6 else endpoints['ipv4']
            return endpoint[1] if endpoint else None
        else:
            return None

    def get_connect_host(self, use_ipv6=False):
        """Get public hostname.
        """
        if not self.firewall_host:
            return _('None')
        proto = 'ipv6' if use_ipv6 else 'ipv4'
        return self.firewall_host.get_hostname(proto=proto)

    def get_connect_uri(self, use_ipv6=False):
        """Get access parameters in URI format.
        """
        try:
            port = self.get_connect_port(use_ipv6=use_ipv6)
            host = self.get_connect_host(use_ipv6=use_ipv6)
            proto = self.access_method
            if proto == 'ssh':
                proto = 'sshterm'
498 499 500
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
501 502 503
        except:
            return

tarokkk committed
504 505
    def get_vm_desc(self):
        return {
506
            'name': self.vm_name,
507
            'vcpu': self.num_cores,
508
            'memory': int(self.ram_size) * 1024,  # convert from MiB to KiB
Őry Máté committed
509
            'memory_max': int(self.max_ram_size) * 1024,  # convert MiB to KiB
510 511 512 513 514
            'cpu_share': self.priority,
            'arch': self.arch,
            'boot_menu': self.boot_menu,
            'network_list': [n.get_vmnetwork_desc()
                             for n in self.interface_set.all()],
515 516 517 518 519
            'disk_list': [d.get_vmdisk_desc() for d in self.disks.all()],
            'graphics': {
                'type': 'vnc',
                'listen': '0.0.0.0',
                'passwd': '',
Guba Sándor committed
520
                'port': self.vnc_port
521
            },
522
            'boot_token': signing.dumps(self.id, salt='activate'),
Guba Sándor committed
523
            'raw_data': "" if not self.raw_data else self.raw_data
524
        }
tarokkk committed
525

526 527 528 529 530 531
    def get_remote_queue_name(self, queue_id):
        """Get the remote worker queue name of this instance with the specified
           queue ID.
        """
        return self.node.host.hostname + "." + queue_id

Dudás Ádám committed
532 533 534 535 536 537 538 539 540 541 542
    def renew(self, which='both'):
        """Renew virtual machine instance leases.
        """
        if which not in ['suspend', 'delete', 'both']:
            raise ValueError('No such expiration type.')
        if which in ['suspend', 'both']:
            self.time_of_suspend = timezone.now() + self.lease.suspend_interval
        if which in ['delete', 'both']:
            self.time_of_delete = timezone.now() + self.lease.delete_interval
        self.save()

543
    def deploy(self, user=None, task_uuid=None):
Dudás Ádám committed
544 545 546 547 548 549 550 551 552 553 554
        """Deploy new virtual machine with network

        :param self: The virtual machine to deploy.
        :type self: vm.models.Instance

        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str
555
        """
556 557
        with instance_activity(code_suffix='deploy', instance=self,
                               task_uuid=task_uuid, user=user) as act:
558

559 560 561 562 563 564 565 566 567 568
            # Find unused port for VNC
            if self.vnc_port is None:
                used = Instance.objects.values_list('vnc_port', flat=True)
                for p in xrange(*VNC_PORT_RANGE):
                    if p not in used:
                        self.vnc_port = p
                        break
                else:
                    raise Exception("No unused port could be found for VNC.")

569
            # Schedule
570 571
            self.node = scheduler.get_node(self, Node.objects.all())
            self.save()
tarokkk committed
572

573 574 575 576
            # Deploy virtual images
            with act.sub_activity('deploying_disks'):
                for disk in self.disks.all():
                    disk.deploy()
577

578 579 580
            queue_name = self.get_remote_queue_name('vm')
            # Deploy VM on remote machine
            with act.sub_activity('deploying_vm'):
581
                vm_tasks.deploy.apply_async(args=[self.get_vm_desc()],
582
                                            queue=queue_name).get()
tarokkk committed
583

584 585 586 587
            # Estabilish network connection (vmdriver)
            with act.sub_activity('deploying_net'):
                for net in self.interface_set.all():
                    net.deploy()
tarokkk committed
588

589 590 591 592
            # Resume vm
            with act.sub_activity('booting'):
                vm_tasks.resume.apply_async(args=[self.vm_name],
                                            queue=queue_name).get()
593

594 595 596
    def deploy_async(self, user=None):
        """Execute deploy asynchronously.
        """
597 598
        return local_tasks.deploy.apply_async(args=[self, user],
                                              queue="localhost.man")
599

600
    def destroy(self, user=None, task_uuid=None):
Dudás Ádám committed
601 602 603 604 605 606 607 608 609 610 611
        """Remove virtual machine and its networks.

        :param self: The virtual machine to destroy.
        :type self: vm.models.Instance

        :param user: The user who's issuing the command.
        :type user: django.contrib.auth.models.User

        :param task_uuid: The task's UUID, if the command is being executed
                          asynchronously.
        :type task_uuid: str
612
        """
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
        with instance_activity(code_suffix='destroy', instance=self,
                               task_uuid=task_uuid, user=user) as act:

            # Destroy networks
            with act.sub_activity('destroying_net'):
                for net in self.interface_set.all():
                    net.destroy()

            # Destroy virtual machine
            with act.sub_activity('destroying_vm'):
                queue_name = self.get_remote_queue_name('vm')
                vm_tasks.destroy.apply_async(args=[self.vm_name],
                                             queue=queue_name).get()

            # Destroy disks
            with act.sub_activity('destroying_disks'):
                for disk in self.disks.all():
                    disk.destroy()

632 633 634 635
            # Clear node and VNC port association
            self.node = None
            self.vnc_port = None

636 637
            self.destoryed = timezone.now()
            self.save()
638 639

    def destroy_async(self, user=None):
Dudás Ádám committed
640
        """Execute destroy asynchronously.
641
        """
642 643
        return local_tasks.destroy.apply_async(args=[self, user],
                                               queue="localhost.man")
644 645 646 647

    def sleep(self, user=None, task_uuid=None):
        """Suspend virtual machine with memory dump.
        """
648 649
        with instance_activity(code_suffix='sleep', instance=self,
                               task_uuid=task_uuid, user=user):
650

651 652 653
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.sleep.apply_async(args=[self.vm_name, self.mem_dump],
                                       queue=queue_name).get()
Guba Sándor committed
654

655
    def sleep_async(self, user=None):
Dudás Ádám committed
656
        """Execute sleep asynchronously.
657
        """
658 659
        return local_tasks.sleep.apply_async(args=[self, user],
                                             queue="localhost.man")
Guba Sándor committed
660

661
    def wake_up(self, user=None, task_uuid=None):
662 663
        with instance_activity(code_suffix='wake_up', instance=self,
                               task_uuid=task_uuid, user=user):
664

665 666 667
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.resume.apply_async(args=[self.vm_name, self.dump_mem],
                                        queue=queue_name).get()
668

669
    def wake_up_async(self, user=None):
Dudás Ádám committed
670
        """Execute wake_up asynchronously.
671
        """
672 673
        return local_tasks.wake_up.apply_async(args=[self, user],
                                               queue="localhost.man")
674

675 676 677
    def shutdown(self, user=None, task_uuid=None):
        """Shutdown virtual machine with ACPI signal.
        """
678 679
        with instance_activity(code_suffix='shutdown', instance=self,
                               task_uuid=task_uuid, user=user):
680

681 682 683
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.shutdown.apply_async(args=[self.vm_name],
                                          queue=queue_name).get()
Guba Sándor committed
684

685 686
    def shutdown_async(self, user=None):
        """Execute shutdown asynchronously.
687
        """
688 689
        return local_tasks.shutdown.apply_async(args=[self, user],
                                                queue="localhost.man")
Guba Sándor committed
690

691 692 693
    def reset(self, user=None, task_uuid=None):
        """Reset virtual machine (reset button)
        """
694 695
        with instance_activity(code_suffix='reset', instance=self,
                               task_uuid=task_uuid, user=user):
696

697 698 699
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.restart.apply_async(args=[self.vm_name],
                                         queue=queue_name).get()
Guba Sándor committed
700

701 702
    def reset_async(self, user=None):
        """Execute reset asynchronously.
703
        """
704 705
        return local_tasks.restart.apply_async(args=[self, user],
                                               queue="localhost.man")
Guba Sándor committed
706

707
    def reboot(self, user=None, task_uuid=None):
Dudás Ádám committed
708
        """Reboot virtual machine with Ctrl+Alt+Del signal.
709
        """
710 711
        with instance_activity(code_suffix='reboot', instance=self,
                               task_uuid=task_uuid, user=user):
712

713 714 715
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.reboot.apply_async(args=[self.vm_name],
                                        queue=queue_name).get()
716

717 718
    def reboot_async(self, user=None):
        """Execute reboot asynchronously.
719
        """
720 721
        return local_tasks.reboot.apply_async(args=[self, user],
                                              queue="localhost.man")
722

723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
    def save_as_template(self, name, **kwargs):
        # prepare parameters
        kwargs.setdefault('name', name)
        kwargs.setdefault('description', self.description)
        kwargs.setdefault('parent', self.template)
        kwargs.setdefault('num_cores', self.num_cores)
        kwargs.setdefault('ram_size', self.ram_size)
        kwargs.setdefault('max_ram_size', self.max_ram_size)
        kwargs.setdefault('arch', self.arch)
        kwargs.setdefault('priority', self.priority)
        kwargs.setdefault('boot_menu', self.boot_menu)
        kwargs.setdefault('raw_data', self.raw_data)
        kwargs.setdefault('lease', self.lease)
        kwargs.setdefault('access_method', self.access_method)
        kwargs.setdefault('system', self.template.system
                          if self.template else None)
        # create template and do additional setup
        tmpl = InstanceTemplate(**kwargs)
        # save template
        tmpl.save()
        # create related entities
        for disk in self.disks.all():
            try:
                d = disk.save_as()
            except Disk.WrongDiskTypeError:
                d = disk

            tmpl.disks.add(d)

        for i in self.interface_set.all():
            i.save_as_template(tmpl)

        return tmpl
tarokkk committed
756

757

758 759 760 761 762
class InstanceActivity(ActivityModel):
    instance = ForeignKey(Instance, related_name='activity_log',
                          help_text=_('Instance this activity works on.'),
                          verbose_name=_('instance'))

763 764
    def __unicode__(self):
        if self.parent:
Őry Máté committed
765 766 767
            return '{}({})->{}'.format(self.parent.activity_code,
                                       self.instance.name,
                                       self.activity_code)
768
        else:
Őry Máté committed
769 770
            return '{}({})'.format(self.activity_code,
                                   self.instance.name)
771

772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
    @classmethod
    def create(cls, code_suffix, instance, task_uuid=None, user=None):
        act = cls(activity_code='vm.Instance.' + code_suffix,
                  instance=instance, parent=None, started=timezone.now(),
                  task_uuid=task_uuid, user=user)
        act.save()
        return act

    def create_sub(self, code_suffix, task_uuid=None):
        act = InstanceActivity(
            activity_code=self.activity_code + '.' + code_suffix,
            instance=self.instance, parent=self, started=timezone.now(),
            task_uuid=task_uuid, user=self.user)
        act.save()
        return act

    @contextmanager
    def sub_activity(self, code_suffix, task_uuid=None):
        act = self.create_sub(code_suffix, task_uuid)
791
        return activitycontextimpl(act)
792 793 794 795 796


@contextmanager
def instance_activity(code_suffix, instance, task_uuid=None, user=None):
    act = InstanceActivity.create(code_suffix, instance, task_uuid, user)
797
    return activitycontextimpl(act)
tarokkk committed
798

799

Őry Máté committed
800
class Interface(Model):
801 802 803

    """Network interface for an instance.
    """
Őry Máté committed
804 805 806 807 808
    vlan = ForeignKey(Vlan, verbose_name=_('vlan'),
                      related_name="vm_interface")
    host = ForeignKey(Host, verbose_name=_('host'),  blank=True, null=True)
    instance = ForeignKey(Instance, verbose_name=_('instance'),
                          related_name='interface_set')
809

810 811 812
    def __unicode__(self):
        return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)

813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
    @property
    def mac(self):
        try:
            return self.host.mac
        except:
            return Interface.generate_mac(self.instance, self.vlan)

    @classmethod
    def generate_mac(cls, instance, vlan):
        """Generate MAC address for a VM instance on a VLAN.
        """
        # MAC 02:XX:XX:XX:XX:XX
        #        \________/\__/
        #           VM ID   VLAN ID
        i = instance.id & 0xfffffff
        v = vlan.vid & 0xfff
        m = (0x02 << 40) | (i << 12) | v
830
        return EUI(m, dialect=mac_unix)
831 832 833

    def get_vmnetwork_desc(self):
        return {
Dudás Ádám committed
834
            'name': self.__unicode__(),
835
            'bridge': 'cloud',
836
            'mac': str(self.mac),
837 838
            'ipv4': str(self.host.ipv4) if self.host is not None else None,
            'ipv6': str(self.host.ipv6) if self.host is not None else None,
839 840 841 842
            'vlan': self.vlan.vid,
            'managed': self.host is not None
        }

843 844 845
    def deploy(self, user=None, task_uuid=None):
        net_tasks.create.apply_async(
            args=[self.get_vmnetwork_desc()],
846
            queue=self.instance.get_remote_queue_name('net'))
847

848 849
    def destroy(self, user=None, task_uuid=None):
        net_tasks.destroy.apply_async(
850
            args=[self.get_vmnetwork_desc()],
851
            queue=self.instance.get_remote_queue_name('net'))
Guba Sándor committed
852

853
    @classmethod
854 855
    def create(cls, instance, vlan, managed, owner=None):
        """Create a new interface for a VM instance to the specified VLAN.
856
        """
857
        if managed:
858
            host = Host()
859
            host.vlan = vlan
860
            # TODO change Host's mac field's type to EUI in firewall
861
            host.mac = str(cls.generate_mac(instance, vlan))
862
            host.hostname = instance.vm_name
863
            # Get adresses from firewall
864
            addresses = vlan.get_new_address()
865 866
            host.ipv4 = addresses['ipv4']
            host.ipv6 = addresses['ipv6']
867
            host.owner = owner
868 869 870
            host.save()
        else:
            host = None
871

872
        iface = cls(vlan=vlan, host=host, instance=instance)
873 874
        iface.save()
        return iface
875 876 877 878 879 880 881 882

    def save_as_template(self, instance_template):
        """Create a template based on this interface.
        """
        i = InterfaceTemplate(vlan=self.vlan, managed=self.host is not None,
                              template=instance_template)
        i.save()
        return i