Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ 5abebfaa

History | View | Annotate | Download (38.5 kB)

1
# vim: ts=4 sts=4 et ai sw=4 fileencoding=utf-8
2
#
3
# Copyright © 2010 Greek Research and Technology Network
4

    
5
from django.conf import settings
6
from django.utils import simplejson as json
7
from django.http import HttpResponse
8
from piston.handler import BaseHandler, AnonymousBaseHandler
9
from synnefo.api.faults import fault, noContent, accepted, created, notModified
10
from synnefo.api.helpers import instance_to_server, paginator
11
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError, CertificateError
12
from synnefo.db.models import *
13
from time import sleep
14
import random
15
import string
16
import logging
17
from datetime import datetime, timedelta
18

    
19
from logic import backend, utils
20

    
21
log = logging.getLogger('synnefo.api.handlers')
22

    
23
try:
24
    rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
25
    rapi.GetVersion()
26
except Exception, e:
27
    log.exception('Unexpected error: %s' % e)
28
    raise fault.serviceUnavailable
29
#If we can't connect to the rapi successfully, don't do anything
30

    
31
VERSIONS = [
32
    {
33
        "status": "CURRENT",
34
        "id": "v1.0",
35
        "docURL" : "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20110112.pdf",
36
        "wadl" : "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl"
37
    },
38
    {
39
        "status": "CURRENT",
40
        "id": "v1.1",
41
        "docURL" : "http://docs.openstack.org/openstack-compute/developer/content/",
42
        "wadl" : "None yet"
43
    },
44
    {
45
        "status": "CURRENT",
46
        "id": "v1.0grnet1",
47
        "docURL" : "None yet",
48
        "wad1" : "None yet"
49
    }
50
]
51

    
52
#read is called on GET requests
53
#create is called on POST, and creates new objects
54
#update is called on PUT, and should update an existing product
55
#delete is called on DELETE, and should delete an existing object
56

    
57

    
58
class VersionHandler(AnonymousBaseHandler):
59
    allowed_methods = ('GET',)
60

    
61
    def read(self, request, number=None):
62
        try:
63
            if number is None:
64
                versions = map(lambda v: {
65
                            "status": v["status"],
66
                            "id": v["id"],
67
                        }, VERSIONS)
68
                return { "versions": versions }
69
            else:
70
                for version in VERSIONS:
71
                    if version["id"] == number:
72
                        return { "version": version }
73
                raise fault.itemNotFound
74
        except Exception, e:
75
            log.exception('Unexpected error: %s' % e)
76
            raise fault.serviceUnavailable
77

    
78

    
79
class ServerHandler(BaseHandler):
80
    """Handler responsible for the Servers
81

82
     handles the listing of Virtual Machines, Creates and Destroys VM's
83

84
     @HTTP methods: POST, DELETE, PUT, GET
85
     @Parameters: POST data with the create data (cpu, ram, etc)
86
     @Responses: HTTP 200 if successfully call rapi, 304 if not modified, itemNotFound or serviceUnavailable otherwise
87

88
    """
89
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
90

    
91
    def read(self, request, id=None):
92
        from time import sleep
93
        sleep(0.5)
94
        #TODO: delete the sleep once the mock objects are removed
95
        if id is None:
96
            return self.read_all(request)
97
        elif id == "detail":
98
            return self.read_all(request, detail=True)
99
        else:
100
            return self.read_one(request, id)
101

    
102
    def read_one(self, request, id):
103
        try:
104
            server = VirtualMachine.objects.get(id=id)
105

    
106
            server = {'status': utils.get_rsapi_state(server),
107
                     'flavorRef': server.flavor.id, 
108
                     'name': server.name, 
109
                     'id': server.id, 
110
                     'imageRef': server.sourceimage.id,
111
                     'created': server.created, 
112
                     'updated': server.updated,
113
                     'hostId': server.hostid, 
114
                     'progress': utils.get_rsapi_state(server) == 'ACTIVE' and 100 or 0,
115
                     #'metadata': {'Server_Label': server.description },
116
                     'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
117
                     'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
118
                    }
119
            return { "server": server } 
120
        except VirtualMachine.DoesNotExist:
121
            raise fault.itemNotFound
122
        except VirtualMachine.MultipleObjectsReturned:
123
            raise fault.serviceUnavailable
124
        except Exception, e:
125
            log.exception('Unexpected error: %s' % e)
126
            raise fault.serviceUnavailable
127

    
128

    
129
    @paginator
130
    def read_all(self, request, detail=False):
131
        #changes_since should be on ISO 8601 format
132
        try:
133
            changes_since = request.GET.get("changes-since", 0)
134
            if changes_since:
135
                last_update = datetime.strptime(changes_since, "%Y-%m-%dT%H:%M:%S" )
136
                #return a badRequest if the changes_since is older than a limit
137
                if datetime.now() - last_update > timedelta(seconds=settings.POLL_LIMIT):
138
                    raise fault.badRequest        
139
                virtual_servers = VirtualMachine.objects.filter(updated__gt=last_update)
140
                if not len(virtual_servers):
141
                    return notModified
142
            else:
143
                virtual_servers = VirtualMachine.objects.filter(deleted=False)
144
            #get all VM's for now, FIX it to take the user's VMs only yet. also don't get deleted VM's
145
        except Exception, e:
146
            raise fault.badRequest        
147
        try:
148
            if not detail:
149
                return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] }
150
            else:
151
                virtual_servers_list = [{'status': utils.get_rsapi_state(server),
152
                                         'flavorRef': server.flavor.id, 
153
                                         'name': server.name, 
154
                                         'id': server.id, 
155
                                         'created': server.created, 
156
                                         'updated': server.updated,
157
                                         'imageRef': server.sourceimage.id, 
158
                                         'hostId': server.hostid, 
159
                                         'progress': utils.get_rsapi_state(server) == 'ACTIVE' and 100 or 0,
160
                                         #'metadata': {'Server_Label': server.description },
161
                                         'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
162
                                         'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
163

    
164
                                        } for server in virtual_servers]
165
                #pass some fake data regarding ip, since we don't have any such data            
166
                return { "servers":  virtual_servers_list }                
167
        except Exception, e:
168
            log.exception('Unexpected error: %s' % e)
169
            raise fault.serviceUnavailable
170

    
171

    
172
    def create(self, request):
173
        """ Parse RackSpace API create request to generate rapi create request
174
        
175
            TODO: auto generate and set password
176
        """
177
        # Check if we have all the necessary data in the JSON request       
178
        try:
179
            server = json.loads(request.raw_post_data)['server']
180
            name = server['name']
181
            flavorRef = server['flavorRef']
182
            flavor = Flavor.objects.get(id=flavorRef)
183
            imageRef = server['imageRef']
184
            image = Image.objects.get(id=imageRef)
185
            metadata = server['metadata']
186
            personality = server.get('personality', None)
187
        except (Flavor.DoesNotExist, Image.DoesNotExist):
188
            raise fault.itemNotFound
189
        except (Flavor.MultipleObjectsReturned, Image.MultipleObjectsReturned):
190
            raise fault.serviceUnavailable
191
        except Exception as e:
192
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))    
193
            raise fault.badRequest
194

    
195
        # TODO: Proper Authn, Authz
196
        # Everything belongs to a single SynnefoUser for now.
197
        try:          
198
            owner = SynnefoUser.objects.all()[0]
199
        except Exception as e:
200
            log.exception('Cannot find a single SynnefoUser in the DB: %s' % (e));
201
            raise fault.unauthorized
202

    
203
        # add the new VM to the local db
204
        try:
205
            vm = VirtualMachine.objects.create(sourceimage=image, ipfour='0.0.0.0', ipsix='::1', flavor=flavor, owner=owner)
206
        except Exception as e:
207
            log.exception("Can't save vm: %s" % e)
208
            raise fault.serviceUnavailable
209

    
210
        try:
211
            vm.name = name
212
            #vm.description = descr
213
            vm.save()            
214
            jobId = rapi.CreateInstance(
215
                'create',
216
                request.META['SERVER_NAME'] == 'testserver' and 'test-server' or vm.backend_id,
217
                'plain',
218
                # disk field of Flavor object is in GB, value specified here is in MB
219
                # FIXME: Always ask for a 2GB disk, current LVM physical groups are too small:
220
                # [{"size": flavor.disk * 1000}],
221
                [{"size": 2000}],
222
                [{}],
223
                #TODO: select OS from imageRef
224
                os='debootstrap+default',
225
                ip_check=False,
226
                name_check=False,
227
                #TODO: verify if this is necessary
228
                pnode = rapi.GetNodes()[0],
229
                # Dry run when called by unit tests
230
                dry_run = request.META['SERVER_NAME'] == 'testserver',
231
                beparams={
232
                            'auto_balance': True,
233
                            'vcpus': flavor.cpu,
234
                            'memory': flavor.ram,
235
                        },
236
                )
237
            log.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
238
        except (GanetiApiError, CertificateError) as e:
239
            log.exception('CreateInstance failed: %s' % e)
240
            # FIX: if the instance creation have failed, why it is saved in the db?
241
            vm.deleted = True
242
            vm.save()
243
            raise fault.serviceUnavailable
244
        except Exception as e:
245
            log.exception('Unexpected error: %s' % e)
246
            vm.deleted = True
247
            vm.save()
248
            raise fault.serviceUnavailable            
249
        
250

    
251
        ret = {'server': {
252
                'id' : vm.id,
253
                'name' : vm.name,
254
                "imageRef" : imageRef,
255
                "flavorRef" : flavorRef,
256
                "hostId" : vm.hostid,
257
                "progress" : 0,
258
                "status" : 'BUILD',
259
                "adminPass" : self.random_password(),
260
                "metadata" : {"My Server Name" : vm.name},
261
                "addresses" : {
262
                    "public" : [  ],
263
                    "private" : [  ],
264
                    },
265
                },
266
        }
267
        return HttpResponse(json.dumps(ret), mimetype="application/json", status=202)
268

    
269

    
270
    def random_password(self):
271
        "return random password"
272
        number_of_chars = 8
273
        possible_chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
274
        return ''.join(random.choice(possible_chars) for x in range(number_of_chars))
275

    
276

    
277
    def update(self, request, id):
278
        """Sets and updates Virtual Machine Metadata. 
279
 
280
        """
281
        try:
282
            metadata_request = json.loads(request.raw_post_data)['metadata']
283
            metadata_key = metadata_request.get('metadata_key')
284
            metadata_value = metadata_request.get('metadata_value')
285
 
286
            vm = VirtualMachine.objects.get(id=id)
287
            #we only update virtual machine's name atm
288
            if metadata_key == 'name':
289
                vm.name = metadata_value
290
                vm.save()
291
                return accepted
292
        except VirtualMachine.DoesNotExist:
293
            raise fault.itemNotFound
294
        except VirtualMachine.MultipleObjectsReturned:
295
            raise fault.serviceUnavailable
296
        except Exception, e:
297
            log.exception('Unexpected error: %s' % e)
298
            raise fault.serviceUnavailable
299

    
300
        raise fault.itemNotFound
301

    
302

    
303
    def delete(self, request, id):
304
        try:
305
            vm = VirtualMachine.objects.get(id=id)
306
            #TODO: set the status to DESTROYED
307
            backend.start_action(vm, 'DESTROY')
308
            rapi.DeleteInstance(vm.backend_id)
309
            return accepted        
310
        except VirtualMachine.DoesNotExist:
311
            raise fault.itemNotFound
312
        except VirtualMachine.MultipleObjectsReturned:
313
            raise fault.serviceUnavailable
314
        except GanetiApiError, CertificateError:
315
            raise fault.serviceUnavailable
316
        except Exception, e:
317
            log.exception('Unexpected error: %s' % e)
318
            raise fault.serviceUnavailable
319

    
320

    
321

    
322
class ServerAddressHandler(BaseHandler):
323
    """Handler responsible for Server Addresses
324

325
     handles Reboot, Shutdown and Start actions. 
326

327
     @HTTP methods: GET
328
     @Parameters: Id of server and networkID (eg public, private)
329
     @Responses: HTTP 200 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
330

331
    """
332
    allowed_methods = ('GET',)
333

    
334
    def read(self, request, id, networkID=None):
335
        """List IP addresses for a server"""
336

    
337
        try:
338
            server = VirtualMachine.objects.get(id=id)
339
            address =  {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''}                                          
340
        except VirtualMachine.DoesNotExist:
341
            raise fault.itemNotFound
342
        except VirtualMachine.MultipleObjectsReturned:
343
            raise fault.serviceUnavailable
344
        except Exception, e:
345
            log.exception('Unexpected error: %s' % e)
346
            raise fault.serviceUnavailable
347

    
348
        if networkID == "public":
349
            address = {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}}}                            
350
        elif networkID == "private":
351
            address = {'private': ''}    
352
        elif networkID is not None:
353
            raise fault.badRequest
354
        return { "addresses": address } 
355

    
356

    
357

    
358
class ServerActionHandler(BaseHandler):
359
    """Handler responsible for Server Actions
360

361
     handles Reboot, Shutdown and Start actions. 
362

363
     @HTTP methods: POST, DELETE, PUT
364
     @Parameters: POST data with the action (reboot, shutdown, start)
365
     @Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise
366

367
    """
368

    
369
    allowed_methods = ('POST', 'DELETE',  'PUT')
370

    
371
    def create(self, request, id):
372
        """Reboot, Shutdown, Start virtual machine"""
373
        
374
        try:
375
            requested_action = json.loads(request.raw_post_data)
376
            reboot_request = requested_action.get('reboot', None)
377
            shutdown_request = requested_action.get('shutdown', None)
378
            start_request = requested_action.get('start', None)
379
            #action not implemented
380
            action = reboot_request and 'REBOOT' or shutdown_request and 'STOP' or start_request and 'START'
381

    
382
            if not action:
383
                raise fault.notImplemented 
384
            #test if we can get the vm
385
            vm = VirtualMachine.objects.get(id=id)
386
            backend.start_action(vm, action)
387

    
388
            if reboot_request:
389
                rapi.RebootInstance(vm.backend_id)
390
            elif shutdown_request:
391
                rapi.ShutdownInstance(vm.backend_id)
392
            elif start_request:
393
                rapi.StartupInstance(vm.backend_id)
394
            return accepted
395
        except VirtualMachine.DoesNotExist:
396
            raise fault.itemNotFound
397
        except VirtualMachine.MultipleObjectsReturned:
398
            raise fault.serviceUnavailable
399
        except GanetiApiError, CertificateError:
400
            raise fault.serviceUnavailable
401
        except Exception, e:
402
            log.exception('Unexpected error: %s' % e)
403
            raise fault.serviceUnavailable
404

    
405
    def delete(self, request, id):
406
        """Delete an Instance"""
407
        return accepted
408

    
409
    def update(self, request, id):
410
        raise fault.itemNotFound
411

    
412

    
413
class ServerBackupHandler(BaseHandler):
414
    """ Backup Schedules are not implemented yet, return notImplemented """
415
    allowed_methods = ('GET', 'POST', 'DELETE')
416

    
417
    def read(self, request, id):
418
        raise fault.notImplemented
419

    
420
    def create(self, request, id):
421
        raise fault.notImplemented
422

    
423
    def delete(self, request, id):
424
        raise fault.notImplemented
425

    
426

    
427
class ServerMetadataHandler(BaseHandler):
428
    """Handles Metadata of a specific Server
429

430
    the handler Lists, Creates, Updates and Deletes Metadata values
431

432
    @HTTP methods: POST, DELETE, PUT, GET
433
    @Parameters: POST data with key value pairs
434

435
    """
436
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
437

    
438
    def read(self, request, id, key=None):
439
        """List metadata of the specific server"""
440
        if key is None:
441
            return self.read_allkeys(request, id)
442
        else:
443
            return self.read_onekey(request, id, key)
444

    
445
    def read_allkeys(self, request, id):
446
        """Returns all the key value pairs of the specified server"""
447
        try:
448
            server = VirtualMachine.objects.get(pk=id)
449
            return {
450
                "metadata": {
451
                    "values": [
452
                        {m.meta_key: m.meta_value} for m in server.virtualmachinemetadata_set.all()
453
                    ]
454
                }
455
            }
456
        except VirtualMachine.DoesNotExist:
457
            raise fault.itemNotFound
458
        except VirtualMachine.MultipleObjectsReturned:
459
            raise fault.serviceUnavailable
460
        except Exception, e:
461
            log.exception('Unexpected error: %s' % e)
462
            raise fault.serviceUnavailable
463
        
464
    def read_onekey(self, request, id, key):
465
        """Returns the specified metadata key of the specified server"""
466
        try:
467
            server = VirtualMachine.objects.get(pk=id)
468
            return {
469
                "metadata": {
470
                    "values": [
471
                        {m.meta_key: m.meta_value} for m in server.virtualmachinemetadata_set.filter(meta_key=key)
472
                    ]
473
                }
474
            }
475
        except VirtualMachineMetadata.DoesNotExist:
476
            raise fault.itemNotFound            
477
        except VirtualMachine.DoesNotExist:
478
            raise fault.itemNotFound
479
        except VirtualMachine.MultipleObjectsReturned:
480
            raise fault.serviceUnavailable
481
        except Exception, e:
482
            log.exception('Unexpected error: %s' % e)
483
            raise fault.serviceUnavailable
484

    
485
    def create(self, request, id, key=None):
486
        """Create or Update all metadata for the specified VM"""
487
        if key is not None:
488
            log.exception('The POST request should not pass a key in the URL')
489
            raise fault.badRequest
490
        try:
491
            metadata = json.loads(request.raw_post_data)['metadata']
492
        except Exception as e:
493
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
494
            raise fault.badRequest
495

    
496
        try:
497
            vm = VirtualMachine.objects.get(pk=id)
498
            for x in metadata.keys():
499
                vm_meta, created = vm.virtualmachinemetadata_set.get_or_create(meta_key=x)
500
                vm_meta.meta_value = metadata[x] 
501
                vm_meta.save()
502
            return {
503
                "metadata": [{
504
                    "meta": { 
505
                        "key": {m.meta_key: m.meta_value}}} for m in vm.virtualmachinemetadata_set.all()]
506
            }        
507
        except VirtualMachine.DoesNotExist:
508
            raise fault.itemNotFound
509
        except VirtualMachine.MultipleObjectsReturned:
510
            raise fault.serviceUnavailable
511
        except VirtualMachineMetadata.DoesNotExist:
512
            raise fault.itemNotFound
513
        except VirtualMachineMetadata.MultipleObjectsReturned:
514
            raise fault.serviceUnavailable
515
        except Exception, e:
516
            log.exception('Unexpected error: %s' % e)
517
            raise fault.serviceUnavailable
518

    
519
    def update(self, request, id, key=None):
520
        """Update or Create the specified metadata key for the specified VM"""
521
        if key is None:
522
            log.exception('No metadata key specified in URL')
523
            raise fault.badRequest
524
        try:
525
            metadata = json.loads(request.raw_post_data)['meta']
526
            metadata_value = metadata[key]
527
        except Exception as e:
528
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
529
            raise fault.badRequest
530

    
531
        try:
532
            server = VirtualMachine.objects.get(pk=id)
533
            vm_meta, created = server.virtualmachinemetadata_set.get_or_create(meta_key=key)
534
            vm_meta.meta_value = metadata_value 
535
            vm_meta.save()
536
            return {"meta": {vm_meta.meta_key: vm_meta.meta_value}}
537
        
538
        except VirtualMachine.DoesNotExist:
539
            raise fault.itemNotFound
540
        except VirtualMachine.MultipleObjectsReturned:
541
            raise fault.serviceUnavailable
542
        except VirtualMachineMetadata.DoesNotExist:
543
            raise fault.itemNotFound
544
        except VirtualMachineMetadata.MultipleObjectsReturned:
545
            raise fault.serviceUnavailable
546
        except Exception, e:
547
            log.exception('Unexpected error: %s' % e)
548
            raise fault.serviceUnavailable
549

    
550
    def delete(self, request, id, key=None):
551
        """Delete the specified metadata key"""
552
        if key is None:
553
            log.exception('No metadata key specified in URL')
554
            raise fault.badRequest
555
        try:
556
            server = VirtualMachine.objects.get(pk=id)
557
            server.virtualmachinemetadata_set.get(meta_key=key).delete()
558
        except VirtualMachineMetadata.DoesNotExist:
559
            raise fault.itemNotFound
560
        except VirtualMachine.DoesNotExist:
561
            raise fault.itemNotFound
562
        except VirtualMachineMetadata.MultipleObjectsReturned:
563
            raise fault.serviceUnavailable
564
        except VirtualMachine.MultipleObjectsReturned:
565
            raise fault.serviceUnavailable
566
        except Exception, e:
567
            log.exception('Unexpected error: %s' % e)
568
            raise fault.serviceUnavailable
569

    
570

    
571
class FlavorHandler(BaseHandler):
572
    """Handler responsible for Flavors
573

574
    """
575
    allowed_methods = ('GET',)
576

    
577
    def read(self, request, id=None):
578
        """
579
        List flavors or retrieve one
580

581
        Returns: OK
582
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
583
                badRequest, itemNotFound
584
        """
585
        try:
586
            flavors = Flavor.objects.all()
587
            flavors = [ {'id': flavor.id, 'name': flavor.name, 'ram': flavor.ram, \
588
                     'disk': flavor.disk, 'cpu': flavor.cpu} for flavor in flavors]
589

    
590
            if id is None:
591
                simple = map(lambda v: {
592
                            "id": v['id'],
593
                            "name": v['name'],
594
                        }, flavors)
595
                return { "flavors": simple }
596
            elif id == "detail":
597
                return { "flavors": flavors }
598
            else:
599
                flavor = Flavor.objects.get(id=id)
600
                return { "flavor":  {
601
                    'id': flavor.id,
602
                    'name': flavor.name,
603
                    'ram': flavor.ram,
604
                    'disk': flavor.disk,  
605
                    'cpu': flavor.cpu,  
606
                   } }
607

    
608
        except Flavor.DoesNotExist:
609
            raise fault.itemNotFound
610
        except Flavor.MultipleObjectsReturned:
611
            raise fault.serviceUnavailable
612
        except Exception, e:
613
            log.exception('Unexpected error: %s' % e)
614
            raise fault.serviceUnavailable
615

    
616

    
617
class ImageHandler(BaseHandler):
618
    """Handler responsible for Images
619

620
     handles the listing, creation and delete of Images. 
621

622
     @HTTP methods: GET, POST
623
     @Parameters: POST data 
624
     @Responses: HTTP 202 if successfully create Image or get the Images list, itemNotFound, serviceUnavailable otherwise
625

626
    """
627

    
628

    
629
    allowed_methods = ('GET', 'POST')
630

    
631
    def read(self, request, id=None):
632
        """
633
        List images or retrieve one
634

635
        Returns: OK
636
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
637
                badRequest, itemNotFound
638
        """
639

    
640
        #changes_since should be on ISO 8601 format
641
        try:
642
            changes_since = request.GET.get("changes-since", 0)
643
            if changes_since:
644
                last_update = datetime.strptime(changes_since, "%Y-%m-%dT%H:%M:%S" )
645
                #return a badRequest if the changes_since is older than a limit
646
                if datetime.now() - last_update > timedelta(seconds=settings.POLL_LIMIT):
647
                    raise fault.badRequest        
648
                images = Image.objects.filter(updated__gt=last_update)
649
                if not len(images):
650
                    return notModified
651
            else:
652
                images = Image.objects.all()
653
        except Exception, e:
654
            raise fault.badRequest        
655
        try:
656
            images_list = [ {'created': image.created.isoformat(), 
657
                        'id': image.id,
658
                        'name': image.name,
659
                        'updated': image.updated.isoformat(),    
660
                        'status': image.state, 
661
                        'progress': image.state == 'ACTIVE' and 100 or 0, 
662
                        'size': image.size, 
663
                        'serverId': image.sourcevm and image.sourcevm.id or "",
664
                        #'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in image.imagemetadata_set.all()]
665
                        'metadata':{'meta': { 'key': {'description': image.description}}},
666
                       } for image in images]
667
            # Images info is stored in the DB. Ganeti is not aware of this
668
            if id == "detail":
669
                return { "images": images_list }
670
            elif id is None:
671
                return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] }
672
            else:        
673
                image = images.get(id=id)
674
                return { "image":  {'created': image.created.isoformat(), 
675
                    'id': image.id,
676
                    'name': image.name,
677
                    'updated': image.updated.isoformat(),    
678
                    'description': image.description, 
679
                    'status': image.state, 
680
                    'progress': image.state == 'ACTIVE' and 100 or 0, 
681
                    'size': image.size, 
682
                    'serverId': image.sourcevm and image.sourcevm.id or "",
683
                    #'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in image.imagemetadata_set.all()]
684
                    'metadata':{'meta': { 'key': {'description': image.description}}},
685
                   } }
686
        except Image.DoesNotExist:
687
                    raise fault.itemNotFound
688
        except Image.MultipleObjectsReturned:
689
                    raise fault.serviceUnavailable
690
        except Exception, e:
691
                    log.exception('Unexpected error: %s' % e)
692
                    raise fault.serviceUnavailable
693

    
694
    def create(self, request):
695
        """Create a new image"""
696
        return accepted
697

    
698

    
699
class ImageMetadataHandler(BaseHandler):
700
    """Handles Metadata of a specific Image
701

702
    the handler Lists, Creates, Updates and Deletes Metadata values
703

704
    @HTTP methods: POST, DELETE, PUT, GET
705
    @Parameters: POST data with key value pairs
706

707
    """
708
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
709

    
710
    def read(self, request, id, key=None):
711
        """List metadata of the specific server"""
712
        if key is None:
713
            return self.read_allkeys(request, id)
714
        else:
715
            return self.read_onekey(request, id, key)
716

    
717
    def read_allkeys(self, request, id):
718
        """Returns all the key value pairs of the specified server"""
719
        try:
720
            image = Image.objects.get(pk=id)
721
            return {
722
                "metadata": [{
723
                    "meta": { 
724
                        "key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()]
725
            }
726
        except Image.DoesNotExist:
727
            raise fault.itemNotFound
728
        except Image.MultipleObjectsReturned:
729
            raise fault.serviceUnavailable
730
        except Exception, e:
731
            log.exception('Unexpected error: %s' % e)
732
            raise fault.serviceUnavailable
733
        
734
    def read_onekey(self, request, id, key):
735
        """Returns the specified metadata key of the specified server"""
736
        try:
737
            image = Image.objects.get(pk=id)
738
            return {
739
                "metadata": {
740
                    "values": [
741
                        {m.meta_key: m.meta_value} for m in image.imagemetadata_set.filter(meta_key=key)
742
                    ]
743
                }
744
            }
745
        except ImageMetadata.DoesNotExist:
746
            raise fault.itemNotFound            
747
        except Image.DoesNotExist:
748
            raise fault.itemNotFound
749
        except Image.MultipleObjectsReturned:
750
            raise fault.serviceUnavailable
751
        except Exception, e:
752
            log.exception('Unexpected error: %s' % e)
753
            raise fault.serviceUnavailable
754

    
755
    def create(self, request, id, key=None):
756
        """Create or Update all metadata for the specified Image"""
757
        if key is not None:
758
            log.exception('The POST request should not pass a key in the URL')
759
            raise fault.badRequest
760
        try:
761
            metadata = json.loads(request.raw_post_data)['metadata']
762
        except Exception as e:
763
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
764
            raise fault.badRequest
765

    
766
        try:
767
            image = Image.objects.get(pk=id)
768
            for x in metadata.keys():
769
                img_meta, created = image.imagemetadata_set.get_or_create(meta_key=x)
770
                img_meta.meta_value = metadata[x] 
771
                img_meta.save()
772
            return {
773
                "metadata": [{
774
                    "meta": { 
775
                        "key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()]
776
            }        
777
        except Image.DoesNotExist:
778
            raise fault.itemNotFound
779
        except Image.MultipleObjectsReturned:
780
            raise fault.serviceUnavailable
781
        except ImageMetadata.DoesNotExist:
782
            raise fault.itemNotFound
783
        except ImageMetadata.MultipleObjectsReturned:
784
            raise fault.serviceUnavailable
785
        except Exception, e:
786
            log.exception('Unexpected error: %s' % e)
787
            raise fault.serviceUnavailable
788

    
789
    def update(self, request, id, key=None):
790
        """Update or Create the specified metadata key for the specified Image"""
791
        if key is None:
792
            log.exception('No metadata key specified in URL')
793
            raise fault.badRequest
794
        try:
795
            metadata = json.loads(request.raw_post_data)['meta']
796
            metadata_value = metadata[key]
797
        except Exception as e:
798
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
799
            raise fault.badRequest
800

    
801
        try:
802
            image = Image.objects.get(pk=id)
803
            img_meta, created = image.imagemetadata_set.get_or_create(meta_key=key)
804
            img_meta.meta_value = metadata_value 
805
            img_meta.save()
806
            return {"meta": {img_meta.meta_key: img_meta.meta_value}}
807
        
808
        except Image.DoesNotExist:
809
            raise fault.itemNotFound
810
        except Image.MultipleObjectsReturned:
811
            raise fault.serviceUnavailable
812
        except ImageMetadata.DoesNotExist:
813
            raise fault.itemNotFound
814
        except ImageMetadata.MultipleObjectsReturned:
815
            raise fault.serviceUnavailable
816
        except Exception, e:
817
            log.exception('Unexpected error: %s' % e)
818
            raise fault.serviceUnavailable
819

    
820
    def delete(self, request, id, key=None):
821
        """Delete the specified metadata key"""
822
        if key is None:
823
            log.exception('No metadata key specified in URL')
824
            raise fault.badRequest
825
        try:
826
            image = Image.objects.get(pk=id)
827
            image.imagemetadata_set.get(meta_key=key).delete()
828
        except ImageMetadata.DoesNotExist:
829
            raise fault.itemNotFound
830
        except Image.DoesNotExist:
831
            raise fault.itemNotFound
832
        except ImageMetadata.MultipleObjectsReturned:
833
            raise fault.serviceUnavailable
834
        except Image.MultipleObjectsReturned:
835
            raise fault.serviceUnavailable
836
        except Exception, e:
837
            log.exception('Unexpected error: %s' % e)
838
            raise fault.serviceUnavailable
839

    
840

    
841
class SharedIPGroupHandler(BaseHandler):
842
    allowed_methods = ('GET', 'POST', 'DELETE')
843

    
844
    def read(self, request, id=None):
845
        """List Shared IP Groups"""
846
        if id is None:
847
            return {}
848
        elif id == "detail":
849
            return {}
850
        else:
851
            raise fault.itemNotFound
852

    
853
    def create(self, request, id):
854
        """Creates a new Shared IP Group"""
855
        return created
856

    
857
    def delete(self, request, id):
858
        """Deletes a Shared IP Group"""
859
        raise fault.itemNotFound
860

    
861

    
862
class VirtualMachineGroupHandler(BaseHandler):
863
    """Handler responsible for Virtual Machine Groups
864

865
     creates, lists, deletes virtual machine groups
866

867
     @HTTP methods: GET, POST, DELETE
868
     @Parameters: POST data 
869
     @Responses: HTTP 202 if successfully get the Groups list, itemNotFound, serviceUnavailable otherwise
870

871
    """
872

    
873
    allowed_methods = ('GET', 'POST', 'DELETE')
874

    
875
    def read(self, request, id=None):
876
        """List Groups"""
877
        try:
878
            vmgroups = VirtualMachineGroup.objects.all() 
879
            vmgroups_list = [ {'id': vmgroup.id, \
880
                  'name': vmgroup.name,  \
881
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
882
                   } for vmgroup in vmgroups]
883
            # Group info is stored in the DB. Ganeti is not aware of this
884
            if id == "detail":
885
                return { "groups": vmgroups_list }
886
            elif id is None:
887
                return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups_list ] }
888
            else:
889
                vmgroup = vmgroups.get(id=id)
890

    
891
                return { "group":  {'id': vmgroup.id, \
892
                  'name': vmgroup.name,  \
893
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
894
                   } }
895

    
896

    
897
        except VirtualMachineGroup.DoesNotExist:
898
                    raise fault.itemNotFound
899
        except VirtualMachineGroup.MultipleObjectsReturned:
900
                    raise fault.serviceUnavailable
901
        except Exception, e:
902
                    log.exception('Unexpected error: %s' % e)
903
                    raise fault.serviceUnavailable
904

    
905

    
906

    
907
    def create(self, request, id):
908
        """Creates a Group"""
909
        return created
910

    
911
    def delete(self, request, id):
912
        """Deletes a  Group"""
913
        raise fault.itemNotFound
914

    
915

    
916
class LimitHandler(BaseHandler):
917
    allowed_methods = ('GET',)
918

    
919
    # XXX: hookup with @throttle
920

    
921
    rate = [
922
        {
923
           "verb" : "POST",
924
           "URI" : "*",
925
           "regex" : ".*",
926
           "value" : 10,
927
           "remaining" : 2,
928
           "unit" : "MINUTE",
929
           "resetTime" : 1244425439
930
        },
931
        {
932
           "verb" : "POST",
933
           "URI" : "*/servers",
934
           "regex" : "^/servers",
935
           "value" : 25,
936
           "remaining" : 24,
937
           "unit" : "DAY",
938
           "resetTime" : 1244511839
939
        },
940
        {
941
           "verb" : "PUT",
942
           "URI" : "*",
943
           "regex" : ".*",
944
           "value" : 10,
945
           "remaining" : 2,
946
           "unit" : "MINUTE",
947
           "resetTime" : 1244425439
948
        },
949
        {
950
           "verb" : "GET",
951
           "URI" : "*",
952
           "regex" : ".*",
953
           "value" : 3,
954
           "remaining" : 3,
955
           "unit" : "MINUTE",
956
           "resetTime" : 1244425439
957
        },
958
        {
959
           "verb" : "DELETE",
960
           "URI" : "*",
961
           "regex" : ".*",
962
           "value" : 100,
963
           "remaining" : 100,
964
           "unit" : "MINUTE",
965
           "resetTime" : 1244425439
966
        }
967
    ]
968

    
969
    absolute = {
970
        "maxTotalRAMSize" : 51200,
971
        "maxIPGroups" : 50,
972
        "maxIPGroupMembers" : 25
973
    }
974

    
975
    def read(self, request):
976
        return { "limits": {
977
                "rate": self.rate,
978
                "absolute": self.absolute,
979
               }
980
            }
981

    
982

    
983
class DiskHandler(BaseHandler):
984
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
985

    
986
    def read(self, request, id=None):
987
        """List Disks"""
988
        if id is None:
989
            return self.read_all(request)
990
        elif id == "detail":
991
            return self.read_all(request, detail=True)
992
        else:
993
            return self.read_one(request, id)
994

    
995
    def read_one(self, request, id):
996
        """List one Disk with the specified id with all details"""
997
        # FIXME Get detailed info from the DB 
998
        # for the Disk with the specified id
999
        try:
1000
            disk = Disk.objects.get(pk=id)
1001
            disk_details = {
1002
                "id" : disk.id, 
1003
                "name" : disk.name, 
1004
                "size" : disk.size,
1005
                "created" : disk.created, 
1006
                "serverId" : disk.vm.id
1007
            }
1008
            return { "disks" : disk_details }
1009
        except:
1010
            raise fault.itemNotFound
1011

    
1012
    @paginator
1013
    def read_all(self, request, detail=False):
1014
        """List all Disks. If -detail- is set list them with all details"""
1015
        if not detail:
1016
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1017
            return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] }
1018
        else:
1019
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1020
            disks_details = [ {
1021
                "id" : disk.id, 
1022
                "name" : disk.name,
1023
                "size" : disk.size,
1024
                "created" : disk.created, 
1025
                "serverId" : disk.vm.id,
1026
            } for disk in disks ]
1027
            return { "disks":  disks_details }                
1028

    
1029
    def create(self, request):
1030
        """Create a new Disk"""
1031
        # FIXME Create a partial DB entry, 
1032
        # then call the backend for actual creation
1033
        pass
1034

    
1035
    def update(self, request, id):
1036
        """Rename the Disk with the specified id"""
1037
        # FIXME Change the Disk's name in the DB
1038
        pass
1039

    
1040
    def delete(self, request, id):
1041
        """Destroy the Disk with the specified id"""
1042
        # Call the backend for actual destruction
1043
        pass