Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 6a03d89f

History | View | Annotate | Download (36 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import datetime
31

    
32
from copy import deepcopy
33
from django.conf import settings
34
from django.db import models
35

    
36
import utils
37
from contextlib import contextmanager
38
from hashlib import sha1
39
from snf_django.lib.api import faults
40
from django.conf import settings as snf_settings
41
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
42

    
43
from synnefo.db import pools, fields
44

    
45
from synnefo.logic.rapi_pool import (get_rapi_client,
46
                                     put_rapi_client)
47

    
48
import logging
49
log = logging.getLogger(__name__)
50

    
51

    
52
class Flavor(models.Model):
53
    cpu = models.IntegerField('Number of CPUs', default=0)
54
    ram = models.IntegerField('RAM size in MiB', default=0)
55
    disk = models.IntegerField('Disk size in GiB', default=0)
56
    disk_template = models.CharField('Disk template', max_length=32)
57
    deleted = models.BooleanField('Deleted', default=False)
58

    
59
    class Meta:
60
        verbose_name = u'Virtual machine flavor'
61
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
62

    
63
    @property
64
    def name(self):
65
        """Returns flavor name (generated)"""
66
        return u'C%dR%dD%d%s' % (self.cpu, self.ram, self.disk,
67
                                 self.disk_template)
68

    
69
    def __unicode__(self):
70
        return "<%s:%s>" % (str(self.id), self.name)
71

    
72

    
73
class Backend(models.Model):
74
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
75
    port = models.PositiveIntegerField('Port', default=5080)
76
    username = models.CharField('Username', max_length=64, blank=True,
77
                                null=True)
78
    password_hash = models.CharField('Password', max_length=128, blank=True,
79
                                     null=True)
80
    # Sha1 is up to 40 characters long
81
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
82
    # Unique index of the Backend, used for the mac-prefixes of the
83
    # BackendNetworks
84
    index = models.PositiveIntegerField('Index', null=False, unique=True,
85
                                        default=0)
86
    drained = models.BooleanField('Drained', default=False, null=False)
87
    offline = models.BooleanField('Offline', default=False, null=False)
88
    # Type of hypervisor
89
    hypervisor = models.CharField('Hypervisor', max_length=32, default="kvm",
90
                                  null=False)
91
    disk_templates = fields.SeparatedValuesField("Disk Templates", null=True)
92
    # Last refresh of backend resources
93
    updated = models.DateTimeField(auto_now_add=True)
94
    # Backend resources
95
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
96
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
97
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
98
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
99
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
100
                                            null=False)
101
    ctotal = models.PositiveIntegerField('Total number of logical processors',
102
                                         default=0, null=False)
103

    
104
    HYPERVISORS = (
105
        ("kvm", "Linux KVM hypervisor"),
106
        ("xen-pvm", "Xen PVM hypervisor"),
107
        ("xen-hvm", "Xen KVM hypervisor"),
108
    )
109

    
110
    class Meta:
111
        verbose_name = u'Backend'
112
        ordering = ["clustername"]
113

    
114
    def __unicode__(self):
115
        return self.clustername + "(id=" + str(self.id) + ")"
116

    
117
    @property
118
    def backend_id(self):
119
        return self.id
120

    
121
    def get_client(self):
122
        """Get or create a client. """
123
        if self.offline:
124
            raise faults.ServiceUnavailable("Backend '%s' is offline" %
125
                                            self)
126
        return get_rapi_client(self.id, self.hash,
127
                               self.clustername,
128
                               self.port,
129
                               self.username,
130
                               self.password)
131

    
132
    @staticmethod
133
    def put_client(client):
134
            put_rapi_client(client)
135

    
136
    def create_hash(self):
137
        """Create a hash for this backend. """
138
        sha = sha1('%s%s%s%s' %
139
                   (self.clustername, self.port, self.username, self.password))
140
        return sha.hexdigest()
141

    
142
    @property
143
    def password(self):
144
        return decrypt_db_charfield(self.password_hash)
145

    
146
    @password.setter
147
    def password(self, value):
148
        self.password_hash = encrypt_db_charfield(value)
149

    
150
    def save(self, *args, **kwargs):
151
        # Create a new hash each time a Backend is saved
152
        old_hash = self.hash
153
        self.hash = self.create_hash()
154
        super(Backend, self).save(*args, **kwargs)
155
        if self.hash != old_hash:
156
            # Populate the new hash to the new instances
157
            self.virtual_machines.filter(deleted=False)\
158
                                 .update(backend_hash=self.hash)
159

    
160
    def __init__(self, *args, **kwargs):
161
        super(Backend, self).__init__(*args, **kwargs)
162
        if not self.pk:
163
            # Generate a unique index for the Backend
164
            indexes = Backend.objects.all().values_list('index', flat=True)
165
            try:
166
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
167
                self.index = first_free
168
            except IndexError:
169
                raise Exception("Cannot create more than 16 backends")
170

    
171
    def use_hotplug(self):
172
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
173

    
174
    def get_create_params(self):
175
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
176
        params["hvparams"] = params.get("hvparams", {})\
177
                                   .get(self.hypervisor, {})
178
        return params
179

    
180

    
181
# A backend job may be in one of the following possible states
182
BACKEND_STATUSES = (
183
    ('queued', 'request queued'),
184
    ('waiting', 'request waiting for locks'),
185
    ('canceling', 'request being canceled'),
186
    ('running', 'request running'),
187
    ('canceled', 'request canceled'),
188
    ('success', 'request completed successfully'),
189
    ('error', 'request returned error')
190
)
191

    
192

    
193
class QuotaHolderSerial(models.Model):
194
    """Model representing a serial for a Quotaholder Commission.
195

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

203
    """
204
    serial = models.BigIntegerField(null=False, primary_key=True,
205
                                    db_index=True)
206
    pending = models.BooleanField(default=True, db_index=True)
207
    accept = models.BooleanField(default=False)
208
    resolved = models.BooleanField(default=False)
209

    
210
    class Meta:
211
        verbose_name = u'Quota Serial'
212
        ordering = ["serial"]
213

    
214
    def __unicode__(self):
215
        return u"<serial: %s>" % self.serial
216

    
217

    
218
class VirtualMachine(models.Model):
219
    # The list of possible actions for a VM
220
    ACTIONS = (
221
        ('CREATE', 'Create VM'),
222
        ('START', 'Start VM'),
223
        ('STOP', 'Shutdown VM'),
224
        ('SUSPEND', 'Admin Suspend VM'),
225
        ('REBOOT', 'Reboot VM'),
226
        ('DESTROY', 'Destroy VM'),
227
        ('RESIZE', 'Resize a VM'),
228
        ('ADDFLOATINGIP', 'Add floating IP to VM'),
229
        ('REMOVEFLOATINGIP', 'Add floating IP to VM'),
230
    )
231

    
232
    # The internal operating state of a VM
233
    OPER_STATES = (
234
        ('BUILD', 'Queued for creation'),
235
        ('ERROR', 'Creation failed'),
236
        ('STOPPED', 'Stopped'),
237
        ('STARTED', 'Started'),
238
        ('DESTROYED', 'Destroyed'),
239
        ('RESIZE', 'Resizing')
240
    )
241

    
242
    # The list of possible operations on the backend
243
    BACKEND_OPCODES = (
244
        ('OP_INSTANCE_CREATE', 'Create Instance'),
245
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
246
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
247
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
248
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
249

    
250
        # These are listed here for completeness,
251
        # and are ignored for the time being
252
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
253
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
254
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
255
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
256
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
257
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
258
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
259
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
260
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
261
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
262
    )
263

    
264
    # The operating state of a VM,
265
    # upon the successful completion of a backend operation.
266
    # IMPORTANT: Make sure all keys have a corresponding
267
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
268
    OPER_STATE_FROM_OPCODE = {
269
        'OP_INSTANCE_CREATE': 'STARTED',
270
        'OP_INSTANCE_REMOVE': 'DESTROYED',
271
        'OP_INSTANCE_STARTUP': 'STARTED',
272
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
273
        'OP_INSTANCE_REBOOT': 'STARTED',
274
        'OP_INSTANCE_SET_PARAMS': None,
275
        'OP_INSTANCE_QUERY_DATA': None,
276
        'OP_INSTANCE_REINSTALL': None,
277
        'OP_INSTANCE_ACTIVATE_DISKS': None,
278
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
279
        'OP_INSTANCE_REPLACE_DISKS': None,
280
        'OP_INSTANCE_MIGRATE': None,
281
        'OP_INSTANCE_CONSOLE': None,
282
        'OP_INSTANCE_RECREATE_DISKS': None,
283
        'OP_INSTANCE_FAILOVER': None
284
    }
285

    
286
    # This dictionary contains the correspondence between
287
    # internal operating states and Server States as defined
288
    # by the Rackspace API.
289
    RSAPI_STATE_FROM_OPER_STATE = {
290
        "BUILD": "BUILD",
291
        "ERROR": "ERROR",
292
        "STOPPED": "STOPPED",
293
        "STARTED": "ACTIVE",
294
        'RESIZE': 'RESIZE',
295
        'DESTROYED': 'DELETED',
296
    }
297

    
298
    name = models.CharField('Virtual Machine Name', max_length=255)
299
    userid = models.CharField('User ID of the owner', max_length=100,
300
                              db_index=True, null=False)
301
    backend = models.ForeignKey(Backend, null=True,
302
                                related_name="virtual_machines",
303
                                on_delete=models.PROTECT)
304
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
305
    created = models.DateTimeField(auto_now_add=True)
306
    updated = models.DateTimeField(auto_now=True)
307
    imageid = models.CharField(max_length=100, null=False)
308
    hostid = models.CharField(max_length=100)
309
    flavor = models.ForeignKey(Flavor, on_delete=models.PROTECT)
310
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
311
    suspended = models.BooleanField('Administratively Suspended',
312
                                    default=False)
313
    serial = models.ForeignKey(QuotaHolderSerial,
314
                               related_name='virtual_machine', null=True,
315
                               on_delete=models.SET_NULL)
316

    
317
    # VM State
318
    # The following fields are volatile data, in the sense
319
    # that they need not be persistent in the DB, but rather
320
    # get generated at runtime by quering Ganeti and applying
321
    # updates received from Ganeti.
322

    
323
    # In the future they could be moved to a separate caching layer
324
    # and removed from the database.
325
    # [vkoukis] after discussion with [faidon].
326
    action = models.CharField(choices=ACTIONS, max_length=30, null=True,
327
                              default=None)
328
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
329
                                 null=False, default="BUILD")
330
    backendjobid = models.PositiveIntegerField(null=True)
331
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
332
                                     null=True)
333
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
334
                                        max_length=30, null=True)
335
    backendlogmsg = models.TextField(null=True)
336
    buildpercentage = models.IntegerField(default=0)
337
    backendtime = models.DateTimeField(default=datetime.datetime.min)
338

    
339
    # Latest action and corresponding Ganeti job ID, for actions issued
340
    # by the API
341
    task = models.CharField(max_length=64, null=True)
342
    task_job_id = models.BigIntegerField(null=True)
343

    
344
    def get_client(self):
345
        if self.backend:
346
            return self.backend.get_client()
347
        else:
348
            raise faults.ServiceUnavailable("VirtualMachine without backend")
349

    
350
    def get_last_diagnostic(self, **filters):
351
        try:
352
            return self.diagnostics.filter()[0]
353
        except IndexError:
354
            return None
355

    
356
    @staticmethod
357
    def put_client(client):
358
            put_rapi_client(client)
359

    
360
    def save(self, *args, **kwargs):
361
        # Store hash for first time saved vm
362
        if (self.id is None or self.backend_hash == '') and self.backend:
363
            self.backend_hash = self.backend.hash
364
        super(VirtualMachine, self).save(*args, **kwargs)
365

    
366
    @property
367
    def backend_vm_id(self):
368
        """Returns the backend id for this VM by prepending backend-prefix."""
369
        if not self.id:
370
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
371
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
372

    
373
    class Meta:
374
        verbose_name = u'Virtual machine instance'
375
        get_latest_by = 'created'
376

    
377
    def __unicode__(self):
378
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
379

    
380
    # Error classes
381
    class InvalidBackendIdError(Exception):
382
        def __init__(self, value):
383
            self.value = value
384

    
385
        def __str__(self):
386
            return repr(self.value)
387

    
388
    class InvalidBackendMsgError(Exception):
389
        def __init__(self, opcode, status):
390
            self.opcode = opcode
391
            self.status = status
392

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

    
397
    class InvalidActionError(Exception):
398
        def __init__(self, action):
399
            self._action = action
400

    
401
        def __str__(self):
402
            return repr(str(self._action))
403

    
404

    
405
class VirtualMachineMetadata(models.Model):
406
    meta_key = models.CharField(max_length=50)
407
    meta_value = models.CharField(max_length=500)
408
    vm = models.ForeignKey(VirtualMachine, related_name='metadata',
409
                           on_delete=models.CASCADE)
410

    
411
    class Meta:
412
        unique_together = (('meta_key', 'vm'),)
413
        verbose_name = u'Key-value pair of metadata for a VM.'
414

    
415
    def __unicode__(self):
416
        return u'%s: %s' % (self.meta_key, self.meta_value)
417

    
418

    
419
class Network(models.Model):
420
    OPER_STATES = (
421
        ('PENDING', 'Pending'),  # Unused because of lazy networks
422
        ('ACTIVE', 'Active'),
423
        ('DELETED', 'Deleted'),
424
        ('ERROR', 'Error')
425
    )
426

    
427
    ACTIONS = (
428
        ('CREATE', 'Create Network'),
429
        ('DESTROY', 'Destroy Network'),
430
        ('ADD', 'Add server to Network'),
431
        ('REMOVE', 'Remove server from Network'),
432
    )
433

    
434
    RSAPI_STATE_FROM_OPER_STATE = {
435
        'PENDING': 'PENDING',
436
        'ACTIVE': 'ACTIVE',
437
        'DELETED': 'DELETED',
438
        'ERROR': 'ERROR'
439
    }
440

    
441
    FLAVORS = {
442
        'CUSTOM': {
443
            'mode': 'bridged',
444
            'link': settings.DEFAULT_BRIDGE,
445
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
446
            'tags': None,
447
            'desc': "Basic flavor used for a bridged network",
448
        },
449
        'IP_LESS_ROUTED': {
450
            'mode': 'routed',
451
            'link': settings.DEFAULT_ROUTING_TABLE,
452
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
453
            'tags': 'ip-less-routed',
454
            'desc': "Flavor used for an IP-less routed network using"
455
                    " Proxy ARP",
456
        },
457
        'MAC_FILTERED': {
458
            'mode': 'bridged',
459
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
460
            'mac_prefix': 'pool',
461
            'tags': 'private-filtered',
462
            'desc': "Flavor used for bridged networks that offer isolation"
463
                    " via filtering packets based on their src "
464
                    " MAC (ebtables)",
465
        },
466
        'PHYSICAL_VLAN': {
467
            'mode': 'bridged',
468
            'link': 'pool',
469
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
470
            'tags': 'physical-vlan',
471
            'desc': "Flavor used for bridged network that offer isolation"
472
                    " via dedicated physical vlan",
473
        },
474
    }
475

    
476
    NETWORK_NAME_LENGTH = 128
477

    
478
    name = models.CharField('Network Name', max_length=NETWORK_NAME_LENGTH)
479
    userid = models.CharField('User ID of the owner', max_length=128,
480
                              null=True, db_index=True)
481
    flavor = models.CharField('Flavor', max_length=32, null=False)
482
    mode = models.CharField('Network Mode', max_length=16, null=True)
483
    link = models.CharField('Network Link', max_length=32, null=True)
484
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
485
    tags = models.CharField('Network Tags', max_length=128, null=True)
486
    public = models.BooleanField(default=False, db_index=True)
487
    created = models.DateTimeField(auto_now_add=True)
488
    updated = models.DateTimeField(auto_now=True)
489
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
490
    state = models.CharField(choices=OPER_STATES, max_length=32,
491
                             default='PENDING')
492
    machines = models.ManyToManyField(VirtualMachine,
493
                                      through='NetworkInterface')
494
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
495
                              default=None)
496
    drained = models.BooleanField("Drained", default=False, null=False)
497
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
498
                                           default=False)
499
    external_router = models.BooleanField(default=False)
500
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
501
                               null=True, on_delete=models.SET_NULL)
502

    
503
    def __unicode__(self):
504
        return "<Network: %s>" % str(self.id)
505

    
506
    @property
507
    def backend_id(self):
508
        """Return the backend id by prepending backend-prefix."""
509
        if not self.id:
510
            raise Network.InvalidBackendIdError("self.id is None")
511
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
512

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

517
        """
518
        if self.tags:
519
            return self.tags.split(',')
520
        else:
521
            return []
522

    
523
    def create_backend_network(self, backend=None):
524
        """Create corresponding BackendNetwork entries."""
525

    
526
        backends = [backend] if backend else\
527
            Backend.objects.filter(offline=False)
528
        for backend in backends:
529
            backend_exists =\
530
                BackendNetwork.objects.filter(backend=backend, network=self)\
531
                                      .exists()
532
            if not backend_exists:
533
                BackendNetwork.objects.create(backend=backend, network=self)
534

    
535
    def get_ip_pools(self, locked=True):
536
        subnets = self.subnets.filter(ipversion=4, deleted=False)\
537
                              .prefetch_related("ip_pools")
538
        return [ip_pool for subnet in subnets
539
                for ip_pool in subnet.get_ip_pools(locked=locked)]
540

    
541
    def reserve_address(self, address, external=False):
542
        for ip_pool in self.get_ip_pools():
543
            if ip_pool.contains(address):
544
                ip_pool.reserve(address, external=external)
545
                ip_pool.save()
546
                return
547
        raise pools.InvalidValue("Network %s does not have an IP pool that"
548
                                 " contains address %s" % (self, address))
549

    
550
    def release_address(self, address, external=False):
551
        for ip_pool in self.get_ip_pools():
552
            if ip_pool.contains(address):
553
                ip_pool.put(address, external=external)
554
                ip_pool.save()
555
                return
556
        raise pools.InvalidValue("Network %s does not have an IP pool that"
557
                                 " contains address %s" % (self, address))
558

    
559
    @property
560
    def subnet4(self):
561
        return self.get_subnet(version=4)
562

    
563
    @property
564
    def subnet6(self):
565
        return self.get_subnet(version=6)
566

    
567
    def get_subnet(self, version=4):
568
        for subnet in self.subnets.all():
569
            if subnet.ipversion == version:
570
                return subnet
571
        return None
572

    
573
    def ip_count(self):
574
        """Return the total and free IPv4 addresses of the network."""
575
        total, free = 0, 0
576
        ip_pools = self.get_ip_pools(locked=False)
577
        for ip_pool in ip_pools:
578
            total += ip_pool.pool_size
579
            free += ip_pool.count_available()
580
        return total, free
581

    
582
    class InvalidBackendIdError(Exception):
583
        def __init__(self, value):
584
            self.value = value
585

    
586
        def __str__(self):
587
            return repr(self.value)
588

    
589
    class InvalidBackendMsgError(Exception):
590
        def __init__(self, opcode, status):
591
            self.opcode = opcode
592
            self.status = status
593

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

    
598
    class InvalidActionError(Exception):
599
        def __init__(self, action):
600
            self._action = action
601

    
602
        def __str__(self):
603
            return repr(str(self._action))
604

    
605

    
606
class Subnet(models.Model):
607
    SUBNET_NAME_LENGTH = 128
608

    
609
    network = models.ForeignKey('Network', null=False, db_index=True,
610
                                related_name="subnets",
611
                                on_delete=models.PROTECT)
612
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
613
                            null=True, default="")
614
    ipversion = models.IntegerField('IP Version', default=4, null=False)
615
    cidr = models.CharField('Subnet', max_length=64, null=False)
616
    gateway = models.CharField('Gateway', max_length=64, null=True)
617
    dhcp = models.BooleanField('DHCP', default=True, null=False)
618
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
619
                                  null=False)
620
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
621
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
622

    
623
    def __unicode__(self):
624
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
625
        return msg % (self.id, self.network_id, self.cidr)
626

    
627
    def get_ip_pools(self, locked=True):
628
        ip_pools = self.ip_pools
629
        if locked:
630
            ip_pools = ip_pools.select_for_update()
631
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
632

    
633

    
634
class BackendNetwork(models.Model):
635
    OPER_STATES = (
636
        ('PENDING', 'Pending'),
637
        ('ACTIVE', 'Active'),
638
        ('DELETED', 'Deleted'),
639
        ('ERROR', 'Error')
640
    )
641

    
642
    # The list of possible operations on the backend
643
    BACKEND_OPCODES = (
644
        ('OP_NETWORK_ADD', 'Create Network'),
645
        ('OP_NETWORK_CONNECT', 'Activate Network'),
646
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
647
        ('OP_NETWORK_REMOVE', 'Remove Network'),
648
        # These are listed here for completeness,
649
        # and are ignored for the time being
650
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
651
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
652
    )
653

    
654
    # The operating state of a Netowork,
655
    # upon the successful completion of a backend operation.
656
    # IMPORTANT: Make sure all keys have a corresponding
657
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
658
    OPER_STATE_FROM_OPCODE = {
659
        'OP_NETWORK_ADD': 'PENDING',
660
        'OP_NETWORK_CONNECT': 'ACTIVE',
661
        'OP_NETWORK_DISCONNECT': 'PENDING',
662
        'OP_NETWORK_REMOVE': 'DELETED',
663
        'OP_NETWORK_SET_PARAMS': None,
664
        'OP_NETWORK_QUERY_DATA': None
665
    }
666

    
667
    network = models.ForeignKey(Network, related_name='backend_networks',
668
                                on_delete=models.PROTECT)
669
    backend = models.ForeignKey(Backend, related_name='networks',
670
                                on_delete=models.PROTECT)
671
    created = models.DateTimeField(auto_now_add=True)
672
    updated = models.DateTimeField(auto_now=True)
673
    deleted = models.BooleanField('Deleted', default=False)
674
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
675
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
676
                                 default='PENDING')
677
    backendjobid = models.PositiveIntegerField(null=True)
678
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
679
                                     null=True)
680
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
681
                                        max_length=30, null=True)
682
    backendlogmsg = models.TextField(null=True)
683
    backendtime = models.DateTimeField(null=False,
684
                                       default=datetime.datetime.min)
685

    
686
    class Meta:
687
        # Ensure one entry for each network in each backend
688
        unique_together = (("network", "backend"))
689

    
690
    def __init__(self, *args, **kwargs):
691
        """Initialize state for just created BackendNetwork instances."""
692
        super(BackendNetwork, self).__init__(*args, **kwargs)
693
        if not self.mac_prefix:
694
            # Generate the MAC prefix of the BackendNetwork, by combining
695
            # the Network prefix with the index of the Backend
696
            net_prefix = self.network.mac_prefix
697
            backend_suffix = hex(self.backend.index).replace('0x', '')
698
            mac_prefix = net_prefix + backend_suffix
699
            try:
700
                utils.validate_mac(mac_prefix + ":00:00:00")
701
            except utils.InvalidMacAddress:
702
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
703
                                              mac_prefix)
704
            self.mac_prefix = mac_prefix
705

    
706
    def __unicode__(self):
707
        return '<%s@%s>' % (self.network, self.backend)
708

    
709

    
710
class IPAddress(models.Model):
711
    subnet = models.ForeignKey("Subnet", related_name="ips", null=False,
712
                               on_delete=models.PROTECT)
713
    network = models.ForeignKey(Network, related_name="ips", null=False,
714
                                on_delete=models.PROTECT)
715
    nic = models.ForeignKey("NetworkInterface", related_name="ips", null=True,
716
                            on_delete=models.SET_NULL)
717
    userid = models.CharField("UUID of the owner", max_length=128, null=False,
718
                              db_index=True)
719
    address = models.CharField("IP Address", max_length=64, null=False)
720
    floating_ip = models.BooleanField("Floating IP", null=False, default=False)
721
    created = models.DateTimeField(auto_now_add=True)
722
    updated = models.DateTimeField(auto_now=True)
723
    deleted = models.BooleanField(default=False, null=False)
724

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

    
729
    def __unicode__(self):
730
        ip_type = "floating" if self.floating_ip else "static"
731
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
732
               % (self.address, self.network_id, self.subnet_id, ip_type)
733

    
734
    def in_use(self):
735
        if self.machine is None:
736
            return False
737
        else:
738
            return (not self.machine.deleted)
739

    
740
    class Meta:
741
        unique_together = ("network", "address")
742

    
743
    @property
744
    def ipversion(self):
745
        return self.subnet.ipversion
746

    
747
    @property
748
    def public(self):
749
        return self.network.public
750

    
751
    def release_address(self):
752
        """Release the IPv4 address."""
753
        if self.ipversion == 4:
754
            for pool_row in self.subnet.ip_pools.all():
755
                ip_pool = pool_row.pool
756
                if ip_pool.contains(self.address):
757
                    ip_pool.put(self.address)
758
                    ip_pool.save()
759
                    return
760
            log.error("Cannot release address %s of NIC %s. Address does not"
761
                      " belong to any of the IP pools of the subnet %s !",
762
                      self.address, self.nic, self.subnet_id)
763

    
764

    
765
class IPAddressLog(models.Model):
766
    address = models.CharField("IP Address", max_length=64, null=False,
767
                               db_index=True)
768
    server_id = models.IntegerField("Server", null=False)
769
    network_id = models.IntegerField("Network", null=False)
770
    allocated_at = models.DateTimeField("Datetime IP allocated to server",
771
                                        auto_now_add=True)
772
    released_at = models.DateTimeField("Datetime IP released from server",
773
                                       null=True)
774
    active = models.BooleanField("Whether IP still allocated to server",
775
                                 default=True)
776

    
777
    def __unicode__(self):
778
        return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\
779
               % (self.address, self.network_id, self.server_id,
780
                  self.allocated_at)
781

    
782

    
783
class NetworkInterface(models.Model):
784
    FIREWALL_PROFILES = (
785
        ('ENABLED', 'Enabled'),
786
        ('DISABLED', 'Disabled'),
787
        ('PROTECTED', 'Protected')
788
    )
789

    
790
    STATES = (
791
        ("ACTIVE", "Active"),
792
        ("BUILD", "Building"),
793
        ("ERROR", "Error"),
794
        ("DOWN", "Down"),
795
    )
796

    
797
    NETWORK_IFACE_NAME_LENGTH = 128
798

    
799
    name = models.CharField('NIC name', max_length=128, null=True, default="")
800
    userid = models.CharField("UUID of the owner",
801
                              max_length=NETWORK_IFACE_NAME_LENGTH,
802
                              null=False, db_index=True)
803
    machine = models.ForeignKey(VirtualMachine, related_name='nics',
804
                                on_delete=models.PROTECT, null=True)
805
    network = models.ForeignKey(Network, related_name='nics',
806
                                on_delete=models.PROTECT)
807
    created = models.DateTimeField(auto_now_add=True)
808
    updated = models.DateTimeField(auto_now=True)
809
    index = models.IntegerField(null=True)
810
    mac = models.CharField(max_length=32, null=True, unique=True)
811
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
812
                                        max_length=30, null=True)
813
    security_groups = models.ManyToManyField("SecurityGroup", null=True)
814
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
815
                             choices=STATES)
816
    device_owner = models.CharField('Device owner', max_length=128, null=True)
817

    
818
    def __unicode__(self):
819
        return "<%s:vm:%s network:%s>" % (self.id, self.machine_id,
820
                                          self.network_id)
821

    
822
    @property
823
    def backend_uuid(self):
824
        """Return the backend id by prepending backend-prefix."""
825
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
826

    
827
    @property
828
    def ipv4_address(self):
829
        return self.get_ip_address(version=4)
830

    
831
    @property
832
    def ipv6_address(self):
833
        return self.get_ip_address(version=6)
834

    
835
    def get_ip_address(self, version=4):
836
        for ip in self.ips.all():
837
            if ip.subnet.ipversion == version:
838
                return ip.address
839
        return None
840

    
841
    def get_ip_addresses_subnets(self):
842
        return self.ips.values_list("address", "subnet__id")
843

    
844

    
845
class SecurityGroup(models.Model):
846
    SECURITY_GROUP_NAME_LENGTH = 128
847
    name = models.CharField('group name',
848
                            max_length=SECURITY_GROUP_NAME_LENGTH)
849

    
850

    
851
class PoolTable(models.Model):
852
    available_map = models.TextField(default="", null=False)
853
    reserved_map = models.TextField(default="", null=False)
854
    size = models.IntegerField(null=False)
855

    
856
    # Optional Fields
857
    base = models.CharField(null=True, max_length=32)
858
    offset = models.IntegerField(null=True)
859

    
860
    class Meta:
861
        abstract = True
862

    
863
    @classmethod
864
    def get_pool(cls):
865
        try:
866
            pool_row = cls.objects.select_for_update().get()
867
            return pool_row.pool
868
        except cls.DoesNotExist:
869
            raise pools.EmptyPool
870

    
871
    @property
872
    def pool(self):
873
        return self.manager(self)
874

    
875

    
876
class BridgePoolTable(PoolTable):
877
    manager = pools.BridgePool
878

    
879
    def __unicode__(self):
880
        return u"<BridgePool id:%s>" % self.id
881

    
882

    
883
class MacPrefixPoolTable(PoolTable):
884
    manager = pools.MacPrefixPool
885

    
886
    def __unicode__(self):
887
        return u"<MACPrefixPool id:%s>" % self.id
888

    
889

    
890
class IPPoolTable(PoolTable):
891
    manager = pools.IPPool
892

    
893
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
894
                               on_delete=models.PROTECT,
895
                               db_index=True, null=True)
896

    
897
    def __unicode__(self):
898
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
899

    
900

    
901
@contextmanager
902
def pooled_rapi_client(obj):
903
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
904
            backend = obj.backend
905
        else:
906
            backend = obj
907

    
908
        if backend.offline:
909
            log.warning("Trying to connect with offline backend: %s", backend)
910
            raise faults.ServiceUnavailable("Cannot connect to offline"
911
                                            " backend: %s" % backend)
912

    
913
        b = backend
914
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
915
                                 b.username, b.password)
916
        try:
917
            yield client
918
        finally:
919
            put_rapi_client(client)
920

    
921

    
922
class VirtualMachineDiagnosticManager(models.Manager):
923
    """
924
    Custom manager for :class:`VirtualMachineDiagnostic` model.
925
    """
926

    
927
    # diagnostic creation helpers
928
    def create_for_vm(self, vm, level, message, **kwargs):
929
        attrs = {'machine': vm, 'level': level, 'message': message}
930
        attrs.update(kwargs)
931
        # update instance updated time
932
        self.create(**attrs)
933
        vm.save()
934

    
935
    def create_error(self, vm, **kwargs):
936
        self.create_for_vm(vm, 'ERROR', **kwargs)
937

    
938
    def create_debug(self, vm, **kwargs):
939
        self.create_for_vm(vm, 'DEBUG', **kwargs)
940

    
941
    def since(self, vm, created_since, **kwargs):
942
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
943
                                           **kwargs)
944

    
945

    
946
class VirtualMachineDiagnostic(models.Model):
947
    """
948
    Model to store backend information messages that relate to the state of
949
    the virtual machine.
950
    """
951

    
952
    TYPES = (
953
        ('ERROR', 'Error'),
954
        ('WARNING', 'Warning'),
955
        ('INFO', 'Info'),
956
        ('DEBUG', 'Debug'),
957
    )
958

    
959
    objects = VirtualMachineDiagnosticManager()
960

    
961
    created = models.DateTimeField(auto_now_add=True)
962
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics",
963
                                on_delete=models.CASCADE)
964
    level = models.CharField(max_length=20, choices=TYPES)
965
    source = models.CharField(max_length=100)
966
    source_date = models.DateTimeField(null=True)
967
    message = models.CharField(max_length=255)
968
    details = models.TextField(null=True)
969

    
970
    class Meta:
971
        ordering = ['-created']