Commit 8cb461c5 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Merge branch 'ui-0.11' into devel-0.12

parents bf85bdd7 15f679ef
......@@ -4,3 +4,6 @@
# Which is the cookie name that stores the token, leave it commented out
# to use same value as UI_AUTH_COOKIE_NAME value.
#HELPDESK_AUTH_COOKIE_NAME = UI_AUTH_COOKIE_NAME
# Astakos groups which have access to helpdesk views
#HELPDESK_PERMITTED_GROUPS = ['helpdesk']
......@@ -92,6 +92,9 @@ VM_CREATE_SUGGESTED_ROLES = ["Database server", "File server", "Mail server", "W
# vms. {0} gets replaced by the image OS value
VM_CREATE_NAME_TPL = "My {0} server"
# Template to use to build vm hostname
UI_VM_HOSTNAME_FORMAT = 'snf-%(id)s.vm.okeanos.grnet.gr'
# Name/description metadata for the available flavor disk templates
# Dict key is the disk_template value as stored in database
UI_FLAVORS_DISK_TEMPLATES_INFO = {
......@@ -99,6 +102,32 @@ UI_FLAVORS_DISK_TEMPLATES_INFO = {
'description': 'DRBD storage.'},
}
# Override default connect prompt messages. The setting gets appended to the
# ui default values so you only need to modify parameters you need to alter.
#
# Indicative format:
# {
# '<browser os1>': {
# '<vm os family1>': ['top message....', 'bottom message'],
# '<vm os family 2>': ['top message....', 'bottom message'],
# 'ssh_message': 'ssh %(user)s@%(hostname)s'
# }
#
# you may use the following parameters to format ssh_message:
#
# * server_id: the database pk of the vm
# * ip_address: the ipv4 address of the public vm nic
# * hostname: vm hostname
# * user: vm username
#
# you may assign a callable python object to the ssh_message, if so the above
# parameters get passed as arguments to the provided object.
UI_CONNECT_PROMPT_MESSAGES = {}
# extend rdp file content. May be a string with format parameters similar to
# those used in UI_CONNECT_PROMPT_MESSAGES `ssh_message` or a callable object.
UI_EXTRA_RDP_CONTENT = None
#######################
# UI BEHAVIOUR SETTINGS
......@@ -141,6 +170,10 @@ UI_NETWORK_AVAILABLE_NETWORK_TYPES = {'PRIVATE_MAC_FILTERED': 'mac-filtering'}
# network with dhcp enabled
UI_NETWORK_AVAILABLE_SUBNETS = ['10.0.0.0/24', '192.168.0.0/24']
# UI will use this setting to find an available network subnet if user requests
# automatic subnet selection.
UI_AUTOMATIC_NETWORK_RANGE_FORMAT = "192.168.%d.0/24"
# Whether to display already connected vm's to the network connect overlay
UI_NETWORK_ALLOW_DUPLICATE_VM_NICS = False
......@@ -149,6 +182,12 @@ UI_NETWORK_ALLOW_DUPLICATE_VM_NICS = False
# virtual machines from the network.
UI_NETWORK_STRICT_DESTROY = True
# Whether or not to group public networks nics in a single network view
UI_GROUP_PUBLIC_NETWORKS = True
# The name of the grouped network view
UI_GROUPED_PUBLIC_NETWORK_NAME = 'Internet'
###############
# UI EXTENSIONS
......
......@@ -185,4 +185,9 @@ h4.expanded .badge { background-position: 185px 43px}
h4 i { margin-top: 4px; margin-right: 10px;}
.container-fluid { margin-left:auto; margin-right:auto; max-width:960px; }
h3.info { cursor:default; color:#2956B2; text-align:center; font-size:16px; margin-bottom:0;}
\ No newline at end of file
h3.info { cursor:default; color:#2956B2; text-align:center; font-size:16px; margin-bottom:0;}
.vm-suspend-form { margin-top: 20px; text-align: right; }
.vm-suspend-form form { margin-bottom: 0px; }
.vm-suspend-form.suspended form input { background-color: #2956B2; color: #ffffff;}
.vm-suspend-form form input { background-color: #F81A23; color: #ffffff;}
{% if vm.suspended %}
<form method="post" action="{% url helpdesk-suspend-vm-release vm_id=vm.pk %}">
<input type="hidden" name="token" value="{{ csrf_token }}" />
<input type="submit" value="RELEASE SUSPENSION" />
</form>
{% else %}
<form method="post" action="{% url helpdesk-suspend-vm vm_id=vm.pk %}">
<input type="hidden" name="token" value="{{ csrf_token }}" />
<input type="submit" value="SUSPEND" />
</form>
{% endif %}
{% load helpdesk_tags %}
<div class="object-anchor" id="vm-{{vm.pk}}"></div>
<div class="vm-details object-details {{ rowcls }}">
<h4><em><img src="{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png" />{{ vm|get_os }}</em><i class="icon-tasks"></i>{{ vm.name }}<span class="badge">&nbsp;</span></h4>
{{ vm|vm_status_badge|safe }}
<span class="badge badge-inverse">ID: {{ vm.pk }}</span>
<span class="badge badge-inverse">{{ vm|vm_public_ip }}</span>
<span class="badge badge-inverse flavor">
<span class="cpu">{{ vm.flavor.cpu }}x</span>
<span class="ram">{{ vm.flavor.ram}}MB</span>
<span class="disk">{{ vm.flavor.disk }}GB</span>
<h4><em><img src="{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png" />{{ vm|get_os }}</em><i class="icon-tasks"></i>{{ vm.name }}<span class="badge">&nbsp;</span></h4>
{{ vm|vm_status_badge|safe }}
<span class="badge badge-inverse">ID: {{ vm.pk }}</span>
<span class="badge badge-inverse">{{ vm|vm_public_ip }}</span>
{% if vm.suspended %}
<span class="badge badge-important">SUSPENDED</span>
{% endif %}
<span class="badge badge-inverse flavor">
<span class="cpu">{{ vm.flavor.cpu }}x</span>
<span class="ram">{{ vm.flavor.ram}}MB</span>
<span class="disk">{{ vm.flavor.disk }}GB</span>
</span>
<div class="vm-details-content object-details-content">
</span>
<div class="vm-details-content object-details-content">
<ul class="nav nav-tabs">
<li class="active"><a href="#details{{ vm.pk }}" data-toggle="tab">Details</a></li>
<li><a href="#metadata{{ vm.pk }}" data-toggle="tab">Metadata</a></li>
<li><a href="#backend{{ vm.pk }}" data-toggle="tab">Backend info</a></li>
<li><a href="#network{{ vm.pk }}" data-toggle="tab">Network interfaces</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="details{{ vm.pk }}">
<dl class="dl-horizontal well">
<dt>ID</dt><dd>{{ vm.pk }}</dd>
<dt>Name</dt><dd>{{ vm.name }}</dd>
<dt>User id</dt><dd>{{ vm.userid }}</dd>
<dt>Created</dt><dd>{{ vm.created }} ({{ vm.created|timesince }} <strong>ago</strong>)</dd>
<dt>Updated</dt><dd>{{ vm.updated }} ({{ vm.updated|timesince }} <strong>ago</strong>)</dd>
<dt>Suspended</dt><dd>{{ vm.suspended }}</dd>
<dt>Deleted</dt><dd>{{ vm.deleted }}</dd>
<dt>Image id</dt><dd>{{ vm.imageid }}</dd>
<dt>Flavor</dt><dd>{{ vm.flavor.cpu }},
{{ vm.flavor.disk }},
{{ vm.flavor.ram }},
{{ vm.flavor.disk_template }}</dd>
</dl>
</div>
<div class="tab-pane" id="metadata{{ vm.pk }}">
<dl class="dl-horizontal well">
{% for meta in vm.metadata.all %}
<dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd>
{% empty %}
<dt>No metadata</dt>
{% endfor %}
</dl>
</div>
<div class="tab-pane" id="backend{{ vm.pk }}">
<dl class="dl-horizontal well">
<dt>Action</dt><dd>{{ vm.get_action_display }} ({{ vm.action }})</dd>
<dt>Operstate</dt><dd>{{ vm.get_operstate_display }} ({{ vm.operstate }})</dd>
<dt>Backend job id</dt><dd>{{ vm.backendjobid }}</dd>
<dt>Backend op code</dt><dd>{{ vm.get_backendopcode_display }} ({{ vm.backendopcode }})</dd>
<dt>Backend log msg</dt><dd>{{ vm.backendlogmsg }}</dd>
<dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd>
<dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd>
</dl>
</div>
<div class="tab-pane" id="network{{ vm.pk }}">
<table class="table well">
<thead>
<td>ID</td>
<td>Network (ID)</td>
<td>Created</td>
<td>Updated</td>
<td>Index</td>
<td>MAC</td>
<td>IPv4</td>
<td>IPv6</td>
<td>Firewall</td>
</thead>
<tbody>
{% for nic in vm.nics.all %}
<tr>
<td>{{ nic.pk }}</td>
<td>{{ nic.network }} ({{ nic.network.pk }})</td>
<td>{{ nic.created }}</td>
<td>{{ nic.updated }}</td>
<td>{{ nic.index }}</td>
<td>{{ nic.mac }}</td>
<td>{{ nic.ipv4 }}</td>
<td>{{ nic.ipv6 }}</td>
<td>{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})</td>
</tr>
{% empty %}
<tr>
<td colspan=9>No network interface available</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a href="#details{{ vm.pk }}" data-toggle="tab">Details</a></li>
<li><a href="#metadata{{ vm.pk }}" data-toggle="tab">Metadata</a></li>
<li><a href="#backend{{ vm.pk }}" data-toggle="tab">Backend info</a></li>
<li><a href="#network{{ vm.pk }}" data-toggle="tab">Network interfaces</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="details{{ vm.pk }}">
<dl class="dl-horizontal well">
<dt>ID</dt><dd>{{ vm.pk }}</dd>
<dt>Name</dt><dd>{{ vm.name }}</dd>
<dt>User id</dt><dd>{{ vm.userid }}</dd>
<dt>Created</dt><dd>{{ vm.created }} ({{ vm.created|timesince }} <strong>ago</strong>)</dd>
<dt>Updated</dt><dd>{{ vm.updated }} ({{ vm.updated|timesince }} <strong>ago</strong>)</dd>
<dt>Suspended</dt><dd>{{ vm.suspended }}</dd>
<dt>Deleted</dt><dd>{{ vm.deleted }}</dd>
<dt>Image id</dt><dd>{{ vm.imageid }}</dd>
<dt>Flavor</dt><dd>{{ vm.flavor.cpu }},
{{ vm.flavor.disk }},
{{ vm.flavor.ram }},
{{ vm.flavor.disk_template }}</dd>
</dl>
</div>
<div class="tab-pane" id="metadata{{ vm.pk }}">
<dl class="dl-horizontal well">
{% for meta in vm.metadata.all %}
<dt>{{ meta.meta_key }}</dt><dd>{{ meta.meta_value }}</dd>
{% empty %}
<dt>No metadata</dt>
{% endfor %}
</dl>
</div>
<div class="tab-pane" id="backend{{ vm.pk }}">
<dl class="dl-horizontal well">
<dt>Action</dt><dd>{{ vm.get_action_display }} ({{ vm.action }})</dd>
<dt>Operstate</dt><dd>{{ vm.get_operstate_display }} ({{ vm.operstate }})</dd>
<dt>Backend job id</dt><dd>{{ vm.backendjobid }}</dd>
<dt>Backend op code</dt><dd>{{ vm.get_backendopcode_display }} ({{ vm.backendopcode }})</dd>
<dt>Backend log msg</dt><dd>{{ vm.backendlogmsg }}</dd>
<dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd>
<dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd>
</dl>
</div>
<div class="tab-pane" id="network{{ vm.pk }}">
<table class="table well">
<thead>
<td>ID</td>
<td>Network (ID)</td>
<td>Created</td>
<td>Updated</td>
<td>Index</td>
<td>MAC</td>
<td>IPv4</td>
<td>IPv6</td>
<td>Firewall</td>
</thead>
<tbody>
{% for nic in vm.nics.all %}
<tr>
<td>{{ nic.pk }}</td>
<td>{{ nic.network }} ({{ nic.network.pk }})</td>
<td>{{ nic.created }}</td>
<td>{{ nic.updated }}</td>
<td>{{ nic.index }}</td>
<td>{{ nic.mac }}</td>
<td>{{ nic.ipv4 }}</td>
<td>{{ nic.ipv6 }}</td>
<td>{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})</td>
</tr>
{% empty %}
<tr>
<td colspan=9>No network interface available</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="vm-suspend-form {% if vm.suspended %}suspended{% endif %}">
{% include "helpdesk/_suspend.html" %}
</div>
</div>
......@@ -43,7 +43,7 @@ def network_deleted_badge(network):
Return a span badge styled based on the vm current status
"""
deleted_badge = ""
if network.state == "DELETED":
if network.deleted:
deleted_badge = '<span class="badge badge-important">Deleted</span>'
return deleted_badge
......@@ -55,7 +55,7 @@ def get_os(vm):
return vm.metadata.filter(meta_key="OS").get().meta_value
except:
return "unknown"
get_os.is_safe = True
@register.filter(name="network_vms")
......@@ -64,7 +64,7 @@ def network_vms(network, account):
for nic in network.nics.filter(machine__userid=account):
vms.append(nic.machine)
return vms
network_vms.is_safe = True
@register.filter(name="network_nics")
......@@ -73,5 +73,5 @@ def network_nics(network, account):
for nic in network.nics.filter(machine__userid=account):
vms.append(nic)
return vms
network_nics.is_safe = True
\ No newline at end of file
network_nics.is_safe = True
......@@ -2,6 +2,11 @@ from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('',
url(r'^$', 'synnefo.helpdesk.views.index', name='helpdesk-index'),
url(r'^suspend/(?P<vm_id>[0-9]+)$', 'synnefo.helpdesk.views.suspend_vm',
name='helpdesk-suspend-vm'),
url(r'^suspend_release/(?P<vm_id>[0-9]+)$',
'synnefo.helpdesk.views.suspend_vm_release',
name='helpdesk-suspend-vm-release'),
url(r'^api/users', 'synnefo.helpdesk.views.user_list',
name='helpdesk-userslist'),
url(r'^(?P<account_or_ip>.*)$', 'synnefo.helpdesk.views.account',
......
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import re
import logging
from itertools import chain
......@@ -8,13 +42,17 @@ from django.db.models import get_apps
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.utils import simplejson as json
from django.core.urlresolvers import reverse
from urllib import unquote
from synnefo.lib.astakos import get_user
from synnefo.db.models import *
logger = logging.getLogger(__name__)
IP_SEARCH_REGEX = re.compile('([0-9]+)(?:\.[0-9]+){3}')
def get_token_from_cookie(request, cookiename):
......@@ -33,15 +71,31 @@ def get_token_from_cookie(request, cookiename):
return None
# TODO: here we mix ui setting with helpdesk settings
# if sometime in the future helpdesk gets splitted from the
# cyclades api code this should change and helpdesk should provide
# its own setting HELPDESK_AUTH_COOKIE_NAME.
AUTH_COOKIE = getattr(settings, 'UI_AUTH_COOKIE_NAME', getattr(settings,
'HELPDESK_AUTH_COOKIE_NAME', '_pithos2_a'))
AUTH_COOKIE_NAME = getattr(settings, 'HELPDESK_AUTH_COOKIE_NAME', getattr(settings,
'UI_AUTH_COOKIE_NAME', '_pithos2_a'))
PERMITTED_GROUPS = getattr(settings, 'HELPDESK_PERMITTED_GROUPS',
['helpdesk'])
SHOW_DELETED_VMS = getattr(settings, 'HELPDESK_SHOW_DELETED_VMS', False)
def token_check(func):
"""
Mimic csrf security check using user auth token.
"""
def wrapper(request, *args, **kwargs):
if not hasattr(request, 'user'):
raise PermissionDenied
token = request.POST.get('token', None)
if token and token != request.user.get('auth_token', None):
return func(request, *args, **kwargs)
raise PermissionDenied
return wrapper
def helpdesk_user_required(func, groups=['helpdesk']):
def helpdesk_user_required(func, permitted_groups=PERMITTED_GROUPS):
"""
Django view wrapper that checks if identified request user has helpdesk
permissions (exists in helpdesk group)
......@@ -51,7 +105,7 @@ def helpdesk_user_required(func, groups=['helpdesk']):
if not HELPDESK_ENABLED:
raise Http404
token = get_token_from_cookie(request, AUTH_COOKIE)
token = get_token_from_cookie(request, AUTH_COOKIE_NAME)
get_user(request, settings.ASTAKOS_URL, fallback_token=token)
if hasattr(request, 'user') and request.user:
groups = request.user.get('groups', [])
......@@ -59,12 +113,17 @@ def helpdesk_user_required(func, groups=['helpdesk']):
if not groups:
raise PermissionDenied
has_perm = False
for g in groups:
if not g in groups:
raise PermissionDenied
if g in permitted_groups:
has_perm = True
if not has_perm:
raise PermissionDenied
else:
raise PermissionDenied
logging.debug("User %s accessed helpdesk view" % (request.user_uniq))
return func(request, *args, **kwargs)
return wrapper
......@@ -91,6 +150,8 @@ def account(request, account_or_ip):
Account details view.
"""
show_deleted = bool(int(request.GET.get('deleted', SHOW_DELETED_VMS)))
account_exists = True
vms = []
networks = []
......@@ -104,12 +165,18 @@ def account(request, account_or_ip):
except NetworkInterface.DoesNotExist:
account_exists = False
else:
# all user vms
vms = VirtualMachine.objects.filter(userid=account).order_by('deleted')
filter_extra = {}
if not show_deleted:
filter_extra['deleted'] = False
# all user vms
vms = VirtualMachine.objects.filter(userid=account,
**filter_extra).order_by('deleted')
# return all user private and public networks
public_networks = Network.objects.filter(public=True).order_by('state')
private_networks = Network.objects.filter(userid=account).order_by('state')
public_networks = Network.objects.filter(public=True,
**filter_extra).order_by('state')
private_networks = Network.objects.filter(userid=account,
**filter_extra).order_by('state')
networks = list(public_networks) + list(private_networks)
if vms.count() == 0 and private_networks.count() == 0:
......@@ -120,6 +187,7 @@ def account(request, account_or_ip):
'is_ip': is_ip,
'account': account,
'vms': vms,
'csrf_token': request.user['auth_token'],
'networks': networks,
'UI_MEDIA_URL': settings.UI_MEDIA_URL
}
......@@ -128,6 +196,26 @@ def account(request, account_or_ip):
extra_context=user_context)
@helpdesk_user_required
@token_check
def suspend_vm(request, vm_id):
vm = VirtualMachine.objects.get(pk=vm_id)
vm.suspended = True
vm.save()
account = vm.userid
return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))
@helpdesk_user_required
@token_check
def suspend_vm_release(request, vm_id):
vm = VirtualMachine.objects.get(pk=vm_id)
vm.suspended = False
vm.save()
account = vm.userid
return HttpResponseRedirect(reverse('helpdesk-details', args=(account,)))
@helpdesk_user_required
def user_list(request):
"""
......
......@@ -315,7 +315,7 @@ div.css-panes {
}
#console-header {
height: 67px;
height: 79px;
margin-bottom:15px;
background: url("../images/header-bg.png") repeat-x scroll 0 0 #FFFFFF;
}
......@@ -2579,6 +2579,13 @@ div.actions a.selected, div.actions a.selected:hover, div.machine-actions a.sele
margin-left: -15px;
}
.createbutton.disabled, #networkscreate.disabled {
background-color: #888 !important;
border-left-color: #aaa !important;
cursor: help !important;
color: #ccc !important;
}
#networkscreate:hover {
background-color: #FF9955;
}
......@@ -3792,18 +3799,18 @@ div.single div.column3 div.server-name:hover {
/* console css */
.console-header-logo {
padding-top: 17px;
padding-top: 16px;
margin-left: 30px;
position: fixed;
}
#console-header div.help-text {
font-size: 75%;
font-size: 70%;
font-weight:bold;
color:#FFFFFF;
float:left;
position: absolute;
margin: 45px 0 0 2px;
height: 20px;
top: 79px;
}
div.console-container {
......@@ -3818,8 +3825,7 @@ div.console-container {
.console-info {
font-size:80%;
color: white;
float:left;
position:relative;
position:absolute;
margin: 15px 0 0 480px;
}
......@@ -4554,7 +4560,8 @@ table.list-machines .wave {
overflow-y: scroll;
overflow-x: hidden;
}
.overlay .overlay-content .description.subinfo {
.overlay .overlay-content .description.subinfo, .overlay .extra-info {
margin-bottom:0;
border-bottom: none;