Commit 0c61ec81 authored by Giorgos Verigakis's avatar Giorgos Verigakis
Browse files

Merge branch 'master' into api-redux

Conflicts:
	api/actions.py
	api/errors.py
	api/handlers.py
	api/helpers.py
	api/servers.py
parents 04b30adf 81270321
......@@ -10,6 +10,7 @@ Synnefo depends on the following Python modules
- simplejson
- selenium
- pyzmq-static
- iso8601
also, depending on the database engine of choice, on one of the following:
- MySQL-python
......
......@@ -4,10 +4,15 @@
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 BadRequest, ResizeNotAllowed, 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.utils import get_rsapi_state
server_actions = {}
......@@ -26,9 +31,64 @@ 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 "vnc" type consoles 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)
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")
console_data = rapi.GetInstanceConsole(vm.backend_id)
if console_data['kind'] != 'vnc':
raise ServiceUnavailable()
# Let vncauthproxy decide on the source port.
# FIXME
# sport = 0
sport = console_data['port'] - 1000
daddr = console_data['host']
dport = console_data['port']
passwd = random_password()
request_vnc_forwarding(sport, daddr, dport, passwd)
vnc = { 'host': '62.217.120.67', 'port': sport, 'password': passwd }
# Format to be reviewed by [verigak], FIXME
if request.type == 'xml':
mimetype = 'application/xml'
data = render_to_string('vnc.xml', {'vnc': vnc})
else:
mimetype = 'application/json'
data = json.dumps(vnc)
return HttpResponse(data, mimetype=mimetype, status=200)
@server_action('changePassword')
def change_password(vm, args):
def change_password(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......@@ -47,7 +107,7 @@ def change_password(vm, args):
raise ServiceUnavailable()
@server_action('reboot')
def reboot(vm, args):
def reboot(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......@@ -67,7 +127,7 @@ def reboot(vm, args):
return HttpResponse(status=202)
@server_action('start')
def start(vm, args):
def start(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
......@@ -77,7 +137,7 @@ def start(vm, args):
return HttpResponse(status=202)
@server_action('shutdown')
def shutdown(vm, args):
def shutdown(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: serviceUnavailable (503),
# itemNotFound (404)
......@@ -87,7 +147,7 @@ def shutdown(vm, args):
return HttpResponse(status=202)
@server_action('rebuild')
def rebuild(vm, args):
def rebuild(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......@@ -102,7 +162,7 @@ def rebuild(vm, args):
raise ServiceUnavailable()
@server_action('resize')
def resize(vm, args):
def resize(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......@@ -118,7 +178,7 @@ def resize(vm, args):
raise ResizeNotAllowed()
@server_action('confirmResize')
def confirm_resize(vm, args):
def confirm_resize(request, vm, args):
# Normal Response Code: 204
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......@@ -134,7 +194,7 @@ def confirm_resize(vm, args):
raise ResizeNotAllowed()
@server_action('revertResize')
def revert_resize(vm, args):
def revert_resize(request, vm, args):
# Normal Response Code: 202
# Error Response Codes: computeFault (400, 500),
# serviceUnavailable (503),
......
[
{
"model": "db.SynnefoUser",
"pk": 1,
"fields": {
"name": "Test User",
"credit": 1024,
"created": "2011-02-06 00:00:00",
"updated": "2011-02-06 00:00:00"
}
}
]
\ No newline at end of file
from synnefo.api.errors import Unauthorized
from synnefo.db.models import SynnefoUser
class SynnefoAuthMiddleware(object):
auth_token = "X-Auth-Token"
auth_user = "X-Auth-User"
auth_key = "X-Auth-Key"
def process_request(self, request):
if self.auth_token in request.META:
#Retrieve user from DB
user = SynnefoUser.objects.get(request.META.get(self.auth_token))
if user is None :
return
request.user = user
#An authentication request
if self.auth_user in request.META and 'X-Auth-Key' in request.META \
and '/v1.0' == request.path and 'GET' == request.method:
#Do authenticate or redirect
return
raise Unauthorized
\ No newline at end of file
......@@ -264,7 +264,7 @@ def server_action(request, server_id):
try:
assert isinstance(val, dict)
return server_actions[key](vm, req[key])
return server_actions[key](request, vm, req[key])
except KeyError:
raise BadRequest('Unknown action.')
except AssertionError:
......
<?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>
......@@ -13,8 +13,9 @@ from django.test.client import Client
from synnefo.db.models import VirtualMachine, VirtualMachineGroup
from synnefo.db.models import Flavor, Image
from synnefo.api.tests_redux import APIReduxTestCase
from synnefo.api.tests_auth import AuthTestCase
from logic import utils
from synnefo.logic import utils
class APITestCase(TestCase):
fixtures = ['api_test_data', ]
......
#
# Unit Tests for api
#
# Provides automated tests for api module
#
# Copyright 2011 Greek Research and Technology Network
#
from django.test import TestCase
from django.test.client import Client
class AuthTestCase(TestCase):
fixtures = ['auth_test_data']
apibase = '/api/v1.0'
def setUp(self):
self.client = Client()
def test_auth_headers(self):
""" test whether the authentication mechanism sets the correct headers
"""
#Check with non-existing user
response = self.client.get( self.apibase + '/servers', {},
**{'X-Auth-User':'notme',
'X-Auth-Key':'0xdeadbabe'})
self.assertEquals(response.status_code, 401)
#Check with existing user
response = self.client.get( self.apibase + '/', {},
**{'X-Auth-User':'testuser',
'X-Auth-Key':'testuserpasswd'})
self.assertEquals(response.status_code, 204)
self.assertNotEqual(response['X-Auth-Token'], None)
self.assertEquals(response['X-Server-Management-Url'], '')
self.assertEquals(response['X-Storage-Url'], '')
self.assertEquals(response['X-CDN-Management-Url'], '')
#Check access now that we do have an auth token
token = response['X-Auth-Token']
response = self.client.get (self.apibase + '/servers/detail', {},
**{'X-Auth-Token': token})
self.assertEquals(response.status_code, 200)
.PS
copy "sequence.pic";
# Define the objects
object(B,"Browser");
object(S,"Synnefo");
object(A,"Sibbolleth");
step();
# Message sequences
active(B);
active(S);
active(A);
message(B,S,"GET /");
message(S,B,"304 Go to Sibbolleth");
message(B,A,"Sibbolleth auth");
message(B,A,"Sibbolleth auth");
message(A,S,"auth token");
message(S,S,"store Sibbolleth token");
message(S,A,"get user details");
message(A,S,"user details");
message(S,S,"store user details");
message(S,B,"");
complete(T);
complete(S);
complete(A);
.PE
\ No newline at end of file
......@@ -12,7 +12,7 @@ from logic import credits
def periodically_charge():
"""Scan all virtual machines and charge each user"""
active_vms = VirtualMachine.objects.filter(delete=False)
active_vms = VirtualMachine.objects.filter(deleted=False)
if not len(active_vms):
print "No virtual machines found"
......
......@@ -93,6 +93,19 @@
"flavor" : 30000
}
},
{
"model": "db.Image",
"pk": 30000,
"fields": {
"name": "Debian Squeeze",
"updated": "2011-02-06 00:00:00",
"created": "2011-02-06 00:00:00",
"size" : 2000,
"state": "ACTIVE",
"description": "Full Debian Squeeze Installation",
"owner" : 30000
}
},
{
"model": "db.VirtualMachine",
"pk": 30000,
......@@ -125,19 +138,6 @@
"operstate": "STOPPED"
}
},
{
"model": "db.Image",
"pk": 30000,
"fields": {
"name": "Debian Squeeze",
"updated": "2011-02-06 00:00:00",
"created": "2011-02-06 00:00:00",
"size" : 2000,
"state": "ACTIVE",
"description": "Full Debian Squeeze Installation",
"owner" : 30000
}
},
{
"model": "db.Disk",
"pk": 30000,
......
......@@ -230,6 +230,10 @@ class VirtualMachine(models.Model):
# that they need not be persistent in the DB, but rather
# get generated at runtime by quering Ganeti and applying
# updates received from Ganeti.
# In the future they could be moved to a separate caching layer
# and removed from the database.
# [vkoukis] after discussion with [faidon].
action = models.CharField(choices=ACTIONS, max_length=30, null=True)
operstate = models.CharField(choices=OPER_STATES, max_length=30, null=True)
backendjobid = models.PositiveIntegerField(null=True)
......
......@@ -27,7 +27,11 @@ def process_backend_msg(vm, jobid, opcode, status, logmsg):
# Notifications of success change the operating state
if status == 'success':
utils.update_state(vm, VirtualMachine.OPER_STATE_FROM_OPCODE[opcode])
# Special cases OP_INSTANCE_CREATE fails --> ERROR
# Set the deleted flag explicitly, to cater for admin-initiated removals
if opcode == 'OP_INSTANCE_REMOVE':
vm.deleted = True
# Special case: if OP_INSTANCE_CREATE fails --> ERROR
if status in ('canceled', 'error') and opcode == 'OP_INSTANCE_CREATE':
utils.update_state(vm, 'ERROR')
# Any other notification of failure leaves the operating state unchanged
......
......@@ -32,6 +32,9 @@ def get_rsapi_state(vm):
r = VirtualMachine.RSAPI_STATE_FROM_OPER_STATE[vm.operstate]
except KeyError:
return "UNKNOWN"
# A machine is DELETED if the deleted flag has been set
if vm.deleted:
return "DELETED"
# A machine is in REBOOT if an OP_INSTANCE_REBOOT request is in progress
if r == 'ACTIVE' and vm.backendopcode == 'OP_INSTANCE_REBOOT' and \
vm.backendjobstatus in ('queued', 'waiting', 'running'):
......
......@@ -231,6 +231,21 @@ class StartServer(Command):
self.http_post(path, body)
@command_name('console')
class ServerConsole(Command):
description = 'get VNC console'
syntax = '<server id>'
def add_options(self, parser):
pass
def execute(self, server_id):
path = '/api/%s/servers/%d/action' % (self.api, int(server_id))
body = json.dumps({'console':{'type':'VNC'}})
reply = self.http_post(path, body)
print_dict(reply)
@command_name('lsaddr')
class ListAddresses(Command):
description = 'list server addresses'
......
......@@ -531,13 +531,13 @@ div.machine div.actions a.more {
margin-top: 18px;
}
div.machine div.actions a:hover{
div.actions a.enabled:hover{
color: black !important;
text-decoration: underline;
display: block;
}
div.machine div.actions:hover a, .machine:hover .actions a {
.machine:hover .actions a {
color: #3d3d3d;
display: block;
}
......@@ -547,6 +547,10 @@ div.machine div.display a{
display: block;
}
div.machine div.actions a.selected:hover {
color: orange;
}
div.running div.machine a.action-start {
display: none;
}
......@@ -821,7 +825,7 @@ div.list div.actions a.enabled {
div.list div.actions a.enabled:hover{
cursor: pointer;
color: black !important;
color: black;
text-decoration: underline;
}
......@@ -846,6 +850,12 @@ input.machine {
color: #F49C1A;
}
#error-success div {
height: 142px;
overflow-y: auto;
overflow-x: hidden;
}
#error-success .close {
background-image: url(/static/close.png);
position: absolute;
......@@ -867,7 +877,7 @@ div.confirm_single, div.confirm_multiple {
div.action_error {
float: right;
width: 80px;
height: 70px;
height: 60px;
padding: 5px;
margin: -65px -165px 0 0;
display: none;
......@@ -878,9 +888,8 @@ div.action_error {
}
div.action_error button.details {
margin: 15px 0 0 0px;
margin: 5px 0 0 0px;
padding: 0 15px;
border-color: black;
}
.orange {
......@@ -894,8 +903,12 @@ div.confirm_single button, div.confirm_multiple button, div.action_error button{
color: #3D3D3D;
cursor: pointer;
padding: 0px;
height: 20px !important;
}
div.confirm_single button, .action_error button {
width: 80px !important;
}
div.confirm_single button.yes, div.confirm_multiple button.yes {
border-color: orange;
padding: 0 12px;
......@@ -909,7 +922,7 @@ div.confirm_single button.no, div.confirm_multiple button.no {
padding: 0px 16px;
}
div.confirm_single button.no:hover, div.confirm_multiple button.no:hover {
div.confirm_single button.no:hover, div.confirm_multiple button.no:hover, div.action_error button.details:hover {
background-color: #a5a5a5;
}
......@@ -925,7 +938,7 @@ div.confirm_single button.yes {
}
div.confirm_single button.no {
margin: 22px 0 0 5px;
margin: 20px 0 0 5px;
}
div.action_error {
......@@ -933,20 +946,19 @@ div.action_error {
}
div.confirm_multiple {
bottom: 0;
width: 700px;
height: 30px;
margin: 0 0 0px -36px;
width: 692px;
height: 28px;
margin: 0 0 10px -32px;
}
div.confirm_multiple p {
float: left;
margin: 9px 0 0 200px;
margin: 7px 0 0 200px;
}
div.confirm_multiple button {
float: right;
margin: 6px 5px 0 0;
margin: 4px 5px 0 0;
}
div.confirm_multiple button.no {
......@@ -1140,35 +1152,29 @@ div.list table thead #name {
width: 200px !important;
}
.spinner {
clear: left;
background: url("../static/progress.gif") transparent;
height: 31px;
width: 31px;
margin: 20px 0 0 11px;
position:absolute;
}
.wave {
border: none;
margin: -5px 0px 0 0px !important;
.spinner, .wave {
clear: right;
height: 20px;
float:right !important;
clear: both;
width: 20px;
margin: 5px 16px 0 15px !important;
}
.hidden {
display:none;
}
.selected {
div.actions a.selected, div.actions a.selected:hover {
display:block !important;
color: orange !important;
}
.message {
.action_error .message, .action_error .code {
display: none;
}
.fixed {
margin: 0 0 0 -32px !important;
bottom: 0;
position: fixed;
}
\ No newline at end of file
}
ui/static/progress.gif

2.55 KB | W: | H:

ui/static/progress.gif

2.39 KB | W: | H:

ui/static/progress.gif
ui/static/progress.gif
ui/static/progress.gif
ui/static/progress.gif
  • 2-up
  • Swipe
  • Onion skin
......@@ -11,10 +11,41 @@ function ISODateString(d){
pad(d.getUTCMonth()+1) + '-' +
pad(d.getUTCDate()) + 'T' +
pad(d.getUTCHours()) + ':' +
pad(d.getUTCMinutes())+ ':' +
pad(d.getUTCSeconds())
pad(d.getUTCMinutes()) + ':' +
pad(d.getUTCSeconds()) +'Z'
}
function parse_error(responseText){
var errors = [];
responseObj = JSON.parse(responseText);
//console.info(inp);
for (var err in responseObj){
errors[errors.length] = responseObj[err];
}
return errors;
}
// indexOf prototype for IE
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(elt /*, from*/) {
var len = this.length;
var from = Number(arguments[1]) || 0;
from = (from < 0)
? Math.ceil(from)
: Math.floor(from);
if (from < 0)
from += len;
for (; from < len; from++) {
if (from in this &&
this[from] === elt)
return from;
}
return -1;
};
}
function update_confirmations(){
// hide all confirm boxes to begin with
$('div.confirm_single').hide();
......@@ -200,7 +231,7 @@ function update_vms(interval) {
}
// as for now, just show an error message
try { console.info('update_vms errback:' + jqXHR.status ) } catch(err) {}
ajax_error(jqXHR.status);
ajax_error(jqXHR.status, undefined, 'Update VMs', jqXHR.responseText);
return false;
},
success: function(data, textStatus, jqXHR) {
......@@ -218,11 +249,11 @@ function update_vms(interval) {
if (jqXHR.status == 200 || jqXHR.status == 203) {
try {