Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (34.7 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
125
        return get_rapi_client(self.id, self.hash,
126
                               self.clustername,
127
                               self.port,
128
                               self.username,
129
                               self.password)
130

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

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

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

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

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

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

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

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

    
179

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

    
191

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

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

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

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

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

    
216

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

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

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

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

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

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

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

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

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

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

    
343
    def get_client(self):
344
        if self.backend:
345
            return self.backend.get_client()
346
        else:
347
            raise faults.ServiceUnavailable
348

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

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

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

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

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

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

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

    
384
        def __str__(self):
385
            return repr(self.value)
386

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

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

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

    
400
        def __str__(self):
401
            return repr(str(self._action))
402

    
403

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

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

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

    
417

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

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

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

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

    
475
    NETWORK_NAME_LENGTH = 128
476

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
582
    class InvalidBackendIdError(Exception):
583
        def __init__(self, value):
584
            self.value = value
585

    
586
        def __str__(self):
587
            return repr(self.value)
588

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

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

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

    
602
        def __str__(self):
603
            return repr(str(self._action))
604

    
605

    
606
class Subnet(models.Model):
607
    SUBNET_NAME_LENGTH = 128
608

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

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

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

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

    
641

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

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

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

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

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

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

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

    
717

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

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

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

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

    
748
    class Meta:
749
        unique_together = ("network", "address")
750

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

    
768

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

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

    
782
    NETWORK_IFACE_NAME_LENGTH = 128
783

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

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

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

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

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

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

    
826
    def get_ip_addresses_subnets(self):
827
        return self.ips.values_list("address", "subnet__id")
828

    
829

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

    
835

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

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

    
845
    class Meta:
846
        abstract = True
847

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

    
856
    @property
857
    def pool(self):
858
        return self.manager(self)
859

    
860

    
861
class BridgePoolTable(PoolTable):
862
    manager = pools.BridgePool
863

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

    
867

    
868
class MacPrefixPoolTable(PoolTable):
869
    manager = pools.MacPrefixPool
870

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

    
874

    
875
class IPPoolTable(PoolTable):
876
    manager = pools.IPPool
877

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

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

    
884

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

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

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

    
905

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

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

    
919
    def create_error(self, vm, **kwargs):
920
        self.create_for_vm(vm, 'ERROR', **kwargs)
921

    
922
    def create_debug(self, vm, **kwargs):
923
        self.create_for_vm(vm, 'DEBUG', **kwargs)
924

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

    
929

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

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

    
943
    objects = VirtualMachineDiagnosticManager()
944

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

    
954
    class Meta:
955
        ordering = ['-created']