Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.2 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 django.conf import settings
33
from django.db import models
34
from django.db import IntegrityError
35

    
36
from hashlib import sha1
37
from synnefo.api.faults import ServiceUnavailable
38
from synnefo.util.rapi import GanetiRapiClient
39

    
40

    
41
BACKEND_CLIENTS = {}    #{hash:Backend client}
42
BACKEND_HASHES = {}     #{Backend.id:hash}
43

    
44
def get_client(hash, backend):
45
    """Get a cached backend client or create a new one.
46

47
    @param hash: The hash of the backend
48
    @param backend: Either a backend object or backend ID
49
    """
50

    
51
    if backend is None:
52
        raise Exception("Backend is None. Cannot create a client.")
53

    
54
    if hash in BACKEND_CLIENTS:
55
        # Return cached client
56
        return BACKEND_CLIENTS[hash]
57

    
58
    # Always get a new instance to ensure latest credentials
59
    if isinstance(backend, Backend):
60
        backend = backend.id
61
    (credentials,) = Backend.objects.filter(id=backend).values_list('hash',
62
                                'clustername', 'port', 'username', 'password')
63

    
64
    hash, clustername, port, user, password = credentials
65

    
66
    # Check client for updated hash
67
    if hash in BACKEND_CLIENTS:
68
        return BACKEND_CLIENTS[hash]
69

    
70
    # Delete old version of the client
71
    if backend in BACKEND_HASHES:
72
        del BACKEND_CLIENTS[BACKEND_HASHES[backend]]
73

    
74
    # Create the new client
75
    client = GanetiRapiClient(clustername, port, user, password)
76

    
77
    # Store the client and the hash
78
    BACKEND_CLIENTS[hash] = client
79
    BACKEND_HASHES[backend] = hash
80

    
81
    return client
82

    
83

    
84
def clear_client_cache():
85
    BACKEND_CLIENTS.clear()
86
    BACKEND_HASHES.clear()
87

    
88

    
89
class Flavor(models.Model):
90
    cpu = models.IntegerField('Number of CPUs', default=0)
91
    ram = models.IntegerField('RAM size in MiB', default=0)
92
    disk = models.IntegerField('Disk size in GiB', default=0)
93
    disk_template = models.CharField('Disk template', max_length=32,
94
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
95
    deleted = models.BooleanField('Deleted', default=False)
96

    
97
    class Meta:
98
        verbose_name = u'Virtual machine flavor'
99
        unique_together = ('cpu', 'ram', 'disk', 'disk_template')
100

    
101
    @property
102
    def name(self):
103
        """Returns flavor name (generated)"""
104
        return u'C%dR%dD%d' % (self.cpu, self.ram, self.disk)
105

    
106
    def __unicode__(self):
107
        return self.name
108

    
109

    
110
class BackendQuerySet(models.query.QuerySet):
111
    def delete(self):
112
        for backend in self._clone():
113
            backend.delete()
114

    
115
class ProtectDeleteManager(models.Manager):
116
    def get_query_set(self):
117
        return BackendQuerySet(self.model, using=self._db)
118

    
119

    
120
class Backend(models.Model):
121
    clustername = models.CharField('Cluster Name', max_length=128, unique=True)
122
    port = models.PositiveIntegerField('Port', default=5080)
123
    username = models.CharField('Username', max_length=64, blank=True,
124
                                null=True)
125
    password = models.CharField('Password', max_length=64, blank=True,
126
                                null=True)
127
    # Sha1 is up to 40 characters long
128
    hash = models.CharField('Hash', max_length=40, editable=False, null=False)
129
    drained = models.BooleanField('Drained', default=False, null=False)
130
    offline = models.BooleanField('Offline', default=False, null=False)
131
    # Last refresh of backend resources
132
    updated = models.DateTimeField(auto_now_add=True)
133
    # Backend resources
134
    mfree = models.PositiveIntegerField('Free Memory', default=0, null=False)
135
    mtotal = models.PositiveIntegerField('Total Memory', default=0, null=False)
136
    dfree = models.PositiveIntegerField('Free Disk', default=0, null=False)
137
    dtotal = models.PositiveIntegerField('Total Disk', default=0, null=False)
138
    pinst_cnt = models.PositiveIntegerField('Primary Instances', default=0,
139
                                            null=False)
140
    ctotal = models.PositiveIntegerField('Total number of logical processors',
141
                                         default=0, null=False)
142
    # Custom object manager to protect from cascade delete
143
    objects = ProtectDeleteManager()
144

    
145
    class Meta:
146
        verbose_name = u'Backend'
147
        ordering = ["clustername"]
148

    
149
    def __unicode__(self):
150
        return self.clustername
151

    
152
    @property
153
    def backend_id(self):
154
        return self.id
155

    
156
    @property
157
    def client(self):
158
        """Get or create a client. """
159
        if not self.offline:
160
            return get_client(self.hash, self)
161
        else:
162
            raise ServiceUnavailable
163

    
164
    def create_hash(self):
165
        """Create a hash for this backend. """
166
        return sha1('%s%s%s%s' % \
167
                (self.clustername, self.port, self.username, self.password)) \
168
                .hexdigest()
169

    
170
    def save(self, *args, **kwargs):
171
        # Create a new hash each time a Backend is saved
172
        old_hash = self.hash
173
        self.hash = self.create_hash()
174
        super(Backend, self).save(*args, **kwargs)
175
        if self.hash != old_hash:
176
            # Populate the new hash to the new instances
177
            self.virtual_machines.filter(deleted=False).update(backend_hash=self.hash)
178

    
179
    def delete(self, *args, **kwargs):
180
        # Integrity Error if non-deleted VMs are associated with Backend
181
        if self.virtual_machines.filter(deleted=False).count():
182
            raise IntegrityError("Non-deleted virtual machines are associated "
183
                                 "with backend: %s" % self)
184
        else:
185
            # ON_DELETE = SET NULL
186
            self.virtual_machines.all().backend=None
187
            super(Backend, self).delete(*args, **kwargs)
188

    
189

    
190
class VirtualMachine(models.Model):
191
    # The list of possible actions for a VM
192
    ACTIONS = (
193
       ('CREATE', 'Create VM'),
194
       ('START', 'Start VM'),
195
       ('STOP', 'Shutdown VM'),
196
       ('SUSPEND', 'Admin Suspend VM'),
197
       ('REBOOT', 'Reboot VM'),
198
       ('DESTROY', 'Destroy VM')
199
    )
200

    
201
    # The internal operating state of a VM
202
    OPER_STATES = (
203
        ('BUILD', 'Queued for creation'),
204
        ('ERROR', 'Creation failed'),
205
        ('STOPPED', 'Stopped'),
206
        ('STARTED', 'Started'),
207
        ('DESTROYED', 'Destroyed')
208
    )
209

    
210
    # The list of possible operations on the backend
211
    BACKEND_OPCODES = (
212
        ('OP_INSTANCE_CREATE', 'Create Instance'),
213
        ('OP_INSTANCE_REMOVE', 'Remove Instance'),
214
        ('OP_INSTANCE_STARTUP', 'Startup Instance'),
215
        ('OP_INSTANCE_SHUTDOWN', 'Shutdown Instance'),
216
        ('OP_INSTANCE_REBOOT', 'Reboot Instance'),
217

    
218
        # These are listed here for completeness,
219
        # and are ignored for the time being
220
        ('OP_INSTANCE_SET_PARAMS', 'Set Instance Parameters'),
221
        ('OP_INSTANCE_QUERY_DATA', 'Query Instance Data'),
222
        ('OP_INSTANCE_REINSTALL', 'Reinstall Instance'),
223
        ('OP_INSTANCE_ACTIVATE_DISKS', 'Activate Disks'),
224
        ('OP_INSTANCE_DEACTIVATE_DISKS', 'Deactivate Disks'),
225
        ('OP_INSTANCE_REPLACE_DISKS', 'Replace Disks'),
226
        ('OP_INSTANCE_MIGRATE', 'Migrate Instance'),
227
        ('OP_INSTANCE_CONSOLE', 'Get Instance Console'),
228
        ('OP_INSTANCE_RECREATE_DISKS', 'Recreate Disks'),
229
        ('OP_INSTANCE_FAILOVER', 'Failover Instance')
230
    )
231

    
232
    # A backend job may be in one of the following possible states
233
    BACKEND_STATUSES = (
234
        ('queued', 'request queued'),
235
        ('waiting', 'request waiting for locks'),
236
        ('canceling', 'request being canceled'),
237
        ('running', 'request running'),
238
        ('canceled', 'request canceled'),
239
        ('success', 'request completed successfully'),
240
        ('error', 'request returned error')
241
    )
242

    
243
    # The operating state of a VM,
244
    # upon the successful completion of a backend operation.
245
    # IMPORTANT: Make sure all keys have a corresponding
246
    # entry in BACKEND_OPCODES if you update this field, see #1035, #1111.
247
    OPER_STATE_FROM_OPCODE = {
248
        'OP_INSTANCE_CREATE': 'STARTED',
249
        'OP_INSTANCE_REMOVE': 'DESTROYED',
250
        'OP_INSTANCE_STARTUP': 'STARTED',
251
        'OP_INSTANCE_SHUTDOWN': 'STOPPED',
252
        'OP_INSTANCE_REBOOT': 'STARTED',
253
        'OP_INSTANCE_SET_PARAMS': None,
254
        'OP_INSTANCE_QUERY_DATA': None,
255
        'OP_INSTANCE_REINSTALL': None,
256
        'OP_INSTANCE_ACTIVATE_DISKS': None,
257
        'OP_INSTANCE_DEACTIVATE_DISKS': None,
258
        'OP_INSTANCE_REPLACE_DISKS': None,
259
        'OP_INSTANCE_MIGRATE': None,
260
        'OP_INSTANCE_CONSOLE': None,
261
        'OP_INSTANCE_RECREATE_DISKS': None,
262
        'OP_INSTANCE_FAILOVER': None
263
    }
264

    
265
    # This dictionary contains the correspondence between
266
    # internal operating states and Server States as defined
267
    # by the Rackspace API.
268
    RSAPI_STATE_FROM_OPER_STATE = {
269
        "BUILD": "BUILD",
270
        "ERROR": "ERROR",
271
        "STOPPED": "STOPPED",
272
        "STARTED": "ACTIVE",
273
        "DESTROYED": "DELETED"
274
    }
275

    
276
    name = models.CharField('Virtual Machine Name', max_length=255)
277
    userid = models.CharField('User ID of the owner', max_length=100)
278
    backend = models.ForeignKey(Backend, null=True,
279
                                related_name="virtual_machines",)
280
    backend_hash = models.CharField(max_length=128, null=True, editable=False)
281
    created = models.DateTimeField(auto_now_add=True)
282
    updated = models.DateTimeField(auto_now=True)
283
    imageid = models.CharField(max_length=100, null=False)
284
    hostid = models.CharField(max_length=100)
285
    flavor = models.ForeignKey(Flavor)
286
    deleted = models.BooleanField('Deleted', default=False)
287
    suspended = models.BooleanField('Administratively Suspended',
288
                                    default=False)
289

    
290
    # VM State
291
    # The following fields are volatile data, in the sense
292
    # that they need not be persistent in the DB, but rather
293
    # get generated at runtime by quering Ganeti and applying
294
    # updates received from Ganeti.
295

    
296
    # In the future they could be moved to a separate caching layer
297
    # and removed from the database.
298
    # [vkoukis] after discussion with [faidon].
299
    action = models.CharField(choices=ACTIONS, max_length=30, null=True)
300
    operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
301
    backendjobid = models.PositiveIntegerField(null=True)
302
    backendopcode = models.CharField(choices=BACKEND_OPCODES, max_length=30,
303
                                     null=True)
304
    backendjobstatus = models.CharField(choices=BACKEND_STATUSES,
305
                                        max_length=30, null=True)
306
    backendlogmsg = models.TextField(null=True)
307
    buildpercentage = models.IntegerField(default=0)
308
    backendtime = models.DateTimeField(default=datetime.datetime.min)
309

    
310
    @property
311
    def client(self):
312
        if self.backend and not self.backend.offline:
313
            return get_client(self.backend_hash, self.backend_id)
314
        else:
315
            raise ServiceUnavailable
316

    
317
    # Error classes
318
    class InvalidBackendIdError(Exception):
319
        def __init__(self, value):
320
            self.value = value
321

    
322
        def __str__(self):
323
            return repr(self.value)
324

    
325
    class InvalidBackendMsgError(Exception):
326
        def __init__(self, opcode, status):
327
            self.opcode = opcode
328
            self.status = status
329

    
330
        def __str__(self):
331
            return repr('<opcode: %s, status: %s>' % (self.opcode,
332
                        self.status))
333

    
334
    class InvalidActionError(Exception):
335
        def __init__(self, action):
336
            self._action = action
337

    
338
        def __str__(self):
339
            return repr(str(self._action))
340

    
341
    class DeletedError(Exception):
342
        pass
343

    
344
    class BuildingError(Exception):
345
        pass
346

    
347
    def __init__(self, *args, **kw):
348
        """Initialize state for just created VM instances."""
349
        super(VirtualMachine, self).__init__(*args, **kw)
350
        # This gets called BEFORE an instance gets save()d for
351
        # the first time.
352
        if not self.pk:
353
            self.action = None
354
            self.backendjobid = None
355
            self.backendjobstatus = None
356
            self.backendopcode = None
357
            self.backendlogmsg = None
358
            self.operstate = 'BUILD'
359

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

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

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

    
377
    def __unicode__(self):
378
        return self.name
379

    
380

    
381
class VirtualMachineMetadata(models.Model):
382
    meta_key = models.CharField(max_length=50)
383
    meta_value = models.CharField(max_length=500)
384
    vm = models.ForeignKey(VirtualMachine, related_name='metadata')
385

    
386
    class Meta:
387
        unique_together = (('meta_key', 'vm'),)
388
        verbose_name = u'Key-value pair of metadata for a VM.'
389

    
390
    def __unicode__(self):
391
        return u'%s: %s' % (self.meta_key, self.meta_value)
392

    
393

    
394
class Network(models.Model):
395
    NETWORK_STATES = (
396
        ('ACTIVE', 'Active'),
397
        ('DELETED', 'Deleted')
398
    )
399

    
400
    name = models.CharField(max_length=255)
401
    created = models.DateTimeField(auto_now_add=True)
402
    updated = models.DateTimeField(auto_now=True)
403
    userid = models.CharField('User ID of the owner', max_length=100,
404
                              null=True)
405
    state = models.CharField(choices=NETWORK_STATES, max_length=30)
406
    public = models.BooleanField(default=False)
407
    link = models.ForeignKey('NetworkLink', related_name='+')
408
    machines = models.ManyToManyField(VirtualMachine,
409
                                      through='NetworkInterface')
410

    
411
    def __unicode__(self):
412
        return self.name
413

    
414

    
415
class NetworkInterface(models.Model):
416
    FIREWALL_PROFILES = (
417
        ('ENABLED', 'Enabled'),
418
        ('DISABLED', 'Disabled'),
419
        ('PROTECTED', 'Protected')
420
    )
421

    
422
    machine = models.ForeignKey(VirtualMachine, related_name='nics')
423
    network = models.ForeignKey(Network, related_name='nics')
424
    created = models.DateTimeField(auto_now_add=True)
425
    updated = models.DateTimeField(auto_now=True)
426
    index = models.IntegerField(null=True)
427
    mac = models.CharField(max_length=17, null=True)
428
    ipv4 = models.CharField(max_length=15, null=True)
429
    ipv6 = models.CharField(max_length=100, null=True)
430
    firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
431
                                        max_length=30, null=True)
432

    
433
    def __unicode__(self):
434
        return '%s@%s' % (self.machine.name, self.network.name)
435

    
436

    
437
class NetworkLink(models.Model):
438
    network = models.ForeignKey(Network, null=True, related_name='+')
439
    index = models.IntegerField()
440
    name = models.CharField(max_length=255)
441
    available = models.BooleanField(default=True)
442

    
443
    def __unicode__(self):
444
        return self.name
445

    
446
    class NotAvailable(Exception):
447
        pass
448