models.py 49.1 KB
Newer Older
1
# Copyright (C) 2010-2015 GRNET S.A. and individual contributors
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3 4 5 6
# This program 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.
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8 9 10 11
# This program 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.
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13 14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16 17
import datetime

18
from copy import deepcopy
19
from django.conf import settings
20
from django.db import models
21 22

import utils
23
from contextlib import contextmanager
24
from hashlib import sha1
25
from snf_django.lib.api import faults
26
from django.conf import settings as snf_settings
27
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
28

29
from synnefo.db import pools, fields
30

31 32
from synnefo.logic.rapi_pool import (get_rapi_client,
                                     put_rapi_client)
33

34 35 36
import logging
log = logging.getLogger(__name__)

37

38 39
class VolumeType(models.Model):
    NAME_LENGTH = 255
40
    DISK_TEMPLATE_LENGTH = 32
41
    name = models.CharField("Name", max_length=NAME_LENGTH)
42 43
    disk_template = models.CharField('Disk Template',
                                     max_length=DISK_TEMPLATE_LENGTH)
44 45 46 47 48 49 50 51 52
    deleted = models.BooleanField('Deleted', default=False)

    def __str__(self):
        return self.__unicode__()

    def __unicode__(self):
        return u"<VolumeType %s(disk_template:%s)>" % \
            (self.name, self.disk_template)

53 54 55 56 57 58 59 60 61 62 63
    @property
    def template(self):
        return self.disk_template.split("_")[0]

    @property
    def provider(self):
        if "_" in self.disk_template:
            return self.disk_template.split("_", 1)[1]
        else:
            return None

64

65
class Flavor(models.Model):
66
    cpu = models.IntegerField('Number of CPUs', default=0)
67 68
    ram = models.IntegerField('RAM size in MiB', default=0)
    disk = models.IntegerField('Disk size in GiB', default=0)
69 70
    volume_type = models.ForeignKey(VolumeType, related_name="flavors",
                                    on_delete=models.PROTECT, null=False)
71
    deleted = models.BooleanField('Deleted', default=False)
72 73
    # Whether the flavor can be used to create new servers
    allow_create = models.BooleanField(default=True, null=False)
74

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
75
    class Meta:
76
        verbose_name = u'Virtual machine flavor'
77
        unique_together = ('cpu', 'ram', 'disk', 'volume_type')
78

79 80
    @property
    def name(self):
81
        """Returns flavor name (generated)"""
82
        return u'C%sR%sD%s%s' % (self.cpu, self.ram, self.disk,
83
                                 self.volume_type.disk_template)
84

85 86 87
    def __str__(self):
        return self.__unicode__()

88
    def __unicode__(self):
89
        return u"<%s:%s>" % (self.id, self.name)
90

91

92 93 94 95 96
class Backend(models.Model):
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
    port = models.PositiveIntegerField('Port', default=5080)
    username = models.CharField('Username', max_length=64, blank=True,
                                null=True)
97
    password_hash = models.CharField('Password', max_length=128, blank=True,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
98
                                     null=True)
99 100
    # Sha1 is up to 40 characters long
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
101 102 103 104
    # Unique index of the Backend, used for the mac-prefixes of the
    # BackendNetworks
    index = models.PositiveIntegerField('Index', null=False, unique=True,
                                        default=0)
105 106
    drained = models.BooleanField('Drained', default=False, null=False)
    offline = models.BooleanField('Offline', default=False, null=False)
107 108 109
    # Type of hypervisor
    hypervisor = models.CharField('Hypervisor', max_length=32, default="kvm",
                                  null=False)
110
    disk_templates = fields.SeparatedValuesField("Disk Templates", null=True)
111 112 113 114 115 116 117 118 119 120 121 122
    # Last refresh of backend resources
    updated = models.DateTimeField(auto_now_add=True)
    # Backend resources
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
                                            null=False)
    ctotal = models.PositiveIntegerField('Total number of logical processors',
                                         default=0, null=False)

123 124 125 126 127 128
    HYPERVISORS = (
        ("kvm", "Linux KVM hypervisor"),
        ("xen-pvm", "Xen PVM hypervisor"),
        ("xen-hvm", "Xen KVM hypervisor"),
    )

129 130 131 132
    class Meta:
        verbose_name = u'Backend'
        ordering = ["clustername"]

133 134 135
    def __str__(self):
        return self.__unicode__()

136
    def __unicode__(self):
137
        return u"%s(id:%s)" % (self.clustername, self.id)
138 139 140 141 142

    @property
    def backend_id(self):
        return self.id

143
    def get_client(self):
144
        """Get or create a client. """
145
        if self.offline:
146 147
            raise faults.ServiceUnavailable("Backend '%s' is offline" %
                                            self)
148
        return get_rapi_client(self.id, self.hash,
149 150
                               self.clustername,
                               self.port,
151 152 153 154 155 156
                               self.username,
                               self.password)

    @staticmethod
    def put_client(client):
            put_rapi_client(client)
157 158 159

    def create_hash(self):
        """Create a hash for this backend. """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
160 161 162
        sha = sha1('%s%s%s%s' %
                   (self.clustername, self.port, self.username, self.password))
        return sha.hexdigest()
163

164 165 166 167 168 169 170 171
    @property
    def password(self):
        return decrypt_db_charfield(self.password_hash)

    @password.setter
    def password(self, value):
        self.password_hash = encrypt_db_charfield(value)

172 173 174 175 176 177 178
    def save(self, *args, **kwargs):
        # Create a new hash each time a Backend is saved
        old_hash = self.hash
        self.hash = self.create_hash()
        super(Backend, self).save(*args, **kwargs)
        if self.hash != old_hash:
            # Populate the new hash to the new instances
Christos Stavrakakis's avatar
Christos Stavrakakis committed
179 180
            self.virtual_machines.filter(deleted=False)\
                                 .update(backend_hash=self.hash)
181

182 183 184 185 186
    def __init__(self, *args, **kwargs):
        super(Backend, self).__init__(*args, **kwargs)
        if not self.pk:
            # Generate a unique index for the Backend
            indexes = Backend.objects.all().values_list('index', flat=True)
187 188 189 190
            try:
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
                self.index = first_free
            except IndexError:
191
                raise Exception("Cannot create more than 16 backends")
192

193 194 195 196 197 198 199 200 201
    def use_hotplug(self):
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG

    def get_create_params(self):
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
        params["hvparams"] = params.get("hvparams", {})\
                                   .get(self.hypervisor, {})
        return params

202

203 204 205 206 207 208 209 210 211 212 213 214
# A backend job may be in one of the following possible states
BACKEND_STATUSES = (
    ('queued', 'request queued'),
    ('waiting', 'request waiting for locks'),
    ('canceling', 'request being canceled'),
    ('running', 'request running'),
    ('canceled', 'request canceled'),
    ('success', 'request completed successfully'),
    ('error', 'request returned error')
)


215
class QuotaHolderSerial(models.Model):
216 217 218 219 220 221 222 223 224 225
    """Model representing a serial for a Quotaholder Commission.

    serial:   The serial that Quotaholder assigned to this commission
    pending:  Whether it has been decided to accept or reject this commission
    accept:   If pending is False, this attribute indicates whether to accept
              or reject this commission
    resolved: Whether this commission has been accepted or rejected to
              Quotaholder.

    """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
226 227
    serial = models.BigIntegerField(null=False, primary_key=True,
                                    db_index=True)
228
    pending = models.BooleanField(default=True, db_index=True)
229 230
    accept = models.BooleanField(default=False)
    resolved = models.BooleanField(default=False)
231 232 233 234 235

    class Meta:
        verbose_name = u'Quota Serial'
        ordering = ["serial"]

236 237 238
    def __str__(self):
        return self.__unicode__()

239 240 241
    def __unicode__(self):
        return u"<serial: %s>" % self.serial

242

243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
class VirtualMachineManager(models.Manager):
    """Custom manager for :class:`VirtualMachine` model."""

    def for_user(self, userid=None, projects=None):
        """Return VMs that are accessible by the user.

        VMs that are accessible by the user are those that are owned by the
        user and those that are shared to the projects that the user is member.

        """

        _filter = models.Q()

        if userid:
            _filter |= models.Q(userid=userid)
        if projects:
            _filter |= (models.Q(shared_to_project=True) &\
                        models.Q(project__in=projects))

        return self.get_query_set().filter(_filter)


Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
265
class VirtualMachine(models.Model):
266
    # The list of possible actions for a VM
267
    ACTIONS = (
Christos Stavrakakis's avatar
Christos Stavrakakis committed
268 269 270 271 272
        ('CREATE', 'Create VM'),
        ('START', 'Start VM'),
        ('STOP', 'Shutdown VM'),
        ('SUSPEND', 'Admin Suspend VM'),
        ('REBOOT', 'Reboot VM'),
273 274
        ('DESTROY', 'Destroy VM'),
        ('RESIZE', 'Resize a VM'),
275 276
        ('ADDFLOATINGIP', 'Add floating IP to VM'),
        ('REMOVEFLOATINGIP', 'Add floating IP to VM'),
277
    )
278

279
    # The internal operating state of a VM
280 281 282 283 284
    OPER_STATES = (
        ('BUILD', 'Queued for creation'),
        ('ERROR', 'Creation failed'),
        ('STOPPED', 'Stopped'),
        ('STARTED', 'Started'),
285 286
        ('DESTROYED', 'Destroyed'),
        ('RESIZE', 'Resizing')
287
    )
288

289
    # The list of possible operations on the backend
290 291 292 293 294
    BACKEND_OPCODES = (
        ('OP_INSTANCE_CREATE', 'Create Instance'),
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
295 296 297 298 299 300
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),

        # These are listed here for completeness,
        # and are ignored for the time being
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
301 302 303 304 305 306
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
307 308
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
309
    )
310

311 312
    # The operating state of a VM,
    # upon the successful completion of a backend operation.
313 314
    # IMPORTANT: Make sure all keys have a corresponding
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
315 316 317 318 319
    OPER_STATE_FROM_OPCODE = {
        'OP_INSTANCE_CREATE': 'STARTED',
        'OP_INSTANCE_REMOVE': 'DESTROYED',
        'OP_INSTANCE_STARTUP': 'STARTED',
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
320 321
        'OP_INSTANCE_REBOOT': 'STARTED',
        'OP_INSTANCE_SET_PARAMS': None,
322
        'OP_INSTANCE_QUERY_DATA': None,
323 324
        'OP_INSTANCE_REINSTALL': None,
        'OP_INSTANCE_ACTIVATE_DISKS': None,
325
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
326
        'OP_INSTANCE_REPLACE_DISKS': None,
327 328
        'OP_INSTANCE_MIGRATE': None,
        'OP_INSTANCE_CONSOLE': None,
329 330
        'OP_INSTANCE_RECREATE_DISKS': None,
        'OP_INSTANCE_FAILOVER': None
331 332
    }

333 334 335
    # This dictionary contains the correspondence between
    # internal operating states and Server States as defined
    # by the Rackspace API.
336 337 338 339 340
    RSAPI_STATE_FROM_OPER_STATE = {
        "BUILD": "BUILD",
        "ERROR": "ERROR",
        "STOPPED": "STOPPED",
        "STARTED": "ACTIVE",
341 342
        'RESIZE': 'RESIZE',
        'DESTROYED': 'DELETED',
343 344
    }

345 346
    VIRTUAL_MACHINE_NAME_LENGTH = 255

347 348
    objects = VirtualMachineManager()

349 350
    name = models.CharField('Virtual Machine Name',
                            max_length=VIRTUAL_MACHINE_NAME_LENGTH)
351
    userid = models.CharField('User ID of the owner', max_length=100,
352
                              db_index=True, null=False)
353
    project = models.CharField(max_length=255, null=True, db_index=True)
354 355
    shared_to_project = models.BooleanField('Shared to project',
                                            default=False)
356
    backend = models.ForeignKey(Backend, null=True,
357 358
                                related_name="virtual_machines",
                                on_delete=models.PROTECT)
359
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
360 361
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
362
    imageid = models.CharField(max_length=100, null=False)
363
    image_version = models.IntegerField(null=True)
364
    hostid = models.CharField(max_length=100)
365
    flavor = models.ForeignKey(Flavor, on_delete=models.PROTECT)
366
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
367
    suspended = models.BooleanField('Administratively Suspended',
368
                                    default=False)
369
    serial = models.ForeignKey(QuotaHolderSerial,
370 371
                               related_name='virtual_machine', null=True,
                               on_delete=models.SET_NULL)
372
    helper = models.BooleanField(default=False, null=False)
373

374
    # VM State
375 376 377 378
    # The following fields are volatile data, in the sense
    # that they need not be persistent in the DB, but rather
    # get generated at runtime by quering Ganeti and applying
    # updates received from Ganeti.
379

380 381 382
    # In the future they could be moved to a separate caching layer
    # and removed from the database.
    # [vkoukis] after discussion with [faidon].
383 384 385 386
    action = models.CharField(choices=ACTIONS, max_length=30, null=True,
                              default=None)
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
                                 null=False, default="BUILD")
387
    backendjobid = models.PositiveIntegerField(null=True)
388
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
389
                                     null=True)
390
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
391
                                        max_length=30, null=True)
392
    backendlogmsg = models.TextField(null=True)
393
    buildpercentage = models.IntegerField(default=0)
394
    backendtime = models.DateTimeField(default=datetime.datetime.min)
395

396 397 398
    # Latest action and corresponding Ganeti job ID, for actions issued
    # by the API
    task = models.CharField(max_length=64, null=True)
399
    task_job_id = models.BigIntegerField(null=True)
400

401 402
    def get_client(self):
        if self.backend:
403
            return self.backend.get_client()
404
        else:
405
            raise faults.ServiceUnavailable("VirtualMachine without backend")
406

407 408 409 410 411 412
    def get_last_diagnostic(self, **filters):
        try:
            return self.diagnostics.filter()[0]
        except IndexError:
            return None

413 414 415 416
    @staticmethod
    def put_client(client):
            put_rapi_client(client)

417 418 419 420 421 422
    def save(self, *args, **kwargs):
        # Store hash for first time saved vm
        if (self.id is None or self.backend_hash == '') and self.backend:
            self.backend_hash = self.backend.hash
        super(VirtualMachine, self).save(*args, **kwargs)

423
    @property
424
    def backend_vm_id(self):
425
        """Returns the backend id for this VM by prepending backend-prefix."""
426 427
        if not self.id:
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
428
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
429

430 431 432
    class Meta:
        verbose_name = u'Virtual machine instance'
        get_latest_by = 'created'
433

434 435 436
    def __str__(self):
        return self.__unicode__()

437
    def __unicode__(self):
438
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
439

Christos Stavrakakis's avatar
Christos Stavrakakis committed
440
    # Error classes
441
    class InvalidBackendIdError(ValueError):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    class InvalidBackendMsgError(Exception):
        def __init__(self, opcode, status):
            self.opcode = opcode
            self.status = status

        def __str__(self):
            return repr('<opcode: %s, status: %s>' % (self.opcode,
                        self.status))

    class InvalidActionError(Exception):
        def __init__(self, action):
            self._action = action

        def __str__(self):
            return repr(str(self._action))

464

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
465
class VirtualMachineMetadata(models.Model):
466 467 468 469
    KEY_LENGTH = 50
    VALUE_LENGTH = 500
    meta_key = models.CharField(max_length=KEY_LENGTH)
    meta_value = models.CharField(max_length=VALUE_LENGTH)
470 471
    vm = models.ForeignKey(VirtualMachine, related_name='metadata',
                           on_delete=models.CASCADE)
472

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
473
    class Meta:
474
        unique_together = (('meta_key', 'vm'),)
475
        verbose_name = u'Key-value pair of metadata for a VM.'
476

477 478 479
    def __str__(self):
        return self.__unicode__()

Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
480
    def __unicode__(self):
481
        return u'<Metadata %s: %s>' % (self.meta_key, self.meta_value)
Vassilios Karakoidas's avatar
Vassilios Karakoidas committed
482 483


484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
class Image(models.Model):
    """Model representing Images of created VirtualMachines.

    This model stores basic information about Images which have been used to
    create VirtualMachines or Volumes.

    """

    uuid = models.CharField(max_length=128)
    version = models.IntegerField(null=False)
    owner = models.CharField(max_length=128, null=False)
    name = models.CharField(max_length=256, null=False)
    location = models.TextField()
    mapfile = models.CharField(max_length=256, null=False)
    is_public = models.BooleanField(default=False, null=False)
    is_snapshot = models.BooleanField(default=False, null=False)
    is_system = models.BooleanField(default=False, null=False)
    os = models.CharField(max_length=256)
    osfamily = models.CharField(max_length=256)

    class Meta:
        unique_together = (('uuid', 'version'),)


508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
class NetworkManager(models.Manager):
    """Custom manager for :class:`Network` model."""

    def for_user(self, userid=None, projects=None, public=True):
        """Return networks that are accessible by the user.

        Networks that are accessible by the user are those that are owned by
        the user, those that are shared to the projects that the user is
        member, and public networks.

        """

        _filter = models.Q()

        if userid:
            _filter |= models.Q(userid=userid)
        if projects:
            _filter |= (models.Q(shared_to_project=True) &\
                        models.Q(project__in=projects))
        if public:
            _filter |= models.Q(public=True)

        return self.get_query_set().filter(_filter)


533
class Network(models.Model):
534
    OPER_STATES = (
535
        ('PENDING', 'Pending'),  # Unused because of lazy networks
536
        ('ACTIVE', 'Active'),
537 538 539 540 541
        ('DELETED', 'Deleted'),
        ('ERROR', 'Error')
    )

    ACTIONS = (
Christos Stavrakakis's avatar
Christos Stavrakakis committed
542 543
        ('CREATE', 'Create Network'),
        ('DESTROY', 'Destroy Network'),
544 545
        ('ADD', 'Add server to Network'),
        ('REMOVE', 'Remove server from Network'),
546 547 548 549 550 551 552 553 554
    )

    RSAPI_STATE_FROM_OPER_STATE = {
        'PENDING': 'PENDING',
        'ACTIVE': 'ACTIVE',
        'DELETED': 'DELETED',
        'ERROR': 'ERROR'
    }

555 556
    FLAVORS = {
        'CUSTOM': {
Christos Stavrakakis's avatar
Christos Stavrakakis committed
557 558 559 560 561
            'mode': 'bridged',
            'link': settings.DEFAULT_BRIDGE,
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
            'tags': None,
            'desc': "Basic flavor used for a bridged network",
562 563
        },
        'IP_LESS_ROUTED': {
Christos Stavrakakis's avatar
Christos Stavrakakis committed
564
            'mode': 'routed',
565
            'link': None,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
566 567 568 569
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
            'tags': 'ip-less-routed',
            'desc': "Flavor used for an IP-less routed network using"
                    " Proxy ARP",
570 571
        },
        'MAC_FILTERED': {
Christos Stavrakakis's avatar
Christos Stavrakakis committed
572 573 574 575 576 577 578
            'mode': 'bridged',
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
            'mac_prefix': 'pool',
            'tags': 'private-filtered',
            'desc': "Flavor used for bridged networks that offer isolation"
                    " via filtering packets based on their src "
                    " MAC (ebtables)",
579 580
        },
        'PHYSICAL_VLAN': {
Christos Stavrakakis's avatar
Christos Stavrakakis committed
581 582 583 584 585 586
            'mode': 'bridged',
            'link': 'pool',
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
            'tags': 'physical-vlan',
            'desc': "Flavor used for bridged network that offer isolation"
                    " via dedicated physical vlan",
587 588
        },
    }
589

590 591
    NETWORK_NAME_LENGTH = 128

592 593
    objects = NetworkManager()

594
    name = models.CharField('Network Name', max_length=NETWORK_NAME_LENGTH)
595 596
    userid = models.CharField('User ID of the owner', max_length=128,
                              null=True, db_index=True)
597
    project = models.CharField(max_length=255, null=True, db_index=True)
598 599
    shared_to_project = models.BooleanField('Shared to project',
                                            default=False)
600 601 602
    flavor = models.CharField('Flavor', max_length=32, null=False)
    mode = models.CharField('Network Mode', max_length=16, null=True)
    link = models.CharField('Network Link', max_length=32, null=True)
603
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
604
    tags = models.CharField('Network Tags', max_length=128, null=True)
605
    public = models.BooleanField(default=False, db_index=True)
606 607
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
608
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
609 610
    state = models.CharField(choices=OPER_STATES, max_length=32,
                             default='PENDING')
611
    machines = models.ManyToManyField(VirtualMachine,
612
                                      through='NetworkInterface')
613 614
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
                              default=None)
615
    drained = models.BooleanField("Drained", default=False, null=False)
616
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
617
                                           default=False)
618
    external_router = models.BooleanField(default=False)
619
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
620
                               null=True, on_delete=models.SET_NULL)
621
    subnet_ids = fields.SeparatedValuesField("Subnet IDs", null=True)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
622

623 624 625
    def __str__(self):
        return self.__unicode__()

626
    def __unicode__(self):
627
        return u"<Network: %s>" % self.id
628

629 630
    @property
    def backend_id(self):
631
        """Return the backend id by prepending backend-prefix."""
632 633
        if not self.id:
            raise Network.InvalidBackendIdError("self.id is None")
634
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
635 636 637 638 639 640

    @property
    def backend_tag(self):
        """Return the network tag to be used in backend

        """
641 642 643 644
        if self.tags:
            return self.tags.split(',')
        else:
            return []
645

646 647 648
    def create_backend_network(self, backend=None):
        """Create corresponding BackendNetwork entries."""

Christos Stavrakakis's avatar
Christos Stavrakakis committed
649 650
        backends = [backend] if backend else\
            Backend.objects.filter(offline=False)
651
        for backend in backends:
Christos Stavrakakis's avatar
Christos Stavrakakis committed
652 653 654 655
            backend_exists =\
                BackendNetwork.objects.filter(backend=backend, network=self)\
                                      .exists()
            if not backend_exists:
656
                BackendNetwork.objects.create(backend=backend, network=self)
657

658 659 660 661 662
    def get_ip_pools(self, locked=True):
        subnets = self.subnets.filter(ipversion=4, deleted=False)\
                              .prefetch_related("ip_pools")
        return [ip_pool for subnet in subnets
                for ip_pool in subnet.get_ip_pools(locked=locked)]
Christos Stavrakakis's avatar
Christos Stavrakakis committed
663

664
    def reserve_address(self, address, external=False):
665 666 667 668 669 670 671
        for ip_pool in self.get_ip_pools():
            if ip_pool.contains(address):
                ip_pool.reserve(address, external=external)
                ip_pool.save()
                return
        raise pools.InvalidValue("Network %s does not have an IP pool that"
                                 " contains address %s" % (self, address))
Christos Stavrakakis's avatar
Christos Stavrakakis committed
672

673
    def release_address(self, address, external=False):
674 675
        for ip_pool in self.get_ip_pools():
            if ip_pool.contains(address):
676
                ip_pool.put(address, external=external)
677 678 679 680
                ip_pool.save()
                return
        raise pools.InvalidValue("Network %s does not have an IP pool that"
                                 " contains address %s" % (self, address))
Christos Stavrakakis's avatar
Christos Stavrakakis committed
681

682 683 684 685 686 687 688 689 690 691 692
    @property
    def subnet4(self):
        return self.get_subnet(version=4)

    @property
    def subnet6(self):
        return self.get_subnet(version=6)

    def get_subnet(self, version=4):
        for subnet in self.subnets.all():
            if subnet.ipversion == version:
693 694
                return subnet
        return None
695

696 697 698
    def ip_count(self):
        """Return the total and free IPv4 addresses of the network."""
        total, free = 0, 0
699 700 701 702
        ip_pools = self.get_ip_pools(locked=False)
        for ip_pool in ip_pools:
            total += ip_pool.pool_size
            free += ip_pool.count_available()
703 704
        return total, free

705
    class InvalidBackendIdError(ValueError):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
706 707 708 709 710 711 712 713 714 715 716 717
        def __init__(self, value):
            self.value = value

        def __str__(self):
            return repr(self.value)

    class InvalidBackendMsgError(Exception):
        def __init__(self, opcode, status):
            self.opcode = opcode
            self.status = status

        def __str__(self):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
718 719
            return repr('<opcode: %s, status: %s>'
                        % (self.opcode, self.status))
Christos Stavrakakis's avatar
Christos Stavrakakis committed
720 721 722 723 724 725 726 727

    class InvalidActionError(Exception):
        def __init__(self, action):
            self._action = action

        def __str__(self):
            return repr(str(self._action))

728

729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
class SubnetManager(models.Manager):
    """Custom manager for :class:`Subnet` model."""

    def for_user(self, userid=None, projects=None, public=True):
        """Return subnets that are accessible by the user.

        Subnets that are accessible by the user are those that belong
        to a network that is accessible by the user.

        """

        networks = Network.objects.for_user(userid, projects, public=public)

        return self.get_query_set().filter(network__in=networks)


745 746 747
class Subnet(models.Model):
    SUBNET_NAME_LENGTH = 128

748 749
    objects = SubnetManager()

750 751 752 753
    userid = models.CharField('User ID of the owner', max_length=128,
                              null=True, db_index=True)
    public = models.BooleanField(default=False, db_index=True)

754
    network = models.ForeignKey('Network', null=False, db_index=True,
755 756
                                related_name="subnets",
                                on_delete=models.PROTECT)
757
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
758
                            null=True, default="")
759
    ipversion = models.IntegerField('IP Version', default=4, null=False)
760
    cidr = models.CharField('Subnet', max_length=64, null=False)
761
    gateway = models.CharField('Gateway', max_length=64, null=True)
762 763 764
    dhcp = models.BooleanField('DHCP', default=True, null=False)
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
                                  null=False)
765 766
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
767 768
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
769

770 771 772
    def __str__(self):
        return self.__unicode__()

773
    def __unicode__(self):
774 775
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
        return msg % (self.id, self.network_id, self.cidr)
776

777
    def get_ip_pools(self, locked=True):
778 779 780
        ip_pools = self.ip_pools
        if locked:
            ip_pools = ip_pools.select_for_update()
781
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
782

783

784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816
class BackendNetwork(models.Model):
    OPER_STATES = (
        ('PENDING', 'Pending'),
        ('ACTIVE', 'Active'),
        ('DELETED', 'Deleted'),
        ('ERROR', 'Error')
    )

    # The list of possible operations on the backend
    BACKEND_OPCODES = (
        ('OP_NETWORK_ADD', 'Create Network'),
        ('OP_NETWORK_CONNECT', 'Activate Network'),
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
        ('OP_NETWORK_REMOVE', 'Remove Network'),
        # These are listed here for completeness,
        # and are ignored for the time being
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
    )

    # The operating state of a Netowork,
    # upon the successful completion of a backend operation.
    # IMPORTANT: Make sure all keys have a corresponding
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
    OPER_STATE_FROM_OPCODE = {
        'OP_NETWORK_ADD': 'PENDING',
        'OP_NETWORK_CONNECT': 'ACTIVE',
        'OP_NETWORK_DISCONNECT': 'PENDING',
        'OP_NETWORK_REMOVE': 'DELETED',
        'OP_NETWORK_SET_PARAMS': None,
        'OP_NETWORK_QUERY_DATA': None
    }

817
    network = models.ForeignKey(Network, related_name='backend_networks',
818
                                on_delete=models.PROTECT)
819 820
    backend = models.ForeignKey(Backend, related_name='networks',
                                on_delete=models.PROTECT)
821 822 823
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    deleted = models.BooleanField('Deleted', default=False)
824
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
825 826 827 828 829 830 831 832 833 834 835
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
                                 default='PENDING')
    backendjobid = models.PositiveIntegerField(null=True)
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
                                     null=True)
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
                                        max_length=30, null=True)
    backendlogmsg = models.TextField(null=True)
    backendtime = models.DateTimeField(null=False,
                                       default=datetime.datetime.min)

836 837 838 839
    class Meta:
        # Ensure one entry for each network in each backend
        unique_together = (("network", "backend"))

840 841 842 843 844 845 846 847 848 849 850 851
    def __init__(self, *args, **kwargs):
        """Initialize state for just created BackendNetwork instances."""
        super(BackendNetwork, self).__init__(*args, **kwargs)
        if not self.mac_prefix:
            # Generate the MAC prefix of the BackendNetwork, by combining
            # the Network prefix with the index of the Backend
            net_prefix = self.network.mac_prefix
            backend_suffix = hex(self.backend.index).replace('0x', '')
            mac_prefix = net_prefix + backend_suffix
            try:
                utils.validate_mac(mac_prefix + ":00:00:00")
            except utils.InvalidMacAddress:
Christos Stavrakakis's avatar
Christos Stavrakakis committed
852 853
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
                                              mac_prefix)
854 855
            self.mac_prefix = mac_prefix

856 857 858
    def __str__(self):
        return self.__unicode__()

859
    def __unicode__(self):
860
        return u'<BackendNetwork %s@%s>' % (self.network, self.backend)
861

862

863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883
class IPAddressManager(models.Manager):
    """Custom manager for :class:`IPAddress` model."""

    def for_user(self, userid=None, projects=None):
        """Return IP addresses that are accessible by the user.

        IP addresses that are accessible by the user are those that are owned
        by the user or are shared to a project that the user is member.

        """
        _filter = models.Q()

        if userid:
            _filter |= models.Q(userid=userid)
        if projects:
            _filter |= (models.Q(shared_to_project=True) &\
                        models.Q(project__in=projects))

        return self.get_query_set().filter(_filter)


884
class IPAddress(models.Model):
885 886
    objects = IPAddressManager()

887
    subnet = models.ForeignKey("Subnet", related_name="ips", null=False,
888
                               on_delete=models.PROTECT)
889
    network = models.ForeignKey(Network, related_name="ips", null=False,
890
                                on_delete=models.PROTECT)
891 892 893 894
    nic = models.ForeignKey("NetworkInterface", related_name="ips", null=True,
                            on_delete=models.SET_NULL)
    userid = models.CharField("UUID of the owner", max_length=128, null=False,
                              db_index=True)
895
    project = models.CharField(max_length=255, null=True, db_index=True)
896 897
    shared_to_project = models.BooleanField('Shared to project',
                                            default=False)
898 899
    address = models.CharField("IP Address", max_length=64, null=False)
    floating_ip = models.BooleanField("Floating IP", null=False, default=False)
900
    ipversion = models.IntegerField("IP Version", null=False)
901 902 903 904 905 906 907 908
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    deleted = models.BooleanField(default=False, null=False)

    serial = models.ForeignKey(QuotaHolderSerial,
                               related_name="ips", null=True,
                               on_delete=models.SET_NULL)

909 910 911
    def __str__(self):
        return self.__unicode__()

912 913 914 915 916 917
    def __unicode__(self):
        ip_type = "floating" if self.floating_ip else "static"
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
               % (self.address, self.network_id, self.subnet_id, ip_type)

    def in_use(self):
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
918
        if self.nic is None or self.nic.machine is None:
919 920
            return False
        else:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
921
            return (not self.nic.machine.deleted)
922 923

    class Meta:
924
        unique_together = ("network", "address", "deleted")
925

926 927 928 929 930 931 932 933
    def release_address(self):
        """Release the IPv4 address."""
        if self.ipversion == 4:
            for pool_row in self.subnet.ip_pools.all():
                ip_pool = pool_row.pool
                if ip_pool.contains(self.address):
                    ip_pool.put(self.address)
                    ip_pool.save()
934
                    return
935
            log.error("Cannot release address %s of NIC %s. Address does not"
936 937
                      " belong to any of the IP pools of the subnet %s !",
                      self.address, self.nic, self.subnet_id)
938

939

940 941 942 943 944 945 946 947 948 949 950 951
class IPAddressLog(models.Model):
    address = models.CharField("IP Address", max_length=64, null=False,
                               db_index=True)
    server_id = models.IntegerField("Server", null=False)
    network_id = models.IntegerField("Network", null=False)
    allocated_at = models.DateTimeField("Datetime IP allocated to server",
                                        auto_now_add=True)
    released_at = models.DateTimeField("Datetime IP released from server",
                                       null=True)
    active = models.BooleanField("Whether IP still allocated to server",
                                 default=True)

952 953 954
    def __str__(self):
        return self.__unicode__()

955 956
    def __unicode__(self):
        return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\
957
               % (self.address, self.server_id, self.network_id,
958 959 960
                  self.allocated_at)


961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
class NetworkInterfaceManager(models.Manager):
    """Custom manager for :class:`NetworkInterface` model."""

    def for_user(self, userid=None, projects=None):
        """Return ports (NetworkInterfaces) that are accessible by the user.

        Ports that are accessible by the user are those that:
        * are owned by the user
        * are attached to a VM that is accessible by the user
        * are attached to a Network that is accessible by the user (but
          not public)

        """

        vms = VirtualMachine.objects.for_user(userid, projects)
        networks = Network.objects.for_user(userid, projects, public=False)\
                                  .filter(public=False)
978
        ips = IPAddress.objects.for_user(userid, projects).filter(floating_ip=True)
979 980 981 982 983 984 985

        _filter = models.Q()
        if userid:
            _filter |= models.Q(userid=userid)

        _filter |= models.Q(machine__in=vms)
        _filter |= models.Q(network__in=networks)
986
        _filter |= models.Q(ips__in=ips)
987 988 989 990

        return self.get_query_set().filter(_filter)


991 992 993
class NetworkInterface(models.Model):
    FIREWALL_PROFILES = (
        ('ENABLED', 'Enabled'),
994 995
        ('DISABLED', 'Disabled'),
        ('PROTECTED', 'Protected')
996
    )