Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.9 kB)

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

    
30
import datetime
31

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

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

    
43
from synnefo.db import pools, fields
44

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

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

    
51

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

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

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

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

    
72

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
180

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

    
192

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

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

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

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

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

    
217

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

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

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

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

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

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

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

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

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

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

    
344
    def get_client(self):
345
        if self.backend:
346
            return self.backend.get_client()
347
        else:
348
            raise faults.ServiceUnavailable("VirtualMachine without backend")
349

    
350
    def get_last_diagnostic(self, **filters):
351
        try:
352
            return self.diagnostics.filter()[0]
353
        except IndexError:
354
            return None
355

    
356
    @staticmethod
357
    def put_client(client):
358
            put_rapi_client(client)
359

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

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

    
373
    class Meta:
374
        verbose_name = u'Virtual machine instance'
375
        get_latest_by = 'created'
376

    
377
    def __unicode__(self):
378
        return u"<vm:%s@backend:%s router:%s>" % (self.id, self.backend_id,
379
                                                  self.router)
380

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

    
386
        def __str__(self):
387
            return repr(self.value)
388

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

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

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

    
402
        def __str__(self):
403
            return repr(str(self._action))
404

    
405

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

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

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

    
419

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

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

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

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

    
477
    NETWORK_NAME_LENGTH = 128
478

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

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

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

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

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

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

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

    
536
    def get_pool(self, locked=True):
537
        try:
538
            subnet = self.subnets.get(ipversion=4, deleted=False)
539
        except Subnet.DoesNotExist:
540
            raise pools.EmptyPool
541
        return subnet.get_pool(locked=locked)
542

    
543
    def allocate_address(self, userid):
544
        try:
545
            subnet = self.subnets.get(ipversion=4, deleted=False)
546
        except Subnet.DoesNotExist:
547
            raise pools.EmptyPool
548
        return subnet.allocate_address(userid)
549

    
550
    def reserve_address(self, address, external=False):
551
        pool = self.get_pool()
552
        pool.reserve(address, external=external)
553
        pool.save()
554

    
555
    def release_address(self, address, external=True):
556
        pool = self.get_pool()
557
        pool.put(address, external=external)
558
        pool.save()
559

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

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

    
568
    def get_subnet(self, version=4):
569
        for subnet in self.subnets.all():
570
            if subnet.ipversion == version:
571
                return subnet.cidr
572

    
573
    def ip_count(self):
574
        """Return the total and free IPv4 addresses of the network."""
575
        subnets = self.subnets.filter(ipversion=4).prefetch_related("ip_pools")
576
        total, free = 0, 0
577
        for subnet in subnets:
578
            for ip_pool in subnet.ip_pools.all():
579
                pool = ip_pool.pool
580
                total += pool.pool_size
581
                free += pool.count_available()
582
        return total, free
583

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

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

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

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

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

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

    
607

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

    
611
    network = models.ForeignKey('Network', null=False, db_index=True,
612
                                related_name="subnets")
613
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
614
                            null=True)
615
    ipversion = models.IntegerField('IP Version', default=4, null=False)
616
    cidr = models.CharField('Subnet', max_length=64, null=False)
617
    gateway = models.CharField('Gateway', max_length=64, null=True)
618
    dhcp = models.BooleanField('DHCP', default=True, null=False)
619
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
620
                                  null=False)
621
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
622
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
623

    
624
    def __unicode__(self):
625
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
626
        return msg % (self.id, self.network_id, self.cidr)
627

    
628
    def get_pool(self, locked=True):
629
        if self.ipversion == 6:
630
            raise Exception("IPv6 Subnets have no IP Pool.")
631
        ip_pools = self.ip_pools
632
        if locked:
633
            ip_pools = ip_pools.select_for_update()
634
        return ip_pools.all()[0].pool
635

    
636
    def allocate_address(self, userid):
637
        pool = self.get_pool(locked=True)
638
        address = pool.get()
639
        pool.save()
640
        return IPAddress.objects.create(network=self.network, subnet=self,
641
                                        address=address, userid=userid)
642

    
643

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

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

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

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

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

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

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

    
719

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

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

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

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

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

    
753
    @property
754
    def ipversion(self):
755
        return self.subnet.ipversion
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

    
770

    
771
class NetworkInterface(models.Model):
772
    FIREWALL_PROFILES = (
773
        ('ENABLED', 'Enabled'),
774
        ('DISABLED', 'Disabled'),
775
        ('PROTECTED', 'Protected')
776
    )
777

    
778
    STATES = (
779
        ("ACTIVE", "Active"),
780
        ("BUILDING", "Building"),
781
        ("ERROR", "Error"),
782
    )
783

    
784
    NETWORK_IFACE_NAME_LENGTH = 128
785

    
786
    name = models.CharField('NIC name', max_length=128, null=True)
787
    userid = models.CharField("UUID of the owner",
788
                              max_length=NETWORK_IFACE_NAME_LENGTH,
789
                              null=True, db_index=True)
790
    machine = models.ForeignKey(VirtualMachine, related_name='nics',
791
                                on_delete=models.CASCADE)
792
    network = models.ForeignKey(Network, related_name='nics',
793
                                on_delete=models.CASCADE)
794
    created = models.DateTimeField(auto_now_add=True)
795
    updated = models.DateTimeField(auto_now=True)
796
    index = models.IntegerField(null=True)
797
    mac = models.CharField(max_length=32, null=True, unique=True)
798
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
799
                                        max_length=30, null=True)
800
    security_groups = models.ManyToManyField("SecurityGroup", null=True)
801
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
802
                             choices=STATES)
803
    device_owner = models.CharField('Device owner', max_length=128, null=True)
804

    
805
    def __unicode__(self):
806
        return "<%s:vm:%s network:%s>" % (self.id, self.machine_id,
807
                                          self.network_id)
808

    
809
    @property
810
    def backend_uuid(self):
811
        """Return the backend id by prepending backend-prefix."""
812
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
813

    
814
    @property
815
    def ipv4_address(self):
816
        return self.get_ip_address(version=4)
817

    
818
    @property
819
    def ipv6_address(self):
820
        return self.get_ip_address(version=6)
821

    
822
    def get_ip_address(self, version=4):
823
        for ip in self.ips.all():
824
            if ip.subnet.ipversion == version:
825
                return ip.address
826
        return None
827

    
828
    def get_ip_addresses_subnets(self):
829
        return self.ips.values_list("address", "subnet__id")
830

    
831

    
832
class SecurityGroup(models.Model):
833
    SECURITY_GROUP_NAME_LENGTH = 128
834
    name = models.CharField('group name',
835
                            max_length=SECURITY_GROUP_NAME_LENGTH)
836

    
837

    
838
class PoolTable(models.Model):
839
    available_map = models.TextField(default="", null=False)
840
    reserved_map = models.TextField(default="", null=False)
841
    size = models.IntegerField(null=False)
842

    
843
    # Optional Fields
844
    base = models.CharField(null=True, max_length=32)
845
    offset = models.IntegerField(null=True)
846

    
847
    class Meta:
848
        abstract = True
849

    
850
    @classmethod
851
    def get_pool(cls):
852
        try:
853
            pool_row = cls.objects.select_for_update().get()
854
            return pool_row.pool
855
        except cls.DoesNotExist:
856
            raise pools.EmptyPool
857

    
858
    @property
859
    def pool(self):
860
        return self.manager(self)
861

    
862

    
863
class BridgePoolTable(PoolTable):
864
    manager = pools.BridgePool
865

    
866
    def __unicode__(self):
867
        return u"<BridgePool id:%s>" % self.id
868

    
869

    
870
class MacPrefixPoolTable(PoolTable):
871
    manager = pools.MacPrefixPool
872

    
873
    def __unicode__(self):
874
        return u"<MACPrefixPool id:%s>" % self.id
875

    
876

    
877
class IPPoolTable(PoolTable):
878
    manager = pools.IPPool
879

    
880
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
881
                               db_index=True, null=True)
882

    
883
    def __unicode__(self):
884
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
885

    
886

    
887
@contextmanager
888
def pooled_rapi_client(obj):
889
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
890
            backend = obj.backend
891
        else:
892
            backend = obj
893

    
894
        if backend.offline:
895
            log.warning("Trying to connect with offline backend: %s", backend)
896
            raise faults.ServiceUnavailable("Can not connect to offline"
897
                                            " backend: %s" % backend)
898

    
899
        b = backend
900
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
901
                                 b.username, b.password)
902
        try:
903
            yield client
904
        finally:
905
            put_rapi_client(client)
906

    
907

    
908
class VirtualMachineDiagnosticManager(models.Manager):
909
    """
910
    Custom manager for :class:`VirtualMachineDiagnostic` model.
911
    """
912

    
913
    # diagnostic creation helpers
914
    def create_for_vm(self, vm, level, message, **kwargs):
915
        attrs = {'machine': vm, 'level': level, 'message': message}
916
        attrs.update(kwargs)
917
        # update instance updated time
918
        self.create(**attrs)
919
        vm.save()
920

    
921
    def create_error(self, vm, **kwargs):
922
        self.create_for_vm(vm, 'ERROR', **kwargs)
923

    
924
    def create_debug(self, vm, **kwargs):
925
        self.create_for_vm(vm, 'DEBUG', **kwargs)
926

    
927
    def since(self, vm, created_since, **kwargs):
928
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
929
                                           **kwargs)
930

    
931

    
932
class VirtualMachineDiagnostic(models.Model):
933
    """
934
    Model to store backend information messages that relate to the state of
935
    the virtual machine.
936
    """
937

    
938
    TYPES = (
939
        ('ERROR', 'Error'),
940
        ('WARNING', 'Warning'),
941
        ('INFO', 'Info'),
942
        ('DEBUG', 'Debug'),
943
    )
944

    
945
    objects = VirtualMachineDiagnosticManager()
946

    
947
    created = models.DateTimeField(auto_now_add=True)
948
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics",
949
                                on_delete=models.CASCADE)
950
    level = models.CharField(max_length=20, choices=TYPES)
951
    source = models.CharField(max_length=100)
952
    source_date = models.DateTimeField(null=True)
953
    message = models.CharField(max_length=255)
954
    details = models.TextField(null=True)
955

    
956
    class Meta:
957
        ordering = ['-created']