Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (36.9 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(Exception):
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(Exception):
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
    @property
760
    def public(self):
761
        return self.network.public
762

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

    
776

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

    
789
    def __unicode__(self):
790
        return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\
791
               % (self.address, self.network_id, self.server_id,
792
                  self.allocated_at)
793

    
794

    
795
class NetworkInterface(models.Model):
796
    FIREWALL_PROFILES = (
797
        ('ENABLED', 'Enabled'),
798
        ('DISABLED', 'Disabled'),
799
        ('PROTECTED', 'Protected')
800
    )
801

    
802
    STATES = (
803
        ("ACTIVE", "Active"),
804
        ("BUILD", "Building"),
805
        ("ERROR", "Error"),
806
        ("DOWN", "Down"),
807
    )
808

    
809
    NETWORK_IFACE_NAME_LENGTH = 128
810

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

    
830
    def __unicode__(self):
831
        return "<%s:vm:%s network:%s>" % (self.id, self.machine_id,
832
                                          self.network_id)
833

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

    
839
    @property
840
    def ipv4_address(self):
841
        return self.get_ip_address(version=4)
842

    
843
    @property
844
    def ipv6_address(self):
845
        return self.get_ip_address(version=6)
846

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

    
853
    def get_ip_addresses_subnets(self):
854
        return self.ips.values_list("address", "subnet__id")
855

    
856

    
857
class SecurityGroup(models.Model):
858
    SECURITY_GROUP_NAME_LENGTH = 128
859
    name = models.CharField('group name',
860
                            max_length=SECURITY_GROUP_NAME_LENGTH)
861

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

    
867

    
868
class PoolTable(models.Model):
869
    available_map = models.TextField(default="", null=False)
870
    reserved_map = models.TextField(default="", null=False)
871
    size = models.IntegerField(null=False)
872

    
873
    # Optional Fields
874
    base = models.CharField(null=True, max_length=32)
875
    offset = models.IntegerField(null=True)
876

    
877
    class Meta:
878
        abstract = True
879

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

    
888
    @property
889
    def pool(self):
890
        return self.manager(self)
891

    
892

    
893
class BridgePoolTable(PoolTable):
894
    manager = pools.BridgePool
895

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

    
899

    
900
class MacPrefixPoolTable(PoolTable):
901
    manager = pools.MacPrefixPool
902

    
903
    def __unicode__(self):
904
        return u"<MACPrefixPool id:%s>" % self.id
905

    
906

    
907
class IPPoolTable(PoolTable):
908
    manager = pools.IPPool
909

    
910
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
911
                               on_delete=models.PROTECT,
912
                               db_index=True, null=True)
913

    
914
    def __unicode__(self):
915
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
916

    
917

    
918
@contextmanager
919
def pooled_rapi_client(obj):
920
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
921
            backend = obj.backend
922
        else:
923
            backend = obj
924

    
925
        if backend.offline:
926
            log.warning("Trying to connect with offline backend: %s", backend)
927
            raise faults.ServiceUnavailable("Cannot connect to offline"
928
                                            " backend: %s" % backend)
929

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

    
938

    
939
class VirtualMachineDiagnosticManager(models.Manager):
940
    """
941
    Custom manager for :class:`VirtualMachineDiagnostic` model.
942
    """
943

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

    
952
    def create_error(self, vm, **kwargs):
953
        self.create_for_vm(vm, 'ERROR', **kwargs)
954

    
955
    def create_debug(self, vm, **kwargs):
956
        self.create_for_vm(vm, 'DEBUG', **kwargs)
957

    
958
    def since(self, vm, created_since, **kwargs):
959
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
960
                                           **kwargs)
961

    
962

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

    
969
    TYPES = (
970
        ('ERROR', 'Error'),
971
        ('WARNING', 'Warning'),
972
        ('INFO', 'Info'),
973
        ('DEBUG', 'Debug'),
974
    )
975

    
976
    objects = VirtualMachineDiagnosticManager()
977

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

    
987
    class Meta:
988
        ordering = ['-created']