started extracting code
This commit is contained in:
parent
dc4e77017e
commit
ecfc3abae8
3
README
3
README
@ -0,0 +1,3 @@
|
||||
StackTach is a debugging tool for OpenStack Nova.
|
||||
|
||||
It takes events from AMQP and sends them to the StackTach server for web display.
|
0
__init__.py
Normal file
0
__init__.py
Normal file
14
manage.py
Normal file
14
manage.py
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
from django.core.management import execute_manager
|
||||
import imp
|
||||
try:
|
||||
imp.find_module('settings') # Assumed to be in the same directory.
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
|
||||
sys.exit(1)
|
||||
|
||||
import settings
|
||||
|
||||
if __name__ == "__main__":
|
||||
execute_manager(settings)
|
0
stacktach/__init__.py
Normal file
0
stacktach/__init__.py
Normal file
53
stacktach/models.py
Normal file
53
stacktach/models.py
Normal file
@ -0,0 +1,53 @@
|
||||
# Copyright 2012 - Dark Secret Software Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Tenant(models.Model):
|
||||
email = models.CharField(max_length=50)
|
||||
project_name = models.CharField(max_length=50)
|
||||
tenant_id = models.AutoField(primary_key=True, unique=True)
|
||||
|
||||
|
||||
class RawData(models.Model):
|
||||
tenant = models.ForeignKey(Tenant, db_index=True,
|
||||
to_field='tenant_id')
|
||||
nova_tenant = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
json = models.TextField()
|
||||
routing_key = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
state = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
when = models.DateTimeField(db_index=True)
|
||||
microseconds = models.IntegerField(default=0)
|
||||
publisher = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
event = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
service = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
host = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
instance = models.CharField(max_length=50, null=True,
|
||||
blank=True, db_index=True)
|
||||
|
||||
|
||||
class TenantForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = ('email', 'project_name')
|
28
stacktach/urls.py
Normal file
28
stacktach/urls.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.conf.urls.defaults import patterns, include, url
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', 'dss.stackmon.views.welcome', name='welcome'),
|
||||
url(r'new_tenant', 'dss.stackmon.views.new_tenant', name='new_tenant'),
|
||||
url(r'logout', 'dss.stackmon.views.logout', name='logout'),
|
||||
url(r'^(?P<tenant_id>\d+)/$', 'dss.stackmon.views.home', name='home'),
|
||||
url(r'^(?P<tenant_id>\d+)/data/$', 'dss.stackmon.views.data',
|
||||
name='data'),
|
||||
url(r'^(?P<tenant_id>\d+)/details/(?P<column>\w+)/(?P<row_id>\d+)/$',
|
||||
'dss.stackmon.views.details', name='details'),
|
||||
url(r'^(?P<tenant_id>\d+)/expand/(?P<row_id>\d+)/$',
|
||||
'dss.stackmon.views.expand', name='expand'),
|
||||
url(r'^(?P<tenant_id>\d+)/host_status/$',
|
||||
'dss.stackmon.views.host_status', name='host_status'),
|
||||
url(r'^(?P<tenant_id>\d+)/instance_status/$',
|
||||
'dss.stackmon.views.instance_status', name='instance_status'),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
# url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
249
stacktach/views.py
Normal file
249
stacktach/views.py
Normal file
@ -0,0 +1,249 @@
|
||||
# Copyright 2012 - Dark Secret Software Inc.
|
||||
|
||||
from django.shortcuts import render_to_response
|
||||
from django import http
|
||||
from django import template
|
||||
from django.utils.functional import wraps
|
||||
|
||||
from dss.stackmon import models
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
import random
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VERSION = 4
|
||||
|
||||
|
||||
class My401(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class HttpResponseUnauthorized(http.HttpResponse):
|
||||
status_code = 401
|
||||
|
||||
|
||||
def _monitor_message(routing_key, body):
|
||||
event = body['event_type']
|
||||
publisher = body['publisher_id']
|
||||
parts = publisher.split('.')
|
||||
service = parts[0]
|
||||
host = parts[1]
|
||||
payload = body['payload']
|
||||
request_spec = payload.get('request_spec', None)
|
||||
instance = None
|
||||
instance = payload.get('instance_id', instance)
|
||||
nova_tenant = body.get('_context_project_id', None)
|
||||
nova_tenant = payload.get('tenant_id', nova_tenant)
|
||||
return dict(host=host, instance=instance, publisher=publisher,
|
||||
service=service, event=event, nova_tenant=nova_tenant)
|
||||
|
||||
|
||||
def _compute_update_message(routing_key, body):
|
||||
publisher = None
|
||||
instance = None
|
||||
args = body['args']
|
||||
host = args['host']
|
||||
service = args['service_name']
|
||||
event = body['method']
|
||||
nova_tenant = args.get('_context_project_id', None)
|
||||
return dict(host=host, instance=instance, publisher=publisher,
|
||||
service=service, event=event, nova_tenant=nova_tenant)
|
||||
|
||||
|
||||
# routing_key : handler
|
||||
HANDLERS = {'monitor.info':_monitor_message,
|
||||
'monitor.error':_monitor_message,
|
||||
'':_compute_update_message}
|
||||
|
||||
|
||||
def _parse(tenant, args, json_args):
|
||||
routing_key, body = args
|
||||
handler = HANDLERS.get(routing_key, None)
|
||||
if handler:
|
||||
values = handler(routing_key, body)
|
||||
if not values:
|
||||
return {}
|
||||
|
||||
values['tenant'] = tenant
|
||||
when = body['_context_timestamp']
|
||||
when = datetime.datetime.strptime(when, "%Y-%m-%dT%H:%M:%S.%f")
|
||||
values['when'] = when
|
||||
values['microseconds'] = when.microsecond
|
||||
values['routing_key'] = routing_key
|
||||
values['json'] = json_args
|
||||
record = models.RawData(**values)
|
||||
record.save()
|
||||
return values
|
||||
return {}
|
||||
|
||||
|
||||
def _post_process_raw_data(rows, highlight=None):
|
||||
for row in rows:
|
||||
if "error" in row.routing_key:
|
||||
row.is_error = True
|
||||
if highlight and row.id == int(highlight):
|
||||
row.highlight = True
|
||||
row.when += datetime.timedelta(microseconds=row.microseconds)
|
||||
|
||||
class State(object):
|
||||
def __init__(self):
|
||||
self.version = VERSION
|
||||
self.tenant = None
|
||||
|
||||
def __str__(self):
|
||||
tenant = "?"
|
||||
if self.tenant:
|
||||
tenant = "'%s' - %s (%d)" % (self.tenant.project_name,
|
||||
self.tenant.email, self.tenant.id)
|
||||
return "[Version %s, Tenant %s]" % (self.version, tenant)
|
||||
|
||||
|
||||
def _reset_state(request):
|
||||
state = State()
|
||||
request.session['state'] = state
|
||||
return state
|
||||
|
||||
|
||||
def _get_state(request, tenant_id=None):
|
||||
tenant = None
|
||||
if tenant_id:
|
||||
try:
|
||||
tenant = models.Tenant.objects.get(tenant_id=tenant_id)
|
||||
except models.Tenant.DoesNotExist:
|
||||
raise My401()
|
||||
|
||||
if 'state' in request.session:
|
||||
state = request.session['state']
|
||||
else:
|
||||
state =_reset_state(request)
|
||||
|
||||
if hasattr(state, 'version') and state.version < VERSION:
|
||||
state =_reset_state(request)
|
||||
|
||||
state.tenant = tenant
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def tenant_check(view):
|
||||
@wraps(view)
|
||||
def inner(*args, **kwargs):
|
||||
try:
|
||||
return view(*args, **kwargs)
|
||||
# except HttpResponseUnauthorized, e:
|
||||
except My401:
|
||||
return HttpResponseUnauthorized()
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def _default_context(state):
|
||||
context = dict(utc=datetime.datetime.utcnow(), state=state)
|
||||
return context
|
||||
|
||||
|
||||
def welcome(request):
|
||||
state = _reset_state(request, None)
|
||||
return render_to_response('stackmon/welcome.html', _default_context(state))
|
||||
|
||||
|
||||
@tenant_check
|
||||
def home(request, tenant_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
return render_to_response('stackmon/index.html', _default_context(state))
|
||||
|
||||
|
||||
def logout(request):
|
||||
del request.session['state']
|
||||
return render_to_response('stackmon/welcome.html', _default_context(None))
|
||||
|
||||
|
||||
@tenant_check
|
||||
def new_tenant(request):
|
||||
state = _get_state(request)
|
||||
context = _default_context(state)
|
||||
if request.method == 'POST':
|
||||
form = models.TenantForm(request.POST)
|
||||
if form.is_valid():
|
||||
rec = models.Tenant(**form.cleaned_data)
|
||||
rec.save()
|
||||
_reset_state(request, rec.tenant_id)
|
||||
return http.HttpResponseRedirect('/stacktach/%d' % rec.tenant_id)
|
||||
else:
|
||||
form = models.TenantForm()
|
||||
context['form'] = form
|
||||
return render_to_response('stackmon/new_tenant.html', context)
|
||||
|
||||
|
||||
@tenant_check
|
||||
def data(request, tenant_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
raw_args = request.POST.get('args', "{}")
|
||||
args = json.loads(raw_args)
|
||||
c = _default_context(state)
|
||||
fields = _parse(state.tenant, args, raw_args)
|
||||
c['cooked_args'] = fields
|
||||
return render_to_response('stackmon/data.html', c)
|
||||
|
||||
|
||||
@tenant_check
|
||||
def details(request, tenant_id, column, row_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
c = _default_context(state)
|
||||
row = models.RawData.objects.get(pk=row_id)
|
||||
value = getattr(row, column)
|
||||
rows = models.RawData.objects.filter(tenant_id=tenant_id)
|
||||
if column != 'when':
|
||||
rows = rows.filter(**{column:value})
|
||||
else:
|
||||
from_time = value - datetime.timedelta(minutes=1)
|
||||
to_time = value + datetime.timedelta(minutes=1)
|
||||
rows = rows.filter(when__range=(from_time, to_time))
|
||||
|
||||
rows = rows.order_by('-when', '-microseconds')[:200]
|
||||
_post_process_raw_data(rows, highlight=row_id)
|
||||
c['rows'] = rows
|
||||
c['allow_expansion'] = True
|
||||
c['show_absolute_time'] = True
|
||||
return render_to_response('stackmon/rows.html', c)
|
||||
|
||||
|
||||
@tenant_check
|
||||
def expand(request, tenant_id, row_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
c = _default_context(state)
|
||||
row = models.RawData.objects.get(pk=row_id)
|
||||
payload = json.loads(row.json)
|
||||
pp = pprint.PrettyPrinter()
|
||||
c['payload'] = pp.pformat(payload)
|
||||
return render_to_response('stackmon/expand.html', c)
|
||||
|
||||
|
||||
@tenant_check
|
||||
def host_status(request, tenant_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
c = _default_context(state)
|
||||
hosts = models.RawData.objects.filter(tenant_id=tenant_id).\
|
||||
filter(host__gt='').\
|
||||
order_by('-when', '-microseconds')[:20]
|
||||
_post_process_raw_data(hosts)
|
||||
c['rows'] = hosts
|
||||
return render_to_response('stackmon/host_status.html', c)
|
||||
|
||||
|
||||
@tenant_check
|
||||
def instance_status(request, tenant_id):
|
||||
state = _get_state(request, tenant_id)
|
||||
c = _default_context(state)
|
||||
instances = models.RawData.objects.filter(tenant_id=tenant_id).\
|
||||
exclude(instance='n/a').\
|
||||
exclude(instance__isnull=True).\
|
||||
order_by('-when', '-microseconds')[:20]
|
||||
_post_process_raw_data(instances)
|
||||
c['rows'] = instances
|
||||
return render_to_response('stackmon/instance_status.html', c)
|
111
templates/base.html
Normal file
111
templates/base.html
Normal file
@ -0,0 +1,111 @@
|
||||
<!--
|
||||
Copyright 2012 - Dark Secret Software Inc.
|
||||
All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js"></script>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/static/js/jquery.timers.js"></script>
|
||||
<link href='http://fonts.googleapis.com/css?family=Kaushan+Script' rel='stylesheet' type='text/css'>
|
||||
<link href='http://fonts.googleapis.com/css?family=PT+Sans&subset=latin' rel='stylesheet' type='text/css'>
|
||||
|
||||
<style type="text/css">
|
||||
.fancy {
|
||||
font-family: 'Kaushan Script';
|
||||
font-style: normal;
|
||||
padding-bottom: 1em;
|
||||
font-size: 3em;
|
||||
color:#C63520;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: 'PT Sans', serif;
|
||||
font-style: normal;
|
||||
letter-spacing: -0.076em;
|
||||
line-height: 1em;
|
||||
}
|
||||
body {
|
||||
font-size:75%;
|
||||
color:#222;
|
||||
background:#fff;
|
||||
font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
a {
|
||||
text-decoration:none;
|
||||
color:#3f3f7f;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration:underline;
|
||||
}
|
||||
.cell-border {
|
||||
border-left: 1px solid #bbbbcc;
|
||||
}
|
||||
.title {
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
td {
|
||||
padding-right: 1em;
|
||||
}
|
||||
.status {
|
||||
border:1px #bbbbcc solid;
|
||||
width:-1em;
|
||||
margin-bottom:2em;
|
||||
}
|
||||
.status-title {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
background-color: white;
|
||||
font-family: 'PT Sans', serif;
|
||||
color:#E5574D;
|
||||
font-weight:bold;
|
||||
font-size:1.5em;
|
||||
}
|
||||
.status-inner {
|
||||
background-color:#fafaff;
|
||||
padding-left:.5em;
|
||||
margin-left:.5em;
|
||||
margin-right:1em;
|
||||
padding-bottom:1em;
|
||||
margin-bottom:1em;
|
||||
}
|
||||
.std-height {
|
||||
height:5em;
|
||||
overflow-y:scroll;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
{% block extra_js %}
|
||||
{% endblock %}
|
||||
$(document).ready(function() {
|
||||
{% block extra_init_js %}
|
||||
{% endblock %}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class='fancy'>StackTach</div>
|
||||
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
1
templates/data.html
Normal file
1
templates/data.html
Normal file
@ -0,0 +1 @@
|
||||
{{cooked_args|safe}}
|
5
templates/expand.html
Normal file
5
templates/expand.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<pre>
|
||||
{{payload|safe}}
|
||||
</pre>
|
||||
</div>
|
8
templates/host_status.html
Normal file
8
templates/host_status.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% include "rows.html" %}
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).oneTime(2000, function() {
|
||||
$('#host_activity').load('/{{state.tenant.tenant_id}}/host_status');
|
||||
});
|
||||
</script>
|
||||
|
50
templates/index.html
Normal file
50
templates/index.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extra_js %}
|
||||
function details(tenant_id, column, row_id)
|
||||
{
|
||||
$("#detail").load('/' + tenant_id + '/details/' + column + '/' + row_id);
|
||||
};
|
||||
|
||||
function expand(tenant_id, row_id)
|
||||
{
|
||||
$("#row_expansion_" + row_id).load('/' + tenant_id + '/expand/' + row_id);
|
||||
};
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_init_js %}
|
||||
$('#host-box').resizable();
|
||||
$('#instance-box').resizable();
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div style='float:right;'>{{state.tenant.email}} (TID:{{state.tenant.tenant_id}}) - {{state.tenant.project_name}} <a href='/logout'>logout</a></div>
|
||||
<div class='status-title'>Recent Host Activity</div>
|
||||
<div id='host-box' class='status std-height'>
|
||||
<div id='host_activity' class='status-inner'>
|
||||
{% include "host_status.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='status-title'>Recent Instance Activity</div>
|
||||
<div id='instance-box' class='status std-height'>
|
||||
<div id='instance_activity' class='status-inner'>
|
||||
{% include "instance_status.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class='status-title'>Commands</div>
|
||||
<div class='status'>
|
||||
<div class='status-inner'>
|
||||
<input type=text style='width:100%; background-color:black;color:white;'
|
||||
value='<custom query here>'/>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class='status-title'>Details</div>
|
||||
<div class='status'>
|
||||
<div id='detail' class='status-inner'>
|
||||
<div>click on an item above to see more of the same type.</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
8
templates/instance_status.html
Normal file
8
templates/instance_status.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% include "rows.html" %}
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).oneTime(2000, function() {
|
||||
$('#instance_activity').load('/{{state.tenant.tenant_id}}/instance_status');
|
||||
});
|
||||
</script>
|
||||
|
13
templates/new_tenant.html
Normal file
13
templates/new_tenant.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class='status-title'>New Tenant</div>
|
||||
<div id='host-box' class='status'>
|
||||
<div id='host_activity' class='status-inner'>
|
||||
<form action='/new_tenant/' method='post'>{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
50
templates/rows.html
Normal file
50
templates/rows.html
Normal file
@ -0,0 +1,50 @@
|
||||
<table style='font-size:1em;'>
|
||||
<th>
|
||||
<td class='title'></td>
|
||||
<td class='title'>source</td>
|
||||
<td class='title'>tenant</td>
|
||||
<td class='title'>service</td>
|
||||
<td class='title'>host</td>
|
||||
<td class='title'>event</td>
|
||||
<td class='title'>instance</td>
|
||||
<td class='title'>when</td>
|
||||
</th>
|
||||
{% for row in rows %}
|
||||
<tr {% if row.highlight %}style='background-color:#FFD88F;'{% endif %} >
|
||||
<td>
|
||||
{% if allow_expansion %}
|
||||
<a href='#' onclick='expand({{state.tenant.tenant_id}}, {{row.id}});'>[+]</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td><span style='{% if row.is_error %}background-color:#ffaaaa;{% endif %}'>
|
||||
<a href='#' onclick='details({{state.tenant.tenant_id}}, "routing_key", {{row.id}});'>{{row.routing_key}}</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class='cell-border'>
|
||||
<a href='#' onclick='details({{state.tenant.tenant_id}}, "nova_tenant", {{row.id}});'>
|
||||
{% if row.nova_tenant %}{{row.nova_tenant}}{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class='cell-border'><a href='#' onclick='details({{state.tenant.tenant_id}}, "service", {{row.id}});'>{{row.service}}</a></td>
|
||||
<td class='cell-border'><a href='#' onclick='details({{state.tenant.tenant_id}}, "host", {{row.id}});'>{{row.host}}</a></td>
|
||||
<td class='cell-border'><b><a href='#' onclick='details({{state.tenant.tenant_id}}, "event", {{row.id}});'>{{row.event}}</a></b></td>
|
||||
<td class='cell-border'>
|
||||
<a href='#' onclick='details({{state.tenant.tenant_id}}, "instance", {{row.id}});'>
|
||||
{% if row.instance %}
|
||||
{{row.instance}}
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class='cell-border'><a href='#' onclick='details({{state.tenant.tenant_id}}, "when", {{row.id}});'>{% if show_absolute_time %}{{row.when}}{%else%}{{row.when|timesince:utc}} ago{%endif%}</a></td>
|
||||
</tr>
|
||||
{% if allow_expansion %}
|
||||
<tr>
|
||||
<td colspan=8>
|
||||
<div id='row_expansion_{{row.id}}' style='font-size:1.2em'></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
24
templates/welcome.html
Normal file
24
templates/welcome.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class='status-title'>About</div>
|
||||
<div id='host-box' class='status'>
|
||||
<div id='host_activity' class='status-inner'>
|
||||
StackTach is a hosted debug/monitoring tool for OpenStack Nova
|
||||
deployments.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='status-title'>Connecting StackTach to OpenStack</div>
|
||||
<div id='instance-box' class='status'>
|
||||
<div id='instance_activity' class='status-inner'>
|
||||
<ul>
|
||||
<li>Get a <a href='/new_tenant'>StackTach Tenant ID</a>
|
||||
<li>Add <pre>--notification_driver=nova.notifier.rabbit_notifier</pre> and
|
||||
<li><pre>--notification_topics=monitor</pre> to your nova.conf file.
|
||||
<li>Configure and run the <a target='_blank' href='https://github.com/SandyWalsh/StackTach'>StackTach Worker</a> somewhere in your Nova development environment.
|
||||
<li>Restart Nova and visit http://darksecretsoftware.com/stacktach/[your_tenant_id]/ to see your Nova installation in action!
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
15
urls.py
Normal file
15
urls.py
Normal file
@ -0,0 +1,15 @@
|
||||
from django.conf.urls.defaults import patterns, include, url
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^', include('stacktach.urls')),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
# url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
27
worker.py
27
worker.py
@ -1,4 +1,20 @@
|
||||
# Copyright 2012 - Dark Secret Software Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# This is the worker you run in your OpenStack environment. You need
|
||||
# to set TENANT_ID and URL to point to your StackTach web server.
|
||||
|
||||
import json
|
||||
import kombu
|
||||
@ -9,7 +25,9 @@ import threading
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
url = 'http://darksecretsoftware.com/stacktach/data/'
|
||||
# CHANGE THESE FOR YOUR INSTALLATION ...
|
||||
TENANT_ID = 1
|
||||
URL = 'http://darksecretsoftware.com/stacktach/%d/data/' % TENANT_ID
|
||||
|
||||
# For now we'll just grab all the fanout messages from compute to scheduler ...
|
||||
scheduler_exchange = kombu.entity.Exchange("scheduler_fanout", type="fanout",
|
||||
@ -57,18 +75,21 @@ class SchedulerFanoutConsumer(kombu.mixins.ConsumerMixin):
|
||||
try:
|
||||
raw_data = dict(args=jvalues)
|
||||
cooked_data = urllib.urlencode(raw_data)
|
||||
req = urllib2.Request(url, cooked_data)
|
||||
req = urllib2.Request(URL, cooked_data)
|
||||
response = urllib2.urlopen(req)
|
||||
page = response.read()
|
||||
print page
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 401:
|
||||
print "Unauthorized. Correct tenant id of %d?" % TENANT_ID
|
||||
print e
|
||||
page = e.read()
|
||||
print page
|
||||
raise
|
||||
|
||||
def on_scheduler(self, body, message):
|
||||
self._process(body, message)
|
||||
# Uncomment if you want periodic compute node status updates.
|
||||
# self._process(body, message)
|
||||
message.ack()
|
||||
|
||||
def on_nova(self, body, message):
|
||||
|
Loading…
Reference in New Issue
Block a user