Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (39.5 kB)

1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
import datetime
31

    
32
from copy import deepcopy
33
from django.conf import settings
34
from django.db import models
35

    
36
import utils
37
from contextlib import contextmanager
38
from hashlib import sha1
39
from snf_django.lib.api import faults
40
from django.conf import settings as snf_settings
41
from aes_encrypt import encrypt_db_charfield, decrypt_db_charfield
42

    
43
from synnefo.db import pools, fields
44

    
45
from synnefo.logic.rapi_pool import (get_rapi_client,
46
                                     put_rapi_client)
47

    
48
import logging
49
log = logging.getLogger(__name__)
50

    
51

    
52
class Flavor(models.Model):
53
    cpu = models.IntegerField('Number of CPUs', default=0)
54
    ram = models.IntegerField('RAM size in MiB', default=0)
55
    disk = models.IntegerField('Disk size in GiB', default=0)
56
    disk_template = models.CharField('Disk template', max_length=32)
57
    deleted = models.BooleanField('Deleted', default=False)
58

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

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

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

    
72

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

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

    
110
    class Meta:
111
        verbose_name = u'Backend'
112
        ordering = ["clustername"]
113

    
114
    def __unicode__(self):
115
        return self.clustername + "(id=" + str(self.id) + ")"
116

    
117
    @property
118
    def backend_id(self):
119
        return self.id
120

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

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

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

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

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

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

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

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

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

    
180

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

    
192

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

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

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

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

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

    
217

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

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

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

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

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

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

    
298
    VIRTUAL_MACHINE_NAME_LENGTH = 255
299

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
407

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

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

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

    
421

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

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

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

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

    
479
    NETWORK_NAME_LENGTH = 128
480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
608

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

    
612
    userid = models.CharField('User ID of the owner', max_length=128,
613
                              null=True, db_index=True)
614
    public = models.BooleanField(default=False, db_index=True)
615

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

    
632
    def __unicode__(self):
633
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
634
        return msg % (self.id, self.network_id, self.cidr)
635

    
636
    def get_ip_pools(self, locked=True):
637
        ip_pools = self.ip_pools
638
        if locked:
639
            ip_pools = ip_pools.select_for_update()
640
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
641

    
642

    
643
class BackendNetwork(models.Model):
644
    OPER_STATES = (
645
        ('PENDING', 'Pending'),
646
        ('ACTIVE', 'Active'),
647
        ('DELETED', 'Deleted'),
648
        ('ERROR', 'Error')
649
    )
650

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

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

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

    
695
    class Meta:
696
        # Ensure one entry for each network in each backend
697
        unique_together = (("network", "backend"))
698

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

    
715
    def __unicode__(self):
716
        return '<%s@%s>' % (self.network, self.backend)
717

    
718

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

    
735
    serial = models.ForeignKey(QuotaHolderSerial,
736
                               related_name="ips", null=True,
737
                               on_delete=models.SET_NULL)
738

    
739
    def __unicode__(self):
740
        ip_type = "floating" if self.floating_ip else "static"
741
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
742
               % (self.address, self.network_id, self.subnet_id, ip_type)
743

    
744
    def in_use(self):
745
        if self.nic is None or self.nic.machine is None:
746
            return False
747
        else:
748
            return (not self.nic.machine.deleted)
749

    
750
    class Meta:
751
        unique_together = ("network", "address", "deleted")
752

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

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

    
770

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

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

    
788

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

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

    
803
    NETWORK_IFACE_NAME_LENGTH = 128
804

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

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

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

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

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

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

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

    
850

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

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

    
861

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

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

    
871
    class Meta:
872
        abstract = True
873

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

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

    
886

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

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

    
893

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

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

    
900

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

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

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

    
911

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

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

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

    
932

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

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

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

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

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

    
956

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

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

    
970
    objects = VirtualMachineDiagnosticManager()
971

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

    
981
    class Meta:
982
        ordering = ['-created']
983

    
984

    
985
class Volume(models.Model):
986
    """Model representing a detachable block storage device."""
987

    
988
    STATUS_VALUES = (
989
        ("CREATING", "The volume is being created"),
990
        ("AVAILABLE", "The volume is ready to be attached to an instance"),
991
        ("ATTACHING", "The volume is attaching to an instance"),
992
        ("DETACHING", "The volume is detaching from an instance"),
993
        ("IN_USE", "The volume is attached to an instance"),
994
        ("DELETING", "The volume is being deleted"),
995
        ("ERROR", "An error has occured with the volume"),
996
        ("ERROR_DELETING", "There was an error deleting this volume"),
997
        ("BACKING_UP", "The volume is being backed up"),
998
        ("RESTORING_BACKUP", "A backup is being restored to the volume"),
999
        ("ERROR_RESTORING", "There was an error restoring a backup from the"
1000
                            " volume")
1001
    )
1002

    
1003
    name = models.CharField("Name", max_length=255, null=True)
1004
    description = models.CharField("Description", max_length=255, null=True)
1005
    userid = models.CharField("Owner's UUID", max_length=100, null=False,
1006
                              db_index=True)
1007
    size = models.IntegerField("Volume size in GB",  null=False)
1008
    source_image_id = models.CharField(max_length=100, null=True)
1009
    source_snapshot_id = models.CharField(max_length=100, null=True)
1010
    source_volume = models.ForeignKey("Volume",
1011
                                      related_name="cloned_volumes",
1012
                                      null=True)
1013
    # TODO: volume_type should be foreign key to VolumeType model
1014
    volume_type = None
1015
    deleted = models.BooleanField("Deleted", default=False, null=False)
1016
    # Datetime fields
1017
    created = models.DateTimeField(auto_now_add=True)
1018
    updated = models.DateTimeField(auto_now=True)
1019
    # Status
1020
    status = models.CharField("Status", max_length=64,
1021
                              choices=STATUS_VALUES,
1022
                              default="CREATING", null=False)
1023
    snapshot_counter = models.PositiveIntegerField(default=0, null=False)
1024

    
1025
    machine = models.ForeignKey("VirtualMachine",
1026
                                related_name="volumes",
1027
                                null=True)
1028
    index = models.IntegerField("Index", null=True)
1029
    backendjobid = models.PositiveIntegerField(null=True)
1030

    
1031
    @property
1032
    def backend_volume_uuid(self):
1033
        return u"%svolume-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1034

    
1035
    @property
1036
    def backend_disk_uuid(self):
1037
        return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1038

    
1039

    
1040
class Metadata(models.Model):
1041
    key = models.CharField("Metadata Key", max_length=64)
1042
    value = models.CharField("Metadata Value", max_length=255)
1043

    
1044
    class Meta:
1045
        abstract = True
1046

    
1047
    def __unicode__(self):
1048
        return u"<%s: %s>" % (self.key, self.value)
1049

    
1050

    
1051
class VolumeMetadata(Metadata):
1052
    volume = models.ForeignKey("Volume", related_name="metadata")
1053

    
1054
    class Meta:
1055
        unique_together = (("volume", "key"),)
1056
        verbose_name = u"Key-Value pair of Volumes metadata"