Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.4 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
    def get_last_diagnostic(self, **filters):
341
        try:
342
            return self.diagnostics.filter()[0]
343
        except IndexError:
344
            return None
345

    
346
    # Error classes
347
    class InvalidBackendIdError(Exception):
348
        def __init__(self, value):
349
            self.value = value
350

    
351
        def __str__(self):
352
            return repr(self.value)
353

    
354
    class InvalidBackendMsgError(Exception):
355
        def __init__(self, opcode, status):
356
            self.opcode = opcode
357
            self.status = status
358

    
359
        def __str__(self):
360
            return repr('<opcode: %s, status: %s>' % (self.opcode,
361
                        self.status))
362

    
363
    class InvalidActionError(Exception):
364
        def __init__(self, action):
365
            self._action = action
366

    
367
        def __str__(self):
368
            return repr(str(self._action))
369

    
370
    class DeletedError(Exception):
371
        pass
372

    
373
    class BuildingError(Exception):
374
        pass
375

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

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

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

    
402
    class Meta:
403
        verbose_name = u'Virtual machine instance'
404
        get_latest_by = 'created'
405

    
406
    def __unicode__(self):
407
        return self.name
408

    
409

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

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

    
419
    def __unicode__(self):
420
        return u'%s: %s' % (self.meta_key, self.meta_value)
421

    
422

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

    
431
    ACTIONS = (
432
       ('CREATE', 'Create Network'),
433
       ('DESTROY', 'Destroy Network'),
434
    )
435

    
436
    RSAPI_STATE_FROM_OPER_STATE = {
437
        'PENDING': 'PENDING',
438
        'ACTIVE': 'ACTIVE',
439
        'DELETED': 'DELETED',
440
        'ERROR': 'ERROR'
441
    }
442

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

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

    
473
    reservations = models.TextField(default='')
474

    
475
    ip_pool = None
476

    
477
    objects = ForUpdateManager()
478

    
479
    class InvalidBackendIdError(Exception):
480
        def __init__(self, value):
481
            self.value = value
482

    
483
        def __str__(self):
484
            return repr(self.value)
485

    
486
    class InvalidBackendMsgError(Exception):
487
        def __init__(self, opcode, status):
488
            self.opcode = opcode
489
            self.status = status
490

    
491
        def __str__(self):
492
            return repr('<opcode: %s, status: %s>' % (self.opcode,
493
                    self.status))
494

    
495
    class InvalidActionError(Exception):
496
        def __init__(self, action):
497
            self._action = action
498

    
499
        def __str__(self):
500
            return repr(str(self._action))
501

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

    
509
    @property
510
    def backend_tag(self):
511
        """Return the network tag to be used in backend
512

513
        """
514
        return getattr(snf_settings, self.type + '_TAGS')
515

    
516
    def __unicode__(self):
517
        return self.name
518

    
519
    @transaction.commit_on_success
520
    def update_state(self):
521
        """Update state of the Network.
522

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

528
        """
529

    
530
        old_state = self.state
531

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

    
538
        all_equal = len(set(backend_states)) <= 1
539
        self.state = all_equal and backend_states[0] or 'PENDING'
540

    
541
        # Release the resources on the deletion of the Network
542
        if old_state != 'DELETED' and self.state == 'DELETED':
543
            self.deleted = True
544

    
545
            if self.mac_prefix:
546
                MacPrefixPool.set_available(self.mac_prefix)
547

    
548
            if self.link and self.type == 'PRIVATE_VLAN':
549
                BridgePool.set_available(self.link)
550

    
551
        self.save()
552

    
553
    def __init__(self, *args, **kwargs):
554
        super(Network, self).__init__(*args, **kwargs)
555
        if not self.mac_prefix:
556
            # Allocate a MAC prefix for just created Network instances
557
            mac_prefix = MacPrefixPool.get_available().value
558
            self.mac_prefix = mac_prefix
559

    
560
    def save(self, *args, **kwargs):
561
        pk = self.pk
562
        super(Network, self).save(*args, **kwargs)
563
        if not pk:
564
            # In case of a new Network, corresponding BackendNetwork's must
565
            # be created!
566
            for back in Backend.objects.all():
567
                BackendNetwork.objects.create(backend=back, network=self)
568

    
569
    @property
570
    def pool(self):
571
        if self.ip_pool:
572
            return self.ip_pool
573
        else:
574
            self.ip_pool = IPPool(self)
575
            return self.ip_pool
576

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

    
583
    def release_address(self, address, pool=None):
584
        pool = pool or self.pool
585
        pool.release(address)
586
        pool._update_network()
587
        self.save()
588

    
589

    
590
class BackendNetwork(models.Model):
591
    OPER_STATES = (
592
        ('PENDING', 'Pending'),
593
        ('ACTIVE', 'Active'),
594
        ('DELETED', 'Deleted'),
595
        ('ERROR', 'Error')
596
    )
597

    
598
    # The list of possible operations on the backend
599
    BACKEND_OPCODES = (
600
        ('OP_NETWORK_ADD', 'Create Network'),
601
        ('OP_NETWORK_CONNECT', 'Activate Network'),
602
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
603
        ('OP_NETWORK_REMOVE', 'Remove Network'),
604
        # These are listed here for completeness,
605
        # and are ignored for the time being
606
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
607
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
608
    )
609

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

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

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

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

    
660
    def delete(self, *args, **kwargs):
661
        super(BackendNetwork, self).delete(*args, **kwargs)
662
        self.network.update_state()
663

    
664

    
665
class NetworkInterface(models.Model):
666
    FIREWALL_PROFILES = (
667
        ('ENABLED', 'Enabled'),
668
        ('DISABLED', 'Disabled'),
669
        ('PROTECTED', 'Protected')
670
    )
671

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

    
684
    def __unicode__(self):
685
        return '%s@%s' % (self.machine.name, self.network.name)
686

    
687

    
688
class Pool(models.Model):
689
    """ Abstract class modeling a generic pool of resources
690

691
        Subclasses must implement 'value_from_index' method which
692
        converts and index(Integer) to an arbitrary Char value.
693

694
        Methods of this class must be invoked inside a transaction
695
        to ensure consistency of the pool.
696
    """
697
    available = models.BooleanField(default=True, null=False)
698
    index = models.IntegerField(null=False, unique=True)
699
    value = models.CharField(max_length=128, null=False, unique=True)
700
    max_index = 0
701

    
702
    objects = ForUpdateManager()
703

    
704
    class Meta:
705
        abstract = True
706
        ordering = ['index']
707

    
708
    @classmethod
709
    def get_available(cls):
710
        try:
711
            entry = cls.objects.select_for_update().filter(available=True)[0]
712
            entry.available = False
713
            entry.save()
714
            return entry
715
        except IndexError:
716
            return cls.generate_new()
717

    
718
    @classmethod
719
    def generate_new(cls):
720
        try:
721
            last = cls.objects.order_by('-index')[0]
722
            index = last.index + 1
723
        except IndexError:
724
            index = 1
725

    
726
        if index <= cls.max_index:
727
            return cls.objects.create(index=index,
728
                                      value=cls.value_from_index(index),
729
                                      available=False)
730

    
731
        raise Pool.PoolExhausted()
732

    
733
    @classmethod
734
    def set_available(cls, value):
735
        entry = cls.objects.select_for_update().get(value=value)
736
        entry.available = True
737
        entry.save()
738

    
739
    class PoolExhausted(Exception):
740
        pass
741

    
742

    
743
class BridgePool(Pool):
744
    max_index = snf_settings.PRIVATE_PHYSICAL_VLAN_MAX_NUMBER
745

    
746
    @staticmethod
747
    def value_from_index(index):
748
        return snf_settings.PRIVATE_PHYSICAL_VLAN_BRIDGE_PREFIX + str(index)
749

    
750

    
751
class MacPrefixPool(Pool):
752
    max_index = snf_settings.MAC_POOL_LIMIT
753

    
754
    @staticmethod
755
    def value_from_index(index):
756
        """Convert number to mac prefix
757

758
        """
759
        base = snf_settings.MAC_POOL_BASE
760
        a = hex(int(base.replace(":", ""), 16) + index).replace("0x", '')
761
        mac_prefix = ":".join([a[x:x + 2] for x in xrange(0, len(a), 2)])
762
        return mac_prefix
763

    
764

    
765
class VirtualMachineDiagnosticManager(models.Manager):
766
    """
767
    Custom manager for :class:`VirtualMachineDiagnostic` model.
768
    """
769

    
770
    # diagnostic creation helpers
771
    def create_for_vm(self, vm, level, message, **kwargs):
772
        attrs = {'machine': vm, 'level': level, 'message': message}
773
        attrs.update(kwargs)
774
        # update instance updated time
775
        self.create(**attrs)
776
        vm.save()
777

    
778
    def create_error(self, vm, **kwargs):
779
        self.create_for_vm(vm, 'ERROR', **kwargs)
780

    
781
    def create_debug(self, vm, **kwargs):
782
        self.create_for_vm(vm, 'DEBUG', **kwargs)
783

    
784
    def since(self, vm, created_since, **kwargs):
785
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
786
                **kwargs)
787

    
788

    
789
class VirtualMachineDiagnostic(models.Model):
790
    """
791
    Model to store backend information messages that relate to the state of
792
    the virtual machine.
793
    """
794

    
795
    TYPES = (
796
        ('ERROR', 'Error'),
797
        ('WARNING', 'Warning'),
798
        ('INFO', 'Info'),
799
        ('DEBUG', 'Debug'),
800
    )
801

    
802
    objects = VirtualMachineDiagnosticManager()
803

    
804
    created = models.DateTimeField(auto_now_add=True)
805
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
806
    level = models.CharField(max_length=20, choices=TYPES)
807
    source = models.CharField(max_length=100)
808
    source_date = models.DateTimeField(null=True)
809
    message = models.CharField(max_length=255)
810
    details = models.TextField(null=True)
811

    
812
    class Meta:
813
        ordering = ['-created']
814