Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.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
    # 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
    KEY_LENGTH = 50
412
    VALUE_LENGTH = 500
413
    meta_key = models.CharField(max_length=KEY_LENGTH)
414
    meta_value = models.CharField(max_length=VALUE_LENGTH)
415
    vm = models.ForeignKey(VirtualMachine, related_name='metadata',
416
                           on_delete=models.CASCADE)
417

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

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

    
425

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

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

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

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

    
483
    NETWORK_NAME_LENGTH = 128
484

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
612

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

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

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

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

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

    
646

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

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

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

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

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

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

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

    
722

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

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

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

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

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

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

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

    
774

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

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

    
792

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

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

    
807
    NETWORK_IFACE_NAME_LENGTH = 128
808

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

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

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

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

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

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

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

    
854

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

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

    
865

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

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

    
875
    class Meta:
876
        abstract = True
877

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

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

    
890

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

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

    
897

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

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

    
904

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

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

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

    
915

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

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

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

    
936

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

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

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

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

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

    
960

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

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

    
974
    objects = VirtualMachineDiagnosticManager()
975

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

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

    
988

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

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

    
1008
    NAME_LENGTH = 255
1009
    DESCRIPTION_LENGTH = 255
1010
    SOURCE_IMAGE_PREFIX = "image:"
1011
    SOURCE_SNAPSHOT_PREFIX = "snapshot:"
1012
    SOURCE_VOLUME_PREFIX = "volume:"
1013

    
1014
    name = models.CharField("Name", max_length=NAME_LENGTH, null=True)
1015
    description = models.CharField("Description",
1016
                                   max_length=DESCRIPTION_LENGTH, null=True)
1017
    userid = models.CharField("Owner's UUID", max_length=100, null=False,
1018
                              db_index=True)
1019
    size = models.IntegerField("Volume size in GB",  null=False)
1020
    disk_template = models.CharField('Disk template', max_length=32,
1021
                                     null=False)
1022

    
1023
    delete_on_termination = models.BooleanField("Delete on Server Termination",
1024
                                                default=True, null=False)
1025

    
1026
    source = models.CharField(max_length=128, null=True)
1027
    origin = models.CharField(max_length=128, null=True)
1028

    
1029
    # TODO: volume_type should be foreign key to VolumeType model
1030
    volume_type = None
1031
    deleted = models.BooleanField("Deleted", default=False, null=False)
1032
    # Datetime fields
1033
    created = models.DateTimeField(auto_now_add=True)
1034
    updated = models.DateTimeField(auto_now=True)
1035
    # Status
1036
    status = models.CharField("Status", max_length=64,
1037
                              choices=STATUS_VALUES,
1038
                              default="CREATING", null=False)
1039
    snapshot_counter = models.PositiveIntegerField(default=0, null=False)
1040

    
1041
    machine = models.ForeignKey("VirtualMachine",
1042
                                related_name="volumes",
1043
                                null=True)
1044
    index = models.IntegerField("Index", null=True)
1045
    backendjobid = models.PositiveIntegerField(null=True)
1046

    
1047
    @property
1048
    def backend_volume_uuid(self):
1049
        return u"%svol-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1050

    
1051
    @property
1052
    def backend_disk_uuid(self):
1053
        return u"%sdisk-%d" % (settings.BACKEND_PREFIX_ID, self.id)
1054

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

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

    
1071
    @property
1072
    def source_volume_id(self):
1073
        src = self.source
1074
        if src and src.startswith(self.SOURCE_VOLUME_PREFIX):
1075
            return src[len(self.SOURCE_VOLUME_PREFIX):]
1076
        else:
1077
            return None
1078

    
1079
    @property
1080
    def template(self):
1081
        return self.disk_template.split("_")[0]
1082

    
1083
    @property
1084
    def provider(self):
1085
        if "_" in self.disk_template:
1086
            return self.disk_template.split("_", 1)[1]
1087
        else:
1088
            return None
1089

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

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

    
1104

    
1105
class Metadata(models.Model):
1106
    KEY_LENGTH = 64
1107
    VALUE_LENGTH = 255
1108
    key = models.CharField("Metadata Key", max_length=KEY_LENGTH)
1109
    value = models.CharField("Metadata Value", max_length=VALUE_LENGTH)
1110

    
1111
    class Meta:
1112
        abstract = True
1113

    
1114
    def __unicode__(self):
1115
        return u"<%s: %s>" % (self.key, self.value)
1116

    
1117

    
1118
class VolumeMetadata(Metadata):
1119
    volume = models.ForeignKey("Volume", related_name="metadata")
1120

    
1121
    class Meta:
1122
        unique_together = (("volume", "key"),)
1123
        verbose_name = u"Key-Value pair of Volumes metadata"