Statistics
| Branch: | Tag: | Revision:

root / api / handlers.py @ 5fb55fba

History | View | Annotate | Download (39.7 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.utils import simplejson as json
6
from django.http import HttpResponse
7
from piston.handler import BaseHandler, AnonymousBaseHandler
8
from synnefo.api.faults import fault, noContent, accepted, created, notModified
9
from synnefo.api.helpers import instance_to_server, paginator, authenticate
10
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError
11
from synnefo.util.rapi import CertificateError
12
from synnefo.db.models import *
13
import random
14
import string
15
import logging
16
from datetime import datetime, timedelta
17

    
18
from logic import backend, utils
19

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

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

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

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

    
59

    
60
class VersionHandler(AnonymousBaseHandler):
61
    allowed_methods = ('GET',)
62

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

    
80

    
81
class ServerHandler(BaseHandler):
82
    """Handler responsible for the Servers
83

84
     handles the listing of Virtual Machines, Creates and Destroys VM's
85

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

91
    """
92
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
93

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

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

    
141
    @paginator
142
    def read_all(self, request, detail=False):
143
        #changes_since should be on ISO 8601 format
144
        try:
145
            changes_since = request.GET.get("changes-since", 0)
146
            if changes_since:
147
                last_update = datetime.strptime(changes_since,
148
                                                "%Y-%m-%dT%H:%M:%S")
149
                #return a badRequest if the changes_since is older than a limit
150
                if (datetime.now() - last_update >
151
                    timedelta(seconds=settings.POLL_LIMIT)):
152
                    raise fault.badRequest
153
                virtual_servers = VirtualMachine.objects.filter(
154
                    updated__gt=last_update)
155
                if not len(virtual_servers):
156
                    return notModified
157
            else:
158
                virtual_servers = VirtualMachine.objects.filter(deleted=False)
159
            # get all VM's for now, FIX it to take the user's VMs only yet.
160
            # also don't get deleted VM's
161
        except Exception, e:
162
            raise fault.badRequest
163
        try:
164
            if not detail:
165
                return {"servers": [{
166
                    "id": s.id, "name": s.name} for s in virtual_servers]}
167
            else:
168
                virtual_servers_list = [
169
                    {'status': utils.get_rsapi_state(server),
170
                     'flavorRef': server.flavor.id,
171
                    'name': server.name,
172
                    'id': server.id,
173
                    'created': server.created,
174
                    'updated': server.updated,
175
                    'imageRef': server.sourceimage.id,
176
                    'hostId': server.hostid,
177
                    'progress': (utils.get_rsapi_state(server) == 'ACTIVE' and 100 or 0),
178
                     #'metadata': {'Server_Label': server.description },
179
                    'metadata':[
180
                        {'meta':
181
                            {'key': {metadata.meta_key: metadata.meta_value}}}
182
                                for metadata in
183
                                server.virtualmachinemetadata_set.all()
184
                        ],
185
                    'addresses': {
186
                        'public': {
187
                            'ip': {'addr': server.ipfour},
188
                            'ip6': {'addr': server.ipsix}
189
                        },
190
                        'private': ''},
191
                    } for server in virtual_servers]
192
                #pass fake data regarding ip, since we don't have it yet
193
                return {"servers":  virtual_servers_list}
194
        except Exception, e:
195
            log.exception('Unexpected error: %s' % e)
196
            raise fault.serviceUnavailable
197

    
198
    def create(self, request):
199
        """ Parse RackSpace API create request to generate rapi create request
200
            TODO: auto generate and set password
201
        """
202
        # Check if we have all the necessary data in the JSON request
203
        try:
204
            server = json.loads(request.raw_post_data)['server']
205
            name = server['name']
206
            flavorRef = server['flavorRef']
207
            flavor = Flavor.objects.get(id=flavorRef)
208
            imageRef = server['imageRef']
209
            image = Image.objects.get(id=imageRef)
210
            metadata = server['metadata']
211
            personality = server.get('personality', None)
212
        except (Flavor.DoesNotExist, Image.DoesNotExist):
213
            raise fault.itemNotFound
214
        except (Flavor.MultipleObjectsReturned, Image.MultipleObjectsReturned):
215
            raise fault.serviceUnavailable
216
        except Exception as e:
217
            log.exception('Malformed create request: %s - %s' %
218
                          (e, request.raw_post_data))
219
            raise fault.badRequest
220

    
221
        # TODO: Proper Authn, Authz
222
        # Everything belongs to a single SynnefoUser for now.
223
        try:
224
            owner = SynnefoUser.objects.all()[0]
225
        except Exception as e:
226
            log.exception('Cannot find a single SynnefoUser in the DB: %s' %
227
                          (e))
228
            raise fault.unauthorized
229

    
230
        # add the new VM to the local db
231
        try:
232
            vm = VirtualMachine.objects.create(sourceimage=image,
233
                                               ipfour='0.0.0.0',
234
                                               ipsix='::1',
235
                                               flavor=flavor,
236
                                               owner=owner)
237
        except Exception as e:
238
            log.exception("Can't save vm: %s" % e)
239
            raise fault.serviceUnavailable
240

    
241
        try:
242
            vm.name = name
243
            #vm.description = descr
244
            vm.save()
245
            jobId = rapi.CreateInstance(
246
                'create',
247
                (request.META['SERVER_NAME'] == 'testserver' and
248
                'test-server' or vm.backend_id),
249
                'plain',
250
                # disk field of Flavor object is in GB,
251
                # value specified here is in MB
252
                # FIXME: Always ask for a 2GB disk,
253
                # current LVM physical groups are too small:
254
                # [{"size": flavor.disk * 1000}],
255
                [{"size": 2000}],
256
                [{}],
257
                #TODO: select OS from imageRef
258
                os='debootstrap+default',
259
                ip_check=False,
260
                name_check=False,
261
                #TODO: verify if this is necessary
262
                pnode=rapi.GetNodes()[0],
263
                # Dry run when called by unit tests
264
                dry_run=request.META['SERVER_NAME'] == 'testserver',
265
                beparams={
266
                            'auto_balance': True,
267
                            'vcpus': flavor.cpu,
268
                            'memory': flavor.ram,
269
                        },
270
                )
271
            log.info('created vm with %s cpus, %s ram and %s storage' %
272
                     (flavor.cpu, flavor.ram, flavor.disk))
273
        except (GanetiApiError, CertificateError) as e:
274
            log.exception('CreateInstance failed: %s' % e)
275
            # FIX: if the instance creation have failed, why it is saved in the db?
276
            vm.deleted = True
277
            vm.save()
278
            raise fault.serviceUnavailable
279
        except Exception as e:
280
            log.exception('Unexpected error: %s' % e)
281
            vm.deleted = True
282
            vm.save()
283
            raise fault.serviceUnavailable
284

    
285
        ret = {'server': {
286
                'id': vm.id,
287
                'name': vm.name,
288
                "imageRef": imageRef,
289
                "flavorRef": flavorRef,
290
                "hostId": vm.hostid,
291
                "progress": 0,
292
                "status": 'BUILD',
293
                "adminPass": self.random_password(),
294
                "metadata": {"My Server Name": vm.name},
295
                "addresses": {
296
                    "public": [],
297
                    "private": [],
298
                    },
299
                },
300
        }
301
        return HttpResponse(json.dumps(ret),
302
                            mimetype="application/json", status=202)
303

    
304
    def random_password(self):
305
        "return random password"
306
        number_of_chars = 8
307
        possible_chars = string.ascii_uppercase + string.ascii_lowercase + \
308
                         string.digits
309
        return ''.join(random.choice(possible_chars) \
310
                       for x in range(number_of_chars))
311

    
312
    def update(self, request, id):
313
        """Sets and updates Virtual Machine Metadata.
314

315
        """
316
        try:
317
            metadata_request = json.loads(request.raw_post_data)['metadata']
318
            metadata_key = metadata_request.get('metadata_key')
319
            metadata_value = metadata_request.get('metadata_value')
320

    
321
            vm = VirtualMachine.objects.get(id=id)
322
            #we only update virtual machine's name atm
323
            if metadata_key == 'name':
324
                vm.name = metadata_value
325
                vm.save()
326
                return accepted
327
        except VirtualMachine.DoesNotExist:
328
            raise fault.itemNotFound
329
        except VirtualMachine.MultipleObjectsReturned:
330
            raise fault.serviceUnavailable
331
        except Exception, e:
332
            log.exception('Unexpected error: %s' % e)
333
            raise fault.serviceUnavailable
334

    
335
        raise fault.itemNotFound
336

    
337
    def delete(self, request, id):
338
        try:
339
            vm = VirtualMachine.objects.get(id=id)
340
            #TODO: set the status to DESTROYED
341
            backend.start_action(vm, 'DESTROY')
342
            rapi.DeleteInstance(vm.backend_id)
343
            return accepted
344
        except VirtualMachine.DoesNotExist:
345
            raise fault.itemNotFound
346
        except VirtualMachine.MultipleObjectsReturned:
347
            raise fault.serviceUnavailable
348
        except GanetiApiError, CertificateError:
349
            raise fault.serviceUnavailable
350
        except Exception, e:
351
            log.exception('Unexpected error: %s' % e)
352
            raise fault.serviceUnavailable
353

    
354

    
355
class ServerAddressHandler(BaseHandler):
356
    """Handler responsible for Server Addresses
357

358
     handles Reboot, Shutdown and Start actions.
359

360
     @HTTP methods: GET
361
     @Parameters: Id of server and networkID (eg public, private)
362
     @Responses: HTTP 200 if successfully call rapi, itemNotFound,
363
     serviceUnavailable otherwise
364

365
    """
366
    allowed_methods = ('GET',)
367

    
368
    def read(self, request, id, networkID=None):
369
        """List IP addresses for a server"""
370

    
371
        try:
372
            server = VirtualMachine.objects.get(id=id)
373
            address = {'public': {'ip': {'addr': server.ipfour}, \
374
                'ip6': {'addr': server.ipsix}}, 'private': ''}
375
        except VirtualMachine.DoesNotExist:
376
            raise fault.itemNotFound
377
        except VirtualMachine.MultipleObjectsReturned:
378
            raise fault.serviceUnavailable
379
        except Exception, e:
380
            log.exception('Unexpected error: %s' % e)
381
            raise fault.serviceUnavailable
382

    
383
        if networkID == "public":
384
            address = {'public': {'ip': {'addr': server.ipfour}, \
385
                'ip6': {'addr': server.ipsix}}}
386
        elif networkID == "private":
387
            address = {'private': ''}
388
        elif networkID != None:
389
            raise fault.badRequest
390
        return {"addresses": address}
391

    
392

    
393
class ServerActionHandler(BaseHandler):
394
    """Handler responsible for Server Actions
395

396
     handles Reboot, Shutdown and Start actions.
397

398
     @HTTP methods: POST, DELETE, PUT
399
     @Parameters: POST data with the action (reboot, shutdown, start)
400
     @Responses: HTTP 202 if successfully call rapi, itemNotFound,
401
     serviceUnavailable otherwise
402

403
    """
404

    
405
    allowed_methods = ('POST', 'DELETE',  'PUT')
406

    
407
    def create(self, request, id):
408
        """Reboot, Shutdown, Start virtual machine"""
409

    
410
        try:
411
            requested_action = json.loads(request.raw_post_data)
412
            reboot_request = requested_action.get('reboot', None)
413
            shutdown_request = requested_action.get('shutdown', None)
414
            start_request = requested_action.get('start', None)
415
            #action not implemented
416
            action = reboot_request and 'REBOOT' or shutdown_request \
417
                     and 'STOP' or start_request and 'START'
418

    
419
            if not action:
420
                raise fault.notImplemented
421
            #test if we can get the vm
422
            vm = VirtualMachine.objects.get(id=id)
423
            backend.start_action(vm, action)
424

    
425
            if reboot_request:
426
                rapi.RebootInstance(vm.backend_id)
427
            elif shutdown_request:
428
                rapi.ShutdownInstance(vm.backend_id)
429
            elif start_request:
430
                rapi.StartupInstance(vm.backend_id)
431
            return accepted
432
        except VirtualMachine.DoesNotExist:
433
            raise fault.itemNotFound
434
        except VirtualMachine.MultipleObjectsReturned:
435
            raise fault.serviceUnavailable
436
        except GanetiApiError, CertificateError:
437
            raise fault.serviceUnavailable
438
        except Exception, e:
439
            log.exception('Unexpected error: %s' % e)
440
            raise fault.serviceUnavailable
441

    
442
    def delete(self, request, id):
443
        """Delete an Instance"""
444
        return accepted
445

    
446
    def update(self, request, id):
447
        raise fault.itemNotFound
448

    
449

    
450
class ServerBackupHandler(BaseHandler):
451
    """ Backup Schedules are not implemented yet, return notImplemented """
452
    allowed_methods = ('GET', 'POST', 'DELETE')
453

    
454
    def read(self, request, id):
455
        raise fault.notImplemented
456

    
457
    def create(self, request, id):
458
        raise fault.notImplemented
459

    
460
    def delete(self, request, id):
461
        raise fault.notImplemented
462

    
463

    
464
class ServerMetadataHandler(BaseHandler):
465
    """Handles Metadata of a specific Server
466

467
    the handler Lists, Creates, Updates and Deletes Metadata values
468

469
    @HTTP methods: POST, DELETE, PUT, GET
470
    @Parameters: POST data with key value pairs
471

472
    """
473
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
474

    
475
    def read(self, request, id, key=None):
476
        """List metadata of the specific server"""
477
        if key is None:
478
            return self.read_allkeys(request, id)
479
        else:
480
            return self.read_onekey(request, id, key)
481

    
482
    def read_allkeys(self, request, id):
483
        """Returns all the key value pairs of the specified server"""
484
        try:
485
            server = VirtualMachine.objects.get(pk=id)
486
            return {
487
                "metadata": {
488
                    "values": [
489
                        {m.meta_key: m.meta_value} \
490
                        for m in server.virtualmachinemetadata_set.all()
491
                    ]
492
                }
493
            }
494
        except VirtualMachine.DoesNotExist:
495
            raise fault.itemNotFound
496
        except VirtualMachine.MultipleObjectsReturned:
497
            raise fault.serviceUnavailable
498
        except Exception, e:
499
            log.exception('Unexpected error: %s' % e)
500
            raise fault.serviceUnavailable
501

    
502
    def read_onekey(self, request, id, key):
503
        """Returns the specified metadata key of the specified server"""
504
        try:
505
            server = VirtualMachine.objects.get(pk=id)
506
            return {
507
                "metadata": {
508
                    "values": [
509
                        {m.meta_key: m.meta_value} for m in
510
                        server.virtualmachinemetadata_set.filter(meta_key=key)
511
                    ]
512
                }
513
            }
514
        except VirtualMachineMetadata.DoesNotExist:
515
            raise fault.itemNotFound
516
        except VirtualMachine.DoesNotExist:
517
            raise fault.itemNotFound
518
        except VirtualMachine.MultipleObjectsReturned:
519
            raise fault.serviceUnavailable
520
        except Exception, e:
521
            log.exception('Unexpected error: %s' % e)
522
            raise fault.serviceUnavailable
523

    
524
    def create(self, request, id, key=None):
525
        """Create or Update all metadata for the specified VM"""
526
        if key is not None:
527
            log.exception('The POST request should not pass a key in the URL')
528
            raise fault.badRequest
529
        try:
530
            metadata = json.loads(request.raw_post_data)['metadata']
531
        except Exception as e:
532
            log.exception('Malformed create request: %s - %s' \
533
                          % (e, request.raw_post_data))
534
            raise fault.badRequest
535

    
536
        try:
537
            vm = VirtualMachine.objects.get(pk=id)
538
            for x in metadata.keys():
539
                vm_meta, created = (vm.virtualmachinemetadata_set.
540
                                    get_or_create(meta_key=x))
541
                vm_meta.meta_value = metadata[x]
542
                vm_meta.save()
543
            return {
544
                "metadata": [{
545
                    "meta": {
546
                        "key": {m.meta_key: m.meta_value}}} \
547
                    for m in vm.virtualmachinemetadata_set.all()]
548
            }
549
        except VirtualMachine.DoesNotExist:
550
            raise fault.itemNotFound
551
        except VirtualMachine.MultipleObjectsReturned:
552
            raise fault.serviceUnavailable
553
        except VirtualMachineMetadata.DoesNotExist:
554
            raise fault.itemNotFound
555
        except VirtualMachineMetadata.MultipleObjectsReturned:
556
            raise fault.serviceUnavailable
557
        except Exception, e:
558
            log.exception('Unexpected error: %s' % e)
559
            raise fault.serviceUnavailable
560

    
561
    def update(self, request, id, key=None):
562
        """Update or Create the specified metadata key for the specified VM"""
563
        if key is None:
564
            log.exception('No metadata key specified in URL')
565
            raise fault.badRequest
566
        try:
567
            metadata = json.loads(request.raw_post_data)['meta']
568
            metadata_value = metadata[key]
569
        except Exception as e:
570
            log.exception('Malformed create request: %s - %s' \
571
                          % (e, request.raw_post_data))
572
            raise fault.badRequest
573

    
574
        try:
575
            server = VirtualMachine.objects.get(pk=id)
576
            vm_meta, created = (server.virtualmachinemetadata_set.
577
                                get_or_create(meta_key=key))
578
            vm_meta.meta_value = metadata_value
579
            vm_meta.save()
580
            return {"meta": {vm_meta.meta_key: vm_meta.meta_value}}
581

    
582
        except VirtualMachine.DoesNotExist:
583
            raise fault.itemNotFound
584
        except VirtualMachine.MultipleObjectsReturned:
585
            raise fault.serviceUnavailable
586
        except VirtualMachineMetadata.DoesNotExist:
587
            raise fault.itemNotFound
588
        except VirtualMachineMetadata.MultipleObjectsReturned:
589
            raise fault.serviceUnavailable
590
        except Exception, e:
591
            log.exception('Unexpected error: %s' % e)
592
            raise fault.serviceUnavailable
593

    
594
    def delete(self, request, id, key=None):
595
        """Delete the specified metadata key"""
596
        if key is None:
597
            log.exception('No metadata key specified in URL')
598
            raise fault.badRequest
599
        try:
600
            server = VirtualMachine.objects.get(pk=id)
601
            server.virtualmachinemetadata_set.get(meta_key=key).delete()
602
        except VirtualMachineMetadata.DoesNotExist:
603
            raise fault.itemNotFound
604
        except VirtualMachine.DoesNotExist:
605
            raise fault.itemNotFound
606
        except VirtualMachineMetadata.MultipleObjectsReturned:
607
            raise fault.serviceUnavailable
608
        except VirtualMachine.MultipleObjectsReturned:
609
            raise fault.serviceUnavailable
610
        except Exception, e:
611
            log.exception('Unexpected error: %s' % e)
612
            raise fault.serviceUnavailable
613

    
614

    
615
class FlavorHandler(BaseHandler):
616
    """Handler responsible for Flavors
617

618
    """
619
    allowed_methods = ('GET',)
620

    
621
    def read(self, request, id=None):
622
        """
623
        List flavors or retrieve one
624

625
        Returns: OK
626
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
627
                badRequest, itemNotFound
628
        """
629
        try:
630
            flavors = Flavor.objects.all()
631
            flavors = [{'id': flavor.id,
632
                        'name': flavor.name,
633
                        'ram': flavor.ram,
634
                        'disk': flavor.disk,
635
                        'cpu': flavor.cpu}
636
                        for flavor in flavors]
637

    
638
            if id is None:
639
                simple = map(lambda v: {
640
                            "id": v['id'],
641
                            "name": v['name'],
642
                        }, flavors)
643
                return {"flavors": simple}
644
            elif id == "detail":
645
                return {"flavors": flavors}
646
            else:
647
                flavor = Flavor.objects.get(id=id)
648
                return {"flavor":  {
649
                    'id': flavor.id,
650
                    'name': flavor.name,
651
                    'ram': flavor.ram,
652
                    'disk': flavor.disk,
653
                    'cpu': flavor.cpu,
654
                   }}
655

    
656
        except Flavor.DoesNotExist:
657
            raise fault.itemNotFound
658
        except Flavor.MultipleObjectsReturned:
659
            raise fault.serviceUnavailable
660
        except Exception, e:
661
            log.exception('Unexpected error: %s' % e)
662
            raise fault.serviceUnavailable
663

    
664

    
665
class ImageHandler(BaseHandler):
666
    """Handler responsible for Images
667

668
     handles the listing, creation and delete of Images.
669

670
     @HTTP methods: GET, POST
671
     @Parameters: POST data
672
     @Responses: HTTP 202 if successfully create Image or get the Images list,
673
     itemNotFound, serviceUnavailable otherwise
674

675
    """
676

    
677
    allowed_methods = ('GET', 'POST')
678

    
679
    def read(self, request, id=None):
680
        """
681
        List images or retrieve one
682

683
        Returns: OK
684
        Faults: cloudServersFault, serviceUnavailable, unauthorized,
685
                badRequest, itemNotFound
686
        """
687

    
688
        #changes_since should be on ISO 8601 format
689
        try:
690
            changes_since = request.GET.get("changes-since", 0)
691
            if changes_since:
692
                last_update = datetime.strptime(changes_since,
693
                                                "%Y-%m-%dT%H:%M:%S")
694
                #return a badRequest if the changes_since is older than a limit
695
                if datetime.now() - last_update > timedelta(
696
                    seconds=settings.POLL_LIMIT):
697
                    raise fault.badRequest
698
                images = Image.objects.filter(updated__gt=last_update)
699
                if not len(images):
700
                    return notModified
701
            else:
702
                images = Image.objects.all()
703
        except Exception, e:
704
            raise fault.badRequest
705
        try:
706
            images_list = [{'created': image.created.isoformat(),
707
                        'id': image.id,
708
                        'name': image.name,
709
                        'updated': image.updated.isoformat(),
710
                        'status': image.state,
711
                        'progress': image.state == 'ACTIVE' and 100 or 0,
712
                        'size': image.size,
713
                        'serverId': image.sourcevm and image.sourcevm.id or "",
714
                        #'metadata':[{'meta':
715
                        #{ 'key': {metadata.meta_key: metadata.meta_value}}}
716
                        #for metadata in image.imagemetadata_set.all()]
717
                        'metadata':{'meta':
718
                            {'key': {'description': image.description}}},
719
                       } for image in images]
720
            # Images info is stored in the DB. Ganeti is not aware of this
721
            if id == "detail":
722
                return {"images": images_list}
723
            elif id is None:
724
                return {"images": [{"id": s['id'], "name": s['name']} \
725
                    for s in images_list]}
726
            else:
727
                image = images.get(id=id)
728
                return {"image":  {'created': image.created.isoformat(),
729
                    'id': image.id,
730
                    'name': image.name,
731
                    'updated': image.updated.isoformat(),
732
                    'description': image.description,
733
                    'status': image.state,
734
                    'progress': image.state == 'ACTIVE' and 100 or 0,
735
                    'size': image.size,
736
                    'serverId': image.sourcevm and image.sourcevm.id or "",
737
                    #'metadata':[{'meta': { 'key':
738
                    #{metadata.meta_key: metadata.meta_value}}}
739
                    #for metadata in image.imagemetadata_set.all()]
740
                    'metadata': {
741
                        'meta': {'key': {'description': image.description}}},
742
                   }}
743
        except Image.DoesNotExist:
744
                    raise fault.itemNotFound
745
        except Image.MultipleObjectsReturned:
746
                    raise fault.serviceUnavailable
747
        except Exception, e:
748
                    log.exception('Unexpected error: %s' % e)
749
                    raise fault.serviceUnavailable
750

    
751
    def create(self, request):
752
        """Create a new image"""
753
        return accepted
754

    
755

    
756
class ImageMetadataHandler(BaseHandler):
757
    """Handles Metadata of a specific Image
758

759
    the handler Lists, Creates, Updates and Deletes Metadata values
760

761
    @HTTP methods: POST, DELETE, PUT, GET
762
    @Parameters: POST data with key value pairs
763

764
    """
765
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
766

    
767
    def read(self, request, id, key=None):
768
        """List metadata of the specific server"""
769
        if key is None:
770
            return self.read_allkeys(request, id)
771
        else:
772
            return self.read_onekey(request, id, key)
773

    
774
    def read_allkeys(self, request, id):
775
        """Returns all the key value pairs of the specified server"""
776
        try:
777
            image = Image.objects.get(pk=id)
778
            return {
779
                "metadata": [{
780
                    "meta": {
781
                        "key": {m.meta_key: m.meta_value}}} \
782
                    for m in image.imagemetadata_set.all()]
783
            }
784
        except Image.DoesNotExist:
785
            raise fault.itemNotFound
786
        except Image.MultipleObjectsReturned:
787
            raise fault.serviceUnavailable
788
        except Exception, e:
789
            log.exception('Unexpected error: %s' % e)
790
            raise fault.serviceUnavailable
791

    
792
    def read_onekey(self, request, id, key):
793
        """Returns the specified metadata key of the specified server"""
794
        try:
795
            image = Image.objects.get(pk=id)
796
            return {
797
                "metadata": {
798
                    "values": [
799
                        {m.meta_key: m.meta_value} \
800
                        for m in image.imagemetadata_set.filter(meta_key=key)
801
                    ]
802
                }
803
            }
804
        except ImageMetadata.DoesNotExist:
805
            raise fault.itemNotFound
806
        except Image.DoesNotExist:
807
            raise fault.itemNotFound
808
        except Image.MultipleObjectsReturned:
809
            raise fault.serviceUnavailable
810
        except Exception, e:
811
            log.exception('Unexpected error: %s' % e)
812
            raise fault.serviceUnavailable
813

    
814
    def create(self, request, id, key=None):
815
        """Create or Update all metadata for the specified Image"""
816
        if key is not None:
817
            log.exception('The POST request should not pass a key in the URL')
818
            raise fault.badRequest
819
        try:
820
            metadata = json.loads(request.raw_post_data)['metadata']
821
        except Exception as e:
822
            log.exception('Malformed create request: %s - %s' \
823
                          % (e, request.raw_post_data))
824
            raise fault.badRequest
825

    
826
        try:
827
            image = Image.objects.get(pk=id)
828
            for x in metadata.keys():
829
                img_meta, created = (image.imagemetadata_set.
830
                                     get_or_create(meta_key=x))
831
                img_meta.meta_value = metadata[x]
832
                img_meta.save()
833
            return {
834
                "metadata": [{
835
                    "meta": {
836
                        "key": {m.meta_key: m.meta_value}}} \
837
                    for m in image.imagemetadata_set.all()]
838
            }
839
        except Image.DoesNotExist:
840
            raise fault.itemNotFound
841
        except Image.MultipleObjectsReturned:
842
            raise fault.serviceUnavailable
843
        except ImageMetadata.DoesNotExist:
844
            raise fault.itemNotFound
845
        except ImageMetadata.MultipleObjectsReturned:
846
            raise fault.serviceUnavailable
847
        except Exception, e:
848
            log.exception('Unexpected error: %s' % e)
849
            raise fault.serviceUnavailable
850

    
851
    def update(self, request, id, key=None):
852
        """Update or Create the specified metadata key for the
853
        specified Image"""
854
        if key is None:
855
            log.exception('No metadata key specified in URL')
856
            raise fault.badRequest
857
        try:
858
            metadata = json.loads(request.raw_post_data)['meta']
859
            metadata_value = metadata[key]
860
        except Exception as e:
861
            log.exception('Malformed create request: %s - %s' \
862
                          % (e, request.raw_post_data))
863
            raise fault.badRequest
864

    
865
        try:
866
            image = Image.objects.get(pk=id)
867
            img_meta, created = (image.imagemetadata_set.
868
                                 get_or_create(meta_key=key))
869
            img_meta.meta_value = metadata_value
870
            img_meta.save()
871
            return {"meta": {img_meta.meta_key: img_meta.meta_value}}
872

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

    
885
    def delete(self, request, id, key=None):
886
        """Delete the specified metadata key"""
887
        if key is None:
888
            log.exception('No metadata key specified in URL')
889
            raise fault.badRequest
890
        try:
891
            image = Image.objects.get(pk=id)
892
            image.imagemetadata_set.get(meta_key=key).delete()
893
        except ImageMetadata.DoesNotExist:
894
            raise fault.itemNotFound
895
        except Image.DoesNotExist:
896
            raise fault.itemNotFound
897
        except ImageMetadata.MultipleObjectsReturned:
898
            raise fault.serviceUnavailable
899
        except Image.MultipleObjectsReturned:
900
            raise fault.serviceUnavailable
901
        except Exception, e:
902
            log.exception('Unexpected error: %s' % e)
903
            raise fault.serviceUnavailable
904

    
905

    
906
class SharedIPGroupHandler(BaseHandler):
907
    allowed_methods = ('GET', 'POST', 'DELETE')
908

    
909
    def read(self, request, id=None):
910
        """List Shared IP Groups"""
911
        if id is None:
912
            return {}
913
        elif id == "detail":
914
            return {}
915
        else:
916
            raise fault.itemNotFound
917

    
918
    def create(self, request, id):
919
        """Creates a new Shared IP Group"""
920
        return created
921

    
922
    def delete(self, request, id):
923
        """Deletes a Shared IP Group"""
924
        raise fault.itemNotFound
925

    
926

    
927
class VirtualMachineGroupHandler(BaseHandler):
928
    """Handler responsible for Virtual Machine Groups
929

930
     creates, lists, deletes virtual machine groups
931

932
     @HTTP methods: GET, POST, DELETE
933
     @Parameters: POST data
934
     @Responses: HTTP 202 if successfully get the Groups list,
935
     itemNotFound, serviceUnavailable otherwise
936

937
    """
938

    
939
    allowed_methods = ('GET', 'POST', 'DELETE')
940

    
941
    def read(self, request, id=None):
942
        """List Groups"""
943
        try:
944
            vmgroups = VirtualMachineGroup.objects.all()
945
            vmgroups_list = [{'id': vmgroup.id, \
946
                  'name': vmgroup.name,  \
947
                   'server_id':
948
                    [machine.id for machine in vmgroup.machines.all()]
949
                   } for vmgroup in vmgroups]
950
            # Group info is stored in the DB. Ganeti is not aware of this
951
            if id == "detail":
952
                return {"groups": vmgroups_list}
953
            elif id is None:
954
                return {"groups": [{"id": s['id'],
955
                                    "name": s['name']} for s in vmgroups_list]}
956
            else:
957
                vmgroup = vmgroups.get(id=id)
958

    
959
                return {"group":  {'id': vmgroup.id,
960
                  'name': vmgroup.name,
961
                   'server_id':
962
                    [machine.id for machine in vmgroup.machines.all()]
963
                   }}
964

    
965
        except VirtualMachineGroup.DoesNotExist:
966
                    raise fault.itemNotFound
967
        except VirtualMachineGroup.MultipleObjectsReturned:
968
                    raise fault.serviceUnavailable
969
        except Exception, e:
970
                    log.exception('Unexpected error: %s' % e)
971
                    raise fault.serviceUnavailable
972

    
973
    def create(self, request, id):
974
        """Creates a Group"""
975
        return created
976

    
977
    def delete(self, request, id):
978
        """Deletes a  Group"""
979
        raise fault.itemNotFound
980

    
981

    
982
class LimitHandler(BaseHandler):
983
    allowed_methods = ('GET',)
984

    
985
    # XXX: hookup with @throttle
986

    
987
    rate = [
988
        {
989
           "verb": "POST",
990
           "URI": "*",
991
           "regex": ".*",
992
           "value": 10,
993
           "remaining": 2,
994
           "unit": "MINUTE",
995
           "resetTime": 1244425439
996
        },
997
        {
998
           "verb": "POST",
999
           "URI": "*/servers",
1000
           "regex": "^/servers",
1001
           "value": 25,
1002
           "remaining": 24,
1003
           "unit": "DAY",
1004
           "resetTime": 1244511839
1005
        },
1006
        {
1007
           "verb": "PUT",
1008
           "URI": "*",
1009
           "regex": ".*",
1010
           "value": 10,
1011
           "remaining": 2,
1012
           "unit": "MINUTE",
1013
           "resetTime": 1244425439
1014
        },
1015
        {
1016
           "verb": "GET",
1017
           "URI": "*",
1018
           "regex": ".*",
1019
           "value": 3,
1020
           "remaining": 3,
1021
           "unit": "MINUTE",
1022
           "resetTime": 1244425439
1023
        },
1024
        {
1025
           "verb": "DELETE",
1026
           "URI": "*",
1027
           "regex": ".*",
1028
           "value": 100,
1029
           "remaining": 100,
1030
           "unit": "MINUTE",
1031
           "resetTime": 1244425439
1032
        }
1033
    ]
1034

    
1035
    absolute = {
1036
        "maxTotalRAMSize": 51200,
1037
        "maxIPGroups": 50,
1038
        "maxIPGroupMembers": 25
1039
    }
1040

    
1041
    def read(self, request):
1042
        return {"limits": {
1043
                "rate": self.rate,
1044
                "absolute": self.absolute,
1045
               }
1046
            }
1047

    
1048

    
1049
class DiskHandler(BaseHandler):
1050
    allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
1051

    
1052
    def read(self, request, id=None):
1053
        """List Disks"""
1054
        if id is None:
1055
            return self.read_all(request)
1056
        elif id == "detail":
1057
            return self.read_all(request, detail=True)
1058
        else:
1059
            return self.read_one(request, id)
1060

    
1061
    def read_one(self, request, id):
1062
        """List one Disk with the specified id with all details"""
1063
        # FIXME Get detailed info from the DB
1064
        # for the Disk with the specified id
1065
        try:
1066
            disk = Disk.objects.get(pk=id)
1067
            disk_details = {
1068
                "id": disk.id,
1069
                "name": disk.name,
1070
                "size": disk.size,
1071
                "created": disk.created,
1072
                "serverId": disk.vm.id
1073
            }
1074
            return {"disks": disk_details}
1075
        except:
1076
            raise fault.itemNotFound
1077

    
1078
    @paginator
1079
    def read_all(self, request, detail=False):
1080
        """List all Disks. If -detail- is set list them with all details"""
1081
        if not detail:
1082
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1083
            return {"disks": [{"id": disk.id, "name": disk.name} \
1084
                for disk in disks]}
1085
        else:
1086
            disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
1087
            disks_details = [{
1088
                "id": disk.id,
1089
                "name": disk.name,
1090
                "size": disk.size,
1091
                "created": disk.created,
1092
                "serverId": disk.vm.id,
1093
            } for disk in disks]
1094
            return {"disks":  disks_details}
1095

    
1096
    def create(self, request):
1097
        """Create a new Disk"""
1098
        # FIXME Create a partial DB entry,
1099
        # then call the backend for actual creation
1100
        pass
1101

    
1102
    def update(self, request, id):
1103
        """Rename the Disk with the specified id"""
1104
        # FIXME Change the Disk's name in the DB
1105
        pass
1106

    
1107
    def delete(self, request, id):
1108
        """Destroy the Disk with the specified id"""
1109
        # Call the backend for actual destruction
1110
        pass