Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (37.6 kB)

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

    
30
import datetime
31

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

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

    
43
from synnefo.db import pools, fields
44

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

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

    
51

    
52
class Flavor(models.Model):
53
    cpu = models.IntegerField('Number of CPUs', default=0)
54
    ram = models.IntegerField('RAM size in MiB', default=0)
55
    disk = models.IntegerField('Disk size in GiB', default=0)
56
    disk_template = models.CharField('Disk template', max_length=32)
57
    deleted = models.BooleanField('Deleted', default=False)
58
    # Whether the flavor can be used to create new servers
59
    allow_create = models.BooleanField(default=True, null=False)
60

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

    
65
    @property
66
    def name(self):
67
        """Returns flavor name (generated)"""
68
        return u'C%sR%sD%s%s' % (self.cpu, self.ram, self.disk,
69
                                 self.disk_template)
70

    
71
    def __str__(self):
72
        return self.__unicode__()
73

    
74
    def __unicode__(self):
75
        return u"<%s:%s>" % (self.id, self.name)
76

    
77

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

    
109
    HYPERVISORS = (
110
        ("kvm", "Linux KVM hypervisor"),
111
        ("xen-pvm", "Xen PVM hypervisor"),
112
        ("xen-hvm", "Xen KVM hypervisor"),
113
    )
114

    
115
    class Meta:
116
        verbose_name = u'Backend'
117
        ordering = ["clustername"]
118

    
119
    def __str__(self):
120
        return self.__unicode__()
121

    
122
    def __unicode__(self):
123
        return u"%s(id:%s)" % (self.clustername, self.id)
124

    
125
    @property
126
    def backend_id(self):
127
        return self.id
128

    
129
    def get_client(self):
130
        """Get or create a client. """
131
        if self.offline:
132
            raise faults.ServiceUnavailable("Backend '%s' is offline" %
133
                                            self)
134
        return get_rapi_client(self.id, self.hash,
135
                               self.clustername,
136
                               self.port,
137
                               self.username,
138
                               self.password)
139

    
140
    @staticmethod
141
    def put_client(client):
142
            put_rapi_client(client)
143

    
144
    def create_hash(self):
145
        """Create a hash for this backend. """
146
        sha = sha1('%s%s%s%s' %
147
                   (self.clustername, self.port, self.username, self.password))
148
        return sha.hexdigest()
149

    
150
    @property
151
    def password(self):
152
        return decrypt_db_charfield(self.password_hash)
153

    
154
    @password.setter
155
    def password(self, value):
156
        self.password_hash = encrypt_db_charfield(value)
157

    
158
    def save(self, *args, **kwargs):
159
        # Create a new hash each time a Backend is saved
160
        old_hash = self.hash
161
        self.hash = self.create_hash()
162
        super(Backend, self).save(*args, **kwargs)
163
        if self.hash != old_hash:
164
            # Populate the new hash to the new instances
165
            self.virtual_machines.filter(deleted=False)\
166
                                 .update(backend_hash=self.hash)
167

    
168
    def __init__(self, *args, **kwargs):
169
        super(Backend, self).__init__(*args, **kwargs)
170
        if not self.pk:
171
            # Generate a unique index for the Backend
172
            indexes = Backend.objects.all().values_list('index', flat=True)
173
            try:
174
                first_free = [x for x in xrange(0, 16) if x not in indexes][0]
175
                self.index = first_free
176
            except IndexError:
177
                raise Exception("Cannot create more than 16 backends")
178

    
179
    def use_hotplug(self):
180
        return self.hypervisor == "kvm" and snf_settings.GANETI_USE_HOTPLUG
181

    
182
    def get_create_params(self):
183
        params = deepcopy(snf_settings.GANETI_CREATEINSTANCE_KWARGS)
184
        params["hvparams"] = params.get("hvparams", {})\
185
                                   .get(self.hypervisor, {})
186
        return params
187

    
188

    
189
# A backend job may be in one of the following possible states
190
BACKEND_STATUSES = (
191
    ('queued', 'request queued'),
192
    ('waiting', 'request waiting for locks'),
193
    ('canceling', 'request being canceled'),
194
    ('running', 'request running'),
195
    ('canceled', 'request canceled'),
196
    ('success', 'request completed successfully'),
197
    ('error', 'request returned error')
198
)
199

    
200

    
201
class QuotaHolderSerial(models.Model):
202
    """Model representing a serial for a Quotaholder Commission.
203

204
    serial:   The serial that Quotaholder assigned to this commission
205
    pending:  Whether it has been decided to accept or reject this commission
206
    accept:   If pending is False, this attribute indicates whether to accept
207
              or reject this commission
208
    resolved: Whether this commission has been accepted or rejected to
209
              Quotaholder.
210

211
    """
212
    serial = models.BigIntegerField(null=False, primary_key=True,
213
                                    db_index=True)
214
    pending = models.BooleanField(default=True, db_index=True)
215
    accept = models.BooleanField(default=False)
216
    resolved = models.BooleanField(default=False)
217

    
218
    class Meta:
219
        verbose_name = u'Quota Serial'
220
        ordering = ["serial"]
221

    
222
    def __str__(self):
223
        return self.__unicode__()
224

    
225
    def __unicode__(self):
226
        return u"<serial: %s>" % self.serial
227

    
228

    
229
class VirtualMachine(models.Model):
230
    # The list of possible actions for a VM
231
    ACTIONS = (
232
        ('CREATE', 'Create VM'),
233
        ('START', 'Start VM'),
234
        ('STOP', 'Shutdown VM'),
235
        ('SUSPEND', 'Admin Suspend VM'),
236
        ('REBOOT', 'Reboot VM'),
237
        ('DESTROY', 'Destroy VM'),
238
        ('RESIZE', 'Resize a VM'),
239
        ('ADDFLOATINGIP', 'Add floating IP to VM'),
240
        ('REMOVEFLOATINGIP', 'Add floating IP to VM'),
241
    )
242

    
243
    # The internal operating state of a VM
244
    OPER_STATES = (
245
        ('BUILD', 'Queued for creation'),
246
        ('ERROR', 'Creation failed'),
247
        ('STOPPED', 'Stopped'),
248
        ('STARTED', 'Started'),
249
        ('DESTROYED', 'Destroyed'),
250
        ('RESIZE', 'Resizing')
251
    )
252

    
253
    # The list of possible operations on the backend
254
    BACKEND_OPCODES = (
255
        ('OP_INSTANCE_CREATE', 'Create Instance'),
256
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
257
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
258
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
259
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
260

    
261
        # These are listed here for completeness,
262
        # and are ignored for the time being
263
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
264
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
265
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
266
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
267
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
268
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
269
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
270
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
271
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
272
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
273
    )
274

    
275
    # The operating state of a VM,
276
    # upon the successful completion of a backend operation.
277
    # IMPORTANT: Make sure all keys have a corresponding
278
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
279
    OPER_STATE_FROM_OPCODE = {
280
        'OP_INSTANCE_CREATE': 'STARTED',
281
        'OP_INSTANCE_REMOVE': 'DESTROYED',
282
        'OP_INSTANCE_STARTUP': 'STARTED',
283
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
284
        'OP_INSTANCE_REBOOT': 'STARTED',
285
        'OP_INSTANCE_SET_PARAMS': None,
286
        'OP_INSTANCE_QUERY_DATA': None,
287
        'OP_INSTANCE_REINSTALL': None,
288
        'OP_INSTANCE_ACTIVATE_DISKS': None,
289
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
290
        'OP_INSTANCE_REPLACE_DISKS': None,
291
        'OP_INSTANCE_MIGRATE': None,
292
        'OP_INSTANCE_CONSOLE': None,
293
        'OP_INSTANCE_RECREATE_DISKS': None,
294
        'OP_INSTANCE_FAILOVER': None
295
    }
296

    
297
    # This dictionary contains the correspondence between
298
    # internal operating states and Server States as defined
299
    # by the Rackspace API.
300
    RSAPI_STATE_FROM_OPER_STATE = {
301
        "BUILD": "BUILD",
302
        "ERROR": "ERROR",
303
        "STOPPED": "STOPPED",
304
        "STARTED": "ACTIVE",
305
        'RESIZE': 'RESIZE',
306
        'DESTROYED': 'DELETED',
307
    }
308

    
309
    VIRTUAL_MACHINE_NAME_LENGTH = 255
310

    
311
    name = models.CharField('Virtual Machine Name',
312
                            max_length=VIRTUAL_MACHINE_NAME_LENGTH)
313
    userid = models.CharField('User ID of the owner', max_length=100,
314
                              db_index=True, null=False)
315
    project = models.CharField(max_length=255, null=True)
316
    backend = models.ForeignKey(Backend, null=True,
317
                                related_name="virtual_machines",
318
                                on_delete=models.PROTECT)
319
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
320
    created = models.DateTimeField(auto_now_add=True)
321
    updated = models.DateTimeField(auto_now=True)
322
    imageid = models.CharField(max_length=100, null=False)
323
    hostid = models.CharField(max_length=100)
324
    flavor = models.ForeignKey(Flavor, on_delete=models.PROTECT)
325
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
326
    suspended = models.BooleanField('Administratively Suspended',
327
                                    default=False)
328
    serial = models.ForeignKey(QuotaHolderSerial,
329
                               related_name='virtual_machine', null=True,
330
                               on_delete=models.SET_NULL)
331

    
332
    # VM State
333
    # The following fields are volatile data, in the sense
334
    # that they need not be persistent in the DB, but rather
335
    # get generated at runtime by quering Ganeti and applying
336
    # updates received from Ganeti.
337

    
338
    # In the future they could be moved to a separate caching layer
339
    # and removed from the database.
340
    # [vkoukis] after discussion with [faidon].
341
    action = models.CharField(choices=ACTIONS, max_length=30, null=True,
342
                              default=None)
343
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
344
                                 null=False, default="BUILD")
345
    backendjobid = models.PositiveIntegerField(null=True)
346
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
347
                                     null=True)
348
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
349
                                        max_length=30, null=True)
350
    backendlogmsg = models.TextField(null=True)
351
    buildpercentage = models.IntegerField(default=0)
352
    backendtime = models.DateTimeField(default=datetime.datetime.min)
353

    
354
    # Latest action and corresponding Ganeti job ID, for actions issued
355
    # by the API
356
    task = models.CharField(max_length=64, null=True)
357
    task_job_id = models.BigIntegerField(null=True)
358

    
359
    def get_client(self):
360
        if self.backend:
361
            return self.backend.get_client()
362
        else:
363
            raise faults.ServiceUnavailable("VirtualMachine without backend")
364

    
365
    def get_last_diagnostic(self, **filters):
366
        try:
367
            return self.diagnostics.filter()[0]
368
        except IndexError:
369
            return None
370

    
371
    @staticmethod
372
    def put_client(client):
373
            put_rapi_client(client)
374

    
375
    def save(self, *args, **kwargs):
376
        # Store hash for first time saved vm
377
        if (self.id is None or self.backend_hash == '') and self.backend:
378
            self.backend_hash = self.backend.hash
379
        super(VirtualMachine, self).save(*args, **kwargs)
380

    
381
    @property
382
    def backend_vm_id(self):
383
        """Returns the backend id for this VM by prepending backend-prefix."""
384
        if not self.id:
385
            raise VirtualMachine.InvalidBackendIdError("self.id is None")
386
        return "%s%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
387

    
388
    class Meta:
389
        verbose_name = u'Virtual machine instance'
390
        get_latest_by = 'created'
391

    
392
    def __str__(self):
393
        return self.__unicode__()
394

    
395
    def __unicode__(self):
396
        return u"<vm:%s@backend:%s>" % (self.id, self.backend_id)
397

    
398
    # Error classes
399
    class InvalidBackendIdError(Exception):
400
        def __init__(self, value):
401
            self.value = value
402

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

    
406
    class InvalidBackendMsgError(Exception):
407
        def __init__(self, opcode, status):
408
            self.opcode = opcode
409
            self.status = status
410

    
411
        def __str__(self):
412
            return repr('<opcode: %s, status: %s>' % (self.opcode,
413
                        self.status))
414

    
415
    class InvalidActionError(Exception):
416
        def __init__(self, action):
417
            self._action = action
418

    
419
        def __str__(self):
420
            return repr(str(self._action))
421

    
422

    
423
class VirtualMachineMetadata(models.Model):
424
    meta_key = models.CharField(max_length=50)
425
    meta_value = models.CharField(max_length=500)
426
    vm = models.ForeignKey(VirtualMachine, related_name='metadata',
427
                           on_delete=models.CASCADE)
428

    
429
    class Meta:
430
        unique_together = (('meta_key', 'vm'),)
431
        verbose_name = u'Key-value pair of metadata for a VM.'
432

    
433
    def __str__(self):
434
        return self.__unicode__()
435

    
436
    def __unicode__(self):
437
        return u'<Metadata %s: %s>' % (self.meta_key, self.meta_value)
438

    
439

    
440
class Network(models.Model):
441
    OPER_STATES = (
442
        ('PENDING', 'Pending'),  # Unused because of lazy networks
443
        ('ACTIVE', 'Active'),
444
        ('DELETED', 'Deleted'),
445
        ('ERROR', 'Error')
446
    )
447

    
448
    ACTIONS = (
449
        ('CREATE', 'Create Network'),
450
        ('DESTROY', 'Destroy Network'),
451
        ('ADD', 'Add server to Network'),
452
        ('REMOVE', 'Remove server from Network'),
453
    )
454

    
455
    RSAPI_STATE_FROM_OPER_STATE = {
456
        'PENDING': 'PENDING',
457
        'ACTIVE': 'ACTIVE',
458
        'DELETED': 'DELETED',
459
        'ERROR': 'ERROR'
460
    }
461

    
462
    FLAVORS = {
463
        'CUSTOM': {
464
            'mode': 'bridged',
465
            'link': settings.DEFAULT_BRIDGE,
466
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
467
            'tags': None,
468
            'desc': "Basic flavor used for a bridged network",
469
        },
470
        'IP_LESS_ROUTED': {
471
            'mode': 'routed',
472
            'link': None,
473
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
474
            'tags': 'ip-less-routed',
475
            'desc': "Flavor used for an IP-less routed network using"
476
                    " Proxy ARP",
477
        },
478
        'MAC_FILTERED': {
479
            'mode': 'bridged',
480
            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
481
            'mac_prefix': 'pool',
482
            'tags': 'private-filtered',
483
            'desc': "Flavor used for bridged networks that offer isolation"
484
                    " via filtering packets based on their src "
485
                    " MAC (ebtables)",
486
        },
487
        'PHYSICAL_VLAN': {
488
            'mode': 'bridged',
489
            'link': 'pool',
490
            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
491
            'tags': 'physical-vlan',
492
            'desc': "Flavor used for bridged network that offer isolation"
493
                    " via dedicated physical vlan",
494
        },
495
    }
496

    
497
    NETWORK_NAME_LENGTH = 128
498

    
499
    name = models.CharField('Network Name', max_length=NETWORK_NAME_LENGTH)
500
    userid = models.CharField('User ID of the owner', max_length=128,
501
                              null=True, db_index=True)
502
    project = models.CharField(max_length=255, null=True)
503
    flavor = models.CharField('Flavor', max_length=32, null=False)
504
    mode = models.CharField('Network Mode', max_length=16, null=True)
505
    link = models.CharField('Network Link', max_length=32, null=True)
506
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
507
    tags = models.CharField('Network Tags', max_length=128, null=True)
508
    public = models.BooleanField(default=False, db_index=True)
509
    created = models.DateTimeField(auto_now_add=True)
510
    updated = models.DateTimeField(auto_now=True)
511
    deleted = models.BooleanField('Deleted', default=False, db_index=True)
512
    state = models.CharField(choices=OPER_STATES, max_length=32,
513
                             default='PENDING')
514
    machines = models.ManyToManyField(VirtualMachine,
515
                                      through='NetworkInterface')
516
    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
517
                              default=None)
518
    drained = models.BooleanField("Drained", default=False, null=False)
519
    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
520
                                           default=False)
521
    external_router = models.BooleanField(default=False)
522
    serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
523
                               null=True, on_delete=models.SET_NULL)
524

    
525
    def __str__(self):
526
        return self.__unicode__()
527

    
528
    def __unicode__(self):
529
        return u"<Network: %s>" % self.id
530

    
531
    @property
532
    def backend_id(self):
533
        """Return the backend id by prepending backend-prefix."""
534
        if not self.id:
535
            raise Network.InvalidBackendIdError("self.id is None")
536
        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
537

    
538
    @property
539
    def backend_tag(self):
540
        """Return the network tag to be used in backend
541

542
        """
543
        if self.tags:
544
            return self.tags.split(',')
545
        else:
546
            return []
547

    
548
    def create_backend_network(self, backend=None):
549
        """Create corresponding BackendNetwork entries."""
550

    
551
        backends = [backend] if backend else\
552
            Backend.objects.filter(offline=False)
553
        for backend in backends:
554
            backend_exists =\
555
                BackendNetwork.objects.filter(backend=backend, network=self)\
556
                                      .exists()
557
            if not backend_exists:
558
                BackendNetwork.objects.create(backend=backend, network=self)
559

    
560
    def get_ip_pools(self, locked=True):
561
        subnets = self.subnets.filter(ipversion=4, deleted=False)\
562
                              .prefetch_related("ip_pools")
563
        return [ip_pool for subnet in subnets
564
                for ip_pool in subnet.get_ip_pools(locked=locked)]
565

    
566
    def reserve_address(self, address, external=False):
567
        for ip_pool in self.get_ip_pools():
568
            if ip_pool.contains(address):
569
                ip_pool.reserve(address, external=external)
570
                ip_pool.save()
571
                return
572
        raise pools.InvalidValue("Network %s does not have an IP pool that"
573
                                 " contains address %s" % (self, address))
574

    
575
    def release_address(self, address, external=False):
576
        for ip_pool in self.get_ip_pools():
577
            if ip_pool.contains(address):
578
                ip_pool.put(address, external=external)
579
                ip_pool.save()
580
                return
581
        raise pools.InvalidValue("Network %s does not have an IP pool that"
582
                                 " contains address %s" % (self, address))
583

    
584
    @property
585
    def subnet4(self):
586
        return self.get_subnet(version=4)
587

    
588
    @property
589
    def subnet6(self):
590
        return self.get_subnet(version=6)
591

    
592
    def get_subnet(self, version=4):
593
        for subnet in self.subnets.all():
594
            if subnet.ipversion == version:
595
                return subnet
596
        return None
597

    
598
    def ip_count(self):
599
        """Return the total and free IPv4 addresses of the network."""
600
        total, free = 0, 0
601
        ip_pools = self.get_ip_pools(locked=False)
602
        for ip_pool in ip_pools:
603
            total += ip_pool.pool_size
604
            free += ip_pool.count_available()
605
        return total, free
606

    
607
    class InvalidBackendIdError(Exception):
608
        def __init__(self, value):
609
            self.value = value
610

    
611
        def __str__(self):
612
            return repr(self.value)
613

    
614
    class InvalidBackendMsgError(Exception):
615
        def __init__(self, opcode, status):
616
            self.opcode = opcode
617
            self.status = status
618

    
619
        def __str__(self):
620
            return repr('<opcode: %s, status: %s>'
621
                        % (self.opcode, self.status))
622

    
623
    class InvalidActionError(Exception):
624
        def __init__(self, action):
625
            self._action = action
626

    
627
        def __str__(self):
628
            return repr(str(self._action))
629

    
630

    
631
class Subnet(models.Model):
632
    SUBNET_NAME_LENGTH = 128
633

    
634
    userid = models.CharField('User ID of the owner', max_length=128,
635
                              null=True, db_index=True)
636
    public = models.BooleanField(default=False, db_index=True)
637

    
638
    network = models.ForeignKey('Network', null=False, db_index=True,
639
                                related_name="subnets",
640
                                on_delete=models.PROTECT)
641
    name = models.CharField('Subnet Name', max_length=SUBNET_NAME_LENGTH,
642
                            null=True, default="")
643
    ipversion = models.IntegerField('IP Version', default=4, null=False)
644
    cidr = models.CharField('Subnet', max_length=64, null=False)
645
    gateway = models.CharField('Gateway', max_length=64, null=True)
646
    dhcp = models.BooleanField('DHCP', default=True, null=False)
647
    deleted = models.BooleanField('Deleted', default=False, db_index=True,
648
                                  null=False)
649
    host_routes = fields.SeparatedValuesField('Host Routes', null=True)
650
    dns_nameservers = fields.SeparatedValuesField('DNS Nameservers', null=True)
651
    created = models.DateTimeField(auto_now_add=True)
652
    updated = models.DateTimeField(auto_now=True)
653

    
654
    def __str__(self):
655
        return self.__unicode__()
656

    
657
    def __unicode__(self):
658
        msg = u"<Subnet %s, Network: %s, CIDR: %s>"
659
        return msg % (self.id, self.network_id, self.cidr)
660

    
661
    def get_ip_pools(self, locked=True):
662
        ip_pools = self.ip_pools
663
        if locked:
664
            ip_pools = ip_pools.select_for_update()
665
        return map(lambda ip_pool: ip_pool.pool, ip_pools.all())
666

    
667

    
668
class BackendNetwork(models.Model):
669
    OPER_STATES = (
670
        ('PENDING', 'Pending'),
671
        ('ACTIVE', 'Active'),
672
        ('DELETED', 'Deleted'),
673
        ('ERROR', 'Error')
674
    )
675

    
676
    # The list of possible operations on the backend
677
    BACKEND_OPCODES = (
678
        ('OP_NETWORK_ADD', 'Create Network'),
679
        ('OP_NETWORK_CONNECT', 'Activate Network'),
680
        ('OP_NETWORK_DISCONNECT', 'Deactivate Network'),
681
        ('OP_NETWORK_REMOVE', 'Remove Network'),
682
        # These are listed here for completeness,
683
        # and are ignored for the time being
684
        ('OP_NETWORK_SET_PARAMS', 'Set Network Parameters'),
685
        ('OP_NETWORK_QUERY_DATA', 'Query Network Data')
686
    )
687

    
688
    # The operating state of a Netowork,
689
    # upon the successful completion of a backend operation.
690
    # IMPORTANT: Make sure all keys have a corresponding
691
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
692
    OPER_STATE_FROM_OPCODE = {
693
        'OP_NETWORK_ADD': 'PENDING',
694
        'OP_NETWORK_CONNECT': 'ACTIVE',
695
        'OP_NETWORK_DISCONNECT': 'PENDING',
696
        'OP_NETWORK_REMOVE': 'DELETED',
697
        'OP_NETWORK_SET_PARAMS': None,
698
        'OP_NETWORK_QUERY_DATA': None
699
    }
700

    
701
    network = models.ForeignKey(Network, related_name='backend_networks',
702
                                on_delete=models.PROTECT)
703
    backend = models.ForeignKey(Backend, related_name='networks',
704
                                on_delete=models.PROTECT)
705
    created = models.DateTimeField(auto_now_add=True)
706
    updated = models.DateTimeField(auto_now=True)
707
    deleted = models.BooleanField('Deleted', default=False)
708
    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
709
    operstate = models.CharField(choices=OPER_STATES, max_length=30,
710
                                 default='PENDING')
711
    backendjobid = models.PositiveIntegerField(null=True)
712
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
713
                                     null=True)
714
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
715
                                        max_length=30, null=True)
716
    backendlogmsg = models.TextField(null=True)
717
    backendtime = models.DateTimeField(null=False,
718
                                       default=datetime.datetime.min)
719

    
720
    class Meta:
721
        # Ensure one entry for each network in each backend
722
        unique_together = (("network", "backend"))
723

    
724
    def __init__(self, *args, **kwargs):
725
        """Initialize state for just created BackendNetwork instances."""
726
        super(BackendNetwork, self).__init__(*args, **kwargs)
727
        if not self.mac_prefix:
728
            # Generate the MAC prefix of the BackendNetwork, by combining
729
            # the Network prefix with the index of the Backend
730
            net_prefix = self.network.mac_prefix
731
            backend_suffix = hex(self.backend.index).replace('0x', '')
732
            mac_prefix = net_prefix + backend_suffix
733
            try:
734
                utils.validate_mac(mac_prefix + ":00:00:00")
735
            except utils.InvalidMacAddress:
736
                raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" %
737
                                              mac_prefix)
738
            self.mac_prefix = mac_prefix
739

    
740
    def __str__(self):
741
        return self.__unicode__()
742

    
743
    def __unicode__(self):
744
        return u'<BackendNetwork %s@%s>' % (self.network, self.backend)
745

    
746

    
747
class IPAddress(models.Model):
748
    subnet = models.ForeignKey("Subnet", related_name="ips", null=False,
749
                               on_delete=models.PROTECT)
750
    network = models.ForeignKey(Network, related_name="ips", null=False,
751
                                on_delete=models.PROTECT)
752
    nic = models.ForeignKey("NetworkInterface", related_name="ips", null=True,
753
                            on_delete=models.SET_NULL)
754
    userid = models.CharField("UUID of the owner", max_length=128, null=False,
755
                              db_index=True)
756
    project = models.CharField(max_length=255, null=True)
757
    address = models.CharField("IP Address", max_length=64, null=False)
758
    floating_ip = models.BooleanField("Floating IP", null=False, default=False)
759
    ipversion = models.IntegerField("IP Version", null=False)
760
    created = models.DateTimeField(auto_now_add=True)
761
    updated = models.DateTimeField(auto_now=True)
762
    deleted = models.BooleanField(default=False, null=False)
763

    
764
    serial = models.ForeignKey(QuotaHolderSerial,
765
                               related_name="ips", null=True,
766
                               on_delete=models.SET_NULL)
767

    
768
    def __str__(self):
769
        return self.__unicode__()
770

    
771
    def __unicode__(self):
772
        ip_type = "floating" if self.floating_ip else "static"
773
        return u"<IPAddress: %s, Network: %s, Subnet: %s, Type: %s>"\
774
               % (self.address, self.network_id, self.subnet_id, ip_type)
775

    
776
    def in_use(self):
777
        if self.nic is None or self.nic.machine is None:
778
            return False
779
        else:
780
            return (not self.nic.machine.deleted)
781

    
782
    class Meta:
783
        unique_together = ("network", "address", "deleted")
784

    
785
    @property
786
    def public(self):
787
        return self.network.public
788

    
789
    def release_address(self):
790
        """Release the IPv4 address."""
791
        if self.ipversion == 4:
792
            for pool_row in self.subnet.ip_pools.all():
793
                ip_pool = pool_row.pool
794
                if ip_pool.contains(self.address):
795
                    ip_pool.put(self.address)
796
                    ip_pool.save()
797
                    return
798
            log.error("Cannot release address %s of NIC %s. Address does not"
799
                      " belong to any of the IP pools of the subnet %s !",
800
                      self.address, self.nic, self.subnet_id)
801

    
802

    
803
class IPAddressLog(models.Model):
804
    address = models.CharField("IP Address", max_length=64, null=False,
805
                               db_index=True)
806
    server_id = models.IntegerField("Server", null=False)
807
    network_id = models.IntegerField("Network", null=False)
808
    allocated_at = models.DateTimeField("Datetime IP allocated to server",
809
                                        auto_now_add=True)
810
    released_at = models.DateTimeField("Datetime IP released from server",
811
                                       null=True)
812
    active = models.BooleanField("Whether IP still allocated to server",
813
                                 default=True)
814

    
815
    def __str__(self):
816
        return self.__unicode__()
817

    
818
    def __unicode__(self):
819
        return u"<Address: %s, Server: %s, Network: %s, Allocated at: %s>"\
820
               % (self.address, self.network_id, self.server_id,
821
                  self.allocated_at)
822

    
823

    
824
class NetworkInterface(models.Model):
825
    FIREWALL_PROFILES = (
826
        ('ENABLED', 'Enabled'),
827
        ('DISABLED', 'Disabled'),
828
        ('PROTECTED', 'Protected')
829
    )
830

    
831
    STATES = (
832
        ("ACTIVE", "Active"),
833
        ("BUILD", "Building"),
834
        ("ERROR", "Error"),
835
        ("DOWN", "Down"),
836
    )
837

    
838
    NETWORK_IFACE_NAME_LENGTH = 128
839

    
840
    name = models.CharField('NIC name', max_length=NETWORK_IFACE_NAME_LENGTH,
841
                            null=True, default="")
842
    userid = models.CharField("UUID of the owner", max_length=128,
843
                              null=False, db_index=True)
844
    machine = models.ForeignKey(VirtualMachine, related_name='nics',
845
                                on_delete=models.PROTECT, null=True)
846
    network = models.ForeignKey(Network, related_name='nics',
847
                                on_delete=models.PROTECT)
848
    created = models.DateTimeField(auto_now_add=True)
849
    updated = models.DateTimeField(auto_now=True)
850
    index = models.IntegerField(null=True)
851
    mac = models.CharField(max_length=32, null=True, unique=True)
852
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
853
                                        max_length=30, null=True)
854
    security_groups = models.ManyToManyField("SecurityGroup", null=True)
855
    state = models.CharField(max_length=32, null=False, default="ACTIVE",
856
                             choices=STATES)
857
    device_owner = models.CharField('Device owner', max_length=128, null=True)
858

    
859
    def __str__(self):
860
        return self.__unicode__()
861

    
862
    def __unicode__(self):
863
        return u"<%s:vm:%s network:%s>" % (self.id, self.machine_id,
864
                                           self.network_id)
865

    
866
    @property
867
    def backend_uuid(self):
868
        """Return the backend id by prepending backend-prefix."""
869
        return u"%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
870

    
871
    @property
872
    def ipv4_address(self):
873
        return self.get_ip_address(version=4)
874

    
875
    @property
876
    def ipv6_address(self):
877
        return self.get_ip_address(version=6)
878

    
879
    def get_ip_address(self, version=4):
880
        for ip in self.ips.all():
881
            if ip.ipversion == version:
882
                return ip.address
883
        return None
884

    
885
    def get_ip_addresses_subnets(self):
886
        return self.ips.values_list("address", "subnet__id")
887

    
888

    
889
class SecurityGroup(models.Model):
890
    SECURITY_GROUP_NAME_LENGTH = 128
891
    name = models.CharField('group name',
892
                            max_length=SECURITY_GROUP_NAME_LENGTH)
893

    
894
    @property
895
    def backend_uuid(self):
896
        """Return the name of NIC in Ganeti."""
897
        return "%snic-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
898

    
899

    
900
class PoolTable(models.Model):
901
    available_map = models.TextField(default="", null=False)
902
    reserved_map = models.TextField(default="", null=False)
903
    size = models.IntegerField(null=False)
904

    
905
    # Optional Fields
906
    base = models.CharField(null=True, max_length=32)
907
    offset = models.IntegerField(null=True)
908

    
909
    class Meta:
910
        abstract = True
911

    
912
    @classmethod
913
    def get_pool(cls):
914
        try:
915
            pool_row = cls.objects.select_for_update().get()
916
            return pool_row.pool
917
        except cls.DoesNotExist:
918
            raise pools.EmptyPool
919

    
920
    @property
921
    def pool(self):
922
        return self.manager(self)
923

    
924

    
925
class BridgePoolTable(PoolTable):
926
    manager = pools.BridgePool
927

    
928
    def __str__(self):
929
        return self.__unicode__()
930

    
931
    def __unicode__(self):
932
        return u"<BridgePool id:%s>" % self.id
933

    
934

    
935
class MacPrefixPoolTable(PoolTable):
936
    manager = pools.MacPrefixPool
937

    
938
    def __str__(self):
939
        return self.__unicode__()
940

    
941
    def __unicode__(self):
942
        return u"<MACPrefixPool id:%s>" % self.id
943

    
944

    
945
class IPPoolTable(PoolTable):
946
    manager = pools.IPPool
947

    
948
    subnet = models.ForeignKey('Subnet', related_name="ip_pools",
949
                               on_delete=models.PROTECT,
950
                               db_index=True, null=True)
951

    
952
    def __str__(self):
953
        return self.__unicode__()
954

    
955
    def __unicode__(self):
956
        return u"<IPv4AdressPool, Subnet: %s>" % self.subnet_id
957

    
958

    
959
@contextmanager
960
def pooled_rapi_client(obj):
961
        if isinstance(obj, (VirtualMachine, BackendNetwork)):
962
            backend = obj.backend
963
        else:
964
            backend = obj
965

    
966
        if backend.offline:
967
            log.warning("Trying to connect with offline backend: %s", backend)
968
            raise faults.ServiceUnavailable("Cannot connect to offline"
969
                                            " backend: %s" % backend)
970

    
971
        b = backend
972
        client = get_rapi_client(b.id, b.hash, b.clustername, b.port,
973
                                 b.username, b.password)
974
        try:
975
            yield client
976
        finally:
977
            put_rapi_client(client)
978

    
979

    
980
class VirtualMachineDiagnosticManager(models.Manager):
981
    """
982
    Custom manager for :class:`VirtualMachineDiagnostic` model.
983
    """
984

    
985
    # diagnostic creation helpers
986
    def create_for_vm(self, vm, level, message, **kwargs):
987
        attrs = {'machine': vm, 'level': level, 'message': message}
988
        attrs.update(kwargs)
989
        # update instance updated time
990
        self.create(**attrs)
991
        vm.save()
992

    
993
    def create_error(self, vm, **kwargs):
994
        self.create_for_vm(vm, 'ERROR', **kwargs)
995

    
996
    def create_debug(self, vm, **kwargs):
997
        self.create_for_vm(vm, 'DEBUG', **kwargs)
998

    
999
    def since(self, vm, created_since, **kwargs):
1000
        return self.get_query_set().filter(vm=vm, created__gt=created_since,
1001
                                           **kwargs)
1002

    
1003

    
1004
class VirtualMachineDiagnostic(models.Model):
1005
    """
1006
    Model to store backend information messages that relate to the state of
1007
    the virtual machine.
1008
    """
1009

    
1010
    TYPES = (
1011
        ('ERROR', 'Error'),
1012
        ('WARNING', 'Warning'),
1013
        ('INFO', 'Info'),
1014
        ('DEBUG', 'Debug'),
1015
    )
1016

    
1017
    objects = VirtualMachineDiagnosticManager()
1018

    
1019
    created = models.DateTimeField(auto_now_add=True)
1020
    machine = models.ForeignKey('VirtualMachine', related_name="diagnostics",
1021
                                on_delete=models.CASCADE)
1022
    level = models.CharField(max_length=20, choices=TYPES)
1023
    source = models.CharField(max_length=100)
1024
    source_date = models.DateTimeField(null=True)
1025
    message = models.CharField(max_length=255)
1026
    details = models.TextField(null=True)
1027

    
1028
    class Meta:
1029
        ordering = ['-created']