Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 2a8a60d5

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

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

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

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

    
423
    NETWORK_TYPES = (
424
        ('PUBLIC_ROUTED', 'Public routed network'),
425
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
426
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
427
        ('CUSTOM_ROUTED', 'Custom routed network'),
428
        ('CUSTOM_BRIDGED', 'Custom bridged network')
429
    )
430

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

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

    
455
    ip_pool = None
456

    
457
    objects = ForUpdateManager()
458

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

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

    
466

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

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

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

    
480
        def __str__(self):
481
            return repr(str(self._action))
482

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

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

494
        """
495
        return getattr(snf_settings, self.type + '_TAGS')
496

    
497
    def __unicode__(self):
498
        return self.name
499

    
500
    def update_state(self):
501
        """Update state of the Network.
502

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

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

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

    
518
        if self.state == 'DELETED':
519
            self.deleted = True
520

    
521
            if self.mac_prefix:
522
                MacPrefixPool.set_available(self.mac_prefix)
523

    
524
            if self.link and self.type == 'PRIVATE_VLAN':
525
                BridgePool.set_available(self.link)
526

    
527
        self.save()
528

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

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

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

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

    
558

    
559
    # def get_free_address(self):
560
    #     # Get yourself inside a transaction
561
    #     network = Network.objects.get(id=self.id)
562
    #     # Get the pool object
563
    #     pool = network.pool
564
    #     print network is self
565
    #     try:
566
    #         address = pool.get_free_address()
567
    #     except IPPoolExhausted:
568
    #         raise Network.NetworkIsFull
569

    
570
    #     pool._update_network()
571
    #     network.save()
572
    #     return address
573

    
574
    # class NetworkIsFull(Exception):
575
    #     pass
576

    
577

    
578
class BackendNetwork(models.Model):
579
    OPER_STATES = (
580
        ('PENDING', 'Pending'),
581
        ('ACTIVE', 'Active'),
582
        ('DELETED', 'Deleted'),
583
        ('ERROR', 'Error')
584
    )
585

    
586
    # The list of possible operations on the backend
587
    BACKEND_OPCODES = (
588
        ('OP_NETWORK_ADD', 'Create Network'),
589
        ('OP_NETWORK_CONNECT', 'Activate Network'),
590
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
591
        ('OP_NETWORK_REMOVE', 'Remove Network'),
592
        # These are listed here for completeness,
593
        # and are ignored for the time being
594
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
595
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
596
    )
597

    
598
    # The operating state of a Netowork,
599
    # upon the successful completion of a backend operation.
600
    # IMPORTANT: Make sure all keys have a corresponding
601
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
602
    OPER_STATE_FROM_OPCODE = {
603
        'OP_NETWORK_ADD': 'PENDING',
604
        'OP_NETWORK_CONNECT': 'ACTIVE',
605
        'OP_NETWORK_DISCONNECT': 'PENDING',
606
        'OP_NETWORK_REMOVE': 'DELETED',
607
        'OP_NETWORK_SET_PARAMS': None,
608
        'OP_NETWORK_QUERY_DATA': None
609
    }
610

    
611
    network = models.ForeignKey(Network, related_name='backend_networks')
612
    backend = models.ForeignKey(Backend, related_name='networks')
613
    created = models.DateTimeField(auto_now_add=True)
614
    updated = models.DateTimeField(auto_now=True)
615
    deleted = models.BooleanField('Deleted', default=False)
616
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
617
                                 default='PENDING')
618
    backendjobid = models.PositiveIntegerField(null=True)
619
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
620
                                     null=True)
621
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
622
                                        max_length=30, null=True)
623
    backendlogmsg = models.TextField(null=True)
624
    backendtime = models.DateTimeField(null=False,
625
                                       default=datetime.datetime.min)
626

    
627
    def save(self, *args, **kwargs):
628
        super(BackendNetwork, self).save(*args, **kwargs)
629
        self.network.update_state()
630

    
631
    def delete(self, *args, **kwargs):
632
        super(BackendNetwork, self).delete(*args, **kwargs)
633
        self.network.update_state()
634

    
635

    
636
class NetworkInterface(models.Model):
637
    FIREWALL_PROFILES = (
638
        ('ENABLED', 'Enabled'),
639
        ('DISABLED', 'Disabled'),
640
        ('PROTECTED', 'Protected')
641
    )
642

    
643
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
644
    network = models.ForeignKey(Network, related_name='nics')
645
    created = models.DateTimeField(auto_now_add=True)
646
    updated = models.DateTimeField(auto_now=True)
647
    index = models.IntegerField(null=False)
648
    mac = models.CharField(max_length=32, null=True)
649
    ipv4 = models.CharField(max_length=15, null=True)
650
    ipv6 = models.CharField(max_length=100, null=True)
651
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
652
                                        max_length=30, null=True)
653
    dirty = models.BooleanField(default=False)
654

    
655
    def __unicode__(self):
656
        return '%s@%s' % (self.machine.name, self.network.name)
657

    
658

    
659
class Pool(models.Model):
660
    available = models.BooleanField(default=True, null=False)
661
    index = models.IntegerField(null=False, unique=True)
662
    value = models.CharField(max_length=128, null=False, unique=True)
663
    max_index = 0
664

    
665
    class Meta:
666
        abstract = True
667
        ordering = ['index']
668

    
669
    @classmethod
670
    def get_available(cls):
671
        try:
672
            entry = cls.objects.filter(available=True)[0]
673
            entry.available = False
674
            entry.save()
675
            return entry
676
        except IndexError:
677
            return cls.generate_new()
678

    
679
    @classmethod
680
    def generate_new(cls):
681
        try:
682
            last = cls.objects.order_by('-index')[0]
683
            index = last.index + 1
684
        except IndexError:
685
            index = 1
686

    
687
        if index <= cls.max_index:
688
            return cls.objects.create(index=index,
689
                                      value=cls.value_from_index(index),
690
                                      available=False)
691

    
692
        raise Pool.PoolExhausted()
693

    
694
    @classmethod
695
    def set_available(cls, value):
696
        entry = cls.objects.get(value=value)
697
        entry.available = True
698
        entry.save()
699

    
700

    
701
    class PoolExhausted(Exception):
702
        pass
703

    
704

    
705
class BridgePool(Pool):
706
    max_index = snf_settings.PRIVATE_PHYSICAL_VLAN_MAX_NUMBER
707

    
708
    @staticmethod
709
    def value_from_index(index):
710
        return snf_settings.PRIVATE_PHYSICAL_VLAN_BRIDGE_PREFIX + str(index)
711

    
712

    
713
class MacPrefixPool(Pool):
714
    max_index = snf_settings.PRIVATE_MAC_FILTERED_MAX_PREFIX_NUMBER
715

    
716
    @staticmethod
717
    def value_from_index(index):
718
        """Convert number to mac prefix
719

720
        """
721
        high = snf_settings.PRIVATE_MAC_FILTERED_BASE_MAC_PREFIX
722
        a = hex(int(high.replace(":", ""), 16) + index).replace("0x", '')
723
        mac_prefix = ":".join([a[x:x + 2] for x in xrange(0, len(a), 2)])
724
        return mac_prefix