Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (36.3 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("Backend '%s' is offline" %
125
                                            self)
126
        return get_rapi_client(self.id, self.hash,
127
                               self.clustername,
128
                               self.port,
129
                               self.username,
130
                               self.password)
131

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

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

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

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

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

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

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

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

    
180

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

    
192

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

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

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

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

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

    
217

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

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

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

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

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

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

    
298
    VIRTUAL_MACHINE_NAME_LENGTH = 255
299

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

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

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

    
342
    # Latest action and corresponding Ganeti job ID, for actions issued
343
    # by the API
344
    task = models.CharField(max_length=64, null=True)
345
    task_job_id = models.BigIntegerField(null=True)
346

    
347
    def get_client(self):
348
        if self.backend:
349
            return self.backend.get_client()
350
        else:
351
            raise faults.ServiceUnavailable("VirtualMachine without backend")
352

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

    
359
    @staticmethod
360
    def put_client(client):
361
            put_rapi_client(client)
362

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

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

    
376
    class Meta:
377
        verbose_name = u'Virtual machine instance'
378
        get_latest_by = 'created'
379

    
380
    def __unicode__(self):
381
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
382

    
383
    # Error classes
384
    class InvalidBackendIdError(Exception):
385
        def __init__(self, value):
386
            self.value = value
387

    
388
        def __str__(self):
389
            return repr(self.value)
390

    
391
    class InvalidBackendMsgError(Exception):
392
        def __init__(self, opcode, status):
393
            self.opcode = opcode
394
            self.status = status
395

    
396
        def __str__(self):
397
            return repr('<opcode: %s, status: %s>' % (self.opcode,
398
                        self.status))
399

    
400
    class InvalidActionError(Exception):
401
        def __init__(self, action):
402
            self._action = action
403

    
404
        def __str__(self):
405
            return repr(str(self._action))
406

    
407

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

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

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

    
421

    
422
class Network(models.Model):
423
    OPER_STATES = (
424
        ('PENDING', 'Pending'),  # Unused because of lazy networks
425
        ('ACTIVE', 'Active'),
426
        ('DELETED', 'Deleted'),
427
        ('ERROR', 'Error')
428
    )
429

    
430
    ACTIONS = (
431
        ('CREATE', 'Create Network'),
432
        ('DESTROY', 'Destroy Network'),
433
        ('ADD', 'Add server to Network'),
434
        ('REMOVE', 'Remove server from Network'),
435
    )
436

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

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

    
479
    NETWORK_NAME_LENGTH = 128
480

    
481
    name = models.CharField('Network Name', max_length=NETWORK_NAME_LENGTH)
482
    userid = models.CharField('User ID of the owner', max_length=128,
483
                              null=True, db_index=True)
484
    flavor = models.CharField('Flavor', max_length=32, null=False)
485
    mode = models.CharField('Network Mode', max_length=16, null=True)
486
    link = models.CharField('Network Link', max_length=32, null=True)
487
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
488
    tags = models.CharField('Network Tags', max_length=128, null=True)
489
    public = models.BooleanField(default=False, db_index=True)
490
    created = models.DateTimeField(auto_now_add=True)
491
    updated = models.DateTimeField(auto_now=True)
492
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
493
    state = models.CharField(choices=OPER_STATES, max_length=32,
494
                             default='PENDING')
495
    machines = models.ManyToManyField(VirtualMachine,
496
                                      through='NetworkInterface')
497
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
498
                              default=None)
499
    drained = models.BooleanField("Drained", default=False, null=False)
500
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
501
                                           default=False)
502
    external_router = models.BooleanField(default=False)
503
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
504
                               null=True, on_delete=models.SET_NULL)
505

    
506
    def __unicode__(self):
507
        return "<Network: %s>" % str(self.id)
508

    
509
    @property
510
    def backend_id(self):
511
        """Return the backend id by prepending backend-prefix."""
512
        if not self.id:
513
            raise Network.InvalidBackendIdError("self.id is None")
514
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
515

    
516
    @property
517
    def backend_tag(self):
518
        """Return the network tag to be used in backend
519

520
        """
521
        if self.tags:
522
            return self.tags.split(',')
523
        else:
524
            return []
525

    
526
    def create_backend_network(self, backend=None):
527
        """Create corresponding BackendNetwork entries."""
528

    
529
        backends = [backend] if backend else\
530
            Backend.objects.filter(offline=False)
531
        for backend in backends:
532
            backend_exists =\
533
                BackendNetwork.objects.filter(backend=backend, network=self)\
534
                                      .exists()
535
            if not backend_exists:
536
                BackendNetwork.objects.create(backend=backend, network=self)
537

    
538
    def get_ip_pools(self, locked=True):
539
        subnets = self.subnets.filter(ipversion=4, deleted=False)\
540
                              .prefetch_related("ip_pools")
541
        return [ip_pool for subnet in subnets
542
                for ip_pool in subnet.get_ip_pools(locked=locked)]
543

    
544
    def reserve_address(self, address, external=False):
545
        for ip_pool in self.get_ip_pools():
546
            if ip_pool.contains(address):
547
                ip_pool.reserve(address, external=external)
548
                ip_pool.save()
549
                return
550
        raise pools.InvalidValue("Network %s does not have an IP pool that"
551
                                 " contains address %s" % (self, address))
552

    
553
    def release_address(self, address, external=False):
554
        for ip_pool in self.get_ip_pools():
555
            if ip_pool.contains(address):
556
                ip_pool.put(address, external=external)
557
                ip_pool.save()
558
                return
559
        raise pools.InvalidValue("Network %s does not have an IP pool that"
560
                                 " contains address %s" % (self, address))
561

    
562
    @property
563
    def subnet4(self):
564
        return self.get_subnet(version=4)
565

    
566
    @property
567
    def subnet6(self):
568
        return self.get_subnet(version=6)
569

    
570
    def get_subnet(self, version=4):
571
        for subnet in self.subnets.all():
572
            if subnet.ipversion == version:
573
                return subnet
574
        return None
575

    
576
    def ip_count(self):
577
        """Return the total and free IPv4 addresses of the network."""
578
        total, free = 0, 0
579
        ip_pools = self.get_ip_pools(locked=False)
580
        for ip_pool in ip_pools:
581
            total += ip_pool.pool_size
582
            free += ip_pool.count_available()
583
        return total, free
584

    
585
    class InvalidBackendIdError(Exception):
586
        def __init__(self, value):
587
            self.value = value
588

    
589
        def __str__(self):
590
            return repr(self.value)
591

    
592
    class InvalidBackendMsgError(Exception):
593
        def __init__(self, opcode, status):
594
            self.opcode = opcode
595
            self.status = status
596

    
597
        def __str__(self):
598
            return repr('<opcode: %s, status: %s>'
599
                        % (self.opcode, self.status))
600

    
601
    class InvalidActionError(Exception):
602
        def __init__(self, action):
603
            self._action = action
604

    
605
        def __str__(self):
606
            return repr(str(self._action))
607

    
608

    
609
class Subnet(models.Model):
610
    SUBNET_NAME_LENGTH = 128
611

    
612
    network = models.ForeignKey('Network', null=False, db_index=True,
613
                                related_name="subnets",
614
                                on_delete=models.PROTECT)
615
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
616
                            null=True, default="")
617
    ipversion = models.IntegerField('IP Version', default=4, null=False)
618
    cidr = models.CharField('Subnet', max_length=64, null=False)
619
    gateway = models.CharField('Gateway', max_length=64, null=True)
620
    dhcp = models.BooleanField('DHCP', default=True, null=False)
621
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
622
                                  null=False)
623
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
624
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
625
    created = models.DateTimeField(auto_now_add=True)
626
    updated = models.DateTimeField(auto_now=True)
627

    
628
    def __unicode__(self):
629
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
630
        return msg % (self.id, self.network_id, self.cidr)
631

    
632
    def get_ip_pools(self, locked=True):
633
        ip_pools = self.ip_pools
634
        if locked:
635
            ip_pools = ip_pools.select_for_update()
636
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
637

    
638

    
639
class BackendNetwork(models.Model):
640
    OPER_STATES = (
641
        ('PENDING', 'Pending'),
642
        ('ACTIVE', 'Active'),
643
        ('DELETED', 'Deleted'),
644
        ('ERROR', 'Error')
645
    )
646

    
647
    # The list of possible operations on the backend
648
    BACKEND_OPCODES = (
649
        ('OP_NETWORK_ADD', 'Create Network'),
650
        ('OP_NETWORK_CONNECT', 'Activate Network'),
651
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
652
        ('OP_NETWORK_REMOVE', 'Remove Network'),
653
        # These are listed here for completeness,
654
        # and are ignored for the time being
655
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
656
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
657
    )
658

    
659
    # The operating state of a Netowork,
660
    # upon the successful completion of a backend operation.
661
    # IMPORTANT: Make sure all keys have a corresponding
662
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
663
    OPER_STATE_FROM_OPCODE = {
664
        'OP_NETWORK_ADD': 'PENDING',
665
        'OP_NETWORK_CONNECT': 'ACTIVE',
666
        'OP_NETWORK_DISCONNECT': 'PENDING',
667
        'OP_NETWORK_REMOVE': 'DELETED',
668
        'OP_NETWORK_SET_PARAMS': None,
669
        'OP_NETWORK_QUERY_DATA': None
670
    }
671

    
672
    network = models.ForeignKey(Network, related_name='backend_networks',
673
                                on_delete=models.PROTECT)
674
    backend = models.ForeignKey(Backend, related_name='networks',
675
                                on_delete=models.PROTECT)
676
    created = models.DateTimeField(auto_now_add=True)
677
    updated = models.DateTimeField(auto_now=True)
678
    deleted = models.BooleanField('Deleted', default=False)
679
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
680
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
681
                                 default='PENDING')
682
    backendjobid = models.PositiveIntegerField(null=True)
683
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
684
                                     null=True)
685
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
686
                                        max_length=30, null=True)
687
    backendlogmsg = models.TextField(null=True)
688
    backendtime = models.DateTimeField(null=False,
689
                                       default=datetime.datetime.min)
690

    
691
    class Meta:
692
        # Ensure one entry for each network in each backend
693
        unique_together = (("network", "backend"))
694

    
695
    def __init__(self, *args, **kwargs):
696
        """Initialize state for just created BackendNetwork instances."""
697
        super(BackendNetwork, self).__init__(*args, **kwargs)
698
        if not self.mac_prefix:
699
            # Generate the MAC prefix of the BackendNetwork, by combining
700
            # the Network prefix with the index of the Backend
701
            net_prefix = self.network.mac_prefix
702
            backend_suffix = hex(self.backend.index).replace('0x', '')
703
            mac_prefix = net_prefix + backend_suffix
704
            try:
705
                utils.validate_mac(mac_prefix + ":00:00:00")
706
            except utils.InvalidMacAddress:
707
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
708
                                              mac_prefix)
709
            self.mac_prefix = mac_prefix
710

    
711
    def __unicode__(self):
712
        return '<%s@%s>' % (self.network, self.backend)
713

    
714

    
715
class IPAddress(models.Model):
716
    subnet = models.ForeignKey("Subnet", related_name="ips", null=False,
717
                               on_delete=models.PROTECT)
718
    network = models.ForeignKey(Network, related_name="ips", null=False,
719
                                on_delete=models.PROTECT)
720
    nic = models.ForeignKey("NetworkInterface", related_name="ips", null=True,
721
                            on_delete=models.SET_NULL)
722
    userid = models.CharField("UUID of the owner", max_length=128, null=False,
723
                              db_index=True)
724
    address = models.CharField("IP Address", max_length=64, null=False)
725
    floating_ip = models.BooleanField("Floating IP", null=False, default=False)
726
    created = models.DateTimeField(auto_now_add=True)
727
    updated = models.DateTimeField(auto_now=True)
728
    deleted = models.BooleanField(default=False, null=False)
729

    
730
    serial = models.ForeignKey(QuotaHolderSerial,
731
                               related_name="ips", null=True,
732
                               on_delete=models.SET_NULL)
733

    
734
    def __unicode__(self):
735
        ip_type = "floating" if self.floating_ip else "static"
736
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
737
               % (self.address, self.network_id, self.subnet_id, ip_type)
738

    
739
    def in_use(self):
740
        if self.nic is None or self.nic.machine is None:
741
            return False
742
        else:
743
            return (not self.nic.machine.deleted)
744

    
745
    class Meta:
746
        unique_together = ("network", "address", "deleted")
747

    
748
    @property
749
    def ipversion(self):
750
        return self.subnet.ipversion
751

    
752
    @property
753
    def public(self):
754
        return self.network.public
755

    
756
    def release_address(self):
757
        """Release the IPv4 address."""
758
        if self.ipversion == 4:
759
            for pool_row in self.subnet.ip_pools.all():
760
                ip_pool = pool_row.pool
761
                if ip_pool.contains(self.address):
762
                    ip_pool.put(self.address)
763
                    ip_pool.save()
764
                    return
765
            log.error("Cannot release address %s of NIC %s. Address does not"
766
                      " belong to any of the IP pools of the subnet %s !",
767
                      self.address, self.nic, self.subnet_id)
768

    
769

    
770
class IPAddressLog(models.Model):
771
    address = models.CharField("IP Address", max_length=64, null=False,
772
                               db_index=True)
773
    server_id = models.IntegerField("Server", null=False)
774
    network_id = models.IntegerField("Network", null=False)
775
    allocated_at = models.DateTimeField("Datetime IP allocated to server",
776
                                        auto_now_add=True)
777
    released_at = models.DateTimeField("Datetime IP released from server",
778
                                       null=True)
779
    active = models.BooleanField("Whether IP still allocated to server",
780
                                 default=True)
781

    
782
    def __unicode__(self):
783
        return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\
784
               % (self.address, self.network_id, self.server_id,
785
                  self.allocated_at)
786

    
787

    
788
class NetworkInterface(models.Model):
789
    FIREWALL_PROFILES = (
790
        ('ENABLED', 'Enabled'),
791
        ('DISABLED', 'Disabled'),
792
        ('PROTECTED', 'Protected')
793
    )
794

    
795
    STATES = (
796
        ("ACTIVE", "Active"),
797
        ("BUILD", "Building"),
798
        ("ERROR", "Error"),
799
        ("DOWN", "Down"),
800
    )
801

    
802
    NETWORK_IFACE_NAME_LENGTH = 128
803

    
804
    name = models.CharField('NIC name', max_length=NETWORK_IFACE_NAME_LENGTH,
805
                            null=True, default="")
806
    userid = models.CharField("UUID of the owner", max_length=128,
807
                              null=False, db_index=True)
808
    machine = models.ForeignKey(VirtualMachine, related_name='nics',
809
                                on_delete=models.PROTECT, null=True)
810
    network = models.ForeignKey(Network, related_name='nics',
811
                                on_delete=models.PROTECT)
812
    created = models.DateTimeField(auto_now_add=True)
813
    updated = models.DateTimeField(auto_now=True)
814
    index = models.IntegerField(null=True)
815
    mac = models.CharField(max_length=32, null=True, unique=True)
816
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
817
                                        max_length=30, null=True)
818
    security_groups = models.ManyToManyField("SecurityGroup", null=True)
819
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
820
                             choices=STATES)
821
    device_owner = models.CharField('Device owner', max_length=128, null=True)
822

    
823
    def __unicode__(self):
824
        return "<%s:vm:%s network:%s>" % (self.id, self.machine_id,
825
                                          self.network_id)
826

    
827
    @property
828
    def backend_uuid(self):
829
        """Return the backend id by prepending backend-prefix."""
830
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
831

    
832
    @property
833
    def ipv4_address(self):
834
        return self.get_ip_address(version=4)
835

    
836
    @property
837
    def ipv6_address(self):
838
        return self.get_ip_address(version=6)
839

    
840
    def get_ip_address(self, version=4):
841
        for ip in self.ips.all():
842
            if ip.subnet.ipversion == version:
843
                return ip.address
844
        return None
845

    
846
    def get_ip_addresses_subnets(self):
847
        return self.ips.values_list("address", "subnet__id")
848

    
849

    
850
class SecurityGroup(models.Model):
851
    SECURITY_GROUP_NAME_LENGTH = 128
852
    name = models.CharField('group name',
853
                            max_length=SECURITY_GROUP_NAME_LENGTH)
854

    
855
    @property
856
    def backend_uuid(self):
857
        """Return the name of NIC in Ganeti."""
858
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
859

    
860

    
861
class PoolTable(models.Model):
862
    available_map = models.TextField(default="", null=False)
863
    reserved_map = models.TextField(default="", null=False)
864
    size = models.IntegerField(null=False)
865

    
866
    # Optional Fields
867
    base = models.CharField(null=True, max_length=32)
868
    offset = models.IntegerField(null=True)
869

    
870
    class Meta:
871
        abstract = True
872

    
873
    @classmethod
874
    def get_pool(cls):
875
        try:
876
            pool_row = cls.objects.select_for_update().get()
877
            return pool_row.pool
878
        except cls.DoesNotExist:
879
            raise pools.EmptyPool
880

    
881
    @property
882
    def pool(self):
883
        return self.manager(self)
884

    
885

    
886
class BridgePoolTable(PoolTable):
887
    manager = pools.BridgePool
888

    
889
    def __unicode__(self):
890
        return u"<BridgePool id:%s>" % self.id
891

    
892

    
893
class MacPrefixPoolTable(PoolTable):
894
    manager = pools.MacPrefixPool
895

    
896
    def __unicode__(self):
897
        return u"<MACPrefixPool id:%s>" % self.id
898

    
899

    
900
class IPPoolTable(PoolTable):
901
    manager = pools.IPPool
902

    
903
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
904
                               on_delete=models.PROTECT,
905
                               db_index=True, null=True)
906

    
907
    def __unicode__(self):
908
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
909

    
910

    
911
@contextmanager
912
def pooled_rapi_client(obj):
913
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
914
            backend = obj.backend
915
        else:
916
            backend = obj
917

    
918
        if backend.offline:
919
            log.warning("Trying to connect with offline backend: %s", backend)
920
            raise faults.ServiceUnavailable("Cannot connect to offline"
921
                                            " backend: %s" % backend)
922

    
923
        b = backend
924
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
925
                                 b.username, b.password)
926
        try:
927
            yield client
928
        finally:
929
            put_rapi_client(client)
930

    
931

    
932
class VirtualMachineDiagnosticManager(models.Manager):
933
    """
934
    Custom manager for :class:`VirtualMachineDiagnostic` model.
935
    """
936

    
937
    # diagnostic creation helpers
938
    def create_for_vm(self, vm, level, message, **kwargs):
939
        attrs = {'machine': vm, 'level': level, 'message': message}
940
        attrs.update(kwargs)
941
        # update instance updated time
942
        self.create(**attrs)
943
        vm.save()
944

    
945
    def create_error(self, vm, **kwargs):
946
        self.create_for_vm(vm, 'ERROR', **kwargs)
947

    
948
    def create_debug(self, vm, **kwargs):
949
        self.create_for_vm(vm, 'DEBUG', **kwargs)
950

    
951
    def since(self, vm, created_since, **kwargs):
952
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
953
                                           **kwargs)
954

    
955

    
956
class VirtualMachineDiagnostic(models.Model):
957
    """
958
    Model to store backend information messages that relate to the state of
959
    the virtual machine.
960
    """
961

    
962
    TYPES = (
963
        ('ERROR', 'Error'),
964
        ('WARNING', 'Warning'),
965
        ('INFO', 'Info'),
966
        ('DEBUG', 'Debug'),
967
    )
968

    
969
    objects = VirtualMachineDiagnosticManager()
970

    
971
    created = models.DateTimeField(auto_now_add=True)
972
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics",
973
                                on_delete=models.CASCADE)
974
    level = models.CharField(max_length=20, choices=TYPES)
975
    source = models.CharField(max_length=100)
976
    source_date = models.DateTimeField(null=True)
977
    message = models.CharField(max_length=255)
978
    details = models.TextField(null=True)
979

    
980
    class Meta:
981
        ordering = ['-created']