Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 3b56bbe4

History | View | Annotate | Download (23.5 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 django.conf import settings
33
from django.db import models
34
from django.db import IntegrityError
35

    
36
from hashlib import sha1
37
from synnefo.api.faults import ServiceUnavailable
38
from synnefo.util.rapi import GanetiRapiClient
39
from synnefo import settings as snf_settings
40

    
41

    
42
BACKEND_CLIENTS = {}    #{hash:Backend client}
43
BACKEND_HASHES = {}     #{Backend.id:hash}
44

    
45
def get_client(hash, backend):
46
    """Get a cached backend client or create a new one.
47

48
    @param hash: The hash of the backend
49
    @param backend: Either a backend object or backend ID
50
    """
51

    
52
    if backend is None:
53
        raise Exception("Backend is None. Cannot create a client.")
54

    
55
    if hash in BACKEND_CLIENTS:
56
        # Return cached client
57
        return BACKEND_CLIENTS[hash]
58

    
59
    # Always get a new instance to ensure latest credentials
60
    if isinstance(backend, Backend):
61
        backend = backend.id
62
    (credentials,) = Backend.objects.filter(id=backend).values_list('hash',
63
                                'clustername', 'port', 'username', 'password')
64

    
65
    hash, clustername, port, user, password = credentials
66

    
67
    # Check client for updated hash
68
    if hash in BACKEND_CLIENTS:
69
        return BACKEND_CLIENTS[hash]
70

    
71
    # Delete old version of the client
72
    if backend in BACKEND_HASHES:
73
        del BACKEND_CLIENTS[BACKEND_HASHES[backend]]
74

    
75
    # Create the new client
76
    client = GanetiRapiClient(clustername, port, user, password)
77

    
78
    # Store the client and the hash
79
    BACKEND_CLIENTS[hash] = client
80
    BACKEND_HASHES[backend] = hash
81

    
82
    return client
83

    
84

    
85
def clear_client_cache():
86
    BACKEND_CLIENTS.clear()
87
    BACKEND_HASHES.clear()
88

    
89

    
90
class Flavor(models.Model):
91
    cpu = models.IntegerField('Number of CPUs', default=0)
92
    ram = models.IntegerField('RAM size in MiB', default=0)
93
    disk = models.IntegerField('Disk size in GiB', default=0)
94
    disk_template = models.CharField('Disk template', max_length=32,
95
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
96
    deleted = models.BooleanField('Deleted', default=False)
97

    
98
    class Meta:
99
        verbose_name = u'Virtual machine flavor'
100
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
101

    
102
    @property
103
    def name(self):
104
        """Returns flavor name (generated)"""
105
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
106

    
107
    def __unicode__(self):
108
        return self.name
109

    
110

    
111
class BackendQuerySet(models.query.QuerySet):
112
    def delete(self):
113
        for backend in self._clone():
114
            backend.delete()
115

    
116
class ProtectDeleteManager(models.Manager):
117
    def get_query_set(self):
118
        return BackendQuerySet(self.model, using=self._db)
119

    
120

    
121
class Backend(models.Model):
122
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
123
    port = models.PositiveIntegerField('Port', default=5080)
124
    username = models.CharField('Username', max_length=64, blank=True,
125
                                null=True)
126
    password = models.CharField('Password', max_length=64, blank=True,
127
                                null=True)
128
    # Sha1 is up to 40 characters long
129
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
130
    drained = models.BooleanField('Drained', default=False, null=False)
131
    offline = models.BooleanField('Offline', default=False, null=False)
132
    # Last refresh of backend resources
133
    updated = models.DateTimeField(auto_now_add=True)
134
    # Backend resources
135
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
136
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
137
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
138
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
139
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
140
                                            null=False)
141
    ctotal = models.PositiveIntegerField('Total number of logical processors',
142
                                         default=0, null=False)
143
    # Custom object manager to protect from cascade delete
144
    objects = ProtectDeleteManager()
145

    
146
    class Meta:
147
        verbose_name = u'Backend'
148
        ordering = ["clustername"]
149

    
150
    def __unicode__(self):
151
        return self.clustername
152

    
153
    @property
154
    def backend_id(self):
155
        return self.id
156

    
157
    @property
158
    def client(self):
159
        """Get or create a client. """
160
        if not self.offline:
161
            return get_client(self.hash, self)
162
        else:
163
            raise ServiceUnavailable
164

    
165
    def create_hash(self):
166
        """Create a hash for this backend. """
167
        return sha1('%s%s%s%s' % \
168
                (self.clustername, self.port, self.username, self.password)) \
169
                .hexdigest()
170

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

    
180
    def delete(self, *args, **kwargs):
181
        # Integrity Error if non-deleted VMs are associated with Backend
182
        if self.virtual_machines.filter(deleted=False).count():
183
            raise IntegrityError("Non-deleted virtual machines are associated "
184
                                 "with backend: %s" % self)
185
        else:
186
            # ON_DELETE = SET NULL
187
            self.virtual_machines.all().backend = None
188
            super(Backend, self).delete(*args, **kwargs)
189

    
190

    
191
# A backend job may be in one of the following possible states
192
BACKEND_STATUSES = (
193
    ('queued', 'request queued'),
194
    ('waiting', 'request waiting for locks'),
195
    ('canceling', 'request being canceled'),
196
    ('running', 'request running'),
197
    ('canceled', 'request canceled'),
198
    ('success', 'request completed successfully'),
199
    ('error', 'request returned error')
200
)
201

    
202

    
203
class VirtualMachine(models.Model):
204
    # The list of possible actions for a VM
205
    ACTIONS = (
206
       ('CREATE', 'Create VM'),
207
       ('START', 'Start VM'),
208
       ('STOP', 'Shutdown VM'),
209
       ('SUSPEND', 'Admin Suspend VM'),
210
       ('REBOOT', 'Reboot VM'),
211
       ('DESTROY', 'Destroy VM')
212
    )
213

    
214
    # The internal operating state of a VM
215
    OPER_STATES = (
216
        ('BUILD', 'Queued for creation'),
217
        ('ERROR', 'Creation failed'),
218
        ('STOPPED', 'Stopped'),
219
        ('STARTED', 'Started'),
220
        ('DESTROYED', 'Destroyed')
221
    )
222

    
223
    # The list of possible operations on the backend
224
    BACKEND_OPCODES = (
225
        ('OP_INSTANCE_CREATE', 'Create Instance'),
226
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
227
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
228
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
229
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
230

    
231
        # These are listed here for completeness,
232
        # and are ignored for the time being
233
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
234
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
235
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
236
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
237
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
238
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
239
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
240
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
241
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
242
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
243
    )
244

    
245

    
246
    # The operating state of a VM,
247
    # upon the successful completion of a backend operation.
248
    # IMPORTANT: Make sure all keys have a corresponding
249
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
250
    OPER_STATE_FROM_OPCODE = {
251
        'OP_INSTANCE_CREATE': 'STARTED',
252
        'OP_INSTANCE_REMOVE': 'DESTROYED',
253
        'OP_INSTANCE_STARTUP': 'STARTED',
254
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
255
        'OP_INSTANCE_REBOOT': 'STARTED',
256
        'OP_INSTANCE_SET_PARAMS': None,
257
        'OP_INSTANCE_QUERY_DATA': None,
258
        'OP_INSTANCE_REINSTALL': None,
259
        'OP_INSTANCE_ACTIVATE_DISKS': None,
260
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
261
        'OP_INSTANCE_REPLACE_DISKS': None,
262
        'OP_INSTANCE_MIGRATE': None,
263
        'OP_INSTANCE_CONSOLE': None,
264
        'OP_INSTANCE_RECREATE_DISKS': None,
265
        'OP_INSTANCE_FAILOVER': None
266
    }
267

    
268
    # This dictionary contains the correspondence between
269
    # internal operating states and Server States as defined
270
    # by the Rackspace API.
271
    RSAPI_STATE_FROM_OPER_STATE = {
272
        "BUILD": "BUILD",
273
        "ERROR": "ERROR",
274
        "STOPPED": "STOPPED",
275
        "STARTED": "ACTIVE",
276
        "DESTROYED": "DELETED"
277
    }
278

    
279
    name = models.CharField('Virtual Machine Name', max_length=255)
280
    userid = models.CharField('User ID of the owner', max_length=100)
281
    backend = models.ForeignKey(Backend, null=True,
282
                                related_name="virtual_machines",)
283
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
284
    created = models.DateTimeField(auto_now_add=True)
285
    updated = models.DateTimeField(auto_now=True)
286
    imageid = models.CharField(max_length=100, null=False)
287
    hostid = models.CharField(max_length=100)
288
    flavor = models.ForeignKey(Flavor)
289
    deleted = models.BooleanField('Deleted', default=False)
290
    suspended = models.BooleanField('Administratively Suspended',
291
                                    default=False)
292

    
293
    # VM State
294
    # The following fields are volatile data, in the sense
295
    # that they need not be persistent in the DB, but rather
296
    # get generated at runtime by quering Ganeti and applying
297
    # updates received from Ganeti.
298

    
299
    # In the future they could be moved to a separate caching layer
300
    # and removed from the database.
301
    # [vkoukis] after discussion with [faidon].
302
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
303
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
304
    backendjobid = models.PositiveIntegerField(null=True)
305
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
306
                                     null=True)
307
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
308
                                        max_length=30, null=True)
309
    backendlogmsg = models.TextField(null=True)
310
    buildpercentage = models.IntegerField(default=0)
311
    backendtime = models.DateTimeField(default=datetime.datetime.min)
312

    
313
    @property
314
    def client(self):
315
        if self.backend and not self.backend.offline:
316
            return get_client(self.backend_hash, self.backend_id)
317
        else:
318
            raise ServiceUnavailable
319

    
320
    # Error classes
321
    class InvalidBackendIdError(Exception):
322
        def __init__(self, value):
323
            self.value = value
324

    
325
        def __str__(self):
326
            return repr(self.value)
327

    
328
    class InvalidBackendMsgError(Exception):
329
        def __init__(self, opcode, status):
330
            self.opcode = opcode
331
            self.status = status
332

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

    
337
    class InvalidActionError(Exception):
338
        def __init__(self, action):
339
            self._action = action
340

    
341
        def __str__(self):
342
            return repr(str(self._action))
343

    
344
    class DeletedError(Exception):
345
        pass
346

    
347
    class BuildingError(Exception):
348
        pass
349

    
350
    def __init__(self, *args, **kw):
351
        """Initialize state for just created VM instances."""
352
        super(VirtualMachine, self).__init__(*args, **kw)
353
        # This gets called BEFORE an instance gets save()d for
354
        # the first time.
355
        if not self.pk:
356
            self.action = None
357
            self.backendjobid = None
358
            self.backendjobstatus = None
359
            self.backendopcode = None
360
            self.backendlogmsg = None
361
            self.operstate = 'BUILD'
362

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

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

    
376
    class Meta:
377
        verbose_name = u'Virtual machine instance'
378
        get_latest_by = 'created'
379

    
380
    def __unicode__(self):
381
        return self.name
382

    
383

    
384
class VirtualMachineMetadata(models.Model):
385
    meta_key = models.CharField(max_length=50)
386
    meta_value = models.CharField(max_length=500)
387
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
388

    
389
    class Meta:
390
        unique_together = (('meta_key', 'vm'),)
391
        verbose_name = u'Key-value pair of metadata for a VM.'
392

    
393
    def __unicode__(self):
394
        return u'%s: %s' % (self.meta_key, self.meta_value)
395

    
396

    
397

    
398
class Network(models.Model):
399
    OPER_STATES = (
400
        ('PENDING', 'Pending'),
401
        ('ACTIVE', 'Active'),
402
        ('DELETED', 'Deleted'),
403
        ('ERROR', 'Error')
404
    )
405

    
406
    ACTIONS = (
407
       ('CREATE', 'Create Network'),
408
       ('DESTROY', 'Destroy Network'),
409
    )
410

    
411
    RSAPI_STATE_FROM_OPER_STATE = {
412
        'PENDING': 'PENDING',
413
        'ACTIVE': 'ACTIVE',
414
        'DELETED': 'DELETED',
415
        'ERROR': 'ERROR'
416
    }
417

    
418
    NETWORK_TYPES = (
419
        ('PUBLIC_ROUTED', 'Public routed network'),
420
        ('PRIVATE_VLAN', 'Private vlan network'),
421
        ('PRIVATE_FILTERED', 'Private network with mac-filtering')
422
    )
423

    
424
    NETWORK_TAGS = {
425
        'PUBLIC_ROUTED': ['ip-less-routed'],
426
        'PRIVATE_VLAN': ['physical-vlan'],
427
        'PRIVATE_FILTERED': ['mac-filtered']
428
    }
429

    
430
    name = models.CharField('Network Name', max_length=128)
431
    userid = models.CharField('User ID of the owner', max_length=128, null=True)
432
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
433
    gateway = models.CharField('Gateway', max_length=32, null=True)
434
    dhcp = models.BooleanField('DHCP', default=True)
435
    type = models.CharField(choices=NETWORK_TYPES, max_length=50, default='PRIVATE_VLAN')
436
    link = models.CharField('Network Link', max_length=128, null=True)
437
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=True)
438
    public = models.BooleanField(default=False)
439
    created = models.DateTimeField(auto_now_add=True)
440
    updated = models.DateTimeField(auto_now=True)
441
    deleted = models.BooleanField('Deleted', default=False)
442
    state = models.CharField(choices=OPER_STATES, max_length=32,
443
                             default='PENDING')
444
    machines = models.ManyToManyField(VirtualMachine,
445
                                      through='NetworkInterface')
446
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
447
                              default=None)
448

    
449
    class InvalidBackendIdError(Exception):
450
        def __init__(self, value):
451
            self.value = value
452

    
453
        def __str__(self):
454
            return repr(self.value)
455

    
456

    
457
    class InvalidBackendMsgError(Exception):
458
        def __init__(self, opcode, status):
459
            self.opcode = opcode
460
            self.status = status
461

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

    
466
    class InvalidActionError(Exception):
467
        def __init__(self, action):
468
            self._action = action
469

    
470
        def __str__(self):
471
            return repr(str(self._action))
472

    
473
    @property
474
    def backend_id(self):
475
        """Return the backend id by prepending backend-prefix."""
476
        if not self.id:
477
            raise Network.InvalidBackendIdError("self.id is None")
478
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
479

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

484
        """
485
        return Network.NETWORK_TAGS[self.type]
486

    
487
    def __unicode__(self):
488
        return self.name
489

    
490
    def update_state(self):
491
        """Update state of the Network.
492

493
        Update the state of the Network depending on the related
494
        backend_networks. When backend networks do not have the same operstate,
495
        the Network's state is PENDING. Otherwise it is the same with
496
        the BackendNetworks operstate.
497

498
        """
499
        backend_states = [s.operstate for s in self.backend_networks.all()]
500
        if not backend_states:
501
            self.state = 'PENDING'
502
            self.save()
503
            return
504

    
505
        all_equal = len(set(backend_states)) <= 1
506
        self.state = all_equal and backend_states[0] or 'PENDING'
507

    
508
        if self.state == 'DELETED':
509
            self.deleted = True
510

    
511
            if self.mac_prefix:
512
                MacPrefixPool.set_available(self.mac_prefix)
513

    
514
            if self.link and self.type == 'PRIVATE_VLAN':
515
                BridgePool.set_available(self.link)
516

    
517
        self.save()
518

    
519
    def save(self, *args, **kwargs):
520
        pk = self.pk
521
        super(Network, self).save(*args, **kwargs)
522
        if not pk:
523
            # In case of a new Network, corresponding BackendNetwork's must
524
            # be created!
525
            for back in Backend.objects.all():
526
                BackendNetwork.objects.create(backend=back, network=self)
527

    
528

    
529
class BackendNetwork(models.Model):
530
    OPER_STATES = (
531
        ('PENDING', 'Pending'),
532
        ('ACTIVE', 'Active'),
533
        ('DELETED', 'Deleted'),
534
        ('ERROR', 'Error')
535
    )
536

    
537
    # The list of possible operations on the backend
538
    BACKEND_OPCODES = (
539
        ('OP_NETWORK_ADD', 'Create Network'),
540
        ('OP_NETWORK_CONNECT', 'Activate Network'),
541
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
542
        ('OP_NETWORK_REMOVE', 'Remove Network'),
543
        # These are listed here for completeness,
544
        # and are ignored for the time being
545
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
546
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
547
    )
548

    
549
    # The operating state of a Netowork,
550
    # upon the successful completion of a backend operation.
551
    # IMPORTANT: Make sure all keys have a corresponding
552
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
553
    OPER_STATE_FROM_OPCODE = {
554
        'OP_NETWORK_ADD': 'PENDING',
555
        'OP_NETWORK_CONNECT': 'ACTIVE',
556
        'OP_NETWORK_DISCONNECT': 'PENDING',
557
        'OP_NETWORK_REMOVE': 'DELETED',
558
        'OP_NETWORK_SET_PARAMS': None,
559
        'OP_NETWORK_QUERY_DATA': None
560
    }
561

    
562
    network = models.ForeignKey(Network, related_name='backend_networks')
563
    backend = models.ForeignKey(Backend, related_name='networks')
564
    created = models.DateTimeField(auto_now_add=True)
565
    updated = models.DateTimeField(auto_now=True)
566
    deleted = models.BooleanField('Deleted', default=False)
567
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
568
                                 default='PENDING')
569
    backendjobid = models.PositiveIntegerField(null=True)
570
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
571
                                     null=True)
572
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
573
                                        max_length=30, null=True)
574
    backendlogmsg = models.TextField(null=True)
575
    backendtime = models.DateTimeField(null=False,
576
                                       default=datetime.datetime.min)
577

    
578
    def save(self, *args, **kwargs):
579
        super(BackendNetwork, self).save(*args, **kwargs)
580
        self.network.update_state()
581

    
582
    def delete(self, *args, **kwargs):
583
        super(BackendNetwork, self).delete(*args, **kwargs)
584
        self.network.update_state()
585

    
586

    
587
class NetworkInterface(models.Model):
588
    FIREWALL_PROFILES = (
589
        ('ENABLED', 'Enabled'),
590
        ('DISABLED', 'Disabled'),
591
        ('PROTECTED', 'Protected')
592
    )
593

    
594
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
595
    network = models.ForeignKey(Network, related_name='nics')
596
    created = models.DateTimeField(auto_now_add=True)
597
    updated = models.DateTimeField(auto_now=True)
598
    index = models.IntegerField(null=False)
599
    mac = models.CharField(max_length=17, null=True)
600
    ipv4 = models.CharField(max_length=15, null=True)
601
    ipv6 = models.CharField(max_length=100, null=True)
602
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
603
                                        max_length=30, null=True)
604

    
605
    def __unicode__(self):
606
        return '%s@%s' % (self.machine.name, self.network.name)
607

    
608

    
609
class Pool(models.Model):
610
    available = models.BooleanField(default=True, null=False)
611
    index = models.IntegerField(null=False, unique=True)
612
    value = models.CharField(max_length=128, null=False, unique=True)
613
    max_index = 0
614

    
615
    class Meta:
616
        abstract = True
617
        ordering = ['index']
618

    
619
    @classmethod
620
    def get_available(cls):
621
        try:
622
            entry = cls.objects.filter(available=True)[0]
623
            entry.available = False
624
            entry.save()
625
            return entry
626
        except IndexError:
627
            return cls.generate_new()
628

    
629
    @classmethod
630
    def generate_new(cls):
631
        try:
632
            last = cls.objects.order_by('-index')[0]
633
            index = last.index + 1
634
        except IndexError:
635
            index = 1
636

    
637
        if index <= cls.max_index:
638
            return cls.objects.create(index=index,
639
                                      value=cls.value_from_index(index),
640
                                      available=False)
641

    
642
        raise Pool.PoolExhausted()
643

    
644
    @classmethod
645
    def set_available(cls, value):
646
        entry = cls.objects.get(value=value)
647
        entry.available = True
648
        entry.save()
649

    
650

    
651
    class PoolExhausted(Exception):
652
        pass
653

    
654

    
655
class BridgePool(Pool):
656
    max_index = snf_settings.GANETI_MAX_LINK_NUMBER
657

    
658
    @staticmethod
659
    def value_from_index(index):
660
        return snf_settings.GANETI_LINK_PREFIX + str(index)
661

    
662

    
663
class MacPrefixPool(Pool):
664
    max_index = snf_settings.GANETI_MAX_MAC_PREFIX_NUMBER
665

    
666
    @staticmethod
667
    def value_from_index(index):
668
        """Convert number to mac prefix
669

670
        """
671
        high = snf_settings.GANETI_BASE_MAC_PREFIX
672
        a = hex(int(high.replace(":", ""), 16) + index).replace("0x", '')
673
        mac_prefix = ":".join([a[x:x + 2] for x in xrange(0, len(a), 2)])
674
        return mac_prefix