Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (25.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
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 import settings as snf_settings
43
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
44

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

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

    
51
import logging
52
log = logging.getLogger(__name__)
53

    
54

    
55
def get_client(hash, backend):
56
    """Get a cached backend client or create a new one.
57

58
    @param hash: The hash of the backend
59
    @param backend: Either a backend object or backend ID
60
    """
61

    
62
    if backend is None:
63
        raise Exception("Backend is None. Cannot create a client.")
64

    
65
    if hash in BACKEND_CLIENTS:
66
        # Return cached client
67
        return BACKEND_CLIENTS[hash]
68

    
69
    # Always get a new instance to ensure latest credentials
70
    if isinstance(backend, Backend):
71
        backend = backend.id
72

    
73
    backend = Backend.objects.get(id=backend)
74
    hash = backend.hash
75
    clustername = backend.clustername
76
    port = backend.port
77
    user = backend.username
78
    password = backend.password
79

    
80
    # Check client for updated hash
81
    if hash in BACKEND_CLIENTS:
82
        return BACKEND_CLIENTS[hash]
83

    
84
    # Delete old version of the client
85
    if backend in BACKEND_HASHES:
86
        del BACKEND_CLIENTS[BACKEND_HASHES[backend]]
87

    
88
    # Create the new client
89
    client = GanetiRapiClient(clustername, port, user, password)
90

    
91
    # Store the client and the hash
92
    BACKEND_CLIENTS[hash] = client
93
    BACKEND_HASHES[backend] = hash
94

    
95
    return client
96

    
97

    
98
def clear_client_cache():
99
    BACKEND_CLIENTS.clear()
100
    BACKEND_HASHES.clear()
101

    
102

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

    
111
    class Meta:
112
        verbose_name = u'Virtual machine flavor'
113
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
114

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

    
120
    def __unicode__(self):
121
        return self.name
122

    
123

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

    
153
    class Meta:
154
        verbose_name = u'Backend'
155
        ordering = ["clustername"]
156

    
157
    def __unicode__(self):
158
        return self.clustername
159

    
160
    @property
161
    def backend_id(self):
162
        return self.id
163

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

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

    
178
    @property
179
    def password(self):
180
        return decrypt_db_charfield(self.password_hash)
181

    
182
    @password.setter
183
    def password(self, value):
184
        self.password_hash = encrypt_db_charfield(value)
185

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

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

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

    
213

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

    
225

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

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

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

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

    
268

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

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

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

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

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

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

    
343
    # Error classes
344
    class InvalidBackendIdError(Exception):
345
        def __init__(self, value):
346
            self.value = value
347

    
348
        def __str__(self):
349
            return repr(self.value)
350

    
351
    class InvalidBackendMsgError(Exception):
352
        def __init__(self, opcode, status):
353
            self.opcode = opcode
354
            self.status = status
355

    
356
        def __str__(self):
357
            return repr('<opcode: %s, status: %s>' % (self.opcode,
358
                        self.status))
359

    
360
    class InvalidActionError(Exception):
361
        def __init__(self, action):
362
            self._action = action
363

    
364
        def __str__(self):
365
            return repr(str(self._action))
366

    
367
    class DeletedError(Exception):
368
        pass
369

    
370
    class BuildingError(Exception):
371
        pass
372

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

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

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

    
399
    class Meta:
400
        verbose_name = u'Virtual machine instance'
401
        get_latest_by = 'created'
402

    
403
    def __unicode__(self):
404
        return self.name
405

    
406

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

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

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

    
419

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

    
428
    ACTIONS = (
429
       ('CREATE', 'Create Network'),
430
       ('DESTROY', 'Destroy Network'),
431
    )
432

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

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

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

    
470
    reservations = models.TextField(default='')
471

    
472
    ip_pool = None
473

    
474
    objects = ForUpdateManager()
475

    
476
    class InvalidBackendIdError(Exception):
477
        def __init__(self, value):
478
            self.value = value
479

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

    
483
    class InvalidBackendMsgError(Exception):
484
        def __init__(self, opcode, status):
485
            self.opcode = opcode
486
            self.status = status
487

    
488
        def __str__(self):
489
            return repr('<opcode: %s, status: %s>' % (self.opcode,
490
                    self.status))
491

    
492
    class InvalidActionError(Exception):
493
        def __init__(self, action):
494
            self._action = action
495

    
496
        def __str__(self):
497
            return repr(str(self._action))
498

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

    
506
    @property
507
    def backend_tag(self):
508
        """Return the network tag to be used in backend
509

510
        """
511
        return getattr(snf_settings, self.type + '_TAGS')
512

    
513
    def __unicode__(self):
514
        return self.name
515

    
516
    @transaction.commit_on_success
517
    def update_state(self):
518
        """Update state of the Network.
519

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

525
        """
526

    
527
        old_state = self.state
528

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

    
535
        all_equal = len(set(backend_states)) <= 1
536
        self.state = all_equal and backend_states[0] or 'PENDING'
537

    
538
        # Release the resources on the deletion of the Network
539
        if old_state != 'DELETED' and self.state == 'DELETED':
540
            log.info("Network %r deleted. Releasing link %r mac_prefix %r",
541
                     self.id, self.mac_prefix, self.link)
542
            self.deleted = True
543
            if self.mac_prefix:
544
                mac_pool = MacPrefixPoolTable.get_pool()
545
                mac_pool.put(self.mac_prefix)
546
                mac_pool.save()
547

    
548
            if self.link and self.type == 'PRIVATE_VLAN':
549
                bridge_pool = BridgePoolTable.get_pool()
550
                bridge_pool.put(self.link)
551
                bridge_pool.save()
552

    
553
        self.save()
554

    
555
    def create_backend_network(self, backend=None):
556
        """Create corresponding BackendNetwork entries."""
557

    
558
        backends = [backend] if backend else Backend.objects.all()
559
        for backend in backends:
560
            BackendNetwork.objects.create(backend=backend, network=self)
561

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

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

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

    
582

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

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

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

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

    
633
    class Meta:
634
        # Ensure one entry for each network in each backend
635
        unique_together = (("network", "backend"))
636

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

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

    
657
    def delete(self, *args, **kwargs):
658
        super(BackendNetwork, self).delete(*args, **kwargs)
659
        self.network.update_state()
660

    
661

    
662
class NetworkInterface(models.Model):
663
    FIREWALL_PROFILES = (
664
        ('ENABLED', 'Enabled'),
665
        ('DISABLED', 'Disabled'),
666
        ('PROTECTED', 'Protected')
667
    )
668

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

    
681
    def __unicode__(self):
682
        return '%s@%s' % (self.machine.name, self.network.name)
683

    
684

    
685
class PoolTable(models.Model):
686
    available_map = models.TextField(default="", null=False)
687
    reserved_map = models.TextField(default="", null=False)
688
    size = models.IntegerField(null=False)
689

    
690
    # Optional Fields
691
    base = models.CharField(null=True, max_length=32)
692
    offset = models.IntegerField(null=True)
693

    
694
    objects = ForUpdateManager()
695

    
696
    class Meta:
697
        abstract = True
698

    
699
    @classmethod
700
    def get_pool(cls):
701
        try:
702
            pool_row = cls.objects.select_for_update().all()[0]
703
            return pool_row.pool
704
        except IndexError:
705
            raise pools.EmptyPool
706

    
707
    @property
708
    def pool(self):
709
        return self.manager(self)
710

    
711

    
712
class BridgePoolTable(PoolTable):
713
    manager = pools.BridgePool
714

    
715
class MacPrefixPoolTable(PoolTable):
716
    manager = pools.MacPrefixPool