Commit aa532ec9 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Merge branch 'ui-0.11' into devel-0.12

parents dd9af521 95cbdb84
......@@ -6,6 +6,7 @@ dev
***
* AMQP client which supports connection with multiple brokers using
HA-queues and publisher-confirms to guarantee delivery of messages.
* Added astakos client lib helper get_token_from_cookie
v0.9.7
------
......
# Copyright 2011-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
......@@ -25,7 +25,7 @@
# 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
......@@ -69,13 +69,13 @@ def authenticate(token, authentication_url='http://127.0.0.1:8000/im/authenticat
def user_for_token(token, authentication_url, override_users):
if not token:
return None
if override_users:
try:
return {'uniq': override_users[token].decode('utf8')}
except:
return None
try:
return authenticate(token, authentication_url)
except Exception, e:
......@@ -87,7 +87,7 @@ def user_for_token(token, authentication_url, override_users):
def get_user(request, authentication_url='http://127.0.0.1:8000/im/authenticate', override_users={}, fallback_token=None):
request.user = None
request.user_uniq = None
# Try to find token in a parameter or in a request header.
user = user_for_token(request.GET.get('X-Auth-Token'), authentication_url, override_users)
if not user:
......@@ -96,6 +96,22 @@ def get_user(request, authentication_url='http://127.0.0.1:8000/im/authenticate'
user = user_for_token(fallback_token, authentication_url, override_users)
if not user:
return
request.user = user
request.user_uniq = user['uniq']
def get_token_from_cookie(request, cookiename):
"""
Extract token from the cookie name provided. Cookie should be in the same
form as astakos service sets its cookie contents::
<user_uniq>|<user_token>
"""
try:
cookie_content = unquote(request.COOKIES.get(cookiename, None))
return cookie_content.split("|")[1]
except AttributeError:
pass
return None
......@@ -65,6 +65,6 @@ if os.path.exists(SYNNEFO_SETTINGS_DIR):
try:
execfile(os.path.abspath(f))
except Exception as e:
print >>sys.stderr, "Failed to read settings file: %s [%s]" % \
print >>sys.stderr, "Failed to read settings file: %s [%r]" % \
(os.path.abspath(f), e)
raise SystemExit(1)
Changelog
---------
v0.12.0
*******
FIXES:
UI:
* Fix unclosed DIV's and other minor html fixes.
*
NEW FEATURES:
UI:
* Optionally group public network interfaces in one network view in ui
Configurable by the UI_GROUP_PUBLIC_NETWORKS setting (defaults to True).
* New setting UI_CHANGES_SINCE_ALIGMENT to allow aligment of the date used
by ui in api calls that support changes-since parameter.
v0.11.0
*******
......
......@@ -9,7 +9,7 @@
<script src="/admin/static/main.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<body data-spy="scroll" data-target=".subnav" data-offset="50">
<div class="container">
<div style="padding: 5px 0px 20px 0px">
<img src="/admin/static/banner.png" width="776" height="82">
......
......@@ -64,6 +64,7 @@ urlpatterns = patterns('synnefo.api.servers',
(r'^/(\d+)/meta(?:.json|.xml)?$', 'metadata_demux'),
(r'^/(\d+)/meta/(.+?)(?:.json|.xml)?$', 'metadata_item_demux'),
(r'^/(\d+)/stats(?:.json|.xml)?$', 'server_stats'),
(r'^/(\d+)/diagnostics(?:.json)?$', 'get_server_diagnostics'),
)
......@@ -138,9 +139,43 @@ def vm_to_dict(vm, detail=False):
attachments = [nic_to_dict(nic) for nic in vm.nics.all()]
if attachments:
d['attachments'] = {'values': attachments}
# include the latest vm diagnostic, if set
diagnostic = vm.get_last_diagnostic()
if diagnostic:
d['diagnostics'] = diagnostics_to_dict([diagnostic])
return d
def diagnostics_to_dict(diagnostics):
"""
Extract api data from diagnostics QuerySet.
"""
entries = list()
for diagnostic in diagnostics:
# format source date if set
formatted_source_date = None
if diagnostic.source_date:
formatted_source_date = util.isoformat(diagnostic.source_date)
entry = {
'source': diagnostic.source,
'created': util.isoformat(diagnostic.created),
'message': diagnostic.message,
'details': diagnostic.details,
'level': diagnostic.level,
}
if formatted_source_date:
entry['source_date'] = formatted_source_date
entries.append(entry)
return entries
def render_server(request, server, status=200):
if request.serialization == 'xml':
data = render_to_string('server.xml', {
......@@ -151,6 +186,24 @@ def render_server(request, server, status=200):
return HttpResponse(data, status=status)
def render_diagnostics(request, diagnostics_dict, status=200):
"""
Render diagnostics dictionary to json response.
"""
return HttpResponse(json.dumps(diagnostics_dict), status=status)
@util.api_method('GET')
def get_server_diagnostics(request, server_id):
"""
Virtual machine diagnostics api view.
"""
log.debug('server_diagnostics %s', server_id)
vm = util.get_vm(server_id, request.user_uniq)
diagnostics = diagnostics_to_dict(vm.diagnostics.all())
return render_diagnostics(request, diagnostics)
@util.api_method('GET')
def list_servers(request, detail=False):
# Normal Response Codes: 200, 203
......@@ -164,6 +217,7 @@ def list_servers(request, detail=False):
user_vms = VirtualMachine.objects.filter(userid=request.user_uniq)
since = util.isoparse(request.GET.get('changes-since'))
if since:
user_vms = user_vms.filter(updated__gte=since)
if not user_vms:
......
......@@ -5,7 +5,9 @@ synnefo_web_apps = [
'synnefo.db',
'synnefo.logic',
'synnefo.plankton',
'synnefo.helpdesk',
'synnefo.ui.userdata',
'synnefo.helpdesk',
]
synnefo_web_middleware = []
......@@ -14,4 +16,5 @@ synnefo_web_context_processors = ['synnefo.lib.context_processors.cloudbar']
synnefo_static_files = {
'synnefo.ui': 'ui/static',
'synnefo.admin': 'admin',
'synnefo.helpdesk': 'helpdesk',
}
#Enable the helpdesk application
#HELPDESK_ENABLED = True
# 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
......@@ -35,6 +35,10 @@ UI_UPDATE_INTERVAL_MAX = UI_UPDATE_INTERVAL * 3
# Fast update interval
UI_UPDATE_INTERVAL_FAST = UI_UPDATE_INTERVAL / 2
# Miliseconds to remove from the previous server response time used in
# consecutive API calls (aligning changes-since attribute).
UI_CHANGES_SINCE_ALIGNMENT = 0
# List of emails used for sending the feedback messages to (following the ADMINS format)
FEEDBACK_CONTACTS = (
# ('Contact Name', 'contact_email@domain.com'),
......@@ -79,7 +83,7 @@ VM_CREATE_SUGGESTED_FLAVORS = {
# A list of metadata keys to clone from image
# to the virtual machine on its creation.
VM_IMAGE_COMMON_METADATA = ["OS", "loginname", "logindomain"]
VM_IMAGE_COMMON_METADATA = ["OS", "loginname", "logindomain", "users", "remote"]
# A list of suggested vm roles to display to user on create wizard
VM_CREATE_SUGGESTED_ROLES = ["Database server", "File server", "Mail server", "Web server", "Proxy"]
......@@ -143,7 +147,7 @@ UI_NETWORK_ALLOW_DUPLICATE_VM_NICS = False
# Whether to display destroy action on private networks that contain vms. If
# set to True, destroy action will only get displayed if user disconnect all
# virtual machines from the network.
UI_NETWORK_STRICT_DESTROY = False
UI_NETWORK_STRICT_DESTROY = True
###############
......
......@@ -41,5 +41,6 @@ urlpatterns = patterns('',
name='ui_machines_connect'),
(r'^api/', include('synnefo.api.urls')),
(r'^plankton/', include('synnefo.plankton.urls')),
(r'^helpdesk/', include('synnefo.helpdesk.urls')),
)
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'VirtualMachineDiagnostic'
db.create_table('db_virtualmachinediagnostic', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('machine', self.gf('django.db.models.fields.related.ForeignKey')(related_name='diagnostics', to=orm['db.VirtualMachine'])),
('level', self.gf('django.db.models.fields.CharField')(max_length=20)),
('source', self.gf('django.db.models.fields.CharField')(max_length=100)),
('source_date', self.gf('django.db.models.fields.DateTimeField')(null=True)),
('message', self.gf('django.db.models.fields.CharField')(max_length=255)),
('details', self.gf('django.db.models.fields.TextField')(null=True)),
))
db.send_create_signal('db', ['VirtualMachineDiagnostic'])
def backwards(self, orm):
# Deleting model 'VirtualMachineDiagnostic'
db.delete_table('db_virtualmachinediagnostic')
models = {
'db.backend': {
'Meta': {'ordering': "['clustername']", 'object_name': 'Backend'},
'clustername': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
'ctotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'dfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'drained': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'dtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'hash': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'unique': 'True'}),
'mfree': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'mtotal': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'offline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
'pinst_cnt': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'port': ('django.db.models.fields.PositiveIntegerField', [], {'default': '5080'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
},
'db.backendnetwork': {
'Meta': {'unique_together': "(('network', 'backend'),)", 'object_name': 'BackendNetwork'},
'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'networks'", 'to': "orm['db.Backend']"}),
'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'backend_networks'", 'to': "orm['db.Network']"}),
'operstate': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '30'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.bridgepooltable': {
'Meta': {'object_name': 'BridgePoolTable'},
'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'size': ('django.db.models.fields.IntegerField', [], {})
},
'db.flavor': {
'Meta': {'unique_together': "(('cpu', 'ram', 'disk', 'disk_template'),)", 'object_name': 'Flavor'},
'cpu': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'disk': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'disk_template': ('django.db.models.fields.CharField', [], {'default': "'drbd'", 'max_length': '32'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ram': ('django.db.models.fields.IntegerField', [], {'default': '0'})
},
'db.ippooltable': {
'Meta': {'object_name': 'IPPoolTable'},
'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'size': ('django.db.models.fields.IntegerField', [], {})
},
'db.macprefixpooltable': {
'Meta': {'object_name': 'MacPrefixPoolTable'},
'available_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'base': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'offset': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
'reserved_map': ('django.db.models.fields.TextField', [], {'default': "''"}),
'size': ('django.db.models.fields.IntegerField', [], {})
},
'db.network': {
'Meta': {'object_name': 'Network'},
'action': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '32', 'null': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'dhcp': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'gateway': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'gateway6': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'link': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
'mac_prefix': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'machines': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['db.VirtualMachine']", 'through': "orm['db.NetworkInterface']", 'symmetrical': 'False'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'pool': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'network'", 'unique': 'True', 'null': 'True', 'to': "orm['db.IPPoolTable']"}),
'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'PENDING'", 'max_length': '32'}),
'subnet': ('django.db.models.fields.CharField', [], {'default': "'10.0.0.0/24'", 'max_length': '32'}),
'subnet6': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'default': "'PRIVATE_PHYSICAL_VLAN'", 'max_length': '50'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'userid': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'})
},
'db.networkinterface': {
'Meta': {'object_name': 'NetworkInterface'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'dirty': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'firewall_profile': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index': ('django.db.models.fields.IntegerField', [], {}),
'ipv4': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'}),
'ipv6': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
'mac': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.VirtualMachine']"}),
'network': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nics'", 'to': "orm['db.Network']"}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
},
'db.virtualmachine': {
'Meta': {'object_name': 'VirtualMachine'},
'action': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backend': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'virtual_machines'", 'null': 'True', 'to': "orm['db.Backend']"}),
'backend_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
'backendjobid': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
'backendjobstatus': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendlogmsg': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'backendopcode': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'backendtime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 1, 0, 0)'}),
'buildpercentage': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'flavor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['db.Flavor']"}),
'hostid': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'imageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'operstate': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True'}),
'suspended': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'userid': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'db.virtualmachinediagnostic': {
'Meta': {'ordering': "['-created']", 'object_name': 'VirtualMachineDiagnostic'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'details': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'machine': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'diagnostics'", 'to': "orm['db.VirtualMachine']"}),
'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'source': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'source_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'})
},
'db.virtualmachinemetadata': {
'Meta': {'unique_together': "(('meta_key', 'vm'),)", 'object_name': 'VirtualMachineMetadata'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'meta_key': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'meta_value': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
'vm': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'metadata'", 'to': "orm['db.VirtualMachine']"})
}
}
complete_apps = ['db']
......@@ -295,6 +295,12 @@ class VirtualMachine(models.Model):
else:
raise ServiceUnavailable
def get_last_diagnostic(self, **filters):
try:
return self.diagnostics.filter()[0]
except IndexError:
return None
@staticmethod
def put_client(client):
put_rapi_client(client)
......@@ -700,3 +706,55 @@ def pooled_rapi_client(obj):
yield client
finally:
put_rapi_client(client)
class VirtualMachineDiagnosticManager(models.Manager):
"""
Custom manager for :class:`VirtualMachineDiagnostic` model.
"""
# diagnostic creation helpers
def create_for_vm(self, vm, level, message, **kwargs):
attrs = {'machine': vm, 'level': level, 'message': message}
attrs.update(kwargs)
# update instance updated time
self.create(**attrs)
vm.save()
def create_error(self, vm, **kwargs):
self.create_for_vm(vm, 'ERROR', **kwargs)
def create_debug(self, vm, **kwargs):
self.create_for_vm(vm, 'DEBUG', **kwargs)
def since(self, vm, created_since, **kwargs):
return self.get_query_set().filter(vm=vm, created__gt=created_since,
**kwargs)
class VirtualMachineDiagnostic(models.Model):
"""
Model to store backend information messages that relate to the state of
the virtual machine.
"""
TYPES = (
('ERROR', 'Error'),
('WARNING', 'Warning'),
('INFO', 'Info'),
('DEBUG', 'Debug'),
)
objects = VirtualMachineDiagnosticManager()
created = models.DateTimeField(auto_now_add=True)
machine = models.ForeignKey('VirtualMachine', related_name="diagnostics")
level = models.CharField(max_length=20, choices=TYPES)
source = models.CharField(max_length=100)
source_date = models.DateTimeField(null=True)
message = models.CharField(max_length=255)
details = models.TextField(null=True)
class Meta:
ordering = ['-created']
[
{
"model": "db.VirtualMachine",
"pk": 1001,
"fields": {
"userid": "testuser@test.com",
"name": "user1 vm",
"created": "2011-02-06 00:00:00",
"updated": "2011-02-06 00:00:00",
"imageid": "1",
"hostid": "HAL-9000",
"flavor": 1,
"operstate": "STOPPED"
}
},
{
"model": "db.VirtualMachine",
"pk": 1002,
"fields": {
"userid": "testuser2@test.com",
"name": "user2 vm1",
"created": "2011-02-10 00:00:00",
"updated": "2011-02-10 00:00:00",
"imageid": "1",
"hostid": "HAL-9000",
"flavor": 1,
"operstate": "BUILD"
}
},
{
"model": "db.VirtualMachine",
"pk": 1003,
"fields": {
"userid": "testuser2@test.com",
"name": "user2 vm2",
"created": "2011-02-10 00:00:00",
"updated": "2011-02-10 00:00:00",
"imageid": "1",
"hostid": "HAL-9000",
"flavor": 1,
"operstate": "BUILD"
}
},
{
"model": "db.VirtualMachineMetadata",
"pk": 1,
"fields": {
"meta_key": "OS",
"meta_value": "debian",