Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.4 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
    backend = models.ForeignKey(Backend, null=True,
307
                                related_name="virtual_machines",
308
                                on_delete=models.PROTECT)
309
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
310
    created = models.DateTimeField(auto_now_add=True)
311
    updated = models.DateTimeField(auto_now=True)
312
    imageid = models.CharField(max_length=100, null=False)
313
    hostid = models.CharField(max_length=100)
314
    flavor = models.ForeignKey(Flavor, on_delete=models.PROTECT)
315
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
316
    suspended = models.BooleanField('Administratively Suspended',
317
                                    default=False)
318
    serial = models.ForeignKey(QuotaHolderSerial,
319
                               related_name='virtual_machine', null=True,
320
                               on_delete=models.SET_NULL)
321

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
409

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

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

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

    
423

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

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

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

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

    
481
    NETWORK_NAME_LENGTH = 128
482

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
587
    class InvalidBackendIdError(Exception):
588
        def __init__(self, value):
589
            self.value = value
590

    
591
        def __str__(self):
592
            return repr(self.value)
593

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

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

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

    
607
        def __str__(self):
608
            return repr(str(self._action))
609

    
610

    
611
class Subnet(models.Model):
612
    SUBNET_NAME_LENGTH = 128
613

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

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

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

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

    
644

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

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

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

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

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

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

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

    
720

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

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

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

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

    
752
    class Meta:
753
        unique_together = ("network", "address", "deleted")
754

    
755
    @property
756
    def public(self):
757
        return self.network.public
758

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

    
772

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

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

    
790

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

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

    
805
    NETWORK_IFACE_NAME_LENGTH = 128
806

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

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

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

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

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

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

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

    
852

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

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

    
863

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

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

    
873
    class Meta:
874
        abstract = True
875

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

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

    
888

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

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

    
895

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

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

    
902

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

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

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

    
913

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

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

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

    
934

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

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

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

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

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

    
958

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

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

    
972
    objects = VirtualMachineDiagnosticManager()
973

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

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

    
986

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

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

    
1005
    SOURCE_IMAGE_PREFIX = "image:"
1006
    SOURCE_SNAPSHOT_PREFIX = "snapshot:"
1007
    SOURCE_VOLUME_PREFIX = "volume:"
1008

    
1009
    name = models.CharField("Name", max_length=255, null=True)
1010
    description = models.CharField("Description", max_length=256, null=True)
1011
    userid = models.CharField("Owner's UUID", max_length=100, null=False,
1012
                              db_index=True)
1013
    size = models.IntegerField("Volume size in GB",  null=False)
1014
    delete_on_termination = models.BooleanField("Delete on Server Termination",
1015
                                                default=True, null=False)
1016

    
1017
    source = models.CharField(max_length=128, null=True)
1018
    origin = models.CharField(max_length=128, null=True)
1019

    
1020
    # TODO: volume_type should be foreign key to VolumeType model
1021
    volume_type = None
1022
    deleted = models.BooleanField("Deleted", default=False, null=False)
1023
    # Datetime fields
1024
    created = models.DateTimeField(auto_now_add=True)
1025
    updated = models.DateTimeField(auto_now=True)
1026
    # Status
1027
    status = models.CharField("Status", max_length=64,
1028
                              choices=STATUS_VALUES,
1029
                              default="CREATING", null=False)
1030
    snapshot_counter = models.PositiveIntegerField(default=0, null=False)
1031

    
1032
    machine = models.ForeignKey("VirtualMachine",
1033
                                related_name="volumes",
1034
                                null=True)
1035
    index = models.IntegerField("Index", null=True)
1036
    backendjobid = models.PositiveIntegerField(null=True)
1037

    
1038
    @property
1039
    def backend_volume_uuid(self):
1040
        return u"%svolume-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1041

    
1042
    @property
1043
    def backend_disk_uuid(self):
1044
        return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1045

    
1046
    @property
1047
    def source_image_id(self):
1048
        src = self.source
1049
        if src and src.startswith(self.SOURCE_IMAGE_PREFIX):
1050
            return src[len(self.SOURCE_IMAGE_PREFIX):]
1051
        else:
1052
            return None
1053

    
1054
    @property
1055
    def source_snapshot_id(self):
1056
        src = self.source
1057
        if src and src.startswith(self.SOURCE_SNAPSHOT_PREFIX):
1058
            return src[len(self.SOURCE_SNAPSHOT_PREFIX):]
1059
        else:
1060
            return None
1061

    
1062
    @property
1063
    def source_volume_id(self):
1064
        src = self.source
1065
        if src and src.startswith(self.SOURCE_VOLUME_PREFIX):
1066
            return src[len(self.SOURCE_VOLUME_PREFIX):]
1067
        else:
1068
            return None
1069

    
1070
    @property
1071
    def disk_template(self):
1072
        if self.machine is None:
1073
            return None
1074
        else:
1075
            disk_template = self.machine.flavor.disk_template
1076
            return disk_template.split("_")[0]
1077

    
1078
    @property
1079
    def disk_provider(self):
1080
        if self.machine is None:
1081
            return None
1082
        else:
1083
            disk_template = self.machine.flavor.disk_template
1084
            if "_" in disk_template:
1085
                return disk_template.split("_")[1]
1086
            else:
1087
                return None
1088

    
1089
    @staticmethod
1090
    def prefix_source(source_id, source_type):
1091
        if source_type == "volume":
1092
            return Volume.SOURCE_VOLUME_PREFIX + str(source_id)
1093
        if source_type == "snapshot":
1094
            return Volume.SOURCE_SNAPSHOT_PREFIX + str(source_id)
1095
        if source_type == "image":
1096
            return Volume.SOURCE_IMAGE_PREFIX + str(source_id)
1097
        elif source_type == "blank":
1098
            return None
1099

    
1100
    def __unicode__(self):
1101
        return "<Volume %s:vm:%s>" % (self.id, self.machine_id)
1102

    
1103

    
1104
class Metadata(models.Model):
1105
    key = models.CharField("Metadata Key", max_length=64)
1106
    value = models.CharField("Metadata Value", max_length=255)
1107

    
1108
    class Meta:
1109
        abstract = True
1110

    
1111
    def __unicode__(self):
1112
        return u"<%s: %s>" % (self.key, self.value)
1113

    
1114

    
1115
class VolumeMetadata(Metadata):
1116
    volume = models.ForeignKey("Volume", related_name="metadata")
1117

    
1118
    class Meta:
1119
        unique_together = (("volume", "key"),)
1120
        verbose_name = u"Key-Value pair of Volumes metadata"