Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ ab5282e4

History | View | Annotate | Download (37.1 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 202 if successfully call rapi, itemNotFound, 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

    
143
            if not detail:
144
                return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] }
145
            else:
146
                virtual_servers_list = [{'status': server.rsapi_state, 
147
                                         'flavorRef': server.flavor.id, 
148
                                         'name': server.name, 
149
                                         'id': server.id, 
150
                                         'description': server.description, 
151
                                         'created': server.created, 
152
                                         'updated': server.updated,
153
                                         'deleted': server.deleted,                     
154
                                         'imageRef': server.sourceimage.id, 
155
                                         'hostId': server.hostid, 
156
                                         'progress': server.rsapi_state == 'ACTIVE' and 100 or 0, 
157
                                         #'metadata': {'Server_Label': server.description },
158
                                         'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()],                                    
159
                                         'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''},      
160

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

    
168

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

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

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

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

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

    
265

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

    
272

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

    
296
        raise fault.itemNotFound
297

    
298

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

    
316

    
317

    
318
class ServerAddressHandler(BaseHandler):
319
    """Handler responsible for Server Addresses
320

321
     handles Reboot, Shutdown and Start actions. 
322

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

327
    """
328
    allowed_methods = ('GET',)
329

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

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

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

    
352

    
353

    
354
class ServerActionHandler(BaseHandler):
355
    """Handler responsible for Server Actions
356

357
     handles Reboot, Shutdown and Start actions. 
358

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

363
    """
364

    
365
    allowed_methods = ('POST', 'DELETE',  'PUT')
366

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

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

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

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

    
405
    def update(self, request, id):
406
        raise fault.itemNotFound
407

    
408

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

    
413
    def read(self, request, id):
414
        raise fault.notImplemented
415

    
416
    def create(self, request, id):
417
        raise fault.notImplemented
418

    
419
    def delete(self, request, id):
420
        raise fault.notImplemented
421

    
422

    
423
class ServerMetadataHandler(BaseHandler):
424
    """Handles Metadata of a specific Server
425

426
    the handler Lists, Creates, Updates and Deletes Metadata values
427

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

431
    """
432
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
433

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

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

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

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

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

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

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

    
566

    
567
class FlavorHandler(BaseHandler):
568
    """Handler responsible for Flavors
569

570
    """
571
    allowed_methods = ('GET',)
572

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

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

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

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

    
612

    
613
class ImageHandler(BaseHandler):
614
    """Handler responsible for Images
615

616
     handles the listing, creation and delete of Images. 
617

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

622
    """
623

    
624

    
625
    allowed_methods = ('GET', 'POST')
626

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

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

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

    
676

    
677
class ImageMetadataHandler(BaseHandler):
678
    """Handles Metadata of a specific Image
679

680
    the handler Lists, Creates, Updates and Deletes Metadata values
681

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

685
    """
686
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
687

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

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

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

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

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

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

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

    
818

    
819
class SharedIPGroupHandler(BaseHandler):
820
    allowed_methods = ('GET', 'POST', 'DELETE')
821

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

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

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

    
839

    
840
class VirtualMachineGroupHandler(BaseHandler):
841
    """Handler responsible for Virtual Machine Groups
842

843
     creates, lists, deletes virtual machine groups
844

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

849
    """
850

    
851
    allowed_methods = ('GET', 'POST', 'DELETE')
852

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

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

    
874

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

    
883

    
884

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

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

    
893

    
894
class LimitHandler(BaseHandler):
895
    allowed_methods = ('GET',)
896

    
897
    # XXX: hookup with @throttle
898

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

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

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

    
960

    
961
class DiskHandler(BaseHandler):
962
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
963

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

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

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

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

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

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