Commit d4785d61 authored by Vangelis Koukis's avatar Vangelis Koukis
Browse files

Merge branch 'api-current'

parents c486c236 db507dbb
......@@ -9,23 +9,21 @@ from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
from synnefo.api.faults import BadRequest, ResizeNotAllowed, ServiceUnavailable
from synnefo.api.faults import BadRequest, ServiceUnavailable
from synnefo.api.util import random_password
from synnefo.util.rapi import GanetiRapiClient
from synnefo.util.vapclient import request_forwarding as request_vnc_forwarding
from synnefo.logic import backend
from synnefo.logic.backend import (reboot_instance, startup_instance, shutdown_instance,
get_instance_console)
from synnefo.logic.utils import get_rsapi_state
server_actions = {}
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
def server_action(name):
'''Decorator for functions implementing server actions.
`name` is the key in the dict passed by the client.
`name` is the key in the dict passed by the client.
'''
def decorator(func):
......@@ -33,87 +31,6 @@ def server_action(name):
return func
return decorator
@server_action('console')
def get_console(request, vm, args):
"""Arrange for an OOB console of the specified type
This method arranges for an OOB console of the specified type.
Only consoles of type "vnc" are supported for now.
It uses a running instance of vncauthproxy to setup proper
VNC forwarding with a random password, then returns the necessary
VNC connection info to the caller.
JSON Request: {
"console": {
"type": "vnc"
}
}
JSON Reply: {
"vnc": {
"host": "fqdn_here",
"port": a_port_here,
"password": "a_password_here"
}
}
"""
# Normal Response Code: 200
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
try:
console_type = args.get('type', '')
if console_type != 'vnc':
raise BadRequest(message="type can only be 'vnc'")
except KeyError:
raise BadRequest()
# Use RAPI to get VNC console information for this instance
if get_rsapi_state(vm) != 'ACTIVE':
raise BadRequest(message="Server not in ACTIVE state")
if settings.TEST:
console_data = { 'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000 }
else:
console_data = rapi.GetInstanceConsole(vm.backend_id)
if console_data['kind'] != 'vnc':
raise ServiceUnavailable()
# Let vncauthproxy decide on the source port.
# The alternative: static allocation, e.g.
# sport = console_data['port'] - 1000
sport = 0
daddr = console_data['host']
dport = console_data['port']
passwd = random_password()
try:
if settings.TEST:
fwd = { 'source_port': 1234, 'status': 'OK' }
else:
fwd = request_vnc_forwarding(sport, daddr, dport, passwd)
if fwd['status'] != "OK":
raise ServiceUnavailable()
vnc = { 'host': getfqdn(), 'port': fwd['source_port'], 'password': passwd }
except Exception:
raise ServiceUnavailable("Could not allocate VNC console port")
# Format to be reviewed by [verigak], FIXME
if request.serialization == 'xml':
mimetype = 'application/xml'
data = render_to_string('vnc.xml', {'vnc': vnc})
else:
mimetype = 'application/json'
data = json.dumps({'vnc': vnc})
return HttpResponse(data, mimetype=mimetype, status=200)
@server_action('changePassword')
def change_password(request, vm, args):
......@@ -128,11 +45,11 @@ def change_password(request, vm, args):
# overLimit (413)
try:
adminPass = args['adminPass']
password = args['adminPass']
except KeyError:
raise BadRequest()
raise BadRequest('Malformed request.')
raise ServiceUnavailable()
raise ServiceUnavailable('Changing password is not supported.')
@server_action('reboot')
def reboot(request, vm, args):
......@@ -148,10 +65,8 @@ def reboot(request, vm, args):
reboot_type = args.get('type', '')
if reboot_type not in ('SOFT', 'HARD'):
raise BadRequest()
backend.start_action(vm, 'REBOOT')
rapi.RebootInstance(vm.backend_id, reboot_type.lower())
raise BadRequest('Malformed Request.')
reboot_instance(vm, reboot_type.lower())
return HttpResponse(status=202)
@server_action('start')
......@@ -159,9 +74,10 @@ def start(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
backend.start_action(vm, 'START')
rapi.StartupInstance(vm.backend_id)
if args:
raise BadRequest('Malformed Request.')
startup_instance(vm)
return HttpResponse(status=202)
@server_action('shutdown')
......@@ -170,8 +86,9 @@ def shutdown(request, vm, args):
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
backend.start_action(vm, 'STOP')
rapi.ShutdownInstance(vm.backend_id)
if args:
raise BadRequest('Malformed Request.')
shutdown_instance(vm)
return HttpResponse(status=202)
@server_action('rebuild')
......@@ -187,7 +104,7 @@ def rebuild(request, vm, args):
# serverCapacityUnavailable (503),
# overLimit (413)
raise ServiceUnavailable()
raise ServiceUnavailable('Rebuild not supported.')
@server_action('resize')
def resize(request, vm, args):
......@@ -203,7 +120,7 @@ def resize(request, vm, args):
# overLimit (413),
# resizeNotAllowed (403)
raise ResizeNotAllowed()
raise ServiceUnavailable('Resize not supported.')
@server_action('confirmResize')
def confirm_resize(request, vm, args):
......@@ -219,7 +136,7 @@ def confirm_resize(request, vm, args):
# overLimit (413),
# resizeNotAllowed (403)
raise ResizeNotAllowed()
raise ServiceUnavailable('Resize not supported.')
@server_action('revertResize')
def revert_resize(request, vm, args):
......@@ -235,4 +152,75 @@ def revert_resize(request, vm, args):
# overLimit (413),
# resizeNotAllowed (403)
raise ResizeNotAllowed()
raise ServiceUnavailable('Resize not supported.')
@server_action('console')
def get_console(request, vm, args):
"""Arrange for an OOB console of the specified type
This method arranges for an OOB console of the specified type.
Only consoles of type "vnc" are supported for now.
It uses a running instance of vncauthproxy to setup proper
VNC forwarding with a random password, then returns the necessary
VNC connection info to the caller.
"""
# Normal Response Code: 200
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
# unauthorized (401),
# badRequest (400),
# badMediaType(415),
# itemNotFound (404),
# buildInProgress (409),
# overLimit (413)
console_type = args.get('type', '')
if console_type != 'vnc':
raise BadRequest('Type can only be "vnc".')
# Use RAPI to get VNC console information for this instance
if get_rsapi_state(vm) != 'ACTIVE':
raise BadRequest('Server not in ACTIVE state.')
if settings.TEST:
console_data = {'kind': 'vnc', 'host': 'ganeti_node', 'port': 1000}
else:
console_data = get_instance_console(vm)
if console_data['kind'] != 'vnc':
raise ServiceUnavailable('Could not create a console of requested type.')
# Let vncauthproxy decide on the source port.
# The alternative: static allocation, e.g.
# sport = console_data['port'] - 1000
sport = 0
daddr = console_data['host']
dport = console_data['port']
password = random_password()
try:
if settings.TEST:
fwd = {'source_port': 1234, 'status': 'OK'}
else:
fwd = request_vnc_forwarding(sport, daddr, dport, password)
except Exception:
raise ServiceUnavailable('Could not allocate VNC console port.')
if fwd['status'] != "OK":
raise ServiceUnavailable('Could not allocate VNC console.')
console = {
'type': 'vnc',
'host': getfqdn(),
'port': fwd['source_port'],
'password': password}
if request.serialization == 'xml':
mimetype = 'application/xml'
data = render_to_string('console.xml', {'console': console})
else:
mimetype = 'application/json'
data = json.dumps({'console': console})
return HttpResponse(data, mimetype=mimetype, status=200)
......@@ -25,5 +25,8 @@ class ResizeNotAllowed(Fault):
class ItemNotFound(Fault):
code = 404
class BuildInProgress(Fault):
code = 409
class ServiceUnavailable(Fault):
code = 503
[
{
"model": "db.Image",
"pk": 1,
"fields": {
"name": "Debian Unstable",
"created": "2011-02-06 00:00:00",
"updated": "2011-02-06 00:00:00",
"state": "ACTIVE"
}
},
{
"model": "db.Image",
"pk": 2,
"fields": {
"name": "Red Hat Enterprise Linux",
"created": "2011-02-06 00:00:00",
"updated": "2011-02-06 00:00:00",
"state": "ACTIVE"
}
},
{
"model": "db.Image",
"pk": 3,
"fields": {
"name": "Ubuntu 10.10",
"created": "2011-02-06 00:00:00",
"updated": "2011-02-06 00:00:00",
"state": "ACTIVE"
}
},
{
"model": "db.Flavor",
"pk": 1,
"fields": {
"cpu": 1,
"ram": 1024,
"disk": 20
}
},
{
"model": "db.Flavor",
"pk": 2,
"fields": {
"cpu": 1,
"ram": 1024,
"disk": 40
}
},
{
"model": "db.Flavor",
"pk": 3,
"fields": {
"cpu": 1,
"ram": 1024,
"disk": 80
}
}
]
......@@ -7,8 +7,7 @@ from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
from synnefo.api.faults import ItemNotFound
from synnefo.api.util import get_user, get_request_dict, api_method
from synnefo.api.util import get_flavor, api_method
from synnefo.db.models import Flavor
......@@ -56,16 +55,13 @@ def get_flavor_details(request, flavor_id):
# badRequest (400),
# itemNotFound (404),
# overLimit (413)
try:
falvor_id = int(flavor_id)
flavor = flavor_to_dict(Flavor.objects.get(id=flavor_id))
except Flavor.DoesNotExist:
raise ItemNotFound
flavor = get_flavor(flavor_id)
flavordict = flavor_to_dict(flavor, detail=True)
if request.serialization == 'xml':
data = render_to_string('flavor.xml', {'flavor': flavor})
data = render_to_string('flavor.xml', {'flavor': flavordict})
else:
data = json.dumps({'flavor': flavor})
data = json.dumps({'flavor': flavordict})
return HttpResponse(data, status=200)
......@@ -3,8 +3,10 @@
#
from synnefo.api.common import method_not_allowed
from synnefo.api.util import *
from synnefo.db.models import Image, ImageMetadata, VirtualMachine
from synnefo.api.faults import BadRequest, Unauthorized
from synnefo.api.util import (isoformat, isoparse, get_user, get_vm, get_image, get_image_meta,
get_request_dict, render_metadata, render_meta, api_method)
from synnefo.db.models import Image, ImageMetadata
from django.conf.urls.defaults import patterns
from django.http import HttpResponse
......@@ -173,7 +175,7 @@ def delete_image(request, image_id):
image = get_image(image_id)
if image.owner != get_user():
raise Unauthorized()
raise Unauthorized('Image does not belong to user.')
image.delete()
return HttpResponse(status=204)
......
......@@ -2,7 +2,8 @@
# Copyright (c) 2010 Greek Research and Technology Network
#
from django.conf import settings
import logging
from django.conf.urls.defaults import patterns
from django.http import HttpResponse
from django.template.loader import render_to_string
......@@ -10,17 +11,15 @@ from django.utils import simplejson as json
from synnefo.api.actions import server_actions
from synnefo.api.common import method_not_allowed
from synnefo.api.faults import BadRequest, ItemNotFound
from synnefo.api.util import *
from synnefo.db.models import Image, Flavor, VirtualMachine, VirtualMachineMetadata
from synnefo.api.faults import BadRequest, ItemNotFound, ServiceUnavailable
from synnefo.api.util import (isoformat, isoparse, random_password,
get_user, get_vm, get_vm_meta, get_image, get_flavor,
get_request_dict, render_metadata, render_meta, api_method)
from synnefo.db.models import VirtualMachine, VirtualMachineMetadata
from synnefo.logic.backend import create_instance, delete_instance
from synnefo.logic.utils import get_rsapi_state
from synnefo.util.rapi import GanetiRapiClient, GanetiApiError
from synnefo.logic import backend
import logging
from synnefo.util.rapi import GanetiApiError
rapi = GanetiRapiClient(*settings.GANETI_CLUSTER_INFO)
urlpatterns = patterns('synnefo.api.servers',
(r'^(?:/|.json|.xml)?$', 'demux'),
......@@ -153,44 +152,27 @@ def create_server(request):
name = server['name']
metadata = server.get('metadata', {})
assert isinstance(metadata, dict)
sourceimage = Image.objects.get(id=server['imageRef'])
flavor = Flavor.objects.get(id=server['flavorRef'])
image_id = server['imageRef']
flavor_id = server['flavorRef']
except (KeyError, AssertionError):
raise BadRequest('Malformed request.')
except Image.DoesNotExist:
raise ItemNotFound
except Flavor.DoesNotExist:
raise ItemNotFound
vm = VirtualMachine(
image = get_image(image_id)
flavor = get_flavor(flavor_id)
# We must save the VM instance now, so that it gets a valid vm.backend_id.
vm = VirtualMachine.objects.create(
name=name,
owner=get_user(),
sourceimage=sourceimage,
sourceimage=image,
ipfour='0.0.0.0',
ipsix='::1',
flavor=flavor)
# Pick a random password for the VM.
# FIXME: This must be passed to the Ganeti OS provider via CreateInstance()
passwd = random_password()
# We *must* save the VM instance now,
# so that it gets a vm.id and vm.backend_id is valid.
vm.save()
password = random_password()
try:
jobId = rapi.CreateInstance(
mode='create',
name=vm.backend_id,
disk_template='plain',
disks=[{"size": 2000}], #FIXME: Always ask for a 2GB disk for now
nics=[{}],
os='debootstrap+default', #TODO: select OS from imageRef
ip_check=False,
name_check=False,
pnode=rapi.GetNodes()[0], #TODO: verify if this is necessary
dry_run=settings.TEST,
beparams=dict(auto_balance=True, vcpus=flavor.cpu, memory=flavor.ram))
create_instance(vm, flavor, password)
except GanetiApiError:
vm.delete()
raise ServiceUnavailable('Could not create server.')
......@@ -198,11 +180,12 @@ def create_server(request):
for key, val in metadata.items():
VirtualMachineMetadata.objects.create(meta_key=key, meta_value=val, vm=vm)
logging.info('created vm with %s cpus, %s ram and %s storage' % (flavor.cpu, flavor.ram, flavor.disk))
logging.info('created vm with %s cpus, %s ram and %s storage',
flavor.cpu, flavor.ram, flavor.disk)
server = vm_to_dict(vm, detail=True)
server['status'] = 'BUILD'
server['adminPass'] = passwd
server['adminPass'] = password
return render_server(request, server, status=202)
@api_method('GET')
......@@ -235,7 +218,7 @@ def update_server_name(request, server_id):
try:
name = req['server']['name']
except KeyError:
except (TypeError, KeyError):
raise BadRequest('Malformed request.')
vm = get_vm(server_id)
......@@ -256,8 +239,7 @@ def delete_server(request, server_id):
# overLimit (413)
vm = get_vm(server_id)
backend.start_action(vm, 'DESTROY')
rapi.DeleteInstance(vm.backend_id)
delete_instance(vm)
return HttpResponse(status=204)
@api_method('POST')
......
<?xml version="1.0" encoding="UTF-8"?>
<console xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" type="{{ console.type }}" host="{{ console.host }}" port="{{ console.port }}" password="{{ console.password }}">
</console>
<?xml version="1.0" encoding="UTF-8"?>
<vnc xmlns="http://docs.openstack.org/compute/api/v1.1" xmlns:atom="http://www.w3.org/2005/Atom" host="{{ vnc.host }}" port="{{ vnc.port }}" password="{{ vnc.password }}">
</vnc>
......@@ -721,6 +721,7 @@ class ServerVNCConsole(BaseTestCase):
response = self.client.post(path, data, content_type='application/json')
self.assertEqual(response.status_code, 200)
reply = json.loads(response.content)
self.assertEqual(reply.keys(), ['vnc'])
self.assertEqual(set(reply['vnc'].keys()), set(['host', 'port', 'password']))
self.assertEqual(reply.keys(), ['console'])
console = reply['console']
self.assertEqual(console['type'], 'vnc')
self.assertEqual(set(console.keys()), set(['type', 'host', 'port', 'password']))
......@@ -10,17 +10,19 @@ from time import time
from traceback import format_exc
from wsgiref.handlers import format_date_time
import datetime
import dateutil.parser
import logging
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
from synnefo.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable
from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata
import datetime
import dateutil.parser
import logging
from synnefo.api.faults import (Fault, BadRequest, BuildInProgress, ItemNotFound,
ServiceUnavailable, Unauthorized)
from synnefo.db.models import (SynnefoUser, Flavor, Image, ImageMetadata,
VirtualMachine, VirtualMachineMetadata)
class UTC(tzinfo):
......@@ -35,7 +37,7 @@ class UTC(tzinfo):
def isoformat(d):
"""Return an ISO8601 date string that includes a timezon."""
"""Return an ISO8601 date string that includes a timezone."""
return d.replace(tzinfo=UTC()).isoformat()
......@@ -70,7 +72,7 @@ def get_user():
try:
return SynnefoUser.objects.all()[0]
except IndexError:
raise Unauthorized
raise Unauthorized('No users found.')
def get_vm(server_id):
"""Return a VirtualMachine instance or raise ItemNotFound."""
......@@ -110,6 +112,14 @@ def get_image_meta(image_id, key):
except ImageMetadata.DoesNotExist:
raise ItemNotFound('Metadata key not found.')
def get_flavor(flavor_id):
"""Return a Flavor instance or raise ItemNotFound."""
try:
flavor_id = int(flavor_id)
return Flavor.objects.get(id=flavor_id)
except Flavor.DoesNotExist:
raise ItemNotFound('Flavor not found.')
def get_request_dict(request):
"""Returns data sent by the client as a python dict."""
......@@ -168,7 +178,7 @@ def render_fault(request, fault):
def request_serialization(request, atom_allowed=False):
"""Return the serialization format requested.
Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
Valid formats are 'json', 'xml' and 'atom' if `atom_allowed` is True.
"""
path = request.path
......@@ -205,12 +215,17 @@ def api_method(http_method=None, atom_allowed=False):
resp = func(request, *args, **kwargs)
update_response_headers(request, resp)
return resp
except VirtualMachine.DeletedError:
fault = BadRequest('Server has been deleted.')
return render_fault(request, fault)
except VirtualMachine.BuildingError:
fault = BuildInProgress('Server is being built.')
return render_fault(request, fault)
except Fault, fault:
return render_fault(request, fault)
except BaseException, e:
logging.exception('Unexpected error: %s' % e)
fault = ServiceUnavailable('Unexpected error')
logging.exception('Unexpected error: %s', e)
fault = ServiceUnavailable('Unexpected error.')
return render_fault(request, fault)
return wrapper
return decorator
......@@ -258,7 +258,13 @@ class VirtualMachine(models.Model):
self._action = action
def __str__(self):
return repr(str(self._action))