models.py 32.1 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
5

6
import django.conf
7
from django.contrib.auth.models import User
Őry Máté committed
8 9 10
from django.db.models import (Model, ForeignKey, ManyToManyField, IntegerField,
                              DateTimeField, BooleanField, TextField,
                              CharField, permalink)
11 12 13
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel
14
from netaddr import EUI, mac_unix
15

16
from .tasks import local_tasks, vm_tasks, net_tasks
17
from common.models import ActivityModel, activitycontextimpl
18 19
from firewall.models import Vlan, Host
from storage.models import Disk
20

21 22 23

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


Őry Máté committed
33
class BaseResourceConfigModel(Model):
tarokkk committed
34

35 36 37
    """Abstract base class for models with base resource configuration
       parameters.
    """
Őry Máté committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
    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.'))
    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.'))
55 56 57 58 59 60

    class Meta:
        abstract = True


class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):
tarokkk committed
61

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

    def __unicode__(self):
        return self.name


class Node(TimeStampedModel):
tarokkk committed
73

Őry Máté committed
74
    """A VM host machine, a hypervisor.
75
    """
Őry Máté committed
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
    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.'))
91 92 93 94 95 96 97 98 99 100

    class Meta:
        permissions = ()

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

101 102 103
    def __unicode__(self):
        return self.name

104

105 106 107 108
class NodeActivity(ActivityModel):
    node = ForeignKey(Node, related_name='activity_log',
                      help_text=_('Node this activity works on.'),
                      verbose_name=_('node'))
109

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
    @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)
129
        return activitycontextimpl(act)
130 131 132 133 134


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

Őry Máté committed
137 138

class Lease(Model):
tarokkk committed
139

140 141 142 143 144
    """Lease times for VM instances.

    Specifies a time duration until suspension and deletion of a VM
    instance.
    """
Őry Máté committed
145 146 147 148
    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'))
149

150 151 152
    class Meta:
        ordering = ['name', ]

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    @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

169 170 171
    def __unicode__(self):
        return self.name

172 173

class InstanceTemplate(BaseResourceConfigModel, TimeStampedModel):
tarokkk committed
174

175 176 177 178 179 180 181 182 183 184 185 186 187 188
    """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
189
    STATES = [('NEW', _('new')),        # template has just been created
190
              ('SAVING', _('saving')),  # changes are being saved
Őry Máté committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
              ('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'))
    access_method = CharField(max_length=10, choices=ACCESS_METHODS,
                              verbose_name=_('access method'),
                              help_text=_('Primary remote access method.'))
    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.'))
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233

    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':
234
            return 'win'
235
        else:
236
            return 'linux'
237 238


Őry Máté committed
239
class InterfaceTemplate(Model):
tarokkk committed
240

241 242 243 244
    """Network interface template for an instance template.

    If the interface is managed, a host will be created for it.
    """
Őry Máté committed
245 246 247 248 249 250 251
    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',
252 253
                          help_text=_('Template the interface '
                                      'template belongs to.'))
254 255 256 257 258 259 260 261

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


class Instance(BaseResourceConfigModel, TimeStampedModel):
tarokkk committed
262

263 264 265 266 267 268 269 270 271 272 273
    """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
274
      * current state (libvirt domain state)
275 276
      * time of creation and last modification
      * base resource configuration values
277
      * owner and privilege information
278 279 280 281 282 283 284 285 286
    """
    STATES = [('NOSTATE', _('nostate')),
              ('RUNNING', _('running')),
              ('BLOCKED', _('blocked')),
              ('PAUSED', _('paused')),
              ('SHUTDOWN', _('shutdown')),
              ('SHUTOFF', _('shutoff')),
              ('CRASHED', _('crashed')),
              ('PMSUSPENDED', _('pmsuspended'))]  # libvirt domain states
Őry Máté committed
287
    name = CharField(blank=True, max_length=100, verbose_name=_('name'),
288
                     help_text=_("Human readable name of instance."))
Őry Máté committed
289 290 291
    description = TextField(blank=True, verbose_name=_('description'))
    template = ForeignKey(InstanceTemplate, blank=True, null=True,
                          related_name='instance_set',
292
                          help_text=_("Template the instance derives from."),
Őry Máté committed
293
                          verbose_name=_('template'))
294
    pw = CharField(help_text=_("Original password of the instance."),
Őry Máté committed
295 296 297
                   max_length=20, verbose_name=_('password'))
    time_of_suspend = DateTimeField(blank=True, default=None, null=True,
                                    verbose_name=_('time of suspend'),
298 299
                                    help_text=_("Proposed time of automatic "
                                                "suspension."))
Őry Máté committed
300 301
    time_of_delete = DateTimeField(blank=True, default=None, null=True,
                                   verbose_name=_('time of delete'),
302 303
                                   help_text=_("Proposed time of automatic "
                                               "deletion."))
Őry Máté committed
304
    active_since = DateTimeField(blank=True, null=True,
305 306
                                 help_text=_("Time stamp of successful "
                                             "boot report."),
Őry Máté committed
307 308 309
                                 verbose_name=_('active since'))
    node = ForeignKey(Node, blank=True, null=True,
                      related_name='instance_set',
310
                      help_text=_("Current hypervisor of this instance."),
Őry Máté committed
311 312 313
                      verbose_name=_('host node'))
    state = CharField(choices=STATES, default='NOSTATE', max_length=20)
    disks = ManyToManyField(Disk, related_name='instance_set',
314
                            help_text=_("Set of mounted disks."),
Őry Máté committed
315
                            verbose_name=_('disks'))
316
    lease = ForeignKey(Lease, help_text=_("Preferred expiration periods."))
Őry Máté committed
317
    access_method = CharField(max_length=10, choices=ACCESS_METHODS,
318
                              help_text=_("Primary remote access method."),
Őry Máté committed
319
                              verbose_name=_('access method'))
320
    vnc_port = IntegerField(default=2000, verbose_name=_('vnc_port'),
321
                            help_text=_("TCP port where VNC console listens."))
Őry Máté committed
322
    owner = ForeignKey(User)
323 324 325
    destoryed = DateTimeField(blank=True, null=True,
                              help_text=_("The virtual machine's time of "
                                          "destruction."))
326 327 328 329

    class Meta:
        ordering = ['pk', ]
        permissions = ()
330
        unique_together = ('node', 'vnc_port')
331 332 333 334
        verbose_name = _('instance')
        verbose_name_plural = _('instances')

    def __unicode__(self):
335 336
        parts = [self.name, "(" + str(self.id) + ")"]
        return " ".join([s for s in parts if s != ""])
337 338

    @classmethod
339
    def create_from_template(cls, template, owner, **kwargs):
340 341 342 343 344 345 346
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
        # prepare parameters
        kwargs['template'] = template
347
        kwargs['owner'] = owner
348 349 350 351 352 353 354 355
        kwargs.setdefault('name', template.name)
        kwargs.setdefault('description', template.description)
        kwargs.setdefault('pw', pwgen())
        kwargs.setdefault('num_cores', template.num_cores)
        kwargs.setdefault('ram_size', template.ram_size)
        kwargs.setdefault('max_ram_size', template.max_ram_size)
        kwargs.setdefault('arch', template.arch)
        kwargs.setdefault('priority', template.priority)
356 357
        kwargs.setdefault('boot_menu', template.boot_menu)
        kwargs.setdefault('raw_data', template.raw_data)
358 359 360 361 362 363
        kwargs.setdefault('lease', template.lease)
        kwargs.setdefault('access_method', template.access_method)
        # create instance and do additional setup
        inst = cls(**kwargs)
        # save instance
        inst.save()
364
        # create related entities
365 366
        for disk in template.disks.all():
            inst.disks.add(disk.get_exclusive())
367

368
        for iftmpl in template.interface_set.all():
369 370 371
            i = Interface.create_from_template(instance=inst,
                                               template=iftmpl,
                                               owner=owner)
372 373
            if i.host:
                i.host.enable_net()
374 375 376
                port, proto = ACCESS_PROTOCOLS[i.instance.access_method][1:3]
                # TODO fix this port fw
                i.host.add_port(proto, private=port)
377 378 379

        return inst

Őry Máté committed
380
    @permalink
381
    def get_absolute_url(self):
382
        return ('dashboard.views.detail', None, {'pk': self.id})
383 384

    @property
385 386 387 388 389 390 391 392 393
    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
394 395 396 397 398 399 400 401 402
    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
        return path + '/' + 'cloud-' + str(self.id) + '.dump'

    @property
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
    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):
418 419
        """Primary IPv4 address of the instance.
        """
420 421 422 423
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
424 425
        """Primary IPv6 address of the instance.
        """
426 427 428 429
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
430 431
        """Primary MAC address of the instance.
        """
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
        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.
        """
454
        return self.activity_log.filter(finished__isnull=True).exists()
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

    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'
484 485 486
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
487 488 489
        except:
            return

tarokkk committed
490 491
    def get_vm_desc(self):
        return {
492
            'name': self.vm_name,
493
            'vcpu': self.num_cores,
494 495
            'memory': self.ram_size * 1024,  # convert from MiB to KiB
            'memory_max': self.max_ram_size * 1024,  # convert from MiB to KiB
496 497 498 499 500
            '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()],
501 502 503 504 505
            '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
506
                'port': self.vnc_port
507
            },
Guba Sándor committed
508
            'raw_data': "" if not self.raw_data else self.raw_data
509
        }
tarokkk committed
510

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

528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
    def change_node(self, new_node):
        if self.node == new_node:
            return

        self.node = new_node
        if self.node:
            used = self.node.instance_set.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.")
        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
            # Schedule
560
            self.change_node(scheduler.get_node(self, Node.objects.all()))
tarokkk committed
561

562 563 564 565
            # Deploy virtual images
            with act.sub_activity('deploying_disks'):
                for disk in self.disks.all():
                    disk.deploy()
566

567 568 569 570 571
            queue_name = self.get_remote_queue_name('vm')
            # Deploy VM on remote machine
            with act.sub_activity('deploying_vm'):
                vm_tasks.create.apply_async(args=[self.get_vm_desc()],
                                            queue=queue_name).get()
tarokkk committed
572

573 574 575 576
            # Estabilish network connection (vmdriver)
            with act.sub_activity('deploying_net'):
                for net in self.interface_set.all():
                    net.deploy()
tarokkk committed
577

578 579 580 581
            # Resume vm
            with act.sub_activity('booting'):
                vm_tasks.resume.apply_async(args=[self.vm_name],
                                            queue=queue_name).get()
582

583 584 585
    def deploy_async(self, user=None):
        """Execute deploy asynchronously.
        """
586 587
        return local_tasks.deploy.apply_async(args=[self, user],
                                              queue="localhost.man")
588

589
    def destroy(self, user=None, task_uuid=None):
Dudás Ádám committed
590 591 592 593 594 595 596 597 598 599 600
        """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
601
        """
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
        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()

            self.destoryed = timezone.now()
            self.save()
623 624

    def destroy_async(self, user=None):
Dudás Ádám committed
625
        """Execute destroy asynchronously.
626
        """
627 628
        return local_tasks.destroy.apply_async(args=[self, user],
                                               queue="localhost.man")
629 630 631 632

    def sleep(self, user=None, task_uuid=None):
        """Suspend virtual machine with memory dump.
        """
633 634
        with instance_activity(code_suffix='sleep', instance=self,
                               task_uuid=task_uuid, user=user):
635

636 637 638
            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
639

640
    def sleep_async(self, user=None):
Dudás Ádám committed
641
        """Execute sleep asynchronously.
642
        """
643 644
        return local_tasks.sleep.apply_async(args=[self, user],
                                             queue="localhost.man")
Guba Sándor committed
645

646
    def wake_up(self, user=None, task_uuid=None):
647 648
        with instance_activity(code_suffix='wake_up', instance=self,
                               task_uuid=task_uuid, user=user):
649

650 651 652
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.resume.apply_async(args=[self.vm_name, self.dump_mem],
                                        queue=queue_name).get()
653

654
    def wake_up_async(self, user=None):
Dudás Ádám committed
655
        """Execute wake_up asynchronously.
656
        """
657 658
        return local_tasks.wake_up.apply_async(args=[self, user],
                                               queue="localhost.man")
659

660 661 662
    def shutdown(self, user=None, task_uuid=None):
        """Shutdown virtual machine with ACPI signal.
        """
663 664
        with instance_activity(code_suffix='shutdown', instance=self,
                               task_uuid=task_uuid, user=user):
665

666 667 668
            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
669

670 671
    def shutdown_async(self, user=None):
        """Execute shutdown asynchronously.
672
        """
673 674
        return local_tasks.shutdown.apply_async(args=[self, user],
                                                queue="localhost.man")
Guba Sándor committed
675

676 677 678
    def reset(self, user=None, task_uuid=None):
        """Reset virtual machine (reset button)
        """
679 680
        with instance_activity(code_suffix='reset', instance=self,
                               task_uuid=task_uuid, user=user):
681

682 683 684
            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
685

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

692
    def reboot(self, user=None, task_uuid=None):
Dudás Ádám committed
693
        """Reboot virtual machine with Ctrl+Alt+Del signal.
694
        """
695 696
        with instance_activity(code_suffix='reboot', instance=self,
                               task_uuid=task_uuid, user=user):
697

698 699 700
            queue_name = self.get_remote_queue_name('vm')
            vm_tasks.reboot.apply_async(args=[self.vm_name],
                                        queue=queue_name).get()
701

702 703
    def reboot_async(self, user=None):
        """Execute reboot asynchronously.
704
        """
705 706
        return local_tasks.reboot.apply_async(args=[self, user],
                                              queue="localhost.man")
707

708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
    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
741

742

743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
class InstanceActivity(ActivityModel):
    instance = ForeignKey(Instance, related_name='activity_log',
                          help_text=_('Instance this activity works on.'),
                          verbose_name=_('instance'))

    @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)
767
        return activitycontextimpl(act)
768 769 770 771 772


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

775

Őry Máté committed
776
class Interface(Model):
777 778 779

    """Network interface for an instance.
    """
Őry Máté committed
780 781 782 783 784
    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')
785

786 787 788
    def __unicode__(self):
        return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)

789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
    @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
806
        return EUI(m, dialect=mac_unix)
807 808 809

    def get_vmnetwork_desc(self):
        return {
Dudás Ádám committed
810
            'name': self.__unicode__(),
811
            'bridge': 'cloud',
812
            'mac': str(self.mac),
813 814
            '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,
815 816 817 818
            'vlan': self.vlan.vid,
            'managed': self.host is not None
        }

819 820 821
    def deploy(self, user=None, task_uuid=None):
        net_tasks.create.apply_async(
            args=[self.get_vmnetwork_desc()],
822
            queue=self.instance.get_remote_queue_name('net'))
823

824 825
    def destroy(self, user=None, task_uuid=None):
        net_tasks.destroy.apply_async(
826
            args=[self.get_vmnetwork_desc()],
827
            queue=self.instance.get_remote_queue_name('net'))
Guba Sándor committed
828

829
    @classmethod
830
    def create_from_template(cls, instance, template, owner=None):
831 832 833
        """Create a new interface for an instance based on an
           InterfaceTemplate.
        """
834
        if template.managed:
835 836
            host = Host()
            host.vlan = template.vlan
837
            # TODO change Host's mac field's type to EUI in firewall
838 839
            host.mac = str(cls.generate_mac(instance, template.vlan))
            host.hostname = instance.vm_name
840
            # Get adresses from firewall
841
            addresses = template.vlan.get_new_address()
842 843
            host.ipv4 = addresses['ipv4']
            host.ipv6 = addresses['ipv6']
844
            host.owner = owner
845 846 847
            host.save()
        else:
            host = None
848

849 850 851
        iface = cls(vlan=template.vlan, host=host, instance=instance)
        iface.save()
        return iface
852 853 854 855 856 857 858 859

    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