Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (35 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
    router = models.BooleanField('Router', default=False)
317

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
406

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

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

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

    
420

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

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

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

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

    
478
    NETWORK_NAME_LENGTH = 128
479

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
608

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

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

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

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

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

    
644

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

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

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

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

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

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

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

    
720

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

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

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

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

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

    
754
    @property
755
    def ipversion(self):
756
        return self.subnet.ipversion
757

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

    
762
    def release_address(self):
763
        """Release the IPv4 address."""
764
        if self.ipversion == 4:
765
            for pool_row in self.subnet.ip_pools.all():
766
                ip_pool = pool_row.pool
767
                if ip_pool.contains(self.address):
768
                    ip_pool.put(self.address)
769
                    ip_pool.save()
770

    
771

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

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

    
785
    NETWORK_IFACE_NAME_LENGTH = 128
786

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

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

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

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

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

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

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

    
832

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

    
838

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

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

    
848
    class Meta:
849
        abstract = True
850

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

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

    
863

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

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

    
870

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

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

    
877

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

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

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

    
887

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

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

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

    
908

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

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

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

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

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

    
932

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

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

    
946
    objects = VirtualMachineDiagnosticManager()
947

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

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