Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (36.8 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
    # Whether the flavor can be used to create new servers
59
    allow_create = models.BooleanField(default=True, null=False)
60

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

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

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

    
74

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
182

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

    
194

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

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

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

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

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

    
219

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

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

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

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

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

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

    
300
    VIRTUAL_MACHINE_NAME_LENGTH = 255
301

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

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

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

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

    
350
    def get_client(self):
351
        if self.backend:
352
            return self.backend.get_client()
353
        else:
354
            raise faults.ServiceUnavailable("VirtualMachine without backend")
355

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

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

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

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

    
379
    class Meta:
380
        verbose_name = u'Virtual machine instance'
381
        get_latest_by = 'created'
382

    
383
    def __unicode__(self):
384
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
385

    
386
    # Error classes
387
    class InvalidBackendIdError(ValueError):
388
        def __init__(self, value):
389
            self.value = value
390

    
391
        def __str__(self):
392
            return repr(self.value)
393

    
394
    class InvalidBackendMsgError(Exception):
395
        def __init__(self, opcode, status):
396
            self.opcode = opcode
397
            self.status = status
398

    
399
        def __str__(self):
400
            return repr('<opcode: %s, status: %s>' % (self.opcode,
401
                        self.status))
402

    
403
    class InvalidActionError(Exception):
404
        def __init__(self, action):
405
            self._action = action
406

    
407
        def __str__(self):
408
            return repr(str(self._action))
409

    
410

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

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

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

    
424

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

    
433
    ACTIONS = (
434
        ('CREATE', 'Create Network'),
435
        ('DESTROY', 'Destroy Network'),
436
        ('ADD', 'Add server to Network'),
437
        ('REMOVE', 'Remove server from Network'),
438
    )
439

    
440
    RSAPI_STATE_FROM_OPER_STATE = {
441
        'PENDING': 'PENDING',
442
        'ACTIVE': 'ACTIVE',
443
        'DELETED': 'DELETED',
444
        'ERROR': 'ERROR'
445
    }
446

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

    
482
    NETWORK_NAME_LENGTH = 128
483

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

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

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

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

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

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

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

    
543
    def get_ip_pools(self, locked=True):
544
        subnets = self.subnets.filter(ipversion=4, deleted=False)\
545
                              .prefetch_related("ip_pools")
546
        return [ip_pool for subnet in subnets
547
                for ip_pool in subnet.get_ip_pools(locked=locked)]
548

    
549
    def reserve_address(self, address, external=False):
550
        for ip_pool in self.get_ip_pools():
551
            if ip_pool.contains(address):
552
                ip_pool.reserve(address, external=external)
553
                ip_pool.save()
554
                return
555
        raise pools.InvalidValue("Network %s does not have an IP pool that"
556
                                 " contains address %s" % (self, address))
557

    
558
    def release_address(self, address, external=False):
559
        for ip_pool in self.get_ip_pools():
560
            if ip_pool.contains(address):
561
                ip_pool.put(address, external=external)
562
                ip_pool.save()
563
                return
564
        raise pools.InvalidValue("Network %s does not have an IP pool that"
565
                                 " contains address %s" % (self, address))
566

    
567
    @property
568
    def subnet4(self):
569
        return self.get_subnet(version=4)
570

    
571
    @property
572
    def subnet6(self):
573
        return self.get_subnet(version=6)
574

    
575
    def get_subnet(self, version=4):
576
        for subnet in self.subnets.all():
577
            if subnet.ipversion == version:
578
                return subnet
579
        return None
580

    
581
    def ip_count(self):
582
        """Return the total and free IPv4 addresses of the network."""
583
        total, free = 0, 0
584
        ip_pools = self.get_ip_pools(locked=False)
585
        for ip_pool in ip_pools:
586
            total += ip_pool.pool_size
587
            free += ip_pool.count_available()
588
        return total, free
589

    
590
    class InvalidBackendIdError(ValueError):
591
        def __init__(self, value):
592
            self.value = value
593

    
594
        def __str__(self):
595
            return repr(self.value)
596

    
597
    class InvalidBackendMsgError(Exception):
598
        def __init__(self, opcode, status):
599
            self.opcode = opcode
600
            self.status = status
601

    
602
        def __str__(self):
603
            return repr('<opcode: %s, status: %s>'
604
                        % (self.opcode, self.status))
605

    
606
    class InvalidActionError(Exception):
607
        def __init__(self, action):
608
            self._action = action
609

    
610
        def __str__(self):
611
            return repr(str(self._action))
612

    
613

    
614
class Subnet(models.Model):
615
    SUBNET_NAME_LENGTH = 128
616

    
617
    userid = models.CharField('User ID of the owner', max_length=128,
618
                              null=True, db_index=True)
619
    public = models.BooleanField(default=False, db_index=True)
620

    
621
    network = models.ForeignKey('Network', null=False, db_index=True,
622
                                related_name="subnets",
623
                                on_delete=models.PROTECT)
624
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
625
                            null=True, default="")
626
    ipversion = models.IntegerField('IP Version', default=4, null=False)
627
    cidr = models.CharField('Subnet', max_length=64, null=False)
628
    gateway = models.CharField('Gateway', max_length=64, null=True)
629
    dhcp = models.BooleanField('DHCP', default=True, null=False)
630
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
631
                                  null=False)
632
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
633
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
634
    created = models.DateTimeField(auto_now_add=True)
635
    updated = models.DateTimeField(auto_now=True)
636

    
637
    def __unicode__(self):
638
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
639
        return msg % (self.id, self.network_id, self.cidr)
640

    
641
    def get_ip_pools(self, locked=True):
642
        ip_pools = self.ip_pools
643
        if locked:
644
            ip_pools = ip_pools.select_for_update()
645
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
646

    
647

    
648
class BackendNetwork(models.Model):
649
    OPER_STATES = (
650
        ('PENDING', 'Pending'),
651
        ('ACTIVE', 'Active'),
652
        ('DELETED', 'Deleted'),
653
        ('ERROR', 'Error')
654
    )
655

    
656
    # The list of possible operations on the backend
657
    BACKEND_OPCODES = (
658
        ('OP_NETWORK_ADD', 'Create Network'),
659
        ('OP_NETWORK_CONNECT', 'Activate Network'),
660
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
661
        ('OP_NETWORK_REMOVE', 'Remove Network'),
662
        # These are listed here for completeness,
663
        # and are ignored for the time being
664
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
665
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
666
    )
667

    
668
    # The operating state of a Netowork,
669
    # upon the successful completion of a backend operation.
670
    # IMPORTANT: Make sure all keys have a corresponding
671
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
672
    OPER_STATE_FROM_OPCODE = {
673
        'OP_NETWORK_ADD': 'PENDING',
674
        'OP_NETWORK_CONNECT': 'ACTIVE',
675
        'OP_NETWORK_DISCONNECT': 'PENDING',
676
        'OP_NETWORK_REMOVE': 'DELETED',
677
        'OP_NETWORK_SET_PARAMS': None,
678
        'OP_NETWORK_QUERY_DATA': None
679
    }
680

    
681
    network = models.ForeignKey(Network, related_name='backend_networks',
682
                                on_delete=models.PROTECT)
683
    backend = models.ForeignKey(Backend, related_name='networks',
684
                                on_delete=models.PROTECT)
685
    created = models.DateTimeField(auto_now_add=True)
686
    updated = models.DateTimeField(auto_now=True)
687
    deleted = models.BooleanField('Deleted', default=False)
688
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
689
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
690
                                 default='PENDING')
691
    backendjobid = models.PositiveIntegerField(null=True)
692
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
693
                                     null=True)
694
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
695
                                        max_length=30, null=True)
696
    backendlogmsg = models.TextField(null=True)
697
    backendtime = models.DateTimeField(null=False,
698
                                       default=datetime.datetime.min)
699

    
700
    class Meta:
701
        # Ensure one entry for each network in each backend
702
        unique_together = (("network", "backend"))
703

    
704
    def __init__(self, *args, **kwargs):
705
        """Initialize state for just created BackendNetwork instances."""
706
        super(BackendNetwork, self).__init__(*args, **kwargs)
707
        if not self.mac_prefix:
708
            # Generate the MAC prefix of the BackendNetwork, by combining
709
            # the Network prefix with the index of the Backend
710
            net_prefix = self.network.mac_prefix
711
            backend_suffix = hex(self.backend.index).replace('0x', '')
712
            mac_prefix = net_prefix + backend_suffix
713
            try:
714
                utils.validate_mac(mac_prefix + ":00:00:00")
715
            except utils.InvalidMacAddress:
716
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
717
                                              mac_prefix)
718
            self.mac_prefix = mac_prefix
719

    
720
    def __unicode__(self):
721
        return '<%s@%s>' % (self.network, self.backend)
722

    
723

    
724
class IPAddress(models.Model):
725
    subnet = models.ForeignKey("Subnet", related_name="ips", null=False,
726
                               on_delete=models.PROTECT)
727
    network = models.ForeignKey(Network, related_name="ips", null=False,
728
                                on_delete=models.PROTECT)
729
    nic = models.ForeignKey("NetworkInterface", related_name="ips", null=True,
730
                            on_delete=models.SET_NULL)
731
    userid = models.CharField("UUID of the owner", max_length=128, null=False,
732
                              db_index=True)
733
    project = models.CharField(max_length=255, null=True)
734
    address = models.CharField("IP Address", max_length=64, null=False)
735
    floating_ip = models.BooleanField("Floating IP", null=False, default=False)
736
    ipversion = models.IntegerField("IP Version", null=False)
737
    created = models.DateTimeField(auto_now_add=True)
738
    updated = models.DateTimeField(auto_now=True)
739
    deleted = models.BooleanField(default=False, null=False)
740

    
741
    serial = models.ForeignKey(QuotaHolderSerial,
742
                               related_name="ips", null=True,
743
                               on_delete=models.SET_NULL)
744

    
745
    def __unicode__(self):
746
        ip_type = "floating" if self.floating_ip else "static"
747
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
748
               % (self.address, self.network_id, self.subnet_id, ip_type)
749

    
750
    def in_use(self):
751
        if self.nic is None or self.nic.machine is None:
752
            return False
753
        else:
754
            return (not self.nic.machine.deleted)
755

    
756
    class Meta:
757
        unique_together = ("network", "address", "deleted")
758

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

    
772

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

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

    
790

    
791
class NetworkInterface(models.Model):
792
    FIREWALL_PROFILES = (
793
        ('ENABLED', 'Enabled'),
794
        ('DISABLED', 'Disabled'),
795
        ('PROTECTED', 'Protected')
796
    )
797

    
798
    STATES = (
799
        ("ACTIVE", "Active"),
800
        ("BUILD", "Building"),
801
        ("ERROR", "Error"),
802
        ("DOWN", "Down"),
803
    )
804

    
805
    NETWORK_IFACE_NAME_LENGTH = 128
806

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

    
827
    def __unicode__(self):
828
        return "<%s:vm:%s network:%s>" % (self.id, self.machine_id,
829
                                          self.network_id)
830

    
831
    @property
832
    def backend_uuid(self):
833
        """Return the backend id by prepending backend-prefix."""
834
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
835

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

    
840
    @property
841
    def ipv6_address(self):
842
        return self.get_ip_address(version=6)
843

    
844
    def get_ip_address(self, version=4):
845
        for ip in self.ips.all():
846
            if ip.ipversion == version:
847
                return ip.address
848
        return None
849

    
850
    def get_ip_addresses_subnets(self):
851
        return self.ips.values_list("address", "subnet__id")
852

    
853

    
854
class SecurityGroup(models.Model):
855
    SECURITY_GROUP_NAME_LENGTH = 128
856
    name = models.CharField('group name',
857
                            max_length=SECURITY_GROUP_NAME_LENGTH)
858

    
859
    @property
860
    def backend_uuid(self):
861
        """Return the name of NIC in Ganeti."""
862
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
863

    
864

    
865
class PoolTable(models.Model):
866
    available_map = models.TextField(default="", null=False)
867
    reserved_map = models.TextField(default="", null=False)
868
    size = models.IntegerField(null=False)
869

    
870
    # Optional Fields
871
    base = models.CharField(null=True, max_length=32)
872
    offset = models.IntegerField(null=True)
873

    
874
    class Meta:
875
        abstract = True
876

    
877
    @classmethod
878
    def get_pool(cls):
879
        try:
880
            pool_row = cls.objects.select_for_update().get()
881
            return pool_row.pool
882
        except cls.DoesNotExist:
883
            raise pools.EmptyPool
884

    
885
    @property
886
    def pool(self):
887
        return self.manager(self)
888

    
889

    
890
class BridgePoolTable(PoolTable):
891
    manager = pools.BridgePool
892

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

    
896

    
897
class MacPrefixPoolTable(PoolTable):
898
    manager = pools.MacPrefixPool
899

    
900
    def __unicode__(self):
901
        return u"<MACPrefixPool id:%s>" % self.id
902

    
903

    
904
class IPPoolTable(PoolTable):
905
    manager = pools.IPPool
906

    
907
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
908
                               on_delete=models.PROTECT,
909
                               db_index=True, null=True)
910

    
911
    def __unicode__(self):
912
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
913

    
914

    
915
@contextmanager
916
def pooled_rapi_client(obj):
917
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
918
            backend = obj.backend
919
        else:
920
            backend = obj
921

    
922
        if backend.offline:
923
            log.warning("Trying to connect with offline backend: %s", backend)
924
            raise faults.ServiceUnavailable("Cannot connect to offline"
925
                                            " backend: %s" % backend)
926

    
927
        b = backend
928
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
929
                                 b.username, b.password)
930
        try:
931
            yield client
932
        finally:
933
            put_rapi_client(client)
934

    
935

    
936
class VirtualMachineDiagnosticManager(models.Manager):
937
    """
938
    Custom manager for :class:`VirtualMachineDiagnostic` model.
939
    """
940

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

    
949
    def create_error(self, vm, **kwargs):
950
        self.create_for_vm(vm, 'ERROR', **kwargs)
951

    
952
    def create_debug(self, vm, **kwargs):
953
        self.create_for_vm(vm, 'DEBUG', **kwargs)
954

    
955
    def since(self, vm, created_since, **kwargs):
956
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
957
                                           **kwargs)
958

    
959

    
960
class VirtualMachineDiagnostic(models.Model):
961
    """
962
    Model to store backend information messages that relate to the state of
963
    the virtual machine.
964
    """
965

    
966
    TYPES = (
967
        ('ERROR', 'Error'),
968
        ('WARNING', 'Warning'),
969
        ('INFO', 'Info'),
970
        ('DEBUG', 'Debug'),
971
    )
972

    
973
    objects = VirtualMachineDiagnosticManager()
974

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

    
984
    class Meta:
985
        ordering = ['-created']