Statistics
| Branch: | Tag: | Revision:

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

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

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

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

    
636

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

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

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

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

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

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

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

    
712

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

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

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

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

    
743
    class Meta:
744
        unique_together = ("network", "address", "deleted")
745

    
746
    @property
747
    def ipversion(self):
748
        return self.subnet.ipversion
749

    
750
    @property
751
    def public(self):
752
        return self.network.public
753

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

    
767

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

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

    
785

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

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

    
800
    NETWORK_IFACE_NAME_LENGTH = 128
801

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

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

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

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

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

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

    
844
    def get_ip_addresses_subnets(self):
845
        return self.ips.values_list("address", "subnet__id")
846

    
847

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

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

    
858

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

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

    
868
    class Meta:
869
        abstract = True
870

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

    
879
    @property
880
    def pool(self):
881
        return self.manager(self)
882

    
883

    
884
class BridgePoolTable(PoolTable):
885
    manager = pools.BridgePool
886

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

    
890

    
891
class MacPrefixPoolTable(PoolTable):
892
    manager = pools.MacPrefixPool
893

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

    
897

    
898
class IPPoolTable(PoolTable):
899
    manager = pools.IPPool
900

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

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

    
908

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

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

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

    
929

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

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

    
943
    def create_error(self, vm, **kwargs):
944
        self.create_for_vm(vm, 'ERROR', **kwargs)
945

    
946
    def create_debug(self, vm, **kwargs):
947
        self.create_for_vm(vm, 'DEBUG', **kwargs)
948

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

    
953

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

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

    
967
    objects = VirtualMachineDiagnosticManager()
968

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

    
978
    class Meta:
979
        ordering = ['-created']