Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / db / models.py @ 8731a246

History | View | Annotate | Download (26.7 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
from django.db import transaction
36

    
37
import utils
38

    
39
from hashlib import sha1
40
from synnefo.api.faults import ServiceUnavailable
41
from synnefo.util.rapi import GanetiRapiClient
42
from synnefo.logic.ippool import IPPool
43
from synnefo import settings as snf_settings
44
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
45

    
46
from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
47

    
48
BACKEND_CLIENTS = {}  # {hash:Backend client}
49
BACKEND_HASHES = {}   # {Backend.id:hash}
50

    
51

    
52
def get_client(hash, backend):
53
    """Get a cached backend client or create a new one.
54

55
    @param hash: The hash of the backend
56
    @param backend: Either a backend object or backend ID
57
    """
58

    
59
    if backend is None:
60
        raise Exception("Backend is None. Cannot create a client.")
61

    
62
    if hash in BACKEND_CLIENTS:
63
        # Return cached client
64
        return BACKEND_CLIENTS[hash]
65

    
66
    # Always get a new instance to ensure latest credentials
67
    if isinstance(backend, Backend):
68
        backend = backend.id
69

    
70
    backend = Backend.objects.get(id=backend)
71
    hash = backend.hash
72
    clustername = backend.clustername
73
    port = backend.port
74
    user = backend.username
75
    password = backend.password
76

    
77
    # Check client for updated hash
78
    if hash in BACKEND_CLIENTS:
79
        return BACKEND_CLIENTS[hash]
80

    
81
    # Delete old version of the client
82
    if backend in BACKEND_HASHES:
83
        del BACKEND_CLIENTS[BACKEND_HASHES[backend]]
84

    
85
    # Create the new client
86
    client = GanetiRapiClient(clustername, port, user, password)
87

    
88
    # Store the client and the hash
89
    BACKEND_CLIENTS[hash] = client
90
    BACKEND_HASHES[backend] = hash
91

    
92
    return client
93

    
94

    
95
def clear_client_cache():
96
    BACKEND_CLIENTS.clear()
97
    BACKEND_HASHES.clear()
98

    
99

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

    
108
    class Meta:
109
        verbose_name = u'Virtual machine flavor'
110
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
111

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

    
117
    def __unicode__(self):
118
        return self.name
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_hash = models.CharField('Password', max_length=128, 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
    # Unique index of the Backend, used for the mac-prefixes of the
131
    # BackendNetworks
132
    index = models.PositiveIntegerField('Index', null=False, unique=True,
133
                                        default=0)
134
    drained = models.BooleanField('Drained', default=False, null=False)
135
    offline = models.BooleanField('Offline', default=False, null=False)
136
    # Last refresh of backend resources
137
    updated = models.DateTimeField(auto_now_add=True)
138
    # Backend resources
139
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
140
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
141
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
142
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
143
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
144
                                            null=False)
145
    ctotal = models.PositiveIntegerField('Total number of logical processors',
146
                                         default=0, null=False)
147
    # Custom object manager to protect from cascade delete
148
    objects = ProtectedDeleteManager()
149

    
150
    class Meta:
151
        verbose_name = u'Backend'
152
        ordering = ["clustername"]
153

    
154
    def __unicode__(self):
155
        return self.clustername
156

    
157
    @property
158
    def backend_id(self):
159
        return self.id
160

    
161
    @property
162
    def client(self):
163
        """Get or create a client. """
164
        if not self.offline:
165
            return get_client(self.hash, self)
166
        else:
167
            raise ServiceUnavailable
168

    
169
    def create_hash(self):
170
        """Create a hash for this backend. """
171
        return sha1('%s%s%s%s' % \
172
                (self.clustername, self.port, self.username, self.password)) \
173
                .hexdigest()
174

    
175
    @property
176
    def password(self):
177
        return decrypt_db_charfield(self.password_hash)
178

    
179
    @password.setter
180
    def password(self, value):
181
        self.password_hash = encrypt_db_charfield(value)
182

    
183
    def save(self, *args, **kwargs):
184
        # Create a new hash each time a Backend is saved
185
        old_hash = self.hash
186
        self.hash = self.create_hash()
187
        super(Backend, self).save(*args, **kwargs)
188
        if self.hash != old_hash:
189
            # Populate the new hash to the new instances
190
            self.virtual_machines.filter(deleted=False).update(backend_hash=self.hash)
191

    
192
    def delete(self, *args, **kwargs):
193
        # Integrity Error if non-deleted VMs are associated with Backend
194
        if self.virtual_machines.filter(deleted=False).count():
195
            raise IntegrityError("Non-deleted virtual machines are associated "
196
                                 "with backend: %s" % self)
197
        else:
198
            # ON_DELETE = SET NULL
199
            self.virtual_machines.all().backend = None
200
            super(Backend, self).delete(*args, **kwargs)
201

    
202
    def __init__(self, *args, **kwargs):
203
        super(Backend, self).__init__(*args, **kwargs)
204
        if not self.pk:
205
            # Generate a unique index for the Backend
206
            indexes = Backend.objects.all().values_list('index', flat=True)
207
            first_free = [x for x in xrange(0, 16) if x not in indexes][0]
208
            self.index = first_free
209

    
210

    
211
# A backend job may be in one of the following possible states
212
BACKEND_STATUSES = (
213
    ('queued', 'request queued'),
214
    ('waiting', 'request waiting for locks'),
215
    ('canceling', 'request being canceled'),
216
    ('running', 'request running'),
217
    ('canceled', 'request canceled'),
218
    ('success', 'request completed successfully'),
219
    ('error', 'request returned error')
220
)
221

    
222

    
223
class VirtualMachine(models.Model):
224
    # The list of possible actions for a VM
225
    ACTIONS = (
226
       ('CREATE', 'Create VM'),
227
       ('START', 'Start VM'),
228
       ('STOP', 'Shutdown VM'),
229
       ('SUSPEND', 'Admin Suspend VM'),
230
       ('REBOOT', 'Reboot VM'),
231
       ('DESTROY', 'Destroy VM')
232
    )
233

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

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

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

    
265

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

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

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

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

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

    
333
    @property
334
    def client(self):
335
        if self.backend and not self.backend.offline:
336
            return get_client(self.backend_hash, self.backend_id)
337
        else:
338
            raise ServiceUnavailable
339

    
340
    # Error classes
341
    class InvalidBackendIdError(Exception):
342
        def __init__(self, value):
343
            self.value = value
344

    
345
        def __str__(self):
346
            return repr(self.value)
347

    
348
    class InvalidBackendMsgError(Exception):
349
        def __init__(self, opcode, status):
350
            self.opcode = opcode
351
            self.status = status
352

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

    
357
    class InvalidActionError(Exception):
358
        def __init__(self, action):
359
            self._action = action
360

    
361
        def __str__(self):
362
            return repr(str(self._action))
363

    
364
    class DeletedError(Exception):
365
        pass
366

    
367
    class BuildingError(Exception):
368
        pass
369

    
370
    def __init__(self, *args, **kw):
371
        """Initialize state for just created VM instances."""
372
        super(VirtualMachine, self).__init__(*args, **kw)
373
        # This gets called BEFORE an instance gets save()d for
374
        # the first time.
375
        if not self.pk:
376
            self.action = None
377
            self.backendjobid = None
378
            self.backendjobstatus = None
379
            self.backendopcode = None
380
            self.backendlogmsg = None
381
            self.operstate = 'BUILD'
382

    
383
    def save(self, *args, **kwargs):
384
        # Store hash for first time saved vm
385
        if (self.id is None or self.backend_hash == '') and self.backend:
386
            self.backend_hash = self.backend.hash
387
        super(VirtualMachine, self).save(*args, **kwargs)
388

    
389
    @property
390
    def backend_vm_id(self):
391
        """Returns the backend id for this VM by prepending backend-prefix."""
392
        if not self.id:
393
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
394
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
395

    
396
    class Meta:
397
        verbose_name = u'Virtual machine instance'
398
        get_latest_by = 'created'
399

    
400
    def __unicode__(self):
401
        return self.name
402

    
403

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

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

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

    
416

    
417
class Network(models.Model):
418
    OPER_STATES = (
419
        ('PENDING', 'Pending'),
420
        ('ACTIVE', 'Active'),
421
        ('DELETED', 'Deleted'),
422
        ('ERROR', 'Error')
423
    )
424

    
425
    ACTIONS = (
426
       ('CREATE', 'Create Network'),
427
       ('DESTROY', 'Destroy Network'),
428
    )
429

    
430
    RSAPI_STATE_FROM_OPER_STATE = {
431
        'PENDING': 'PENDING',
432
        'ACTIVE': 'ACTIVE',
433
        'DELETED': 'DELETED',
434
        'ERROR': 'ERROR'
435
    }
436

    
437
    NETWORK_TYPES = (
438
        ('PUBLIC_ROUTED', 'Public routed network'),
439
        ('PRIVATE_PHYSICAL_VLAN', 'Private vlan network'),
440
        ('PRIVATE_MAC_FILTERED', 'Private network with mac-filtering'),
441
        ('CUSTOM_ROUTED', 'Custom routed network'),
442
        ('CUSTOM_BRIDGED', 'Custom bridged network')
443
    )
444

    
445
    name = models.CharField('Network Name', max_length=128)
446
    userid = models.CharField('User ID of the owner', max_length=128, null=True)
447
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
448
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
449
    gateway = models.CharField('Gateway', max_length=32, null=True)
450
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
451
    dhcp = models.BooleanField('DHCP', default=True)
452
    type = models.CharField(choices=NETWORK_TYPES, max_length=50,
453
                            default='PRIVATE_PHYSICAL_VLAN')
454
    link = models.CharField('Network Link', max_length=128, null=True)
455
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
456
    public = models.BooleanField(default=False)
457
    created = models.DateTimeField(auto_now_add=True)
458
    updated = models.DateTimeField(auto_now=True)
459
    deleted = models.BooleanField('Deleted', default=False)
460
    state = models.CharField(choices=OPER_STATES, max_length=32,
461
                             default='PENDING')
462
    machines = models.ManyToManyField(VirtualMachine,
463
                                      through='NetworkInterface')
464
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
465
                              default=None)
466

    
467
    reservations = models.TextField(default='')
468

    
469
    ip_pool = None
470

    
471
    objects = ForUpdateManager()
472

    
473
    class InvalidBackendIdError(Exception):
474
        def __init__(self, value):
475
            self.value = value
476

    
477
        def __str__(self):
478
            return repr(self.value)
479

    
480
    class InvalidBackendMsgError(Exception):
481
        def __init__(self, opcode, status):
482
            self.opcode = opcode
483
            self.status = status
484

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

    
489
    class InvalidActionError(Exception):
490
        def __init__(self, action):
491
            self._action = action
492

    
493
        def __str__(self):
494
            return repr(str(self._action))
495

    
496
    @property
497
    def backend_id(self):
498
        """Return the backend id by prepending backend-prefix."""
499
        if not self.id:
500
            raise Network.InvalidBackendIdError("self.id is None")
501
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
502

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

507
        """
508
        return getattr(snf_settings, self.type + '_TAGS')
509

    
510
    def __unicode__(self):
511
        return self.name
512

    
513
    @transaction.commit_on_success
514
    def update_state(self):
515
        """Update state of the Network.
516

517
        Update the state of the Network depending on the related
518
        backend_networks. When backend networks do not have the same operstate,
519
        the Network's state is PENDING. Otherwise it is the same with
520
        the BackendNetworks operstate.
521

522
        """
523

    
524
        old_state = self.state
525

    
526
        backend_states = [s.operstate for s in self.backend_networks.all()]
527
        if not backend_states:
528
            self.state = 'PENDING'
529
            self.save()
530
            return
531

    
532
        all_equal = len(set(backend_states)) <= 1
533
        self.state = all_equal and backend_states[0] or 'PENDING'
534

    
535
        # Release the resources on the deletion of the Network
536
        if old_state != 'DELETED' and self.state == 'DELETED':
537
            self.deleted = True
538

    
539
            if self.mac_prefix:
540
                MacPrefixPool.set_available(self.mac_prefix)
541

    
542
            if self.link and self.type == 'PRIVATE_VLAN':
543
                BridgePool.set_available(self.link)
544

    
545
        self.save()
546

    
547
    def __init__(self, *args, **kwargs):
548
        super(Network, self).__init__(*args, **kwargs)
549
        if not self.mac_prefix:
550
            # Allocate a MAC prefix for just created Network instances
551
            mac_prefix = MacPrefixPool.get_available().value
552
            self.mac_prefix = mac_prefix
553

    
554
    def save(self, *args, **kwargs):
555
        pk = self.pk
556
        super(Network, self).save(*args, **kwargs)
557
        if not pk:
558
            # In case of a new Network, corresponding BackendNetwork's must
559
            # be created!
560
            for back in Backend.objects.all():
561
                BackendNetwork.objects.create(backend=back, network=self)
562

    
563
    @property
564
    def pool(self):
565
        if self.ip_pool:
566
            return self.ip_pool
567
        else:
568
            self.ip_pool = IPPool(self)
569
            return self.ip_pool
570

    
571
    def reserve_address(self, address, pool=None):
572
        pool = pool or self.pool
573
        pool.reserve(address)
574
        pool._update_network()
575
        self.save()
576

    
577
    def release_address(self, address, pool=None):
578
        pool = pool or self.pool
579
        pool.release(address)
580
        pool._update_network()
581
        self.save()
582

    
583

    
584
class BackendNetwork(models.Model):
585
    OPER_STATES = (
586
        ('PENDING', 'Pending'),
587
        ('ACTIVE', 'Active'),
588
        ('DELETED', 'Deleted'),
589
        ('ERROR', 'Error')
590
    )
591

    
592
    # The list of possible operations on the backend
593
    BACKEND_OPCODES = (
594
        ('OP_NETWORK_ADD', 'Create Network'),
595
        ('OP_NETWORK_CONNECT', 'Activate Network'),
596
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
597
        ('OP_NETWORK_REMOVE', 'Remove Network'),
598
        # These are listed here for completeness,
599
        # and are ignored for the time being
600
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
601
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
602
    )
603

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

    
617
    network = models.ForeignKey(Network, related_name='backend_networks')
618
    backend = models.ForeignKey(Backend, related_name='networks')
619
    created = models.DateTimeField(auto_now_add=True)
620
    updated = models.DateTimeField(auto_now=True)
621
    deleted = models.BooleanField('Deleted', default=False)
622
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
623
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
624
                                 default='PENDING')
625
    backendjobid = models.PositiveIntegerField(null=True)
626
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
627
                                     null=True)
628
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
629
                                        max_length=30, null=True)
630
    backendlogmsg = models.TextField(null=True)
631
    backendtime = models.DateTimeField(null=False,
632
                                       default=datetime.datetime.min)
633

    
634
    def __init__(self, *args, **kwargs):
635
        """Initialize state for just created BackendNetwork instances."""
636
        super(BackendNetwork, self).__init__(*args, **kwargs)
637
        if not self.mac_prefix:
638
            # Generate the MAC prefix of the BackendNetwork, by combining
639
            # the Network prefix with the index of the Backend
640
            net_prefix = self.network.mac_prefix
641
            backend_suffix = hex(self.backend.index).replace('0x', '')
642
            mac_prefix = net_prefix + backend_suffix
643
            try:
644
                utils.validate_mac(mac_prefix + ":00:00:00")
645
            except utils.InvalidMacAddress:
646
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
647
                                               mac_prefix)
648
            self.mac_prefix = mac_prefix
649

    
650
    def save(self, *args, **kwargs):
651
        super(BackendNetwork, self).save(*args, **kwargs)
652
        self.network.update_state()
653

    
654
    def delete(self, *args, **kwargs):
655
        super(BackendNetwork, self).delete(*args, **kwargs)
656
        self.network.update_state()
657

    
658

    
659
class NetworkInterface(models.Model):
660
    FIREWALL_PROFILES = (
661
        ('ENABLED', 'Enabled'),
662
        ('DISABLED', 'Disabled'),
663
        ('PROTECTED', 'Protected')
664
    )
665

    
666
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
667
    network = models.ForeignKey(Network, related_name='nics')
668
    created = models.DateTimeField(auto_now_add=True)
669
    updated = models.DateTimeField(auto_now=True)
670
    index = models.IntegerField(null=False)
671
    mac = models.CharField(max_length=32, null=False, unique=True)
672
    ipv4 = models.CharField(max_length=15, null=True)
673
    ipv6 = models.CharField(max_length=100, null=True)
674
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
675
                                        max_length=30, null=True)
676
    dirty = models.BooleanField(default=False)
677

    
678
    def __unicode__(self):
679
        return '%s@%s' % (self.machine.name, self.network.name)
680

    
681

    
682
class Pool(models.Model):
683
    """ Abstract class modeling a generic pool of resources
684

685
        Subclasses must implement 'value_from_index' method which
686
        converts and index(Integer) to an arbitrary Char value.
687

688
        Methods of this class must be invoked inside a transaction
689
        to ensure consistency of the pool.
690
    """
691
    available = models.BooleanField(default=True, null=False)
692
    index = models.IntegerField(null=False, unique=True)
693
    value = models.CharField(max_length=128, null=False, unique=True)
694
    max_index = 0
695

    
696
    objects = ForUpdateManager()
697

    
698
    class Meta:
699
        abstract = True
700
        ordering = ['index']
701

    
702
    @classmethod
703
    def get_available(cls):
704
        try:
705
            entry = cls.objects.select_for_update().filter(available=True)[0]
706
            entry.available = False
707
            entry.save()
708
            return entry
709
        except IndexError:
710
            return cls.generate_new()
711

    
712
    @classmethod
713
    def generate_new(cls):
714
        try:
715
            last = cls.objects.order_by('-index')[0]
716
            index = last.index + 1
717
        except IndexError:
718
            index = 1
719

    
720
        if index <= cls.max_index:
721
            return cls.objects.create(index=index,
722
                                      value=cls.value_from_index(index),
723
                                      available=False)
724

    
725
        raise Pool.PoolExhausted()
726

    
727
    @classmethod
728
    def set_available(cls, value):
729
        entry = cls.objects.select_for_update().get(value=value)
730
        entry.available = True
731
        entry.save()
732

    
733
    class PoolExhausted(Exception):
734
        pass
735

    
736

    
737
class BridgePool(Pool):
738
    max_index = snf_settings.PRIVATE_PHYSICAL_VLAN_MAX_NUMBER
739

    
740
    @staticmethod
741
    def value_from_index(index):
742
        return snf_settings.PRIVATE_PHYSICAL_VLAN_BRIDGE_PREFIX + str(index)
743

    
744

    
745
class MacPrefixPool(Pool):
746
    max_index = snf_settings.MAC_POOL_LIMIT
747

    
748
    @staticmethod
749
    def value_from_index(index):
750
        """Convert number to mac prefix
751

752
        """
753
        base = snf_settings.MAC_POOL_BASE
754
        a = hex(int(base.replace(":", ""), 16) + index).replace("0x", '')
755
        mac_prefix = ":".join([a[x:x + 2] for x in xrange(0, len(a), 2)])
756
        return mac_prefix