root / api / handlers.py @ 9eef701d
History | View | Annotate | Download (16.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 json |
6 |
from django.conf import settings |
7 |
from piston.handler import BaseHandler, AnonymousBaseHandler |
8 |
from synnefo.api.faults import fault, noContent, accepted, created |
9 |
from synnefo.api.helpers import instance_to_server, paginator |
10 |
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError |
11 |
from synnefo.db.models import * |
12 |
from util.rapi import GanetiRapiClient |
13 |
|
14 |
|
15 |
try:
|
16 |
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO) |
17 |
rapi.GetVersion() |
18 |
except:
|
19 |
raise fault.serviceUnavailable
|
20 |
#If we can't connect to the rapi successfully, don't do anything
|
21 |
#TODO: add logging/admin alerting
|
22 |
|
23 |
backend_prefix_id = settings.BACKEND_PREFIX_ID |
24 |
|
25 |
VERSIONS = [ |
26 |
{ |
27 |
"status": "CURRENT", |
28 |
"id": "v1.0", |
29 |
"docURL" : "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20090714.pdf ", |
30 |
"wadl" : "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl" |
31 |
}, |
32 |
{ |
33 |
"status": "CURRENT", |
34 |
"id": "v1.0grnet1", |
35 |
"docURL" : "None yet", |
36 |
"wad1" : "None yet" |
37 |
} |
38 |
] |
39 |
|
40 |
|
41 |
class VersionHandler(AnonymousBaseHandler): |
42 |
allowed_methods = ('GET',)
|
43 |
|
44 |
def read(self, request, number=None): |
45 |
if number is None: |
46 |
versions = map(lambda v: { |
47 |
"status": v["status"], |
48 |
"id": v["id"], |
49 |
}, VERSIONS) |
50 |
return { "versions": versions } |
51 |
else:
|
52 |
for version in VERSIONS: |
53 |
if version["id"] == number: |
54 |
return { "version": version } |
55 |
raise fault.itemNotFound
|
56 |
|
57 |
|
58 |
class ServerHandler(BaseHandler): |
59 |
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE') |
60 |
|
61 |
def read(self, request, id=None): |
62 |
from time import sleep |
63 |
sleep(0.5)
|
64 |
#TODO: delete the sleep once the mock objects are removed
|
65 |
if id is None: |
66 |
return self.read_all(request) |
67 |
elif id == "detail": |
68 |
return self.read_all(request, detail=True) |
69 |
else:
|
70 |
return self.read_one(request, id) |
71 |
|
72 |
def read_one(self, request, id): |
73 |
try:
|
74 |
instance = VirtualMachine.objects.get(id=id)
|
75 |
return { "server": instance } #FIXME |
76 |
except:
|
77 |
raise fault.itemNotFound
|
78 |
|
79 |
@paginator
|
80 |
def read_all(self, request, detail=False): |
81 |
virtual_servers = VirtualMachine.objects.all() |
82 |
virtual_servers = [virtual_server for virtual_server in virtual_servers if virtual_server.rsapi_state !="DELETED"] |
83 |
#get all VM's for now, FIX it to take the user's VMs only yet. also don't get deleted VM's
|
84 |
|
85 |
if not detail: |
86 |
return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] } |
87 |
else:
|
88 |
virtual_servers_list = [{'status': server.rsapi_state,
|
89 |
'flavorId': server.flavor.id,
|
90 |
'name': server.name,
|
91 |
'id': server.id,
|
92 |
'imageId': server.sourceimage.id,
|
93 |
'metadata': {'Server_Label': server.description, |
94 |
'hostId': '9e107d9d372bb6826bd81d3542a419d6', |
95 |
'addresses': {'public': ['67.23.10.133'], |
96 |
'private': ['10.176.42.17'], |
97 |
} |
98 |
} |
99 |
} for server in virtual_servers] |
100 |
#pass some fake data regarding ip, since we don't have any such data
|
101 |
return { "servers": virtual_servers_list } |
102 |
|
103 |
|
104 |
def create(self, request): |
105 |
print 'create machine was called' |
106 |
#TODO: add random pass, metadata
|
107 |
try:
|
108 |
options_request = json.loads(request.POST.get('create', None)) #here we have the options for cpu, ram etc |
109 |
cpu = options_request.get('cpu','') |
110 |
ram = options_request.get('ram','') |
111 |
name = options_request.get('name','') |
112 |
storage = options_request.get('storage','') |
113 |
pnode = rapi.GetNodes()[0]
|
114 |
rapi.CreateInstance('create', name, 'plain', [{"size": storage}], [{}], os='debootstrap+default', ip_check=False, name_check=False,pnode=pnode, beparams={'auto_balance': True, 'vcpus': cpu, 'memory': ram}) |
115 |
return accepted
|
116 |
except: # something bad happened. FIXME: return code |
117 |
return noContent
|
118 |
|
119 |
#TODO: replace with real data from request.POST
|
120 |
#TODO: create the VM in the database
|
121 |
|
122 |
def update(self, request, id): |
123 |
return noContent
|
124 |
|
125 |
def delete(self, request, id): |
126 |
try:
|
127 |
instance = VirtualMachine.objects.get(id=id)
|
128 |
print 'deleting machine %s' % instance.name |
129 |
instance._operstate = 'DESTROYED'
|
130 |
return accepted
|
131 |
#rapi.DeleteInstance(instance.name)
|
132 |
except:
|
133 |
raise fault.itemNotFound
|
134 |
|
135 |
|
136 |
class ServerAddressHandler(BaseHandler): |
137 |
allowed_methods = ('GET', 'PUT', 'DELETE') |
138 |
|
139 |
def read(self, request, id, type=None): |
140 |
"""List IP addresses for a server"""
|
141 |
|
142 |
if type is None: |
143 |
pass
|
144 |
elif type == "private": |
145 |
pass
|
146 |
elif type == "public": |
147 |
pass
|
148 |
return {}
|
149 |
|
150 |
def update(self, request, id, address): |
151 |
"""Share an IP address to another in the group"""
|
152 |
return accepted
|
153 |
|
154 |
def delete(self, request, id, address): |
155 |
"""Unshare an IP address"""
|
156 |
return accepted
|
157 |
|
158 |
|
159 |
class ServerActionHandler(BaseHandler): |
160 |
allowed_methods = ('POST', 'DELETE', 'GET', 'PUT') |
161 |
#TODO: remove GET/PUT
|
162 |
|
163 |
def read(self, request, id): |
164 |
return accepted
|
165 |
|
166 |
def create(self, request, id): |
167 |
"""Reboot, rebuild, resize, confirm resized, revert resized"""
|
168 |
try:
|
169 |
machine = VirtualMachine.objects.get(id=id)
|
170 |
except:
|
171 |
return noContent
|
172 |
#FIXME: for now make a list with only one machine. This will be a list of machines (for the list view)
|
173 |
reboot_request = request.POST.get('reboot', None) |
174 |
shutdown_request = request.POST.get('shutdown', None) |
175 |
start_request = request.POST.get('start', None) |
176 |
if reboot_request:
|
177 |
return self.action_start([machine], 'reboot') |
178 |
elif shutdown_request:
|
179 |
return self.action_start([machine], 'shutdown') |
180 |
elif start_request:
|
181 |
return self.action_start([machine], 'start') |
182 |
return noContent #FIXME: when does this happen? |
183 |
|
184 |
|
185 |
def delete(self, request, id): |
186 |
"""Delete an Instance"""
|
187 |
return accepted
|
188 |
|
189 |
def update(self, request, id): |
190 |
return noContent
|
191 |
|
192 |
def action_start(self, list_of_machines, action): |
193 |
if action == 'reboot': |
194 |
try:
|
195 |
for machine in list_of_machines: |
196 |
rapi.RebootInstance(machine) |
197 |
return accepted
|
198 |
except: # something bad happened. |
199 |
#FIXME: return code. Rackspace error response code(s): cloudServersFault (400, 500), serviceUnavailable (503), unauthorized(401), badRequest (400), badMediaType(415), itemNotFound (404), buildInProgress (409), overLimit (413)
|
200 |
return noContent
|
201 |
if action == 'shutdown': |
202 |
try:
|
203 |
for machine in list_of_machines: |
204 |
rapi.ShutdownInstance(machine) |
205 |
return accepted
|
206 |
except: # something bad happened. FIXME: return code |
207 |
return noContent
|
208 |
if action == 'start': |
209 |
try:
|
210 |
for machine in list_of_machines: |
211 |
rapi.StartupInstance(machine) |
212 |
return accepted
|
213 |
except: # something bad happened. FIXME: return code |
214 |
return noContent
|
215 |
|
216 |
|
217 |
|
218 |
#read is called on GET requests
|
219 |
#create is called on POST, and creates new objects, and should return them (or rc.CREATED.)
|
220 |
#update is called on PUT, and should update an existing product and return them (or rc.ALL_OK.)
|
221 |
#delete is called on DELETE, and should delete an existing object. Should not return anything, just rc.DELETED.'''
|
222 |
|
223 |
|
224 |
class ServerBackupHandler(BaseHandler): |
225 |
""" Backup Schedules are not implemented yet, return notImplemented """
|
226 |
allowed_methods = ('GET', 'POST', 'DELETE') |
227 |
|
228 |
def read(self, request, id): |
229 |
raise fault.notImplemented
|
230 |
|
231 |
def create(self, request, id): |
232 |
raise fault.notImplemented
|
233 |
|
234 |
def delete(self, request, id): |
235 |
raise fault.notImplemented
|
236 |
|
237 |
|
238 |
class FlavorHandler(BaseHandler): |
239 |
allowed_methods = ('GET',)
|
240 |
flavors = Flavor.objects.all() |
241 |
flavors = [ {'id': flavor.id, 'name': flavor.name, 'ram': flavor.ram, \ |
242 |
'disk': flavor.disk} for flavor in flavors] |
243 |
|
244 |
def read(self, request, id=None): |
245 |
"""
|
246 |
List flavors or retrieve one
|
247 |
|
248 |
Returns: OK
|
249 |
Faults: cloudServersFault, serviceUnavailable, unauthorized,
|
250 |
badRequest, itemNotFound
|
251 |
"""
|
252 |
if id is None: |
253 |
simple = map(lambda v: { |
254 |
"id": v['id'], |
255 |
"name": v['name'], |
256 |
}, self.flavors)
|
257 |
return { "flavors": simple } |
258 |
elif id == "detail": |
259 |
return { "flavors": self.flavors } |
260 |
else:
|
261 |
for flavor in self.flavors: |
262 |
if str(flavor['id']) == id: |
263 |
return { "flavor": flavor } |
264 |
raise fault.itemNotFound
|
265 |
|
266 |
|
267 |
class ImageHandler(BaseHandler): |
268 |
allowed_methods = ('GET', 'POST') |
269 |
|
270 |
def read(self, request, id=None): |
271 |
"""
|
272 |
List images or retrieve one
|
273 |
|
274 |
Returns: OK
|
275 |
Faults: cloudServersFault, serviceUnavailable, unauthorized,
|
276 |
badRequest, itemNotFound
|
277 |
"""
|
278 |
images = Image.objects.all() |
279 |
images_list = [ {'created': image.created.isoformat(),
|
280 |
'id': image.id,
|
281 |
'name': image.name,
|
282 |
'updated': image.updated.isoformat(),
|
283 |
'description': image.description,
|
284 |
'state': image.state,
|
285 |
'vm_id': image.vm_id
|
286 |
} for image in images] |
287 |
if rapi: # Images info is stored in the DB. Ganeti is not aware of this |
288 |
if id == "detail": |
289 |
return { "images": images_list } |
290 |
elif id is None: |
291 |
return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] } |
292 |
else:
|
293 |
try:
|
294 |
image = images.get(id=id)
|
295 |
return { "image": {'created': image.created.isoformat(), |
296 |
'id': image.id,
|
297 |
'name': image.name,
|
298 |
'updated': image.updated.isoformat(),
|
299 |
'description': image.description,
|
300 |
'state': image.state,
|
301 |
'vm_id': image.vm_id
|
302 |
} } |
303 |
except:
|
304 |
raise fault.itemNotFound
|
305 |
else:
|
306 |
raise fault.serviceUnavailable
|
307 |
|
308 |
def create(self, request): |
309 |
"""Create a new image"""
|
310 |
return accepted
|
311 |
|
312 |
|
313 |
class SharedIPGroupHandler(BaseHandler): |
314 |
allowed_methods = ('GET', 'POST', 'DELETE') |
315 |
|
316 |
def read(self, request, id=None): |
317 |
"""List Shared IP Groups"""
|
318 |
if id is None: |
319 |
return {}
|
320 |
elif id == "detail": |
321 |
return {}
|
322 |
else:
|
323 |
raise fault.itemNotFound
|
324 |
|
325 |
def create(self, request, id): |
326 |
"""Creates a new Shared IP Group"""
|
327 |
return created
|
328 |
|
329 |
def delete(self, request, id): |
330 |
"""Deletes a Shared IP Group"""
|
331 |
return noContent
|
332 |
|
333 |
|
334 |
class VirtualMachineGroupHandler(BaseHandler): |
335 |
allowed_methods = ('GET', 'POST', 'DELETE') |
336 |
|
337 |
def read(self, request, id=None): |
338 |
"""List Groups"""
|
339 |
vmgroups = VirtualMachineGroup.objects.all() |
340 |
vmgroups = [ {'id': vmgroup.id, \
|
341 |
'name': vmgroup.name, \
|
342 |
'server_id': [machine.id for machine in vmgroup.machines.all()] \ |
343 |
} for vmgroup in vmgroups] |
344 |
if rapi: # Group info is stored in the DB. Ganeti is not aware of this |
345 |
if id == "detail": |
346 |
return { "groups": vmgroups } |
347 |
elif id is None: |
348 |
return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups ] } |
349 |
else:
|
350 |
return { "groups": vmgroups[0] } |
351 |
|
352 |
|
353 |
def create(self, request, id): |
354 |
"""Creates a Group"""
|
355 |
return created
|
356 |
|
357 |
def delete(self, request, id): |
358 |
"""Deletes a Group"""
|
359 |
return noContent
|
360 |
|
361 |
|
362 |
class LimitHandler(BaseHandler): |
363 |
allowed_methods = ('GET',)
|
364 |
|
365 |
# XXX: hookup with @throttle
|
366 |
|
367 |
rate = [ |
368 |
{ |
369 |
"verb" : "POST", |
370 |
"URI" : "*", |
371 |
"regex" : ".*", |
372 |
"value" : 10, |
373 |
"remaining" : 2, |
374 |
"unit" : "MINUTE", |
375 |
"resetTime" : 1244425439 |
376 |
}, |
377 |
{ |
378 |
"verb" : "POST", |
379 |
"URI" : "*/servers", |
380 |
"regex" : "^/servers", |
381 |
"value" : 25, |
382 |
"remaining" : 24, |
383 |
"unit" : "DAY", |
384 |
"resetTime" : 1244511839 |
385 |
}, |
386 |
{ |
387 |
"verb" : "PUT", |
388 |
"URI" : "*", |
389 |
"regex" : ".*", |
390 |
"value" : 10, |
391 |
"remaining" : 2, |
392 |
"unit" : "MINUTE", |
393 |
"resetTime" : 1244425439 |
394 |
}, |
395 |
{ |
396 |
"verb" : "GET", |
397 |
"URI" : "*", |
398 |
"regex" : ".*", |
399 |
"value" : 3, |
400 |
"remaining" : 3, |
401 |
"unit" : "MINUTE", |
402 |
"resetTime" : 1244425439 |
403 |
}, |
404 |
{ |
405 |
"verb" : "DELETE", |
406 |
"URI" : "*", |
407 |
"regex" : ".*", |
408 |
"value" : 100, |
409 |
"remaining" : 100, |
410 |
"unit" : "MINUTE", |
411 |
"resetTime" : 1244425439 |
412 |
} |
413 |
] |
414 |
|
415 |
absolute = { |
416 |
"maxTotalRAMSize" : 51200, |
417 |
"maxIPGroups" : 50, |
418 |
"maxIPGroupMembers" : 25 |
419 |
} |
420 |
|
421 |
def read(self, request): |
422 |
return { "limits": { |
423 |
"rate": self.rate, |
424 |
"absolute": self.absolute, |
425 |
} |
426 |
} |
427 |
|
428 |
|
429 |
class DiskHandler(BaseHandler): |
430 |
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE') |
431 |
|
432 |
def read(self, request, id=None): |
433 |
"""List Disks"""
|
434 |
if id is None: |
435 |
return self.read_all(request) |
436 |
elif id == "detail": |
437 |
return self.read_all(request, detail=True) |
438 |
else:
|
439 |
return self.read_one(request, id) |
440 |
|
441 |
def read_one(self, request, id): |
442 |
"""List one Disk with the specified id with all details"""
|
443 |
# FIXME Get detailed info from the DB
|
444 |
# for the Disk with the specified id
|
445 |
try:
|
446 |
disk = Disk.objects.get(pk=id)
|
447 |
disk_details = { |
448 |
"id" : disk.id,
|
449 |
"name" : disk.name,
|
450 |
"size" : disk.size,
|
451 |
"created" : disk.created,
|
452 |
"serverId" : disk.vm.id
|
453 |
} |
454 |
return { "disks" : disk_details } |
455 |
except:
|
456 |
raise fault.itemNotFound
|
457 |
|
458 |
@paginator
|
459 |
def read_all(self, request, detail=False): |
460 |
"""List all Disks. If -detail- is set list them with all details"""
|
461 |
if not detail: |
462 |
disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
|
463 |
return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] } |
464 |
else:
|
465 |
disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0])
|
466 |
disks_details = [ { |
467 |
"id" : disk.id,
|
468 |
"name" : disk.name,
|
469 |
"size" : disk.size,
|
470 |
"created" : disk.created,
|
471 |
"serverId" : disk.vm.id,
|
472 |
} for disk in disks ] |
473 |
return { "disks": disks_details } |
474 |
|
475 |
def create(self, request): |
476 |
"""Create a new Disk"""
|
477 |
# FIXME Create a partial DB entry,
|
478 |
# then call the backend for actual creation
|
479 |
pass
|
480 |
|
481 |
def update(self, request, id): |
482 |
"""Rename the Disk with the specified id"""
|
483 |
# FIXME Change the Disk's name in the DB
|
484 |
pass
|
485 |
|
486 |
def delete(self, request, id): |
487 |
"""Destroy the Disk with the specified id"""
|
488 |
# Call the backend for actual destruction
|
489 |
pass
|