Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25 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.logic.ippool import IPPool
40
from synnefo import settings as snf_settings
41
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
42

    
43
from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
44

    
45
BACKEND_CLIENTS = {}  # {hash:Backend client}
46
BACKEND_HASHES = {}   # {Backend.id:hash}
47

    
48

    
49
def get_client(hash, backend):
50
    """Get a cached backend client or create a new one.
51

52
    @param hash: The hash of the backend
53
    @param backend: Either a backend object or backend ID
54
    """
55

    
56
    if backend is None:
57
        raise Exception("Backend is None. Cannot create a client.")
58

    
59
    if hash in BACKEND_CLIENTS:
60
        # Return cached client
61
        return BACKEND_CLIENTS[hash]
62

    
63
    # Always get a new instance to ensure latest credentials
64
    if isinstance(backend, Backend):
65
        backend = backend.id
66

    
67
    backend = Backend.objects.get(id=backend)
68
    hash = backend.hash
69
    clustername = backend.clustername
70
    port = backend.port
71
    user = backend.username
72
    password = backend.password
73

    
74
    # Check client for updated hash
75
    if hash in BACKEND_CLIENTS:
76
        return BACKEND_CLIENTS[hash]
77

    
78
    # Delete old version of the client
79
    if backend in BACKEND_HASHES:
80
        del BACKEND_CLIENTS[BACKEND_HASHES[backend]]
81

    
82
    # Create the new client
83
    client = GanetiRapiClient(clustername, port, user, password)
84

    
85
    # Store the client and the hash
86
    BACKEND_CLIENTS[hash] = client
87
    BACKEND_HASHES[backend] = hash
88

    
89
    return client
90

    
91

    
92
def clear_client_cache():
93
    BACKEND_CLIENTS.clear()
94
    BACKEND_HASHES.clear()
95

    
96

    
97
class Flavor(models.Model):
98
    cpu = models.IntegerField('Number of CPUs', default=0)
99
    ram = models.IntegerField('RAM size in MiB', default=0)
100
    disk = models.IntegerField('Disk size in GiB', default=0)
101
    disk_template = models.CharField('Disk template', max_length=32,
102
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
103
    deleted = models.BooleanField('Deleted', default=False)
104

    
105
    class Meta:
106
        verbose_name = u'Virtual machine flavor'
107
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
108

    
109
    @property
110
    def name(self):
111
        """Returns flavor name (generated)"""
112
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
113

    
114
    def __unicode__(self):
115
        return self.name
116

    
117

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

    
143
    class Meta:
144
        verbose_name = u'Backend'
145
        ordering = ["clustername"]
146

    
147
    def __unicode__(self):
148
        return self.clustername
149

    
150
    @property
151
    def backend_id(self):
152
        return self.id
153

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

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

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

    
172
    @password.setter
173
    def password(self, value):
174
        self.password_hash = encrypt_db_charfield(value)
175

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

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

    
195

    
196
# A backend job may be in one of the following possible states
197
BACKEND_STATUSES = (
198
    ('queued', 'request queued'),
199
    ('waiting', 'request waiting for locks'),
200
    ('canceling', 'request being canceled'),
201
    ('running', 'request running'),
202
    ('canceled', 'request canceled'),
203
    ('success', 'request completed successfully'),
204
    ('error', 'request returned error')
205
)
206

    
207

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

    
219
    # The internal operating state of a VM
220
    OPER_STATES = (
221
        ('BUILD', 'Queued for creation'),
222
        ('ERROR', 'Creation failed'),
223
        ('STOPPED', 'Stopped'),
224
        ('STARTED', 'Started'),
225
        ('DESTROYED', 'Destroyed')
226
    )
227

    
228
    # The list of possible operations on the backend
229
    BACKEND_OPCODES = (
230
        ('OP_INSTANCE_CREATE', 'Create Instance'),
231
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
232
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
233
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
234
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
235

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

    
250

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

    
273
    # This dictionary contains the correspondence between
274
    # internal operating states and Server States as defined
275
    # by the Rackspace API.
276
    RSAPI_STATE_FROM_OPER_STATE = {
277
        "BUILD": "BUILD",
278
        "ERROR": "ERROR",
279
        "STOPPED": "STOPPED",
280
        "STARTED": "ACTIVE",
281
        "DESTROYED": "DELETED"
282
    }
283

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

    
298
    # VM State
299
    # The following fields are volatile data, in the sense
300
    # that they need not be persistent in the DB, but rather
301
    # get generated at runtime by quering Ganeti and applying
302
    # updates received from Ganeti.
303

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

    
318
    @property
319
    def client(self):
320
        if self.backend and not self.backend.offline:
321
            return get_client(self.backend_hash, self.backend_id)
322
        else:
323
            raise ServiceUnavailable
324

    
325
    # Error classes
326
    class InvalidBackendIdError(Exception):
327
        def __init__(self, value):
328
            self.value = value
329

    
330
        def __str__(self):
331
            return repr(self.value)
332

    
333
    class InvalidBackendMsgError(Exception):
334
        def __init__(self, opcode, status):
335
            self.opcode = opcode
336
            self.status = status
337

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

    
342
    class InvalidActionError(Exception):
343
        def __init__(self, action):
344
            self._action = action
345

    
346
        def __str__(self):
347
            return repr(str(self._action))
348

    
349
    class DeletedError(Exception):
350
        pass
351

    
352
    class BuildingError(Exception):
353
        pass
354

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

    
368
    def save(self, *args, **kwargs):
369
        # Store hash for first time saved vm
370
        if (self.id is None or self.backend_hash == '') and self.backend:
371
            self.backend_hash = self.backend.hash
372
        super(VirtualMachine, self).save(*args, **kwargs)
373

    
374
    @property
375
    def backend_vm_id(self):
376
        """Returns the backend id for this VM by prepending backend-prefix."""
377
        if not self.id:
378
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
379
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
380

    
381
    class Meta:
382
        verbose_name = u'Virtual machine instance'
383
        get_latest_by = 'created'
384

    
385
    def __unicode__(self):
386
        return self.name
387

    
388

    
389
class VirtualMachineMetadata(models.Model):
390
    meta_key = models.CharField(max_length=50)
391
    meta_value = models.CharField(max_length=500)
392
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
393

    
394
    class Meta:
395
        unique_together = (('meta_key', 'vm'),)
396
        verbose_name = u'Key-value pair of metadata for a VM.'
397

    
398
    def __unicode__(self):
399
        return u'%s: %s' % (self.meta_key, self.meta_value)
400

    
401

    
402
class Network(models.Model):
403
    OPER_STATES = (
404
        ('PENDING', 'Pending'),
405
        ('ACTIVE', 'Active'),
406
        ('DELETED', 'Deleted'),
407
        ('ERROR', 'Error')
408
    )
409

    
410
    ACTIONS = (
411
       ('CREATE', 'Create Network'),
412
       ('DESTROY', 'Destroy Network'),
413
    )
414

    
415
    RSAPI_STATE_FROM_OPER_STATE = {
416
        'PENDING': 'PENDING',
417
        'ACTIVE': 'ACTIVE',
418
        'DELETED': 'DELETED',
419
        'ERROR': 'ERROR'
420
    }
421

    
422
    NETWORK_TYPES = (
423
        ('PUBLIC_ROUTED', 'Public routed network'),
424
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
425
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
426
        ('CUSTOM_ROUTED', 'Custom routed network'),
427
        ('CUSTOM_BRIDGED', 'Custom bridged network')
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
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
434
    gateway = models.CharField('Gateway', max_length=32, null=True)
435
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
436
    dhcp = models.BooleanField('DHCP', default=True)
437
    type = models.CharField(choices=NETWORK_TYPES, max_length=50,
438
                            default='PRIVATE_PHYSICAL_VLAN')
439
    link = models.CharField('Network Link', max_length=128, null=True)
440
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=True)
441
    public = models.BooleanField(default=False)
442
    created = models.DateTimeField(auto_now_add=True)
443
    updated = models.DateTimeField(auto_now=True)
444
    deleted = models.BooleanField('Deleted', default=False)
445
    state = models.CharField(choices=OPER_STATES, max_length=32,
446
                             default='PENDING')
447
    machines = models.ManyToManyField(VirtualMachine,
448
                                      through='NetworkInterface')
449
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
450
                              default=None)
451

    
452
    reservations = models.TextField(default='')
453

    
454
    ip_pool = None
455

    
456
    objects = ForUpdateManager()
457

    
458
    class InvalidBackendIdError(Exception):
459
        def __init__(self, value):
460
            self.value = value
461

    
462
        def __str__(self):
463
            return repr(self.value)
464

    
465
    class InvalidBackendMsgError(Exception):
466
        def __init__(self, opcode, status):
467
            self.opcode = opcode
468
            self.status = status
469

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

    
474
    class InvalidActionError(Exception):
475
        def __init__(self, action):
476
            self._action = action
477

    
478
        def __str__(self):
479
            return repr(str(self._action))
480

    
481
    @property
482
    def backend_id(self):
483
        """Return the backend id by prepending backend-prefix."""
484
        if not self.id:
485
            raise Network.InvalidBackendIdError("self.id is None")
486
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
487

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

492
        """
493
        return getattr(snf_settings, self.type + '_TAGS')
494

    
495
    def __unicode__(self):
496
        return self.name
497

    
498
    def update_state(self):
499
        """Update state of the Network.
500

501
        Update the state of the Network depending on the related
502
        backend_networks. When backend networks do not have the same operstate,
503
        the Network's state is PENDING. Otherwise it is the same with
504
        the BackendNetworks operstate.
505

506
        """
507

    
508
        old_state = self.state
509

    
510
        backend_states = [s.operstate for s in self.backend_networks.all()]
511
        if not backend_states:
512
            self.state = 'PENDING'
513
            self.save()
514
            return
515

    
516
        all_equal = len(set(backend_states)) <= 1
517
        self.state = all_equal and backend_states[0] or 'PENDING'
518

    
519
        # Release the resources on the deletion of the Network
520
        if old_state != 'DELETED' and self.state == 'DELETED':
521
            self.deleted = True
522

    
523
            if self.mac_prefix:
524
                MacPrefixPool.set_available(self.mac_prefix)
525

    
526
            if self.link and self.type == 'PRIVATE_VLAN':
527
                BridgePool.set_available(self.link)
528

    
529
        self.save()
530

    
531
    def save(self, *args, **kwargs):
532
        pk = self.pk
533
        super(Network, self).save(*args, **kwargs)
534
        if not pk:
535
            # In case of a new Network, corresponding BackendNetwork's must
536
            # be created!
537
            for back in Backend.objects.all():
538
                BackendNetwork.objects.create(backend=back, network=self)
539

    
540
    @property
541
    def pool(self):
542
        if self.ip_pool:
543
            return self.ip_pool
544
        else:
545
            self.ip_pool = IPPool(self)
546
            return self.ip_pool
547

    
548
    def reserve_address(self, address, pool=None):
549
        pool = pool or self.pool
550
        pool.reserve(address)
551
        pool._update_network()
552
        self.save()
553

    
554
    def release_address(self, address, pool=None):
555
        pool = pool or self.pool
556
        pool.release(address)
557
        pool._update_network()
558
        self.save()
559

    
560

    
561
class BackendNetwork(models.Model):
562
    OPER_STATES = (
563
        ('PENDING', 'Pending'),
564
        ('ACTIVE', 'Active'),
565
        ('DELETED', 'Deleted'),
566
        ('ERROR', 'Error')
567
    )
568

    
569
    # The list of possible operations on the backend
570
    BACKEND_OPCODES = (
571
        ('OP_NETWORK_ADD', 'Create Network'),
572
        ('OP_NETWORK_CONNECT', 'Activate Network'),
573
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
574
        ('OP_NETWORK_REMOVE', 'Remove Network'),
575
        # These are listed here for completeness,
576
        # and are ignored for the time being
577
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
578
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
579
    )
580

    
581
    # The operating state of a Netowork,
582
    # upon the successful completion of a backend operation.
583
    # IMPORTANT: Make sure all keys have a corresponding
584
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
585
    OPER_STATE_FROM_OPCODE = {
586
        'OP_NETWORK_ADD': 'PENDING',
587
        'OP_NETWORK_CONNECT': 'ACTIVE',
588
        'OP_NETWORK_DISCONNECT': 'PENDING',
589
        'OP_NETWORK_REMOVE': 'DELETED',
590
        'OP_NETWORK_SET_PARAMS': None,
591
        'OP_NETWORK_QUERY_DATA': None
592
    }
593

    
594
    network = models.ForeignKey(Network, related_name='backend_networks')
595
    backend = models.ForeignKey(Backend, related_name='networks')
596
    created = models.DateTimeField(auto_now_add=True)
597
    updated = models.DateTimeField(auto_now=True)
598
    deleted = models.BooleanField('Deleted', default=False)
599
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
600
                                 default='PENDING')
601
    backendjobid = models.PositiveIntegerField(null=True)
602
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
603
                                     null=True)
604
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
605
                                        max_length=30, null=True)
606
    backendlogmsg = models.TextField(null=True)
607
    backendtime = models.DateTimeField(null=False,
608
                                       default=datetime.datetime.min)
609

    
610
    def save(self, *args, **kwargs):
611
        super(BackendNetwork, self).save(*args, **kwargs)
612
        self.network.update_state()
613

    
614
    def delete(self, *args, **kwargs):
615
        super(BackendNetwork, self).delete(*args, **kwargs)
616
        self.network.update_state()
617

    
618

    
619
class NetworkInterface(models.Model):
620
    FIREWALL_PROFILES = (
621
        ('ENABLED', 'Enabled'),
622
        ('DISABLED', 'Disabled'),
623
        ('PROTECTED', 'Protected')
624
    )
625

    
626
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
627
    network = models.ForeignKey(Network, related_name='nics')
628
    created = models.DateTimeField(auto_now_add=True)
629
    updated = models.DateTimeField(auto_now=True)
630
    index = models.IntegerField(null=False)
631
    mac = models.CharField(max_length=32, null=True)
632
    ipv4 = models.CharField(max_length=15, null=True)
633
    ipv6 = models.CharField(max_length=100, null=True)
634
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
635
                                        max_length=30, null=True)
636
    dirty = models.BooleanField(default=False)
637

    
638
    def __unicode__(self):
639
        return '%s@%s' % (self.machine.name, self.network.name)
640

    
641

    
642
class Pool(models.Model):
643
    """ Abstract class modeling a generic pool of resources
644

645
        Subclasses must implement 'value_from_index' method which
646
        converts and index(Integer) to an arbitrary Char value.
647

648
        Methods of this class must be invoked inside a transaction
649
        to ensure consistency of the pool.
650
    """
651
    available = models.BooleanField(default=True, null=False)
652
    index = models.IntegerField(null=False, unique=True)
653
    value = models.CharField(max_length=128, null=False, unique=True)
654
    max_index = 0
655

    
656
    objects = ForUpdateManager()
657

    
658
    class Meta:
659
        abstract = True
660
        ordering = ['index']
661

    
662
    @classmethod
663
    def get_available(cls):
664
        try:
665
            entry = cls.objects.select_for_update().filter(available=True)[0]
666
            entry.available = False
667
            entry.save()
668
            return entry
669
        except IndexError:
670
            return cls.generate_new()
671

    
672
    @classmethod
673
    def generate_new(cls):
674
        try:
675
            last = cls.objects.order_by('-index')[0]
676
            index = last.index + 1
677
        except IndexError:
678
            index = 1
679

    
680
        if index <= cls.max_index:
681
            return cls.objects.create(index=index,
682
                                      value=cls.value_from_index(index),
683
                                      available=False)
684

    
685
        raise Pool.PoolExhausted()
686

    
687
    @classmethod
688
    def set_available(cls, value):
689
        entry = cls.objects.select_for_update().get(value=value)
690
        entry.available = True
691
        entry.save()
692

    
693
    class PoolExhausted(Exception):
694
        pass
695

    
696

    
697
class BridgePool(Pool):
698
    max_index = snf_settings.PRIVATE_PHYSICAL_VLAN_MAX_NUMBER
699

    
700
    @staticmethod
701
    def value_from_index(index):
702
        return snf_settings.PRIVATE_PHYSICAL_VLAN_BRIDGE_PREFIX + str(index)
703

    
704

    
705
class MacPrefixPool(Pool):
706
    max_index = snf_settings.PRIVATE_MAC_FILTERED_MAX_PREFIX_NUMBER
707

    
708
    @staticmethod
709
    def value_from_index(index):
710
        """Convert number to mac prefix
711

712
        """
713
        high = snf_settings.PRIVATE_MAC_FILTERED_BASE_MAC_PREFIX
714
        a = hex(int(high.replace(":", ""), 16) + index).replace("0x", '')
715
        mac_prefix = ":".join([a[x:x + 2] for x in xrange(0, len(a), 2)])
716
        return mac_prefix