Revision e646ebe5 api/handlers.py
b/api/handlers.py | ||
---|---|---|
7 | 7 |
from piston.handler import BaseHandler, AnonymousBaseHandler |
8 | 8 |
from synnefo.api.faults import fault, noContent, accepted, created, notModified |
9 | 9 |
from synnefo.api.helpers import instance_to_server, paginator |
10 |
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError, CertificateError |
|
10 |
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError |
|
11 |
from synnefo.util.rapi import CertificateError |
|
11 | 12 |
from synnefo.db.models import * |
12 | 13 |
import random |
13 | 14 |
import string |
... | ... | |
30 | 31 |
{ |
31 | 32 |
"status": "CURRENT", |
32 | 33 |
"id": "v1.0", |
33 |
"docURL" : "http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20110112.pdf", |
|
34 |
"wadl" : "http://docs.rackspacecloud.com/servers/api/v1.0/application.wadl" |
|
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" |
|
35 | 38 |
}, |
36 | 39 |
{ |
37 | 40 |
"status": "CURRENT", |
38 | 41 |
"id": "v1.1", |
39 |
"docURL" : "http://docs.openstack.org/openstack-compute/developer/content/", |
|
40 |
"wadl" : "None yet" |
|
42 |
"docURL": |
|
43 |
"http://docs.openstack.org/openstack-compute/developer/content/", |
|
44 |
"wadl": "None yet" |
|
41 | 45 |
}, |
42 | 46 |
{ |
43 | 47 |
"status": "CURRENT", |
44 | 48 |
"id": "v1.0grnet1", |
45 |
"docURL" : "None yet",
|
|
46 |
"wad1" : "None yet"
|
|
49 |
"docURL": "None yet", |
|
50 |
"wad1": "None yet" |
|
47 | 51 |
} |
48 | 52 |
] |
49 | 53 |
|
... | ... | |
63 | 67 |
"status": v["status"], |
64 | 68 |
"id": v["id"], |
65 | 69 |
}, VERSIONS) |
66 |
return { "versions": versions }
|
|
70 |
return {"versions": versions}
|
|
67 | 71 |
else: |
68 | 72 |
for version in VERSIONS: |
69 | 73 |
if version["id"] == number: |
70 |
return { "version": version }
|
|
74 |
return {"version": version}
|
|
71 | 75 |
raise fault.itemNotFound |
72 | 76 |
except Exception, e: |
73 | 77 |
log.exception('Unexpected error: %s' % e) |
... | ... | |
81 | 85 |
|
82 | 86 |
@HTTP methods: POST, DELETE, PUT, GET |
83 | 87 |
@Parameters: POST data with the create data (cpu, ram, etc) |
84 |
@Responses: HTTP 200 if successfully call rapi, 304 if not modified, itemNotFound or serviceUnavailable otherwise |
|
88 |
@Responses: HTTP 200 if successfully call rapi, 304 if not modified, |
|
89 |
itemNotFound or serviceUnavailable otherwise |
|
85 | 90 |
|
86 | 91 |
""" |
87 | 92 |
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE') |
... | ... | |
101 | 106 |
try: |
102 | 107 |
server = VirtualMachine.objects.get(id=id) |
103 | 108 |
|
104 |
server = {'status': server.rsapi_state,
|
|
105 |
'flavorRef': server.flavor.id,
|
|
106 |
'name': server.name,
|
|
107 |
'id': server.id,
|
|
109 |
server = {'status': server.rsapi_state, |
|
110 |
'flavorRef': server.flavor.id, |
|
111 |
'name': server.name, |
|
112 |
'id': server.id, |
|
108 | 113 |
'imageRef': server.sourceimage.id, |
109 |
'created': server.created,
|
|
114 |
'created': server.created, |
|
110 | 115 |
'updated': server.updated, |
111 |
'hostId': server.hostid,
|
|
112 |
'progress': server.rsapi_state == 'ACTIVE' and 100 or 0,
|
|
116 |
'hostId': server.hostid, |
|
117 |
'progress': server.rsapi_state == 'ACTIVE' and 100 or 0, |
|
113 | 118 |
#'metadata': {'Server_Label': server.description }, |
114 |
'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()], |
|
115 |
'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''}, |
|
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': ''}, |
|
116 | 131 |
} |
117 |
return { "server": server }
|
|
132 |
return {"server": server}
|
|
118 | 133 |
except VirtualMachine.DoesNotExist: |
119 | 134 |
raise fault.itemNotFound |
120 | 135 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
123 | 138 |
log.exception('Unexpected error: %s' % e) |
124 | 139 |
raise fault.serviceUnavailable |
125 | 140 |
|
126 |
|
|
127 | 141 |
@paginator |
128 | 142 |
def read_all(self, request, detail=False): |
129 | 143 |
#changes_since should be on ISO 8601 format |
130 | 144 |
try: |
131 | 145 |
changes_since = request.GET.get("changes-since", 0) |
132 | 146 |
if changes_since: |
133 |
last_update = datetime.strptime(changes_since, "%Y-%m-%dT%H:%M:%S" ) |
|
147 |
last_update = datetime.strptime(changes_since, |
|
148 |
"%Y-%m-%dT%H:%M:%S") |
|
134 | 149 |
#return a badRequest if the changes_since is older than a limit |
135 |
if datetime.now() - last_update > timedelta(seconds=settings.POLL_LIMIT): |
|
136 |
raise fault.badRequest |
|
137 |
virtual_servers = VirtualMachine.objects.filter(updated__gt=last_update) |
|
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) |
|
138 | 155 |
if not len(virtual_servers): |
139 | 156 |
return notModified |
140 | 157 |
else: |
141 | 158 |
virtual_servers = VirtualMachine.objects.filter(deleted=False) |
142 |
#get all VM's for now, FIX it to take the user's VMs only yet. also don't get deleted VM's |
|
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 |
|
143 | 161 |
except Exception, e: |
144 |
raise fault.badRequest
|
|
162 |
raise fault.badRequest |
|
145 | 163 |
try: |
146 | 164 |
if not detail: |
147 |
return { "servers": [ { "id": s.id, "name": s.name } for s in virtual_servers ] } |
|
165 |
return {"servers": [{ |
|
166 |
"id": s.id, "name": s.name} for s in virtual_servers]} |
|
148 | 167 |
else: |
149 |
virtual_servers_list = [{'status': server.rsapi_state, |
|
150 |
'flavorRef': server.flavor.id, |
|
151 |
'name': server.name, |
|
152 |
'id': server.id, |
|
153 |
'created': server.created, |
|
154 |
'updated': server.updated, |
|
155 |
'imageRef': server.sourceimage.id, |
|
156 |
'hostId': server.hostid, |
|
157 |
'progress': server.rsapi_state == 'ACTIVE' and 100 or 0, |
|
158 |
#'metadata': {'Server_Label': server.description }, |
|
159 |
'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in server.virtualmachinemetadata_set.all()], |
|
160 |
'addresses': {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''}, |
|
161 |
|
|
162 |
} for server in virtual_servers] |
|
163 |
#pass some fake data regarding ip, since we don't have any such data |
|
164 |
return { "servers": virtual_servers_list } |
|
168 |
virtual_servers_list = [ |
|
169 |
{'status': server.rsapi_state, |
|
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': (server.rsapi_state == '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} |
|
165 | 194 |
except Exception, e: |
166 | 195 |
log.exception('Unexpected error: %s' % e) |
167 | 196 |
raise fault.serviceUnavailable |
168 | 197 |
|
169 |
|
|
170 | 198 |
def create(self, request): |
171 | 199 |
""" Parse RackSpace API create request to generate rapi create request |
172 |
|
|
173 | 200 |
TODO: auto generate and set password |
174 | 201 |
""" |
175 |
# Check if we have all the necessary data in the JSON request
|
|
202 |
# Check if we have all the necessary data in the JSON request |
|
176 | 203 |
try: |
177 | 204 |
server = json.loads(request.raw_post_data)['server'] |
178 | 205 |
name = server['name'] |
... | ... | |
187 | 214 |
except (Flavor.MultipleObjectsReturned, Image.MultipleObjectsReturned): |
188 | 215 |
raise fault.serviceUnavailable |
189 | 216 |
except Exception as e: |
190 |
log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data)) |
|
217 |
log.exception('Malformed create request: %s - %s' % |
|
218 |
(e, request.raw_post_data)) |
|
191 | 219 |
raise fault.badRequest |
192 | 220 |
|
193 | 221 |
# TODO: Proper Authn, Authz |
194 | 222 |
# Everything belongs to a single SynnefoUser for now. |
195 |
try:
|
|
223 |
try: |
|
196 | 224 |
owner = SynnefoUser.objects.all()[0] |
197 | 225 |
except Exception as e: |
198 |
log.exception('Cannot find a single SynnefoUser in the DB: %s' % (e)); |
|
226 |
log.exception('Cannot find a single SynnefoUser in the DB: %s' % |
|
227 |
(e)) |
|
199 | 228 |
raise fault.unauthorized |
200 | 229 |
|
201 | 230 |
# add the new VM to the local db |
202 | 231 |
try: |
203 |
vm = VirtualMachine.objects.create(sourceimage=image, ipfour='0.0.0.0', ipsix='::1', flavor=flavor, owner=owner) |
|
232 |
vm = VirtualMachine.objects.create(sourceimage=image, |
|
233 |
ipfour='0.0.0.0', |
|
234 |
ipsix='::1', |
|
235 |
flavor=flavor, |
|
236 |
owner=owner) |
|
204 | 237 |
except Exception as e: |
205 | 238 |
log.exception("Can't save vm: %s" % e) |
206 | 239 |
raise fault.serviceUnavailable |
... | ... | |
208 | 241 |
try: |
209 | 242 |
vm.name = name |
210 | 243 |
#vm.description = descr |
211 |
vm.save()
|
|
244 |
vm.save() |
|
212 | 245 |
jobId = rapi.CreateInstance( |
213 | 246 |
'create', |
214 |
request.META['SERVER_NAME'] == 'testserver' and 'test-server' or vm.backend_id, |
|
247 |
(request.META['SERVER_NAME'] == 'testserver' and |
|
248 |
'test-server' or vm.backend_id), |
|
215 | 249 |
'plain', |
216 |
# disk field of Flavor object is in GB, value specified here is in MB |
|
217 |
# FIXME: Always ask for a 2GB disk, current LVM physical groups are too small: |
|
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: |
|
218 | 254 |
# [{"size": flavor.disk * 1000}], |
219 | 255 |
[{"size": 2000}], |
220 | 256 |
[{}], |
... | ... | |
223 | 259 |
ip_check=False, |
224 | 260 |
name_check=False, |
225 | 261 |
#TODO: verify if this is necessary |
226 |
pnode = rapi.GetNodes()[0],
|
|
262 |
pnode=rapi.GetNodes()[0],
|
|
227 | 263 |
# Dry run when called by unit tests |
228 |
dry_run = request.META['SERVER_NAME'] == 'testserver',
|
|
264 |
dry_run=request.META['SERVER_NAME'] == 'testserver',
|
|
229 | 265 |
beparams={ |
230 | 266 |
'auto_balance': True, |
231 | 267 |
'vcpus': flavor.cpu, |
232 | 268 |
'memory': flavor.ram, |
233 | 269 |
}, |
234 | 270 |
) |
235 |
log.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk)) |
|
271 |
log.info('created vm with %s cpus, %s ram and %s storage' % |
|
272 |
(flavor.cpu, flavor.ram, flavor.disk)) |
|
236 | 273 |
except (GanetiApiError, CertificateError) as e: |
237 | 274 |
log.exception('CreateInstance failed: %s' % e) |
238 | 275 |
vm.deleted = True |
... | ... | |
242 | 279 |
log.exception('Unexpected error: %s' % e) |
243 | 280 |
vm.deleted = True |
244 | 281 |
vm.save() |
245 |
raise fault.serviceUnavailable |
|
246 |
|
|
282 |
raise fault.serviceUnavailable |
|
247 | 283 |
|
248 | 284 |
ret = {'server': { |
249 |
'id' : vm.id,
|
|
250 |
'name' : vm.name,
|
|
251 |
"imageRef" : imageRef,
|
|
252 |
"flavorRef" : flavorRef,
|
|
253 |
"hostId" : vm.hostid,
|
|
254 |
"progress" : 0,
|
|
255 |
"status" : 'BUILD',
|
|
256 |
"adminPass" : self.random_password(),
|
|
257 |
"metadata" : {"My Server Name" : vm.name},
|
|
258 |
"addresses" : {
|
|
259 |
"public" : [ ],
|
|
260 |
"private" : [ ],
|
|
285 |
'id': vm.id, |
|
286 |
'name': vm.name, |
|
287 |
"imageRef": imageRef, |
|
288 |
"flavorRef": flavorRef, |
|
289 |
"hostId": vm.hostid, |
|
290 |
"progress": 0, |
|
291 |
"status": 'BUILD', |
|
292 |
"adminPass": self.random_password(), |
|
293 |
"metadata": {"My Server Name": vm.name},
|
|
294 |
"addresses": { |
|
295 |
"public": [],
|
|
296 |
"private": [],
|
|
261 | 297 |
}, |
262 | 298 |
}, |
263 | 299 |
} |
264 |
return HttpResponse(json.dumps(ret), mimetype="application/json", status=202)
|
|
265 |
|
|
300 |
return HttpResponse(json.dumps(ret), |
|
301 |
mimetype="application/json", status=202) |
|
266 | 302 |
|
267 | 303 |
def random_password(self): |
268 | 304 |
"return random password" |
269 | 305 |
number_of_chars = 8 |
270 |
possible_chars = string.ascii_uppercase + string.ascii_lowercase + string.digits |
|
271 |
return ''.join(random.choice(possible_chars) for x in range(number_of_chars)) |
|
272 |
|
|
306 |
possible_chars = string.ascii_uppercase + string.ascii_lowercase + \ |
|
307 |
string.digits |
|
308 |
return ''.join(random.choice(possible_chars) \ |
|
309 |
for x in range(number_of_chars)) |
|
273 | 310 |
|
274 | 311 |
def update(self, request, id): |
275 |
"""Sets and updates Virtual Machine Metadata.
|
|
276 |
|
|
312 |
"""Sets and updates Virtual Machine Metadata. |
|
313 |
|
|
277 | 314 |
""" |
278 | 315 |
try: |
279 | 316 |
metadata_request = json.loads(request.raw_post_data)['metadata'] |
280 | 317 |
metadata_key = metadata_request.get('metadata_key') |
281 | 318 |
metadata_value = metadata_request.get('metadata_value') |
282 |
|
|
319 |
|
|
283 | 320 |
vm = VirtualMachine.objects.get(id=id) |
284 | 321 |
#we only update virtual machine's name atm |
285 | 322 |
if metadata_key == 'name': |
... | ... | |
296 | 333 |
|
297 | 334 |
raise fault.itemNotFound |
298 | 335 |
|
299 |
|
|
300 | 336 |
def delete(self, request, id): |
301 | 337 |
try: |
302 | 338 |
vm = VirtualMachine.objects.get(id=id) |
303 | 339 |
#TODO: set the status to DESTROYED |
304 | 340 |
vm.start_action('DESTROY') |
305 | 341 |
rapi.DeleteInstance(vm.backend_id) |
306 |
return accepted
|
|
342 |
return accepted |
|
307 | 343 |
except VirtualMachine.DoesNotExist: |
308 | 344 |
raise fault.itemNotFound |
309 | 345 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
315 | 351 |
raise fault.serviceUnavailable |
316 | 352 |
|
317 | 353 |
|
318 |
|
|
319 | 354 |
class ServerAddressHandler(BaseHandler): |
320 | 355 |
"""Handler responsible for Server Addresses |
321 | 356 |
|
322 |
handles Reboot, Shutdown and Start actions.
|
|
357 |
handles Reboot, Shutdown and Start actions. |
|
323 | 358 |
|
324 | 359 |
@HTTP methods: GET |
325 | 360 |
@Parameters: Id of server and networkID (eg public, private) |
326 |
@Responses: HTTP 200 if successfully call rapi, itemNotFound, serviceUnavailable otherwise |
|
361 |
@Responses: HTTP 200 if successfully call rapi, itemNotFound, |
|
362 |
serviceUnavailable otherwise |
|
327 | 363 |
|
328 | 364 |
""" |
329 | 365 |
allowed_methods = ('GET',) |
... | ... | |
333 | 369 |
|
334 | 370 |
try: |
335 | 371 |
server = VirtualMachine.objects.get(id=id) |
336 |
address = {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}},'private': ''} |
|
372 |
address = {'public': {'ip': {'addr': server.ipfour}, \ |
|
373 |
'ip6': {'addr': server.ipsix}}, 'private': ''} |
|
337 | 374 |
except VirtualMachine.DoesNotExist: |
338 | 375 |
raise fault.itemNotFound |
339 | 376 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
343 | 380 |
raise fault.serviceUnavailable |
344 | 381 |
|
345 | 382 |
if networkID == "public": |
346 |
address = {'public': { 'ip': {'addr': server.ipfour}, 'ip6': {'addr': server.ipsix}}} |
|
383 |
address = {'public': {'ip': {'addr': server.ipfour}, \ |
|
384 |
'ip6': {'addr': server.ipsix}}} |
|
347 | 385 |
elif networkID == "private": |
348 |
address = {'private': ''}
|
|
386 |
address = {'private': ''} |
|
349 | 387 |
elif networkID != None: |
350 | 388 |
raise fault.badRequest |
351 |
return { "addresses": address } |
|
352 |
|
|
389 |
return {"addresses": address} |
|
353 | 390 |
|
354 | 391 |
|
355 | 392 |
class ServerActionHandler(BaseHandler): |
356 | 393 |
"""Handler responsible for Server Actions |
357 | 394 |
|
358 |
handles Reboot, Shutdown and Start actions.
|
|
395 |
handles Reboot, Shutdown and Start actions. |
|
359 | 396 |
|
360 | 397 |
@HTTP methods: POST, DELETE, PUT |
361 | 398 |
@Parameters: POST data with the action (reboot, shutdown, start) |
362 |
@Responses: HTTP 202 if successfully call rapi, itemNotFound, serviceUnavailable otherwise |
|
399 |
@Responses: HTTP 202 if successfully call rapi, itemNotFound, |
|
400 |
serviceUnavailable otherwise |
|
363 | 401 |
|
364 | 402 |
""" |
365 | 403 |
|
... | ... | |
367 | 405 |
|
368 | 406 |
def create(self, request, id): |
369 | 407 |
"""Reboot, Shutdown, Start virtual machine""" |
370 |
|
|
408 |
|
|
371 | 409 |
try: |
372 | 410 |
requested_action = json.loads(request.raw_post_data) |
373 | 411 |
reboot_request = requested_action.get('reboot', None) |
374 | 412 |
shutdown_request = requested_action.get('shutdown', None) |
375 | 413 |
start_request = requested_action.get('start', None) |
376 | 414 |
#action not implemented |
377 |
action = reboot_request and 'REBOOT' or shutdown_request and 'STOP' or start_request and 'START' |
|
415 |
action = reboot_request and 'REBOOT' or shutdown_request \ |
|
416 |
and 'STOP' or start_request and 'START' |
|
378 | 417 |
|
379 | 418 |
if not action: |
380 |
raise fault.notImplemented
|
|
419 |
raise fault.notImplemented |
|
381 | 420 |
#test if we can get the vm |
382 | 421 |
vm = VirtualMachine.objects.get(id=id) |
383 | 422 |
vm.start_action(action) |
... | ... | |
446 | 485 |
return { |
447 | 486 |
"metadata": { |
448 | 487 |
"values": [ |
449 |
{m.meta_key: m.meta_value} for m in server.virtualmachinemetadata_set.all() |
|
488 |
{m.meta_key: m.meta_value} \ |
|
489 |
for m in server.virtualmachinemetadata_set.all() |
|
450 | 490 |
] |
451 | 491 |
} |
452 | 492 |
} |
... | ... | |
457 | 497 |
except Exception, e: |
458 | 498 |
log.exception('Unexpected error: %s' % e) |
459 | 499 |
raise fault.serviceUnavailable |
460 |
|
|
500 |
|
|
461 | 501 |
def read_onekey(self, request, id, key): |
462 | 502 |
"""Returns the specified metadata key of the specified server""" |
463 | 503 |
try: |
... | ... | |
465 | 505 |
return { |
466 | 506 |
"metadata": { |
467 | 507 |
"values": [ |
468 |
{m.meta_key: m.meta_value} for m in server.virtualmachinemetadata_set.filter(meta_key=key) |
|
508 |
{m.meta_key: m.meta_value} for m in |
|
509 |
server.virtualmachinemetadata_set.filter(meta_key=key) |
|
469 | 510 |
] |
470 | 511 |
} |
471 | 512 |
} |
472 | 513 |
except VirtualMachineMetadata.DoesNotExist: |
473 |
raise fault.itemNotFound
|
|
514 |
raise fault.itemNotFound |
|
474 | 515 |
except VirtualMachine.DoesNotExist: |
475 | 516 |
raise fault.itemNotFound |
476 | 517 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
487 | 528 |
try: |
488 | 529 |
metadata = json.loads(request.raw_post_data)['metadata'] |
489 | 530 |
except Exception as e: |
490 |
log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data)) |
|
531 |
log.exception('Malformed create request: %s - %s' \ |
|
532 |
% (e, request.raw_post_data)) |
|
491 | 533 |
raise fault.badRequest |
492 | 534 |
|
493 | 535 |
try: |
494 | 536 |
vm = VirtualMachine.objects.get(pk=id) |
495 | 537 |
for x in metadata.keys(): |
496 |
vm_meta, created = vm.virtualmachinemetadata_set.get_or_create(meta_key=x) |
|
497 |
vm_meta.meta_value = metadata[x] |
|
538 |
vm_meta, created = (vm.virtualmachinemetadata_set. |
|
539 |
get_or_create(meta_key=x)) |
|
540 |
vm_meta.meta_value = metadata[x] |
|
498 | 541 |
vm_meta.save() |
499 | 542 |
return { |
500 | 543 |
"metadata": [{ |
501 |
"meta": { |
|
502 |
"key": {m.meta_key: m.meta_value}}} for m in vm.virtualmachinemetadata_set.all()] |
|
503 |
} |
|
544 |
"meta": { |
|
545 |
"key": {m.meta_key: m.meta_value}}} \ |
|
546 |
for m in vm.virtualmachinemetadata_set.all()] |
|
547 |
} |
|
504 | 548 |
except VirtualMachine.DoesNotExist: |
505 | 549 |
raise fault.itemNotFound |
506 | 550 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
522 | 566 |
metadata = json.loads(request.raw_post_data)['meta'] |
523 | 567 |
metadata_value = metadata[key] |
524 | 568 |
except Exception as e: |
525 |
log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data)) |
|
569 |
log.exception('Malformed create request: %s - %s' \ |
|
570 |
% (e, request.raw_post_data)) |
|
526 | 571 |
raise fault.badRequest |
527 | 572 |
|
528 | 573 |
try: |
529 | 574 |
server = VirtualMachine.objects.get(pk=id) |
530 |
vm_meta, created = server.virtualmachinemetadata_set.get_or_create(meta_key=key) |
|
531 |
vm_meta.meta_value = metadata_value |
|
575 |
vm_meta, created = (server.virtualmachinemetadata_set. |
|
576 |
get_or_create(meta_key=key)) |
|
577 |
vm_meta.meta_value = metadata_value |
|
532 | 578 |
vm_meta.save() |
533 | 579 |
return {"meta": {vm_meta.meta_key: vm_meta.meta_value}} |
534 |
|
|
580 |
|
|
535 | 581 |
except VirtualMachine.DoesNotExist: |
536 | 582 |
raise fault.itemNotFound |
537 | 583 |
except VirtualMachine.MultipleObjectsReturned: |
... | ... | |
581 | 627 |
""" |
582 | 628 |
try: |
583 | 629 |
flavors = Flavor.objects.all() |
584 |
flavors = [ {'id': flavor.id, 'name': flavor.name, 'ram': flavor.ram, \ |
|
585 |
'disk': flavor.disk, 'cpu': flavor.cpu} for flavor in flavors] |
|
630 |
flavors = [{'id': flavor.id, |
|
631 |
'name': flavor.name, |
|
632 |
'ram': flavor.ram, |
|
633 |
'disk': flavor.disk, |
|
634 |
'cpu': flavor.cpu} |
|
635 |
for flavor in flavors] |
|
586 | 636 |
|
587 | 637 |
if id is None: |
588 | 638 |
simple = map(lambda v: { |
589 | 639 |
"id": v['id'], |
590 | 640 |
"name": v['name'], |
591 | 641 |
}, flavors) |
592 |
return { "flavors": simple }
|
|
642 |
return {"flavors": simple}
|
|
593 | 643 |
elif id == "detail": |
594 |
return { "flavors": flavors }
|
|
644 |
return {"flavors": flavors}
|
|
595 | 645 |
else: |
596 | 646 |
flavor = Flavor.objects.get(id=id) |
597 |
return { "flavor": {
|
|
647 |
return {"flavor": { |
|
598 | 648 |
'id': flavor.id, |
599 | 649 |
'name': flavor.name, |
600 | 650 |
'ram': flavor.ram, |
601 |
'disk': flavor.disk,
|
|
602 |
'cpu': flavor.cpu,
|
|
603 |
} }
|
|
651 |
'disk': flavor.disk, |
|
652 |
'cpu': flavor.cpu, |
|
653 |
}} |
|
604 | 654 |
|
605 | 655 |
except Flavor.DoesNotExist: |
606 | 656 |
raise fault.itemNotFound |
... | ... | |
614 | 664 |
class ImageHandler(BaseHandler): |
615 | 665 |
"""Handler responsible for Images |
616 | 666 |
|
617 |
handles the listing, creation and delete of Images.
|
|
667 |
handles the listing, creation and delete of Images. |
|
618 | 668 |
|
619 | 669 |
@HTTP methods: GET, POST |
620 |
@Parameters: POST data |
|
621 |
@Responses: HTTP 202 if successfully create Image or get the Images list, itemNotFound, serviceUnavailable otherwise |
|
670 |
@Parameters: POST data |
|
671 |
@Responses: HTTP 202 if successfully create Image or get the Images list, |
|
672 |
itemNotFound, serviceUnavailable otherwise |
|
622 | 673 |
|
623 | 674 |
""" |
624 | 675 |
|
625 |
|
|
626 | 676 |
allowed_methods = ('GET', 'POST') |
627 | 677 |
|
628 | 678 |
def read(self, request, id=None): |
... | ... | |
638 | 688 |
try: |
639 | 689 |
changes_since = request.GET.get("changes-since", 0) |
640 | 690 |
if changes_since: |
641 |
last_update = datetime.strptime(changes_since, "%Y-%m-%dT%H:%M:%S" ) |
|
691 |
last_update = datetime.strptime(changes_since, |
|
692 |
"%Y-%m-%dT%H:%M:%S") |
|
642 | 693 |
#return a badRequest if the changes_since is older than a limit |
643 |
if datetime.now() - last_update > timedelta(seconds=settings.POLL_LIMIT): |
|
644 |
raise fault.badRequest |
|
694 |
if datetime.now() - last_update > timedelta( |
|
695 |
seconds=settings.POLL_LIMIT): |
|
696 |
raise fault.badRequest |
|
645 | 697 |
images = Image.objects.filter(updated__gt=last_update) |
646 | 698 |
if not len(images): |
647 | 699 |
return notModified |
648 | 700 |
else: |
649 | 701 |
images = Image.objects.all() |
650 | 702 |
except Exception, e: |
651 |
raise fault.badRequest
|
|
703 |
raise fault.badRequest |
|
652 | 704 |
try: |
653 |
images_list = [ {'created': image.created.isoformat(),
|
|
705 |
images_list = [{'created': image.created.isoformat(),
|
|
654 | 706 |
'id': image.id, |
655 | 707 |
'name': image.name, |
656 |
'updated': image.updated.isoformat(),
|
|
657 |
'status': image.state,
|
|
658 |
'progress': image.state == 'ACTIVE' and 100 or 0,
|
|
659 |
'size': image.size,
|
|
708 |
'updated': image.updated.isoformat(), |
|
709 |
'status': image.state, |
|
710 |
'progress': image.state == 'ACTIVE' and 100 or 0, |
|
711 |
'size': image.size, |
|
660 | 712 |
'serverId': image.sourcevm and image.sourcevm.id or "", |
661 |
#'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in image.imagemetadata_set.all()] |
|
662 |
'metadata':{'meta': { 'key': {'description': image.description}}}, |
|
713 |
#'metadata':[{'meta': |
|
714 |
#{ 'key': {metadata.meta_key: metadata.meta_value}}} |
|
715 |
#for metadata in image.imagemetadata_set.all()] |
|
716 |
'metadata':{'meta': |
|
717 |
{'key': {'description': image.description}}}, |
|
663 | 718 |
} for image in images] |
664 | 719 |
# Images info is stored in the DB. Ganeti is not aware of this |
665 | 720 |
if id == "detail": |
666 |
return { "images": images_list }
|
|
721 |
return {"images": images_list}
|
|
667 | 722 |
elif id is None: |
668 |
return { "images": [ { "id": s['id'], "name": s['name'] } for s in images_list ] } |
|
669 |
else: |
|
723 |
return {"images": [{"id": s['id'], "name": s['name']} \ |
|
724 |
for s in images_list]} |
|
725 |
else: |
|
670 | 726 |
image = images.get(id=id) |
671 |
return { "image": {'created': image.created.isoformat(),
|
|
727 |
return {"image": {'created': image.created.isoformat(),
|
|
672 | 728 |
'id': image.id, |
673 | 729 |
'name': image.name, |
674 |
'updated': image.updated.isoformat(),
|
|
675 |
'description': image.description,
|
|
676 |
'status': image.state,
|
|
677 |
'progress': image.state == 'ACTIVE' and 100 or 0,
|
|
678 |
'size': image.size,
|
|
730 |
'updated': image.updated.isoformat(), |
|
731 |
'description': image.description, |
|
732 |
'status': image.state, |
|
733 |
'progress': image.state == 'ACTIVE' and 100 or 0, |
|
734 |
'size': image.size, |
|
679 | 735 |
'serverId': image.sourcevm and image.sourcevm.id or "", |
680 |
#'metadata':[{'meta': { 'key': {metadata.meta_key: metadata.meta_value}}} for metadata in image.imagemetadata_set.all()] |
|
681 |
'metadata':{'meta': { 'key': {'description': image.description}}}, |
|
682 |
} } |
|
736 |
#'metadata':[{'meta': { 'key': |
|
737 |
#{metadata.meta_key: metadata.meta_value}}} |
|
738 |
#for metadata in image.imagemetadata_set.all()] |
|
739 |
'metadata': { |
|
740 |
'meta': {'key': {'description': image.description}}}, |
|
741 |
}} |
|
683 | 742 |
except Image.DoesNotExist: |
684 | 743 |
raise fault.itemNotFound |
685 | 744 |
except Image.MultipleObjectsReturned: |
... | ... | |
717 | 776 |
image = Image.objects.get(pk=id) |
718 | 777 |
return { |
719 | 778 |
"metadata": [{ |
720 |
"meta": { |
|
721 |
"key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()] |
|
779 |
"meta": { |
|
780 |
"key": {m.meta_key: m.meta_value}}} \ |
|
781 |
for m in image.imagemetadata_set.all()] |
|
722 | 782 |
} |
723 | 783 |
except Image.DoesNotExist: |
724 | 784 |
raise fault.itemNotFound |
... | ... | |
727 | 787 |
except Exception, e: |
728 | 788 |
log.exception('Unexpected error: %s' % e) |
729 | 789 |
raise fault.serviceUnavailable |
730 |
|
|
790 |
|
|
731 | 791 |
def read_onekey(self, request, id, key): |
732 | 792 |
"""Returns the specified metadata key of the specified server""" |
733 | 793 |
try: |
... | ... | |
735 | 795 |
return { |
736 | 796 |
"metadata": { |
737 | 797 |
"values": [ |
738 |
{m.meta_key: m.meta_value} for m in image.imagemetadata_set.filter(meta_key=key) |
|
798 |
{m.meta_key: m.meta_value} \ |
|
799 |
for m in image.imagemetadata_set.filter(meta_key=key) |
|
739 | 800 |
] |
740 | 801 |
} |
741 | 802 |
} |
742 | 803 |
except ImageMetadata.DoesNotExist: |
743 |
raise fault.itemNotFound
|
|
804 |
raise fault.itemNotFound |
|
744 | 805 |
except Image.DoesNotExist: |
745 | 806 |
raise fault.itemNotFound |
746 | 807 |
except Image.MultipleObjectsReturned: |
... | ... | |
757 | 818 |
try: |
758 | 819 |
metadata = json.loads(request.raw_post_data)['metadata'] |
759 | 820 |
except Exception as e: |
760 |
log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data)) |
|
821 |
log.exception('Malformed create request: %s - %s' \ |
|
822 |
% (e, request.raw_post_data)) |
|
761 | 823 |
raise fault.badRequest |
762 | 824 |
|
763 | 825 |
try: |
764 | 826 |
image = Image.objects.get(pk=id) |
765 | 827 |
for x in metadata.keys(): |
766 |
img_meta, created = image.imagemetadata_set.get_or_create(meta_key=x) |
|
767 |
img_meta.meta_value = metadata[x] |
|
828 |
img_meta, created = (image.imagemetadata_set. |
|
829 |
get_or_create(meta_key=x)) |
|
830 |
img_meta.meta_value = metadata[x] |
|
768 | 831 |
img_meta.save() |
769 | 832 |
return { |
770 | 833 |
"metadata": [{ |
771 |
"meta": { |
|
772 |
"key": {m.meta_key: m.meta_value}}} for m in image.imagemetadata_set.all()] |
|
773 |
} |
|
834 |
"meta": { |
|
835 |
"key": {m.meta_key: m.meta_value}}} \ |
|
836 |
for m in image.imagemetadata_set.all()] |
|
837 |
} |
|
774 | 838 |
except Image.DoesNotExist: |
775 | 839 |
raise fault.itemNotFound |
776 | 840 |
except Image.MultipleObjectsReturned: |
... | ... | |
784 | 848 |
raise fault.serviceUnavailable |
785 | 849 |
|
786 | 850 |
def update(self, request, id, key=None): |
787 |
"""Update or Create the specified metadata key for the specified Image""" |
|
851 |
"""Update or Create the specified metadata key for the |
|
852 |
specified Image""" |
|
788 | 853 |
if key is None: |
789 | 854 |
log.exception('No metadata key specified in URL') |
790 | 855 |
raise fault.badRequest |
... | ... | |
792 | 857 |
metadata = json.loads(request.raw_post_data)['meta'] |
793 | 858 |
metadata_value = metadata[key] |
794 | 859 |
except Exception as e: |
795 |
log.exception('Malformed create request: %s - %s' % (e, request.raw_post_data)) |
|
860 |
log.exception('Malformed create request: %s - %s' \ |
|
861 |
% (e, request.raw_post_data)) |
|
796 | 862 |
raise fault.badRequest |
797 | 863 |
|
798 | 864 |
try: |
799 | 865 |
image = Image.objects.get(pk=id) |
800 |
img_meta, created = image.imagemetadata_set.get_or_create(meta_key=key) |
|
801 |
img_meta.meta_value = metadata_value |
|
866 |
img_meta, created = (image.imagemetadata_set. |
|
867 |
get_or_create(meta_key=key)) |
|
868 |
img_meta.meta_value = metadata_value |
|
802 | 869 |
img_meta.save() |
803 | 870 |
return {"meta": {img_meta.meta_key: img_meta.meta_value}} |
804 |
|
|
871 |
|
|
805 | 872 |
except Image.DoesNotExist: |
806 | 873 |
raise fault.itemNotFound |
807 | 874 |
except Image.MultipleObjectsReturned: |
... | ... | |
862 | 929 |
creates, lists, deletes virtual machine groups |
863 | 930 |
|
864 | 931 |
@HTTP methods: GET, POST, DELETE |
865 |
@Parameters: POST data |
|
866 |
@Responses: HTTP 202 if successfully get the Groups list, itemNotFound, serviceUnavailable otherwise |
|
932 |
@Parameters: POST data |
|
933 |
@Responses: HTTP 202 if successfully get the Groups list, |
|
934 |
itemNotFound, serviceUnavailable otherwise |
|
867 | 935 |
|
868 | 936 |
""" |
869 | 937 |
|
... | ... | |
872 | 940 |
def read(self, request, id=None): |
873 | 941 |
"""List Groups""" |
874 | 942 |
try: |
875 |
vmgroups = VirtualMachineGroup.objects.all()
|
|
876 |
vmgroups_list = [ {'id': vmgroup.id, \
|
|
943 |
vmgroups = VirtualMachineGroup.objects.all() |
|
944 |
vmgroups_list = [{'id': vmgroup.id, \ |
|
877 | 945 |
'name': vmgroup.name, \ |
878 |
'server_id': [machine.id for machine in vmgroup.machines.all()] \ |
|
946 |
'server_id': |
|
947 |
[machine.id for machine in vmgroup.machines.all()] |
|
879 | 948 |
} for vmgroup in vmgroups] |
880 | 949 |
# Group info is stored in the DB. Ganeti is not aware of this |
881 | 950 |
if id == "detail": |
882 |
return { "groups": vmgroups_list }
|
|
951 |
return {"groups": vmgroups_list}
|
|
883 | 952 |
elif id is None: |
884 |
return { "groups": [ { "id": s['id'], "name": s['name'] } for s in vmgroups_list ] } |
|
953 |
return {"groups": [{"id": s['id'], |
|
954 |
"name": s['name']} for s in vmgroups_list]} |
|
885 | 955 |
else: |
886 | 956 |
vmgroup = vmgroups.get(id=id) |
887 | 957 |
|
888 |
return { "group": {'id': vmgroup.id, \
|
|
889 |
'name': vmgroup.name, \
|
|
890 |
'server_id': [machine.id for machine in vmgroup.machines.all()] \
|
|
891 |
} }
|
|
892 |
|
|
958 |
return {"group": {'id': vmgroup.id,
|
|
959 |
'name': vmgroup.name, |
|
960 |
'server_id': |
|
961 |
[machine.id for machine in vmgroup.machines.all()]
|
|
962 |
}} |
|
893 | 963 |
|
894 | 964 |
except VirtualMachineGroup.DoesNotExist: |
895 | 965 |
raise fault.itemNotFound |
... | ... | |
899 | 969 |
log.exception('Unexpected error: %s' % e) |
900 | 970 |
raise fault.serviceUnavailable |
901 | 971 |
|
902 |
|
|
903 |
|
|
904 | 972 |
def create(self, request, id): |
905 | 973 |
"""Creates a Group""" |
906 | 974 |
return created |
... | ... | |
917 | 985 |
|
918 | 986 |
rate = [ |
919 | 987 |
{ |
920 |
"verb" : "POST",
|
|
921 |
"URI" : "*",
|
|
922 |
"regex" : ".*",
|
|
923 |
"value" : 10,
|
|
924 |
"remaining" : 2,
|
|
925 |
"unit" : "MINUTE",
|
|
926 |
"resetTime" : 1244425439
|
|
988 |
"verb": "POST", |
|
989 |
"URI": "*", |
|
990 |
"regex": ".*", |
|
991 |
"value": 10, |
|
992 |
"remaining": 2, |
|
993 |
"unit": "MINUTE", |
|
994 |
"resetTime": 1244425439 |
|
927 | 995 |
}, |
928 | 996 |
{ |
929 |
"verb" : "POST",
|
|
930 |
"URI" : "*/servers",
|
|
931 |
"regex" : "^/servers",
|
|
932 |
"value" : 25,
|
|
933 |
"remaining" : 24,
|
|
934 |
"unit" : "DAY",
|
|
935 |
"resetTime" : 1244511839
|
|
997 |
"verb": "POST", |
|
998 |
"URI": "*/servers", |
|
999 |
"regex": "^/servers", |
|
1000 |
"value": 25, |
|
1001 |
"remaining": 24, |
|
1002 |
"unit": "DAY", |
|
1003 |
"resetTime": 1244511839 |
|
936 | 1004 |
}, |
937 | 1005 |
{ |
938 |
"verb" : "PUT",
|
|
939 |
"URI" : "*",
|
|
940 |
"regex" : ".*",
|
|
941 |
"value" : 10,
|
|
942 |
"remaining" : 2,
|
|
943 |
"unit" : "MINUTE",
|
|
944 |
"resetTime" : 1244425439
|
|
1006 |
"verb": "PUT", |
|
1007 |
"URI": "*", |
|
1008 |
"regex": ".*", |
|
1009 |
"value": 10, |
|
1010 |
"remaining": 2, |
|
1011 |
"unit": "MINUTE", |
|
1012 |
"resetTime": 1244425439 |
|
945 | 1013 |
}, |
946 | 1014 |
{ |
947 |
"verb" : "GET",
|
|
948 |
"URI" : "*",
|
|
949 |
"regex" : ".*",
|
|
950 |
"value" : 3,
|
|
951 |
"remaining" : 3,
|
|
952 |
"unit" : "MINUTE",
|
|
953 |
"resetTime" : 1244425439
|
|
1015 |
"verb": "GET", |
|
1016 |
"URI": "*", |
|
1017 |
"regex": ".*", |
|
1018 |
"value": 3, |
|
1019 |
"remaining": 3, |
|
1020 |
"unit": "MINUTE", |
|
1021 |
"resetTime": 1244425439 |
|
954 | 1022 |
}, |
955 | 1023 |
{ |
956 |
"verb" : "DELETE",
|
|
957 |
"URI" : "*",
|
|
958 |
"regex" : ".*",
|
|
959 |
"value" : 100,
|
|
960 |
"remaining" : 100,
|
|
961 |
"unit" : "MINUTE",
|
|
962 |
"resetTime" : 1244425439
|
|
1024 |
"verb": "DELETE", |
|
1025 |
"URI": "*", |
|
1026 |
"regex": ".*", |
|
1027 |
"value": 100, |
|
1028 |
"remaining": 100, |
|
1029 |
"unit": "MINUTE", |
|
1030 |
"resetTime": 1244425439 |
|
963 | 1031 |
} |
964 | 1032 |
] |
965 | 1033 |
|
966 | 1034 |
absolute = { |
967 |
"maxTotalRAMSize" : 51200,
|
|
968 |
"maxIPGroups" : 50,
|
|
969 |
"maxIPGroupMembers" : 25
|
|
1035 |
"maxTotalRAMSize": 51200, |
|
1036 |
"maxIPGroups": 50, |
|
1037 |
"maxIPGroupMembers": 25 |
|
970 | 1038 |
} |
971 | 1039 |
|
972 | 1040 |
def read(self, request): |
973 |
return { "limits": {
|
|
1041 |
return {"limits": { |
|
974 | 1042 |
"rate": self.rate, |
975 | 1043 |
"absolute": self.absolute, |
976 | 1044 |
} |
... | ... | |
991 | 1059 |
|
992 | 1060 |
def read_one(self, request, id): |
993 | 1061 |
"""List one Disk with the specified id with all details""" |
994 |
# FIXME Get detailed info from the DB
|
|
1062 |
# FIXME Get detailed info from the DB |
|
995 | 1063 |
# for the Disk with the specified id |
996 | 1064 |
try: |
997 | 1065 |
disk = Disk.objects.get(pk=id) |
998 | 1066 |
disk_details = { |
999 |
"id" : disk.id,
|
|
1000 |
"name" : disk.name,
|
|
1001 |
"size" : disk.size,
|
|
1002 |
"created" : disk.created,
|
|
1003 |
"serverId" : disk.vm.id
|
|
1067 |
"id": disk.id,
|
|
1068 |
"name": disk.name,
|
|
1069 |
"size": disk.size, |
|
1070 |
"created": disk.created,
|
|
1071 |
"serverId": disk.vm.id |
|
1004 | 1072 |
} |
1005 |
return { "disks" : disk_details }
|
|
1073 |
return {"disks": disk_details}
|
|
1006 | 1074 |
except: |
1007 | 1075 |
raise fault.itemNotFound |
1008 | 1076 |
|
... | ... | |
1011 | 1079 |
"""List all Disks. If -detail- is set list them with all details""" |
1012 | 1080 |
if not detail: |
1013 | 1081 |
disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0]) |
1014 |
return { "disks": [ { "id": disk.id, "name": disk.name } for disk in disks ] } |
|
1082 |
return {"disks": [{"id": disk.id, "name": disk.name} \ |
|
1083 |
for disk in disks]} |
|
1015 | 1084 |
else: |
1016 | 1085 |
disks = Disk.objects.filter(owner=SynnefoUser.objects.all()[0]) |
1017 |
disks_details = [ {
|
|
1018 |
"id" : disk.id,
|
|
1019 |
"name" : disk.name,
|
|
1020 |
"size" : disk.size,
|
|
1021 |
"created" : disk.created,
|
|
1022 |
"serverId" : disk.vm.id,
|
|
1023 |
} for disk in disks ]
|
|
1024 |
return { "disks": disks_details }
|
|
1086 |
disks_details = [{ |
|
1087 |
"id": disk.id,
|
|
1088 |
"name": disk.name, |
|
1089 |
"size": disk.size, |
|
1090 |
"created": disk.created,
|
|
1091 |
"serverId": disk.vm.id, |
|
1092 |
} for disk in disks] |
|
1093 |
return {"disks": disks_details}
|
|
1025 | 1094 |
|
1026 | 1095 |
def create(self, request): |
1027 | 1096 |
"""Create a new Disk""" |
1028 |
# FIXME Create a partial DB entry,
|
|
1097 |
# FIXME Create a partial DB entry, |
|
1029 | 1098 |
# then call the backend for actual creation |
1030 | 1099 |
pass |
1031 | 1100 |
|
Also available in: Unified diff