Statistics
| Branch: | Tag: | Revision:

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

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 u"<%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 u"%s(id:%s)" % (self.clustername, 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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
589
    class InvalidBackendIdError(Exception):
590
        def __init__(self, value):
591
            self.value = value
592

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

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

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

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

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

    
612

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

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

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

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

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

    
646

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

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

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

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

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

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

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

    
722

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

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

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

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

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

    
758
    @property
759
    def public(self):
760
        return self.network.public
761

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

    
775

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

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

    
793

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

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

    
808
    NETWORK_IFACE_NAME_LENGTH = 128
809

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

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

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

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

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

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

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

    
855

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

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

    
866

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

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

    
876
    class Meta:
877
        abstract = True
878

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

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

    
891

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

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

    
898

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

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

    
905

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

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

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

    
916

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

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

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

    
937

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

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

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

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

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

    
961

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

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

    
975
    objects = VirtualMachineDiagnosticManager()
976

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

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