Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ b9a77976

History | View | Annotate | Download (37.2 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
import simplejson as json
6
from django.conf import settings
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
18

    
19
log = logging.getLogger('synnefo.api.handlers')
20

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

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

    
129

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

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

    
170

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

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

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

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

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

    
267

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

    
274

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

    
298
        raise fault.itemNotFound
299

    
300

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

    
318

    
319

    
320
class ServerAddressHandler(BaseHandler):
321
    """Handler responsible for Server Addresses
322

323
     handles Reboot, Shutdown and Start actions. 
324

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

329
    """
330
    allowed_methods = ('GET',)
331

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

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

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

    
354

    
355

    
356
class ServerActionHandler(BaseHandler):
357
    """Handler responsible for Server Actions
358

359
     handles Reboot, Shutdown and Start actions. 
360

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

365
    """
366

    
367
    allowed_methods = ('POST', 'DELETE',  'PUT')
368

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

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

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

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

    
407
    def update(self, request, id):
408
        raise fault.itemNotFound
409

    
410

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

    
415
    def read(self, request, id):
416
        raise fault.notImplemented
417

    
418
    def create(self, request, id):
419
        raise fault.notImplemented
420

    
421
    def delete(self, request, id):
422
        raise fault.notImplemented
423

    
424

    
425
class ServerMetadataHandler(BaseHandler):
426
    """Handles Metadata of a specific Server
427

428
    the handler Lists, Creates, Updates and Deletes Metadata values
429

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

433
    """
434
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
435

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

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

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

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

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

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

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

    
568

    
569
class FlavorHandler(BaseHandler):
570
    """Handler responsible for Flavors
571

572
    """
573
    allowed_methods = ('GET',)
574

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

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

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

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

    
614

    
615
class ImageHandler(BaseHandler):
616
    """Handler responsible for Images
617

618
     handles the listing, creation and delete of Images. 
619

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

624
    """
625

    
626

    
627
    allowed_methods = ('GET', 'POST')
628

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

633
        Returns: OK
634
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
635
                badRequest, itemNotFound
636
        """
637
        try:
638
            images = Image.objects.all()
639
            images_list = [ {'created': image.created.isoformat(), 
640
                        'id': image.id,
641
                        'name': image.name,
642
                        'updated': image.updated.isoformat(),    
643
                        'description': image.description, 
644
                        'status': image.state, 
645
                        'progress': image.state == 'ACTIVE' and 100 or 0, 
646
                        'size': image.size, 
647
                        'serverId': image.sourcevm and image.sourcevm.id or ""
648
                       } for image in images]
649
            # Images info is stored in the DB. Ganeti is not aware of this
650
            if id == "detail":
651
                return { "images": images_list }
652
            elif id is None:
653
                return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] }
654
            else:        
655
                image = images.get(id=id)
656
                return { "image":  {'created': image.created.isoformat(), 
657
                    'id': image.id,
658
                    'name': image.name,
659
                    'updated': image.updated.isoformat(),    
660
                    'description': image.description, 
661
                    'status': image.state, 
662
                    'progress': image.state == 'ACTIVE' and 100 or 0, 
663
                    'size': image.size, 
664
                    'serverId': image.sourcevm and image.sourcevm.id or ""
665
                   } }
666
        except Image.DoesNotExist:
667
                    raise fault.itemNotFound
668
        except Image.MultipleObjectsReturned:
669
                    raise fault.serviceUnavailable
670
        except Exception, e:
671
                    log.exception('Unexpected error: %s' % e)
672
                    raise fault.serviceUnavailable
673

    
674
    def create(self, request):
675
        """Create a new image"""
676
        return accepted
677

    
678

    
679
class ImageMetadataHandler(BaseHandler):
680
    """Handles Metadata of a specific Image
681

682
    the handler Lists, Creates, Updates and Deletes Metadata values
683

684
    @HTTP methods: POST, DELETE, PUT, GET
685
    @Parameters: POST data with key value pairs
686

687
    """
688
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
689

    
690
    def read(self, request, id, key=None):
691
        """List metadata of the specific server"""
692
        if key is None:
693
            return self.read_allkeys(request, id)
694
        else:
695
            return self.read_onekey(request, id, key)
696

    
697
    def read_allkeys(self, request, id):
698
        """Returns all the key value pairs of the specified server"""
699
        try:
700
            image = Image.objects.get(pk=id)
701
            return {
702
                "metadata": [{
703
                    "meta": { 
704
                        "key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()]
705
            }
706
        except Image.DoesNotExist:
707
            raise fault.itemNotFound
708
        except Image.MultipleObjectsReturned:
709
            raise fault.serviceUnavailable
710
        except Exception, e:
711
            log.exception('Unexpected error: %s' % e)
712
            raise fault.serviceUnavailable
713
        
714
    def read_onekey(self, request, id, key):
715
        """Returns the specified metadata key of the specified server"""
716
        try:
717
            image = Image.objects.get(pk=id)
718
            return {
719
                "metadata": {
720
                    "values": [
721
                        {m.meta_key: m.meta_value} for m in image.imagemetadata_set.filter(meta_key=key)
722
                    ]
723
                }
724
            }
725
        except ImageMetadata.DoesNotExist:
726
            raise fault.itemNotFound            
727
        except Image.DoesNotExist:
728
            raise fault.itemNotFound
729
        except Image.MultipleObjectsReturned:
730
            raise fault.serviceUnavailable
731
        except Exception, e:
732
            log.exception('Unexpected error: %s' % e)
733
            raise fault.serviceUnavailable
734

    
735
    def create(self, request, id, key=None):
736
        """Create or Update all metadata for the specified Image"""
737
        if key is not None:
738
            log.exception('The POST request should not pass a key in the URL')
739
            raise fault.badRequest
740
        try:
741
            metadata = json.loads(request.raw_post_data)['metadata']
742
        except Exception as e:
743
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
744
            raise fault.badRequest
745

    
746
        try:
747
            image = Image.objects.get(pk=id)
748
            for x in metadata.keys():
749
                img_meta, created = image.imagemetadata_set.get_or_create(meta_key=x)
750
                img_meta.meta_value = metadata[x] 
751
                img_meta.save()
752
            return {
753
                "metadata": [{
754
                    "meta": { 
755
                        "key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()]
756
            }        
757
        except Image.DoesNotExist:
758
            raise fault.itemNotFound
759
        except Image.MultipleObjectsReturned:
760
            raise fault.serviceUnavailable
761
        except ImageMetadata.DoesNotExist:
762
            raise fault.itemNotFound
763
        except ImageMetadata.MultipleObjectsReturned:
764
            raise fault.serviceUnavailable
765
        except Exception, e:
766
            log.exception('Unexpected error: %s' % e)
767
            raise fault.serviceUnavailable
768

    
769
    def update(self, request, id, key=None):
770
        """Update or Create the specified metadata key for the specified Image"""
771
        if key is None:
772
            log.exception('No metadata key specified in URL')
773
            raise fault.badRequest
774
        try:
775
            metadata = json.loads(request.raw_post_data)['meta']
776
            metadata_value = metadata[key]
777
        except Exception as e:
778
            log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data))
779
            raise fault.badRequest
780

    
781
        try:
782
            image = Image.objects.get(pk=id)
783
            img_meta, created = image.imagemetadata_set.get_or_create(meta_key=key)
784
            img_meta.meta_value = metadata_value 
785
            img_meta.save()
786
            return {"meta": {img_meta.meta_key: img_meta.meta_value}}
787
        
788
        except Image.DoesNotExist:
789
            raise fault.itemNotFound
790
        except Image.MultipleObjectsReturned:
791
            raise fault.serviceUnavailable
792
        except ImageMetadata.DoesNotExist:
793
            raise fault.itemNotFound
794
        except ImageMetadata.MultipleObjectsReturned:
795
            raise fault.serviceUnavailable
796
        except Exception, e:
797
            log.exception('Unexpected error: %s' % e)
798
            raise fault.serviceUnavailable
799

    
800
    def delete(self, request, id, key=None):
801
        """Delete the specified metadata key"""
802
        if key is None:
803
            log.exception('No metadata key specified in URL')
804
            raise fault.badRequest
805
        try:
806
            image = Image.objects.get(pk=id)
807
            image.imagemetadata_set.get(meta_key=key).delete()
808
        except ImageMetadata.DoesNotExist:
809
            raise fault.itemNotFound
810
        except Image.DoesNotExist:
811
            raise fault.itemNotFound
812
        except ImageMetadata.MultipleObjectsReturned:
813
            raise fault.serviceUnavailable
814
        except Image.MultipleObjectsReturned:
815
            raise fault.serviceUnavailable
816
        except Exception, e:
817
            log.exception('Unexpected error: %s' % e)
818
            raise fault.serviceUnavailable
819

    
820

    
821
class SharedIPGroupHandler(BaseHandler):
822
    allowed_methods = ('GET', 'POST', 'DELETE')
823

    
824
    def read(self, request, id=None):
825
        """List Shared IP Groups"""
826
        if id is None:
827
            return {}
828
        elif id == "detail":
829
            return {}
830
        else:
831
            raise fault.itemNotFound
832

    
833
    def create(self, request, id):
834
        """Creates a new Shared IP Group"""
835
        return created
836

    
837
    def delete(self, request, id):
838
        """Deletes a Shared IP Group"""
839
        raise fault.itemNotFound
840

    
841

    
842
class VirtualMachineGroupHandler(BaseHandler):
843
    """Handler responsible for Virtual Machine Groups
844

845
     creates, lists, deletes virtual machine groups
846

847
     @HTTP methods: GET, POST, DELETE
848
     @Parameters: POST data 
849
     @Responses: HTTP 202 if successfully get the Groups list, itemNotFound, serviceUnavailable otherwise
850

851
    """
852

    
853
    allowed_methods = ('GET', 'POST', 'DELETE')
854

    
855
    def read(self, request, id=None):
856
        """List Groups"""
857
        try:
858
            vmgroups = VirtualMachineGroup.objects.all() 
859
            vmgroups_list = [ {'id': vmgroup.id, \
860
                  'name': vmgroup.name,  \
861
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
862
                   } for vmgroup in vmgroups]
863
            # Group info is stored in the DB. Ganeti is not aware of this
864
            if id == "detail":
865
                return { "groups": vmgroups_list }
866
            elif id is None:
867
                return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups_list ] }
868
            else:
869
                vmgroup = vmgroups.get(id=id)
870

    
871
                return { "group":  {'id': vmgroup.id, \
872
                  'name': vmgroup.name,  \
873
                   'server_id': [machine.id for machine in vmgroup.machines.all()] \
874
                   } }
875

    
876

    
877
        except VirtualMachineGroup.DoesNotExist:
878
                    raise fault.itemNotFound
879
        except VirtualMachineGroup.MultipleObjectsReturned:
880
                    raise fault.serviceUnavailable
881
        except Exception, e:
882
                    log.exception('Unexpected error: %s' % e)
883
                    raise fault.serviceUnavailable
884

    
885

    
886

    
887
    def create(self, request, id):
888
        """Creates a Group"""
889
        return created
890

    
891
    def delete(self, request, id):
892
        """Deletes a  Group"""
893
        raise fault.itemNotFound
894

    
895

    
896
class LimitHandler(BaseHandler):
897
    allowed_methods = ('GET',)
898

    
899
    # XXX: hookup with @throttle
900

    
901
    rate = [
902
        {
903
           "verb" : "POST",
904
           "URI" : "*",
905
           "regex" : ".*",
906
           "value" : 10,
907
           "remaining" : 2,
908
           "unit" : "MINUTE",
909
           "resetTime" : 1244425439
910
        },
911
        {
912
           "verb" : "POST",
913
           "URI" : "*/servers",
914
           "regex" : "^/servers",
915
           "value" : 25,
916
           "remaining" : 24,
917
           "unit" : "DAY",
918
           "resetTime" : 1244511839
919
        },
920
        {
921
           "verb" : "PUT",
922
           "URI" : "*",
923
           "regex" : ".*",
924
           "value" : 10,
925
           "remaining" : 2,
926
           "unit" : "MINUTE",
927
           "resetTime" : 1244425439
928
        },
929
        {
930
           "verb" : "GET",
931
           "URI" : "*",
932
           "regex" : ".*",
933
           "value" : 3,
934
           "remaining" : 3,
935
           "unit" : "MINUTE",
936
           "resetTime" : 1244425439
937
        },
938
        {
939
           "verb" : "DELETE",
940
           "URI" : "*",
941
           "regex" : ".*",
942
           "value" : 100,
943
           "remaining" : 100,
944
           "unit" : "MINUTE",
945
           "resetTime" : 1244425439
946
        }
947
    ]
948

    
949
    absolute = {
950
        "maxTotalRAMSize" : 51200,
951
        "maxIPGroups" : 50,
952
        "maxIPGroupMembers" : 25
953
    }
954

    
955
    def read(self, request):
956
        return { "limits": {
957
                "rate": self.rate,
958
                "absolute": self.absolute,
959
               }
960
            }
961

    
962

    
963
class DiskHandler(BaseHandler):
964
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
965

    
966
    def read(self, request, id=None):
967
        """List Disks"""
968
        if id is None:
969
            return self.read_all(request)
970
        elif id == "detail":
971
            return self.read_all(request, detail=True)
972
        else:
973
            return self.read_one(request, id)
974

    
975
    def read_one(self, request, id):
976
        """List one Disk with the specified id with all details"""
977
        # FIXME Get detailed info from the DB 
978
        # for the Disk with the specified id
979
        try:
980
            disk = Disk.objects.get(pk=id)
981
            disk_details = {
982
                "id" : disk.id, 
983
                "name" : disk.name, 
984
                "size" : disk.size,
985
                "created" : disk.created, 
986
                "serverId" : disk.vm.id
987
            }
988
            return { "disks" : disk_details }
989
        except:
990
            raise fault.itemNotFound
991

    
992
    @paginator
993
    def read_all(self, request, detail=False):
994
        """List all Disks. If -detail- is set list them with all details"""
995
        if not detail:
996
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
997
            return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] }
998
        else:
999
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1000
            disks_details = [ {
1001
                "id" : disk.id, 
1002
                "name" : disk.name,
1003
                "size" : disk.size,
1004
                "created" : disk.created, 
1005
                "serverId" : disk.vm.id,
1006
            } for disk in disks ]
1007
            return { "disks":  disks_details }                
1008

    
1009
    def create(self, request):
1010
        """Create a new Disk"""
1011
        # FIXME Create a partial DB entry, 
1012
        # then call the backend for actual creation
1013
        pass
1014

    
1015
    def update(self, request, id):
1016
        """Rename the Disk with the specified id"""
1017
        # FIXME Change the Disk's name in the DB
1018
        pass
1019

    
1020
    def delete(self, request, id):
1021
        """Destroy the Disk with the specified id"""
1022
        # Call the backend for actual destruction
1023
        pass