Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.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 copy import deepcopy
33
from django.conf import settings
34
from django.db import models
35
from django.db import IntegrityError
36

    
37
import utils
38
from contextlib import contextmanager
39
from hashlib import sha1
40
from snf_django.lib.api import faults
41
from synnefo import settings as snf_settings
42
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
43

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

    
47
from synnefo.logic.rapi_pool import (get_rapi_client,
48
                                     put_rapi_client)
49

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

    
53

    
54
class Flavor(models.Model):
55
    cpu = models.IntegerField('Number of CPUs', default=0)
56
    ram = models.IntegerField('RAM size in MiB', default=0)
57
    disk = models.IntegerField('Disk size in GiB', default=0)
58
    disk_template = models.CharField('Disk template', max_length=32)
59
    deleted = models.BooleanField('Deleted', default=False)
60

    
61
    class Meta:
62
        verbose_name = u'Virtual machine flavor'
63
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
64

    
65
    @property
66
    def name(self):
67
        """Returns flavor name (generated)"""
68
        return u'C%dR%dD%d%s' % (self.cpu, self.ram, self.disk,
69
                                 self.disk_template)
70

    
71
    def __unicode__(self):
72
        return str(self.id)
73

    
74

    
75
class Backend(models.Model):
76
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
77
    port = models.PositiveIntegerField('Port', default=5080)
78
    username = models.CharField('Username', max_length=64, blank=True,
79
                                null=True)
80
    password_hash = models.CharField('Password', max_length=128, blank=True,
81
                                     null=True)
82
    # Sha1 is up to 40 characters long
83
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
84
    # Unique index of the Backend, used for the mac-prefixes of the
85
    # BackendNetworks
86
    index = models.PositiveIntegerField('Index', null=False, unique=True,
87
                                        default=0)
88
    drained = models.BooleanField('Drained', default=False, null=False)
89
    offline = models.BooleanField('Offline', default=False, null=False)
90
    # Type of hypervisor
91
    hypervisor = models.CharField('Hypervisor', max_length=32, default="kvm",
92
                                  null=False)
93
    # Last refresh of backend resources
94
    updated = models.DateTimeField(auto_now_add=True)
95
    # Backend resources
96
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
97
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
98
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
99
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
100
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
101
                                            null=False)
102
    ctotal = models.PositiveIntegerField('Total number of logical processors',
103
                                         default=0, null=False)
104
    # Custom object manager to protect from cascade delete
105
    objects = ProtectedDeleteManager()
106

    
107
    HYPERVISORS = (
108
        ("kvm", "Linux KVM hypervisor"),
109
        ("xen-pvm", "Xen PVM hypervisor"),
110
        ("xen-hvm", "Xen KVM hypervisor"),
111
    )
112

    
113
    class Meta:
114
        verbose_name = u'Backend'
115
        ordering = ["clustername"]
116

    
117
    def __unicode__(self):
118
        return self.clustername + "(id=" + str(self.id) + ")"
119

    
120
    @property
121
    def backend_id(self):
122
        return self.id
123

    
124
    def get_client(self):
125
        """Get or create a client. """
126
        if self.offline:
127
            raise faults.ServiceUnavailable
128
        return get_rapi_client(self.id, self.hash,
129
                               self.clustername,
130
                               self.port,
131
                               self.username,
132
                               self.password)
133

    
134
    @staticmethod
135
    def put_client(client):
136
            put_rapi_client(client)
137

    
138
    def create_hash(self):
139
        """Create a hash for this backend. """
140
        sha = sha1('%s%s%s%s' %
141
                   (self.clustername, self.port, self.username, self.password))
142
        return sha.hexdigest()
143

    
144
    @property
145
    def password(self):
146
        return decrypt_db_charfield(self.password_hash)
147

    
148
    @password.setter
149
    def password(self, value):
150
        self.password_hash = encrypt_db_charfield(value)
151

    
152
    def save(self, *args, **kwargs):
153
        # Create a new hash each time a Backend is saved
154
        old_hash = self.hash
155
        self.hash = self.create_hash()
156
        super(Backend, self).save(*args, **kwargs)
157
        if self.hash != old_hash:
158
            # Populate the new hash to the new instances
159
            self.virtual_machines.filter(deleted=False)\
160
                                 .update(backend_hash=self.hash)
161

    
162
    def delete(self, *args, **kwargs):
163
        # Integrity Error if non-deleted VMs are associated with Backend
164
        if self.virtual_machines.filter(deleted=False).count():
165
            raise IntegrityError("Non-deleted virtual machines are associated "
166
                                 "with backend: %s" % self)
167
        else:
168
            # ON_DELETE = SET NULL
169
            for vm in self.virtual_machines.all():
170
                vm.backend = None
171
                vm.save()
172
            self.virtual_machines.all().backend = None
173
            # Remove BackendNetworks of this Backend.
174
            # Do not use networks.all().delete(), since delete() method of
175
            # BackendNetwork will not be called!
176
            for net in self.networks.all():
177
                net.delete()
178
            super(Backend, self).delete(*args, **kwargs)
179

    
180
    def __init__(self, *args, **kwargs):
181
        super(Backend, self).__init__(*args, **kwargs)
182
        if not self.pk:
183
            # Generate a unique index for the Backend
184
            indexes = Backend.objects.all().values_list('index', flat=True)
185
            try:
186
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
187
                self.index = first_free
188
            except IndexError:
189
                raise Exception("Can not create more than 16 backends")
190

    
191
    def use_hotplug(self):
192
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
193

    
194
    def get_create_params(self):
195
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
196
        params["hvparams"] = params.get("hvparams", {})\
197
                                   .get(self.hypervisor, {})
198
        return params
199

    
200

    
201
# A backend job may be in one of the following possible states
202
BACKEND_STATUSES = (
203
    ('queued', 'request queued'),
204
    ('waiting', 'request waiting for locks'),
205
    ('canceling', 'request being canceled'),
206
    ('running', 'request running'),
207
    ('canceled', 'request canceled'),
208
    ('success', 'request completed successfully'),
209
    ('error', 'request returned error')
210
)
211

    
212

    
213
class QuotaHolderSerial(models.Model):
214
    """Model representing a serial for a Quotaholder Commission.
215

216
    serial:   The serial that Quotaholder assigned to this commission
217
    pending:  Whether it has been decided to accept or reject this commission
218
    accept:   If pending is False, this attribute indicates whether to accept
219
              or reject this commission
220
    resolved: Whether this commission has been accepted or rejected to
221
              Quotaholder.
222

223
    """
224
    serial = models.BigIntegerField(null=False, primary_key=True,
225
                                    db_index=True)
226
    pending = models.BooleanField(default=True, db_index=True)
227
    accept = models.BooleanField(default=False)
228
    resolved = models.BooleanField(default=False)
229

    
230
    class Meta:
231
        verbose_name = u'Quota Serial'
232
        ordering = ["serial"]
233

    
234

    
235
class VirtualMachine(models.Model):
236
    # The list of possible actions for a VM
237
    ACTIONS = (
238
        ('CREATE', 'Create VM'),
239
        ('START', 'Start VM'),
240
        ('STOP', 'Shutdown VM'),
241
        ('SUSPEND', 'Admin Suspend VM'),
242
        ('REBOOT', 'Reboot VM'),
243
        ('DESTROY', 'Destroy VM')
244
    )
245

    
246
    # The internal operating state of a VM
247
    OPER_STATES = (
248
        ('BUILD', 'Queued for creation'),
249
        ('ERROR', 'Creation failed'),
250
        ('STOPPED', 'Stopped'),
251
        ('STARTED', 'Started'),
252
        ('DESTROYED', 'Destroyed')
253
    )
254

    
255
    # The list of possible operations on the backend
256
    BACKEND_OPCODES = (
257
        ('OP_INSTANCE_CREATE', 'Create Instance'),
258
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
259
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
260
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
261
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
262

    
263
        # These are listed here for completeness,
264
        # and are ignored for the time being
265
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
266
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
267
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
268
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
269
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
270
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
271
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
272
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
273
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
274
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
275
    )
276

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

    
299
    # This dictionary contains the correspondence between
300
    # internal operating states and Server States as defined
301
    # by the Rackspace API.
302
    RSAPI_STATE_FROM_OPER_STATE = {
303
        "BUILD": "BUILD",
304
        "ERROR": "ERROR",
305
        "STOPPED": "STOPPED",
306
        "STARTED": "ACTIVE",
307
        "DESTROYED": "DELETED"
308
    }
309

    
310
    name = models.CharField('Virtual Machine Name', max_length=255)
311
    userid = models.CharField('User ID of the owner', max_length=100,
312
                              db_index=True, null=False)
313
    backend = models.ForeignKey(Backend, null=True,
314
                                related_name="virtual_machines",)
315
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
316
    created = models.DateTimeField(auto_now_add=True)
317
    updated = models.DateTimeField(auto_now=True)
318
    imageid = models.CharField(max_length=100, null=False)
319
    hostid = models.CharField(max_length=100)
320
    flavor = models.ForeignKey(Flavor)
321
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
322
    suspended = models.BooleanField('Administratively Suspended',
323
                                    default=False)
324
    serial = models.ForeignKey(QuotaHolderSerial,
325
                               related_name='virtual_machine', null=True)
326

    
327
    # VM State
328
    # The following fields are volatile data, in the sense
329
    # that they need not be persistent in the DB, but rather
330
    # get generated at runtime by quering Ganeti and applying
331
    # updates received from Ganeti.
332

    
333
    # In the future they could be moved to a separate caching layer
334
    # and removed from the database.
335
    # [vkoukis] after discussion with [faidon].
336
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
337
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
338
    backendjobid = models.PositiveIntegerField(null=True)
339
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
340
                                     null=True)
341
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
342
                                        max_length=30, null=True)
343
    backendlogmsg = models.TextField(null=True)
344
    buildpercentage = models.IntegerField(default=0)
345
    backendtime = models.DateTimeField(default=datetime.datetime.min)
346

    
347
    objects = ForUpdateManager()
348

    
349
    def get_client(self):
350
        if self.backend:
351
            return self.backend.get_client()
352
        else:
353
            raise faults.ServiceUnavailable
354

    
355
    def get_last_diagnostic(self, **filters):
356
        try:
357
            return self.diagnostics.filter()[0]
358
        except IndexError:
359
            return None
360

    
361
    @staticmethod
362
    def put_client(client):
363
            put_rapi_client(client)
364

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

    
378
    def save(self, *args, **kwargs):
379
        # Store hash for first time saved vm
380
        if (self.id is None or self.backend_hash == '') and self.backend:
381
            self.backend_hash = self.backend.hash
382
        super(VirtualMachine, self).save(*args, **kwargs)
383

    
384
    @property
385
    def backend_vm_id(self):
386
        """Returns the backend id for this VM by prepending backend-prefix."""
387
        if not self.id:
388
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
389
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
390

    
391
    class Meta:
392
        verbose_name = u'Virtual machine instance'
393
        get_latest_by = 'created'
394

    
395
    def __unicode__(self):
396
        return str(self.id)
397

    
398
    # Error classes
399
    class InvalidBackendIdError(Exception):
400
        def __init__(self, value):
401
            self.value = value
402

    
403
        def __str__(self):
404
            return repr(self.value)
405

    
406
    class InvalidBackendMsgError(Exception):
407
        def __init__(self, opcode, status):
408
            self.opcode = opcode
409
            self.status = status
410

    
411
        def __str__(self):
412
            return repr('<opcode: %s, status: %s>' % (self.opcode,
413
                        self.status))
414

    
415
    class InvalidActionError(Exception):
416
        def __init__(self, action):
417
            self._action = action
418

    
419
        def __str__(self):
420
            return repr(str(self._action))
421

    
422

    
423
class VirtualMachineMetadata(models.Model):
424
    meta_key = models.CharField(max_length=50)
425
    meta_value = models.CharField(max_length=500)
426
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
427

    
428
    class Meta:
429
        unique_together = (('meta_key', 'vm'),)
430
        verbose_name = u'Key-value pair of metadata for a VM.'
431

    
432
    def __unicode__(self):
433
        return u'%s: %s' % (self.meta_key, self.meta_value)
434

    
435

    
436
class Network(models.Model):
437
    OPER_STATES = (
438
        ('PENDING', 'Pending'),  # Unused because of lazy networks
439
        ('ACTIVE', 'Active'),
440
        ('DELETED', 'Deleted'),
441
        ('ERROR', 'Error')
442
    )
443

    
444
    ACTIONS = (
445
        ('CREATE', 'Create Network'),
446
        ('DESTROY', 'Destroy Network'),
447
    )
448

    
449
    RSAPI_STATE_FROM_OPER_STATE = {
450
        'PENDING': 'PENDING',
451
        'ACTIVE': 'ACTIVE',
452
        'DELETED': 'DELETED',
453
        'ERROR': 'ERROR'
454
    }
455

    
456
    FLAVORS = {
457
        'CUSTOM': {
458
            'mode': 'bridged',
459
            'link': settings.DEFAULT_BRIDGE,
460
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
461
            'tags': None,
462
            'desc': "Basic flavor used for a bridged network",
463
        },
464
        'IP_LESS_ROUTED': {
465
            'mode': 'routed',
466
            'link': settings.DEFAULT_ROUTING_TABLE,
467
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
468
            'tags': 'ip-less-routed',
469
            'desc': "Flavor used for an IP-less routed network using"
470
                    " Proxy ARP",
471
        },
472
        'MAC_FILTERED': {
473
            'mode': 'bridged',
474
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
475
            'mac_prefix': 'pool',
476
            'tags': 'private-filtered',
477
            'desc': "Flavor used for bridged networks that offer isolation"
478
                    " via filtering packets based on their src "
479
                    " MAC (ebtables)",
480
        },
481
        'PHYSICAL_VLAN': {
482
            'mode': 'bridged',
483
            'link': 'pool',
484
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
485
            'tags': 'physical-vlan',
486
            'desc': "Flavor used for bridged network that offer isolation"
487
                    " via dedicated physical vlan",
488
        },
489
    }
490

    
491
    name = models.CharField('Network Name', max_length=128)
492
    userid = models.CharField('User ID of the owner', max_length=128,
493
                              null=True, db_index=True)
494
    subnet = models.CharField('Subnet', max_length=32, default='10.0.0.0/24')
495
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
496
    gateway = models.CharField('Gateway', max_length=32, null=True)
497
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
498
    dhcp = models.BooleanField('DHCP', default=True)
499
    flavor = models.CharField('Flavor', max_length=32, null=False)
500
    mode = models.CharField('Network Mode', max_length=16, null=True)
501
    link = models.CharField('Network Link', max_length=32, null=True)
502
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
503
    tags = models.CharField('Network Tags', max_length=128, null=True)
504
    public = models.BooleanField(default=False, db_index=True)
505
    created = models.DateTimeField(auto_now_add=True)
506
    updated = models.DateTimeField(auto_now=True)
507
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
508
    state = models.CharField(choices=OPER_STATES, max_length=32,
509
                             default='PENDING')
510
    machines = models.ManyToManyField(VirtualMachine,
511
                                      through='NetworkInterface')
512
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
513
                              default=None)
514
    drained = models.BooleanField("Drained", default=False, null=False)
515

    
516
    pool = models.OneToOneField('IPPoolTable', related_name='network',
517
                default=lambda: IPPoolTable.objects.create(available_map='',
518
                                                           reserved_map='',
519
                                                           size=0),
520
                null=True)
521
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
522
                               null=True)
523

    
524
    objects = ForUpdateManager()
525

    
526
    def __unicode__(self):
527
        return str(self.id)
528

    
529
    @property
530
    def backend_id(self):
531
        """Return the backend id by prepending backend-prefix."""
532
        if not self.id:
533
            raise Network.InvalidBackendIdError("self.id is None")
534
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
535

    
536
    @property
537
    def backend_tag(self):
538
        """Return the network tag to be used in backend
539

540
        """
541
        if self.tags:
542
            return self.tags.split(',')
543
        else:
544
            return []
545

    
546
    def create_backend_network(self, backend=None):
547
        """Create corresponding BackendNetwork entries."""
548

    
549
        backends = [backend] if backend\
550
                             else Backend.objects.filter(offline=False)
551
        for backend in backends:
552
            backend_exists =\
553
                BackendNetwork.objects.filter(backend=backend, network=self)\
554
                                      .exists()
555
            if not backend_exists:
556
                BackendNetwork.objects.create(backend=backend, network=self)
557

    
558
    def get_pool(self):
559
        if not self.pool_id:
560
            self.pool = IPPoolTable.objects.create(available_map='',
561
                                                   reserved_map='',
562
                                                   size=0)
563
            self.save()
564
        return IPPoolTable.objects.select_for_update().get(id=self.pool_id)\
565
                                                      .pool
566

    
567
    def reserve_address(self, address):
568
        pool = self.get_pool()
569
        pool.reserve(address)
570
        pool.save()
571

    
572
    def release_address(self, address):
573
        pool = self.get_pool()
574
        pool.put(address)
575
        pool.save()
576

    
577
    class InvalidBackendIdError(Exception):
578
        def __init__(self, value):
579
            self.value = value
580

    
581
        def __str__(self):
582
            return repr(self.value)
583

    
584
    class InvalidBackendMsgError(Exception):
585
        def __init__(self, opcode, status):
586
            self.opcode = opcode
587
            self.status = status
588

    
589
        def __str__(self):
590
            return repr('<opcode: %s, status: %s>'
591
                        % (self.opcode, self.status))
592

    
593
    class InvalidActionError(Exception):
594
        def __init__(self, action):
595
            self._action = action
596

    
597
        def __str__(self):
598
            return repr(str(self._action))
599

    
600

    
601
class BackendNetwork(models.Model):
602
    OPER_STATES = (
603
        ('PENDING', 'Pending'),
604
        ('ACTIVE', 'Active'),
605
        ('DELETED', 'Deleted'),
606
        ('ERROR', 'Error')
607
    )
608

    
609
    # The list of possible operations on the backend
610
    BACKEND_OPCODES = (
611
        ('OP_NETWORK_ADD', 'Create Network'),
612
        ('OP_NETWORK_CONNECT', 'Activate Network'),
613
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
614
        ('OP_NETWORK_REMOVE', 'Remove Network'),
615
        # These are listed here for completeness,
616
        # and are ignored for the time being
617
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
618
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
619
    )
620

    
621
    # The operating state of a Netowork,
622
    # upon the successful completion of a backend operation.
623
    # IMPORTANT: Make sure all keys have a corresponding
624
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
625
    OPER_STATE_FROM_OPCODE = {
626
        'OP_NETWORK_ADD': 'PENDING',
627
        'OP_NETWORK_CONNECT': 'ACTIVE',
628
        'OP_NETWORK_DISCONNECT': 'PENDING',
629
        'OP_NETWORK_REMOVE': 'DELETED',
630
        'OP_NETWORK_SET_PARAMS': None,
631
        'OP_NETWORK_QUERY_DATA': None
632
    }
633

    
634
    network = models.ForeignKey(Network, related_name='backend_networks')
635
    backend = models.ForeignKey(Backend, related_name='networks')
636
    created = models.DateTimeField(auto_now_add=True)
637
    updated = models.DateTimeField(auto_now=True)
638
    deleted = models.BooleanField('Deleted', default=False)
639
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
640
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
641
                                 default='PENDING')
642
    backendjobid = models.PositiveIntegerField(null=True)
643
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
644
                                     null=True)
645
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
646
                                        max_length=30, null=True)
647
    backendlogmsg = models.TextField(null=True)
648
    backendtime = models.DateTimeField(null=False,
649
                                       default=datetime.datetime.min)
650

    
651
    class Meta:
652
        # Ensure one entry for each network in each backend
653
        unique_together = (("network", "backend"))
654

    
655
    def __init__(self, *args, **kwargs):
656
        """Initialize state for just created BackendNetwork instances."""
657
        super(BackendNetwork, self).__init__(*args, **kwargs)
658
        if not self.mac_prefix:
659
            # Generate the MAC prefix of the BackendNetwork, by combining
660
            # the Network prefix with the index of the Backend
661
            net_prefix = self.network.mac_prefix
662
            backend_suffix = hex(self.backend.index).replace('0x', '')
663
            mac_prefix = net_prefix + backend_suffix
664
            try:
665
                utils.validate_mac(mac_prefix + ":00:00:00")
666
            except utils.InvalidMacAddress:
667
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
668
                                              mac_prefix)
669
            self.mac_prefix = mac_prefix
670

    
671
    def __unicode__(self):
672
        return '<%s@%s>' % (self.network, self.backend)
673

    
674

    
675
class NetworkInterface(models.Model):
676
    FIREWALL_PROFILES = (
677
        ('ENABLED', 'Enabled'),
678
        ('DISABLED', 'Disabled'),
679
        ('PROTECTED', 'Protected')
680
    )
681

    
682
    STATES = (
683
        ("ACTIVE", "Active"),
684
        ("BUILDING", "Building"),
685
    )
686

    
687
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
688
    network = models.ForeignKey(Network, related_name='nics')
689
    created = models.DateTimeField(auto_now_add=True)
690
    updated = models.DateTimeField(auto_now=True)
691
    index = models.IntegerField(null=False)
692
    mac = models.CharField(max_length=32, null=True, unique=True)
693
    ipv4 = models.CharField(max_length=15, null=True)
694
    ipv6 = models.CharField(max_length=100, null=True)
695
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
696
                                        max_length=30, null=True)
697
    dirty = models.BooleanField(default=False)
698
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
699
                             choices=STATES)
700

    
701
    def __unicode__(self):
702
        return '%s@%s' % (self.machine.name, self.network.name)
703

    
704
    @property
705
    def backend_uuid(self):
706
        """Return the name of NIC in Ganeti."""
707
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
708

    
709

    
710
class PoolTable(models.Model):
711
    available_map = models.TextField(default="", null=False)
712
    reserved_map = models.TextField(default="", null=False)
713
    size = models.IntegerField(null=False)
714

    
715
    # Optional Fields
716
    base = models.CharField(null=True, max_length=32)
717
    offset = models.IntegerField(null=True)
718

    
719
    objects = ForUpdateManager()
720

    
721
    class Meta:
722
        abstract = True
723

    
724
    @classmethod
725
    def get_pool(cls):
726
        try:
727
            pool_row = cls.objects.select_for_update().get()
728
            return pool_row.pool
729
        except cls.DoesNotExist:
730
            raise pools.EmptyPool
731

    
732
    @property
733
    def pool(self):
734
        return self.manager(self)
735

    
736

    
737
class BridgePoolTable(PoolTable):
738
    manager = pools.BridgePool
739

    
740

    
741
class MacPrefixPoolTable(PoolTable):
742
    manager = pools.MacPrefixPool
743

    
744

    
745
class IPPoolTable(PoolTable):
746
    manager = pools.IPPool
747

    
748

    
749
@contextmanager
750
def pooled_rapi_client(obj):
751
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
752
            backend = obj.backend
753
        else:
754
            backend = obj
755

    
756
        if backend.offline:
757
            log.warning("Trying to connect with offline backend: %s", backend)
758
            raise faults.ServiceUnavailable
759

    
760
        b = backend
761
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
762
                                 b.username, b.password)
763
        try:
764
            yield client
765
        finally:
766
            put_rapi_client(client)
767

    
768

    
769
class VirtualMachineDiagnosticManager(models.Manager):
770
    """
771
    Custom manager for :class:`VirtualMachineDiagnostic` model.
772
    """
773

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

    
782
    def create_error(self, vm, **kwargs):
783
        self.create_for_vm(vm, 'ERROR', **kwargs)
784

    
785
    def create_debug(self, vm, **kwargs):
786
        self.create_for_vm(vm, 'DEBUG', **kwargs)
787

    
788
    def since(self, vm, created_since, **kwargs):
789
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
790
                                           **kwargs)
791

    
792

    
793
class VirtualMachineDiagnostic(models.Model):
794
    """
795
    Model to store backend information messages that relate to the state of
796
    the virtual machine.
797
    """
798

    
799
    TYPES = (
800
        ('ERROR', 'Error'),
801
        ('WARNING', 'Warning'),
802
        ('INFO', 'Info'),
803
        ('DEBUG', 'Debug'),
804
    )
805

    
806
    objects = VirtualMachineDiagnosticManager()
807

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

    
816
    class Meta:
817
        ordering = ['-created']