Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.1 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

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

    
43
from synnefo.db import pools, fields
44

    
45
from synnefo.logic.rapi_pool import (get_rapi_client,
46
                                     put_rapi_client)
47

    
48
import logging
49
log = logging.getLogger(__name__)
50

    
51

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

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

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

    
69
    def __unicode__(self):
70
        return "<%s:%s>" % (str(self.id), self.name)
71

    
72

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

    
104
    HYPERVISORS = (
105
        ("kvm", "Linux KVM hypervisor"),
106
        ("xen-pvm", "Xen PVM hypervisor"),
107
        ("xen-hvm", "Xen KVM hypervisor"),
108
    )
109

    
110
    class Meta:
111
        verbose_name = u'Backend'
112
        ordering = ["clustername"]
113

    
114
    def __unicode__(self):
115
        return self.clustername + "(id=" + str(self.id) + ")"
116

    
117
    @property
118
    def backend_id(self):
119
        return self.id
120

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

    
131
    @staticmethod
132
    def put_client(client):
133
            put_rapi_client(client)
134

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

    
141
    @property
142
    def password(self):
143
        return decrypt_db_charfield(self.password_hash)
144

    
145
    @password.setter
146
    def password(self, value):
147
        self.password_hash = encrypt_db_charfield(value)
148

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

    
159
    def __init__(self, *args, **kwargs):
160
        super(Backend, self).__init__(*args, **kwargs)
161
        if not self.pk:
162
            # Generate a unique index for the Backend
163
            indexes = Backend.objects.all().values_list('index', flat=True)
164
            try:
165
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
166
                self.index = first_free
167
            except IndexError:
168
                raise Exception("Can not create more than 16 backends")
169

    
170
    def use_hotplug(self):
171
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
172

    
173
    def get_create_params(self):
174
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
175
        params["hvparams"] = params.get("hvparams", {})\
176
                                   .get(self.hypervisor, {})
177
        return params
178

    
179

    
180
# A backend job may be in one of the following possible states
181
BACKEND_STATUSES = (
182
    ('queued', 'request queued'),
183
    ('waiting', 'request waiting for locks'),
184
    ('canceling', 'request being canceled'),
185
    ('running', 'request running'),
186
    ('canceled', 'request canceled'),
187
    ('success', 'request completed successfully'),
188
    ('error', 'request returned error')
189
)
190

    
191

    
192
class QuotaHolderSerial(models.Model):
193
    """Model representing a serial for a Quotaholder Commission.
194

195
    serial:   The serial that Quotaholder assigned to this commission
196
    pending:  Whether it has been decided to accept or reject this commission
197
    accept:   If pending is False, this attribute indicates whether to accept
198
              or reject this commission
199
    resolved: Whether this commission has been accepted or rejected to
200
              Quotaholder.
201

202
    """
203
    serial = models.BigIntegerField(null=False, primary_key=True,
204
                                    db_index=True)
205
    pending = models.BooleanField(default=True, db_index=True)
206
    accept = models.BooleanField(default=False)
207
    resolved = models.BooleanField(default=False)
208

    
209
    class Meta:
210
        verbose_name = u'Quota Serial'
211
        ordering = ["serial"]
212

    
213
    def __unicode__(self):
214
        return u"<serial: %s>" % self.serial
215

    
216

    
217
class VirtualMachine(models.Model):
218
    # The list of possible actions for a VM
219
    ACTIONS = (
220
        ('CREATE', 'Create VM'),
221
        ('START', 'Start VM'),
222
        ('STOP', 'Shutdown VM'),
223
        ('SUSPEND', 'Admin Suspend VM'),
224
        ('REBOOT', 'Reboot VM'),
225
        ('DESTROY', 'Destroy VM'),
226
        ('RESIZE', 'Resize a VM'),
227
        ('ADDFLOATINGIP', 'Add floating IP to VM'),
228
        ('REMOVEFLOATINGIP', 'Add floating IP to VM'),
229
    )
230

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

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

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

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

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

    
297
    name = models.CharField('Virtual Machine Name', max_length=255)
298
    userid = models.CharField('User ID of the owner', max_length=100,
299
                              db_index=True, null=False)
300
    backend = models.ForeignKey(Backend, null=True,
301
                                related_name="virtual_machines",
302
                                on_delete=models.PROTECT)
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, on_delete=models.PROTECT)
309
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
310
    suspended = models.BooleanField('Administratively Suspended',
311
                                    default=False)
312
    serial = models.ForeignKey(QuotaHolderSerial,
313
                               related_name='virtual_machine', null=True,
314
                               on_delete=models.SET_NULL)
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
                              default=None)
327
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
328
                                 null=False, default="BUILD")
329
    backendjobid = models.PositiveIntegerField(null=True)
330
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
331
                                     null=True)
332
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
333
                                        max_length=30, null=True)
334
    backendlogmsg = models.TextField(null=True)
335
    buildpercentage = models.IntegerField(default=0)
336
    backendtime = models.DateTimeField(default=datetime.datetime.min)
337

    
338
    # Latest action and corresponding Ganeti job ID, for actions issued
339
    # by the API
340
    task = models.CharField(max_length=64, null=True)
341
    task_job_id = models.BigIntegerField(null=True)
342

    
343
    def get_client(self):
344
        if self.backend:
345
            return self.backend.get_client()
346
        else:
347
            raise faults.ServiceUnavailable
348

    
349
    def get_last_diagnostic(self, **filters):
350
        try:
351
            return self.diagnostics.filter()[0]
352
        except IndexError:
353
            return None
354

    
355
    @staticmethod
356
    def put_client(client):
357
            put_rapi_client(client)
358

    
359
    def save(self, *args, **kwargs):
360
        # Store hash for first time saved vm
361
        if (self.id is None or self.backend_hash == '') and self.backend:
362
            self.backend_hash = self.backend.hash
363
        super(VirtualMachine, self).save(*args, **kwargs)
364

    
365
    @property
366
    def backend_vm_id(self):
367
        """Returns the backend id for this VM by prepending backend-prefix."""
368
        if not self.id:
369
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
370
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
371

    
372
    class Meta:
373
        verbose_name = u'Virtual machine instance'
374
        get_latest_by = 'created'
375

    
376
    def __unicode__(self):
377
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
378

    
379
    # Error classes
380
    class InvalidBackendIdError(Exception):
381
        def __init__(self, value):
382
            self.value = value
383

    
384
        def __str__(self):
385
            return repr(self.value)
386

    
387
    class InvalidBackendMsgError(Exception):
388
        def __init__(self, opcode, status):
389
            self.opcode = opcode
390
            self.status = status
391

    
392
        def __str__(self):
393
            return repr('<opcode: %s, status: %s>' % (self.opcode,
394
                        self.status))
395

    
396
    class InvalidActionError(Exception):
397
        def __init__(self, action):
398
            self._action = action
399

    
400
        def __str__(self):
401
            return repr(str(self._action))
402

    
403

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

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

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

    
417

    
418
class Network(models.Model):
419
    OPER_STATES = (
420
        ('PENDING', 'Pending'),  # Unused because of lazy networks
421
        ('ACTIVE', 'Active'),
422
        ('DELETED', 'Deleted'),
423
        ('ERROR', 'Error')
424
    )
425

    
426
    ACTIONS = (
427
        ('CREATE', 'Create Network'),
428
        ('DESTROY', 'Destroy Network'),
429
        ('ADD', 'Add server to Network'),
430
        ('REMOVE', 'Remove server from Network'),
431
    )
432

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

    
440
    FLAVORS = {
441
        'CUSTOM': {
442
            'mode': 'bridged',
443
            'link': settings.DEFAULT_BRIDGE,
444
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
445
            'tags': None,
446
            'desc': "Basic flavor used for a bridged network",
447
        },
448
        'IP_LESS_ROUTED': {
449
            'mode': 'routed',
450
            'link': settings.DEFAULT_ROUTING_TABLE,
451
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
452
            'tags': 'ip-less-routed',
453
            'desc': "Flavor used for an IP-less routed network using"
454
                    " Proxy ARP",
455
        },
456
        'MAC_FILTERED': {
457
            'mode': 'bridged',
458
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
459
            'mac_prefix': 'pool',
460
            'tags': 'private-filtered',
461
            'desc': "Flavor used for bridged networks that offer isolation"
462
                    " via filtering packets based on their src "
463
                    " MAC (ebtables)",
464
        },
465
        'PHYSICAL_VLAN': {
466
            'mode': 'bridged',
467
            'link': 'pool',
468
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
469
            'tags': 'physical-vlan',
470
            'desc': "Flavor used for bridged network that offer isolation"
471
                    " via dedicated physical vlan",
472
        },
473
    }
474

    
475
    name = models.CharField('Network Name', max_length=128)
476
    userid = models.CharField('User ID of the owner', max_length=128,
477
                              null=True, db_index=True)
478
    # subnet will be null for IPv6 only networks
479
    subnet = models.CharField('Subnet', max_length=32, null=True)
480
    # subnet6 will be null for IPv4 only networks
481
    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
482
    gateway = models.CharField('Gateway', max_length=32, null=True)
483
    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
484
    dhcp = models.BooleanField('DHCP', default=True)
485
    flavor = models.CharField('Flavor', max_length=32, null=False)
486
    mode = models.CharField('Network Mode', max_length=16, null=True)
487
    link = models.CharField('Network Link', max_length=32, null=True)
488
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
489
    tags = models.CharField('Network Tags', max_length=128, null=True)
490
    public = models.BooleanField(default=False, db_index=True)
491
    created = models.DateTimeField(auto_now_add=True)
492
    updated = models.DateTimeField(auto_now=True)
493
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
494
    state = models.CharField(choices=OPER_STATES, max_length=32,
495
                             default='PENDING')
496
    machines = models.ManyToManyField(VirtualMachine,
497
                                      through='NetworkInterface')
498
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
499
                              default=None)
500
    drained = models.BooleanField("Drained", default=False, null=False)
501
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
502
                                           default=False)
503
    pool = models.OneToOneField('IPPoolTable', related_name='network',
504
                                default=lambda: IPPoolTable.objects.create(
505
                                                            available_map='',
506
                                                            reserved_map='',
507
                                                            size=0),
508
                                null=True)
509
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
510
                               null=True, on_delete=models.SET_NULL)
511

    
512
    def __unicode__(self):
513
        return "<Network: %s>" % str(self.id)
514

    
515
    @property
516
    def backend_id(self):
517
        """Return the backend id by prepending backend-prefix."""
518
        if not self.id:
519
            raise Network.InvalidBackendIdError("self.id is None")
520
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
521

    
522
    @property
523
    def backend_tag(self):
524
        """Return the network tag to be used in backend
525

526
        """
527
        if self.tags:
528
            return self.tags.split(',')
529
        else:
530
            return []
531

    
532
    def create_backend_network(self, backend=None):
533
        """Create corresponding BackendNetwork entries."""
534

    
535
        backends = [backend] if backend else\
536
            Backend.objects.filter(offline=False)
537
        for backend in backends:
538
            backend_exists =\
539
                BackendNetwork.objects.filter(backend=backend, network=self)\
540
                                      .exists()
541
            if not backend_exists:
542
                BackendNetwork.objects.create(backend=backend, network=self)
543

    
544
    def get_pool(self, with_lock=True):
545
        if not self.pool_id:
546
            self.pool = IPPoolTable.objects.create(available_map='',
547
                                                   reserved_map='',
548
                                                   size=0)
549
            self.save()
550
        objects = IPPoolTable.objects
551
        if with_lock:
552
            objects = objects.select_for_update()
553
        return objects.get(id=self.pool_id).pool
554

    
555
    def reserve_address(self, address):
556
        pool = self.get_pool()
557
        pool.reserve(address)
558
        pool.save()
559

    
560
    def release_address(self, address):
561
        pool = self.get_pool()
562
        pool.put(address)
563
        pool.save()
564

    
565
    class InvalidBackendIdError(Exception):
566
        def __init__(self, value):
567
            self.value = value
568

    
569
        def __str__(self):
570
            return repr(self.value)
571

    
572
    class InvalidBackendMsgError(Exception):
573
        def __init__(self, opcode, status):
574
            self.opcode = opcode
575
            self.status = status
576

    
577
        def __str__(self):
578
            return repr('<opcode: %s, status: %s>'
579
                        % (self.opcode, self.status))
580

    
581
    class InvalidActionError(Exception):
582
        def __init__(self, action):
583
            self._action = action
584

    
585
        def __str__(self):
586
            return repr(str(self._action))
587

    
588

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

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

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

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

    
641
    class Meta:
642
        # Ensure one entry for each network in each backend
643
        unique_together = (("network", "backend"))
644

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

    
661
    def __unicode__(self):
662
        return '<%s@%s>' % (self.network, self.backend)
663

    
664

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

    
672
    STATES = (
673
        ("ACTIVE", "Active"),
674
        ("BUILDING", "Building"),
675
        ("ERROR", "Error"),
676
    )
677

    
678
    IP_TYPES = (
679
        ("FIXED", "Fixed IP Address"),
680
        ("FLOATING", "Floating IP Address"),
681
    )
682

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

    
700
    @property
701
    def backend_uuid(self):
702
        """Return the backend id by prepending backend-prefix."""
703
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
704

    
705
    def __unicode__(self):
706
        return "<%s:vm:%s network:%s ipv4:%s ipv6:%s>" % \
707
            (self.id, self.machine_id, self.network_id, self.ipv4,
708
             self.ipv6)
709

    
710
    class Meta:
711
        # Assert than an IPv4 address from the same network will not be
712
        # assigned to more than one NICs
713
        unique_together = ("network", "ipv4")
714

    
715
    def delete(self):
716
        """Custom method for deleting NetworkInterfaces.
717

718
        In case the NIC is of 'FLOATING' type, this method clears the 'machine'
719
        flag of the FloatingIP object, before deleting the NIC.
720

721
        """
722
        if self.ip_type == "FLOATING":
723
            FloatingIP.objects.filter(machine=self.machine_id,
724
                                      network=self.network_id,
725
                                      ipv4=self.ipv4).update(machine=None)
726
        super(NetworkInterface, self).delete()
727

    
728

    
729
class FloatingIP(models.Model):
730
    userid = models.CharField("UUID of the owner", max_length=128,
731
                              null=False, db_index=True)
732
    ipv4 = models.IPAddressField(null=False, unique=True, db_index=True)
733
    network = models.ForeignKey(Network, related_name="floating_ips",
734
                                null=False, on_delete=models.CASCADE)
735
    machine = models.ForeignKey(VirtualMachine, related_name="floating_ips",
736
                                null=True, on_delete=models.CASCADE)
737
    created = models.DateTimeField(auto_now_add=True)
738
    deleted = models.BooleanField(default=False, null=False)
739
    serial = models.ForeignKey(QuotaHolderSerial,
740
                               related_name="floating_ips", null=True,
741
                               on_delete=models.SET_NULL)
742

    
743
    def __unicode__(self):
744
        return "<FIP: %s@%s>" % (self.ipv4, self.network.id)
745

    
746
    def in_use(self):
747
        if self.machine is None:
748
            return False
749
        else:
750
            return (not self.machine.deleted)
751

    
752

    
753
class PoolTable(models.Model):
754
    available_map = models.TextField(default="", null=False)
755
    reserved_map = models.TextField(default="", null=False)
756
    size = models.IntegerField(null=False)
757

    
758
    # Optional Fields
759
    base = models.CharField(null=True, max_length=32)
760
    offset = models.IntegerField(null=True)
761

    
762
    class Meta:
763
        abstract = True
764

    
765
    @classmethod
766
    def get_pool(cls):
767
        try:
768
            pool_row = cls.objects.select_for_update().get()
769
            return pool_row.pool
770
        except cls.DoesNotExist:
771
            raise pools.EmptyPool
772

    
773
    @property
774
    def pool(self):
775
        return self.manager(self)
776

    
777

    
778
class BridgePoolTable(PoolTable):
779
    manager = pools.BridgePool
780

    
781
    def __unicode__(self):
782
        return u"<BridgePool id:%s>" % self.id
783

    
784

    
785
class MacPrefixPoolTable(PoolTable):
786
    manager = pools.MacPrefixPool
787

    
788
    def __unicode__(self):
789
        return u"<MACPrefixPool id:%s>" % self.id
790

    
791

    
792
class IPPoolTable(PoolTable):
793
    manager = pools.IPPool
794

    
795
    def __unicode__(self):
796
        return u"<IPv4AdressPool, network: %s>" % self.network
797

    
798

    
799
@contextmanager
800
def pooled_rapi_client(obj):
801
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
802
            backend = obj.backend
803
        else:
804
            backend = obj
805

    
806
        if backend.offline:
807
            log.warning("Trying to connect with offline backend: %s", backend)
808
            raise faults.ServiceUnavailable("Can not connect to offline"
809
                                            " backend: %s" % backend)
810

    
811
        b = backend
812
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
813
                                 b.username, b.password)
814
        try:
815
            yield client
816
        finally:
817
            put_rapi_client(client)
818

    
819

    
820
class VirtualMachineDiagnosticManager(models.Manager):
821
    """
822
    Custom manager for :class:`VirtualMachineDiagnostic` model.
823
    """
824

    
825
    # diagnostic creation helpers
826
    def create_for_vm(self, vm, level, message, **kwargs):
827
        attrs = {'machine': vm, 'level': level, 'message': message}
828
        attrs.update(kwargs)
829
        # update instance updated time
830
        self.create(**attrs)
831
        vm.save()
832

    
833
    def create_error(self, vm, **kwargs):
834
        self.create_for_vm(vm, 'ERROR', **kwargs)
835

    
836
    def create_debug(self, vm, **kwargs):
837
        self.create_for_vm(vm, 'DEBUG', **kwargs)
838

    
839
    def since(self, vm, created_since, **kwargs):
840
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
841
                                           **kwargs)
842

    
843

    
844
class VirtualMachineDiagnostic(models.Model):
845
    """
846
    Model to store backend information messages that relate to the state of
847
    the virtual machine.
848
    """
849

    
850
    TYPES = (
851
        ('ERROR', 'Error'),
852
        ('WARNING', 'Warning'),
853
        ('INFO', 'Info'),
854
        ('DEBUG', 'Debug'),
855
    )
856

    
857
    objects = VirtualMachineDiagnosticManager()
858

    
859
    created = models.DateTimeField(auto_now_add=True)
860
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics",
861
                                on_delete=models.CASCADE)
862
    level = models.CharField(max_length=20, choices=TYPES)
863
    source = models.CharField(max_length=100)
864
    source_date = models.DateTimeField(null=True)
865
    message = models.CharField(max_length=255)
866
    details = models.TextField(null=True)
867

    
868
    class Meta:
869
        ordering = ['-created']