summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Reports
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Reports')
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/importscript.py307
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml43
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/models.py139
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/sql/client.sql7
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html5
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base.html3
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html56
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html18
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html1
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html2
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html42
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html30
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html22
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html16
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py130
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py10
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/urls.py9
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/views.py266
-rw-r--r--src/lib/Bcfg2/Server/Reports/settings.py161
-rw-r--r--src/lib/Bcfg2/Server/Reports/updatefix.py281
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/utils.py4
21 files changed, 742 insertions, 810 deletions
diff --git a/src/lib/Bcfg2/Server/Reports/importscript.py b/src/lib/Bcfg2/Server/Reports/importscript.py
index 16df86a9b..4eced8340 100755
--- a/src/lib/Bcfg2/Server/Reports/importscript.py
+++ b/src/lib/Bcfg2/Server/Reports/importscript.py
@@ -4,17 +4,17 @@ Imports statistics.xml and clients.xml files in to database backend for
new statistics engine
"""
-import binascii
import os
import sys
+import traceback
try:
- import Bcfg2.Server.Reports.settings
+ import Bcfg2.settings
except Exception:
e = sys.exc_info()[1]
sys.stderr.write("Failed to load configuration settings. %s\n" % e)
sys.exit(1)
-project_directory = os.path.dirname(Bcfg2.Server.Reports.settings.__file__)
+project_directory = os.path.dirname(Bcfg2.settings.__file__)
project_name = os.path.basename(project_directory)
sys.path.append(os.path.join(project_directory, '..'))
project_module = __import__(project_name, '', '', [''])
@@ -27,14 +27,14 @@ from lxml.etree import XML, XMLSyntaxError
from getopt import getopt, GetoptError
from datetime import datetime
from time import strptime
-from django.db import connection
-from Bcfg2.Server.Reports.updatefix import update_database
+from django.db import connection, transaction
+from Bcfg2.Server.Plugins.Metadata import ClientMetadata
import logging
import Bcfg2.Logger
import platform
# Compatibility import
-from Bcfg2.Bcfg2Py3k import ConfigParser
+from Bcfg2.Bcfg2Py3k import ConfigParser, b64decode
def build_reason_kwargs(r_ent, encoding, logger):
@@ -53,7 +53,7 @@ def build_reason_kwargs(r_ent, encoding, logger):
# No point in flagging binary if we have no data
binary_file = False
elif r_ent.get('current_bdiff', False):
- rc_diff = binascii.a2b_base64(r_ent.get('current_bdiff'))
+ rc_diff = b64decode(r_ent.get('current_bdiff'))
elif r_ent.get('current_diff', False):
rc_diff = r_ent.get('current_diff')
else:
@@ -86,130 +86,160 @@ def build_reason_kwargs(r_ent, encoding, logger):
is_sensitive=sensitive_file,
unpruned=unpruned_entries)
+def _fetch_reason(elem, kargs, logger):
+ try:
+ rr = None
+ try:
+ rr = Reason.objects.filter(**kargs)[0]
+ except IndexError:
+ rr = Reason(**kargs)
+ rr.save()
+ logger.debug("Created reason: %s" % rr.id)
+ except Exception:
+ ex = sys.exc_info()[1]
+ logger.error("Failed to create reason for %s: %s" % (elem.get('name'), ex))
+ rr = Reason(current_exists=elem.get('current_exists',
+ default="True").capitalize() == "True")
+ rr.save()
+ return rr
-def load_stats(cdata, sdata, encoding, vlevel, logger, quick=False, location=''):
- clients = {}
- [clients.__setitem__(c.name, c) \
- for c in Client.objects.all()]
-
- pingability = {}
- [pingability.__setitem__(n.get('name'), n.get('pingable', default='N')) \
- for n in cdata.findall('Client')]
+def load_stats(sdata, encoding, vlevel, logger, quick=False, location=''):
for node in sdata.findall('Node'):
name = node.get('name')
- c_inst, created = Client.objects.get_or_create(name=name)
- if vlevel > 0:
- logger.info("Client %s added to db" % name)
- clients[name] = c_inst
- try:
- pingability[name]
- except KeyError:
- pingability[name] = 'N'
for statistics in node.findall('Statistics'):
- timestamp = datetime(*strptime(statistics.get('time'))[0:6])
- ilist = Interaction.objects.filter(client=c_inst,
- timestamp=timestamp)
- if ilist:
- current_interaction = ilist[0]
- if vlevel > 0:
- logger.info("Interaction for %s at %s with id %s already exists" % \
- (c_inst.id, timestamp, current_interaction.id))
- continue
- else:
- newint = Interaction(client=c_inst,
- timestamp=timestamp,
- state=statistics.get('state',
+ try:
+ load_stat(name, statistics, encoding, vlevel, logger, quick, location)
+ except:
+ logger.error("Failed to create interaction for %s: %s" %
+ (name, traceback.format_exc().splitlines()[-1]))
+
+@transaction.commit_on_success
+def load_stat(cobj, statistics, encoding, vlevel, logger, quick, location):
+ if isinstance(cobj, ClientMetadata):
+ client_name = cobj.hostname
+ else:
+ client_name = cobj
+ client, created = Client.objects.get_or_create(name=client_name)
+ if created and vlevel > 0:
+ logger.info("Client %s added to db" % client_name)
+
+ timestamp = datetime(*strptime(statistics.get('time'))[0:6])
+ ilist = Interaction.objects.filter(client=client,
+ timestamp=timestamp)
+ if ilist:
+ current_interaction = ilist[0]
+ if vlevel > 0:
+ logger.info("Interaction for %s at %s with id %s already exists" % \
+ (client.id, timestamp, current_interaction.id))
+ return
+ else:
+ newint = Interaction(client=client,
+ timestamp=timestamp,
+ state=statistics.get('state',
+ default="unknown"),
+ repo_rev_code=statistics.get('revision',
default="unknown"),
- repo_rev_code=statistics.get('revision',
- default="unknown"),
- goodcount=statistics.get('good',
- default="0"),
- totalcount=statistics.get('total',
- default="0"),
- server=location)
- newint.save()
- current_interaction = newint
- if vlevel > 0:
- logger.info("Interaction for %s at %s with id %s INSERTED in to db" % (c_inst.id,
- timestamp, current_interaction.id))
-
- counter_fields = {TYPE_CHOICES[0]: 0,
- TYPE_CHOICES[1]: 0,
- TYPE_CHOICES[2]: 0}
- pattern = [('Bad/*', TYPE_CHOICES[0]),
- ('Extra/*', TYPE_CHOICES[2]),
- ('Modified/*', TYPE_CHOICES[1])]
- for (xpath, type) in pattern:
- for x in statistics.findall(xpath):
- counter_fields[type] = counter_fields[type] + 1
- kargs = build_reason_kwargs(x, encoding, logger)
-
- try:
- rr = None
- try:
- rr = Reason.objects.filter(**kargs)[0]
- except IndexError:
- rr = Reason(**kargs)
- rr.save()
- if vlevel > 0:
- logger.info("Created reason: %s" % rr.id)
- except Exception:
- ex = sys.exc_info()[1]
- logger.error("Failed to create reason for %s: %s" % (x.get('name'), ex))
- rr = Reason(current_exists=x.get('current_exists',
- default="True").capitalize() == "True")
- rr.save()
-
- entry, created = Entries.objects.get_or_create(\
- name=x.get('name'), kind=x.tag)
-
- Entries_interactions(entry=entry, reason=rr,
- interaction=current_interaction,
- type=type[0]).save()
- if vlevel > 0:
- logger.info("%s interaction created with reason id %s and entry %s" % (xpath, rr.id, entry.id))
-
- # Update interaction counters
- current_interaction.bad_entries = counter_fields[TYPE_CHOICES[0]]
- current_interaction.modified_entries = counter_fields[TYPE_CHOICES[1]]
- current_interaction.extra_entries = counter_fields[TYPE_CHOICES[2]]
- current_interaction.save()
-
- mperfs = []
- for times in statistics.findall('OpStamps'):
- for metric, value in list(times.items()):
- mmatch = []
- if not quick:
- mmatch = Performance.objects.filter(metric=metric, value=value)
-
- if mmatch:
- mperf = mmatch[0]
- else:
- mperf = Performance(metric=metric, value=value)
- mperf.save()
- mperfs.append(mperf)
- current_interaction.performance_items.add(*mperfs)
-
- for key in list(pingability.keys()):
- if key not in clients:
- continue
+ goodcount=statistics.get('good',
+ default="0"),
+ totalcount=statistics.get('total',
+ default="0"),
+ server=location)
+ newint.save()
+ current_interaction = newint
+ if vlevel > 0:
+ logger.info("Interaction for %s at %s with id %s INSERTED in to db" % (client.id,
+ timestamp, current_interaction.id))
+
+ if isinstance(cobj, ClientMetadata):
try:
- pmatch = Ping.objects.filter(client=clients[key]).order_by('-endtime')[0]
- if pmatch.status == pingability[key]:
- pmatch.endtime = datetime.now()
- pmatch.save()
- continue
- except IndexError:
- pass
- Ping(client=clients[key], status=pingability[key],
- starttime=datetime.now(),
- endtime=datetime.now()).save()
+ imeta = InteractionMetadata(interaction=current_interaction)
+ profile, created = Group.objects.get_or_create(name=cobj.profile)
+ imeta.profile = profile
+ imeta.save() # save here for m2m
+
+ #FIXME - this should be more efficient
+ group_set = []
+ for group_name in cobj.groups:
+ group, created = Group.objects.get_or_create(name=group_name)
+ if created:
+ logger.debug("Added group %s" % group)
+ imeta.groups.add(group)
+ for bundle_name in cobj.bundles:
+ bundle, created = Bundle.objects.get_or_create(name=bundle_name)
+ if created:
+ logger.debug("Added bundle %s" % bundle)
+ imeta.bundles.add(bundle)
+ imeta.save()
+ except:
+ logger.error("Failed to save interaction metadata for %s: %s" %
+ (client_name, traceback.format_exc().splitlines()[-1]))
+
+
+ entries_cache = {}
+ [entries_cache.__setitem__((e.kind, e.name), e) \
+ for e in Entries.objects.all()]
+ counter_fields = {TYPE_BAD: 0,
+ TYPE_MODIFIED: 0,
+ TYPE_EXTRA: 0}
+ pattern = [('Bad/*', TYPE_BAD),
+ ('Extra/*', TYPE_EXTRA),
+ ('Modified/*', TYPE_MODIFIED)]
+ for (xpath, type) in pattern:
+ for x in statistics.findall(xpath):
+ counter_fields[type] = counter_fields[type] + 1
+ rr = _fetch_reason(x, build_reason_kwargs(x, encoding, logger), logger)
- if vlevel > 1:
- logger.info("---------------PINGDATA SYNCED---------------------")
+ try:
+ entry = entries_cache[(x.tag, x.get('name'))]
+ except KeyError:
+ entry, created = Entries.objects.get_or_create(\
+ name=x.get('name'), kind=x.tag)
+
+ Entries_interactions(entry=entry, reason=rr,
+ interaction=current_interaction,
+ type=type).save()
+ if vlevel > 0:
+ logger.info("%s interaction created with reason id %s and entry %s" % (xpath, rr.id, entry.id))
+
+ # add good entries
+ good_reason = None
+ for x in statistics.findall('Good/*'):
+ if good_reason == None:
+ # Do this once. Really need to fix Reasons...
+ good_reason = _fetch_reason(x, build_reason_kwargs(x, encoding, logger), logger)
+ try:
+ entry = entries_cache[(x.tag, x.get('name'))]
+ except KeyError:
+ entry, created = Entries.objects.get_or_create(\
+ name=x.get('name'), kind=x.tag)
+ Entries_interactions(entry=entry, reason=good_reason,
+ interaction=current_interaction,
+ type=TYPE_GOOD).save()
+ if vlevel > 0:
+ logger.info("%s interaction created with reason id %s and entry %s" % (xpath, good_reason.id, entry.id))
+
+ # Update interaction counters
+ current_interaction.bad_entries = counter_fields[TYPE_BAD]
+ current_interaction.modified_entries = counter_fields[TYPE_MODIFIED]
+ current_interaction.extra_entries = counter_fields[TYPE_EXTRA]
+ current_interaction.save()
+
+ mperfs = []
+ for times in statistics.findall('OpStamps'):
+ for metric, value in list(times.items()):
+ mmatch = []
+ if not quick:
+ mmatch = Performance.objects.filter(metric=metric, value=value)
+
+ if mmatch:
+ mperf = mmatch[0]
+ else:
+ mperf = Performance(metric=metric, value=value)
+ mperf.save()
+ mperfs.append(mperf)
+ current_interaction.performance_items.add(*mperfs)
- #Clients are consistent
if __name__ == '__main__':
from sys import argv
@@ -231,18 +261,17 @@ if __name__ == '__main__':
except GetoptError:
mesg = sys.exc_info()[1]
# print help information and exit:
- print("%s\nUsage:\nimportscript.py [-h] [-v] [-u] [-d] [-S] [-C bcfg2 config file] [-c clients-file] [-s statistics-file]" % (mesg))
+ print("%s\nUsage:\nimportscript.py [-h] [-v] [-u] [-d] [-S] [-C bcfg2 config file] [-s statistics-file]" % (mesg))
raise SystemExit(2)
for o, a in opts:
if o in ("-h", "--help"):
- print("Usage:\nimportscript.py [-h] [-v] -c <clients-file> -s <statistics-file> \n")
+ print("Usage:\nimportscript.py [-h] [-v] -s <statistics-file> \n")
print("h : help; this message")
print("v : verbose; print messages on record insertion/skip")
print("u : updates; print status messages as items inserted semi-verbose")
print("d : debug; print most SQL used to manipulate database")
print("C : path to bcfg2.conf config file.")
- print("c : clients.xml file")
print("s : statistics.xml file")
print("S : syslog; output to syslog")
raise SystemExit
@@ -256,7 +285,7 @@ if __name__ == '__main__':
if o in ("-d", "--debug"):
verb = 3
if o in ("-c", "--clients"):
- clientspath = a
+ print("DeprecationWarning: %s is no longer used" % o)
if o in ("-s", "--stats"):
statpath = a
@@ -267,7 +296,7 @@ if __name__ == '__main__':
logging.getLogger().setLevel(logging.INFO)
Bcfg2.Logger.setup_logging('importscript.py',
True,
- syslog)
+ syslog, level=logging.INFO)
cf = ConfigParser.ConfigParser()
cf.read([cpath])
@@ -289,24 +318,16 @@ if __name__ == '__main__':
except:
encoding = 'UTF-8'
- if not clientpath:
- try:
- clientspath = "%s/Metadata/clients.xml" % \
- cf.get('server', 'repository')
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- print("Could not read bcfg2.conf; exiting")
- raise SystemExit(1)
- try:
- clientsdata = XML(open(clientspath).read())
- except (IOError, XMLSyntaxError):
- print("StatReports: Failed to parse %s" % (clientspath))
- raise SystemExit(1)
-
q = '-O3' in sys.argv
+
+ # don't load this at the top. causes a circular import error
+ from Bcfg2.Server.SchemaUpdater import update_database, UpdaterError
# Be sure the database is ready for new schema
- update_database()
- load_stats(clientsdata,
- statsdata,
+ try:
+ update_database()
+ except UpdaterError:
+ raise SystemExit(1)
+ load_stats(statsdata,
encoding,
verb,
logger,
diff --git a/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml b/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
deleted file mode 100644
index bde236989..000000000
--- a/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version='1.0' encoding='utf-8' ?>
-<django-objects version="1.0">
- <object pk="1" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">0</field>
- <field type="DateTimeField" name="updated">2008-08-05 11:03:50</field>
- </object>
- <object pk="2" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">1</field>
- <field type="DateTimeField" name="updated">2008-08-05 11:04:10</field>
- </object>
- <object pk="3" model="reports.internaldatabaseversion">
- <field type="IntegerField" name="version">2</field>
- <field type="DateTimeField" name="updated">2008-08-05 13:37:19</field>
- </object>
- <object pk="4" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>3</field>
- <field type='DateTimeField' name='updated'>2008-08-11 08:44:36</field>
- </object>
- <object pk="5" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>10</field>
- <field type='DateTimeField' name='updated'>2008-08-22 11:28:50</field>
- </object>
- <object pk="5" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>11</field>
- <field type='DateTimeField' name='updated'>2009-01-13 12:26:10</field>
- </object>
- <object pk="6" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>16</field>
- <field type='DateTimeField' name='updated'>2010-06-01 12:26:10</field>
- </object>
- <object pk="7" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>17</field>
- <field type='DateTimeField' name='updated'>2010-07-02 00:00:00</field>
- </object>
- <object pk="8" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>18</field>
- <field type='DateTimeField' name='updated'>2011-06-30 00:00:00</field>
- </object>
- <object pk="8" model="reports.internaldatabaseversion">
- <field type='IntegerField' name='version'>19</field>
- <field type='DateTimeField' name='updated'>2012-03-28 00:00:00</field>
- </object>
-</django-objects>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/models.py b/src/lib/Bcfg2/Server/Reports/reports/models.py
index 35f2a4393..73adaaaaf 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/models.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/models.py
@@ -23,16 +23,13 @@ KIND_CHOICES = (
('Path', 'symlink'),
('Service', 'Service'),
)
-PING_CHOICES = (
- #These are possible ping states
- ('Up (Y)', 'Y'),
- ('Down (N)', 'N')
-)
+TYPE_GOOD = 0
TYPE_BAD = 1
TYPE_MODIFIED = 2
TYPE_EXTRA = 3
TYPE_CHOICES = (
+ (TYPE_GOOD, 'Good'),
(TYPE_BAD, 'Bad'),
(TYPE_MODIFIED, 'Modified'),
(TYPE_EXTRA, 'Extra'),
@@ -87,30 +84,9 @@ class Client(models.Model):
pass
-class Ping(models.Model):
- """Represents a ping of a client (sparsely)."""
- client = models.ForeignKey(Client, related_name="pings")
- starttime = models.DateTimeField()
- endtime = models.DateTimeField()
- status = models.CharField(max_length=4, choices=PING_CHOICES) # up/down
-
- class Meta:
- get_latest_by = 'endtime'
-
-
class InteractiveManager(models.Manager):
"""Manages interactions objects."""
- def recent_interactions_dict(self, maxdate=None, active_only=True):
- """
- Return the most recent interactions for clients as of a date.
-
- This method uses aggregated queries to return a ValuesQueryDict object.
- Faster then raw sql since this is executed as a single query.
- """
-
- return list(self.values('client').annotate(max_timestamp=Max('timestamp')).values())
-
def interaction_per_client(self, maxdate=None, active_only=True):
"""
Returns the most recent interactions for clients as of a date
@@ -154,15 +130,15 @@ class InteractiveManager(models.Manager):
cursor.execute(sql)
return [item[0] for item in cursor.fetchall()]
except:
- '''FIXME - really need some error hadling'''
+ '''FIXME - really need some error handling'''
pass
return []
class Interaction(models.Model):
"""Models each reconfiguration operation interaction between client and server."""
- client = models.ForeignKey(Client, related_name="interactions",)
- timestamp = models.DateTimeField() # Timestamp for this record
+ client = models.ForeignKey(Client, related_name="interactions")
+ timestamp = models.DateTimeField(db_index=True) # Timestamp for this record
state = models.CharField(max_length=32) # good/bad/modified/etc
repo_rev_code = models.CharField(max_length=64) # repo revision at time of interaction
goodcount = models.IntegerField() # of good config-items
@@ -270,27 +246,47 @@ class Interaction(models.Model):
class Reason(models.Model):
"""reason why modified or bad entry did not verify, or changed."""
- owner = models.TextField(max_length=128, blank=True)
- current_owner = models.TextField(max_length=128, blank=True)
- group = models.TextField(max_length=128, blank=True)
- current_group = models.TextField(max_length=128, blank=True)
- perms = models.TextField(max_length=4, blank=True) # txt fixes typing issue
- current_perms = models.TextField(max_length=4, blank=True)
- status = models.TextField(max_length=3, blank=True) # on/off/(None)
- current_status = models.TextField(max_length=1, blank=True) # on/off/(None)
- to = models.TextField(max_length=256, blank=True)
- current_to = models.TextField(max_length=256, blank=True)
- version = models.TextField(max_length=128, blank=True)
- current_version = models.TextField(max_length=128, blank=True)
+ owner = models.CharField(max_length=255, blank=True)
+ current_owner = models.CharField(max_length=255, blank=True)
+ group = models.CharField(max_length=255, blank=True)
+ current_group = models.CharField(max_length=255, blank=True)
+ perms = models.CharField(max_length=4, blank=True)
+ current_perms = models.CharField(max_length=4, blank=True)
+ status = models.CharField(max_length=128, blank=True)
+ current_status = models.CharField(max_length=128, blank=True)
+ to = models.CharField(max_length=1024, blank=True)
+ current_to = models.CharField(max_length=1024, blank=True)
+ version = models.CharField(max_length=1024, blank=True)
+ current_version = models.CharField(max_length=1024, blank=True)
current_exists = models.BooleanField() # False means its missing. Default True
- current_diff = models.TextField(max_length=1280, blank=True)
+ current_diff = models.TextField(max_length=1024*1024, blank=True)
is_binary = models.BooleanField(default=False)
is_sensitive = models.BooleanField(default=False)
- unpruned = models.TextField(max_length=1280, blank=True)
+ unpruned = models.TextField(max_length=4096, blank=True, default='')
def _str_(self):
return "Reason"
+ def short_list(self):
+ rv = []
+ if self.current_owner or self.current_group or self.current_perms:
+ rv.append("File permissions")
+ if self.current_status:
+ rv.append("Incorrect status")
+ if self.current_to:
+ rv.append("Incorrect target")
+ if self.current_version or self.version == 'auto':
+ rv.append("Wrong version")
+ if not self.current_exists:
+ rv.append("Missing")
+ if self.current_diff or self.is_sensitive:
+ rv.append("Incorrect data")
+ if self.unpruned:
+ rv.append("Directory has extra files")
+ if len(rv) == 0:
+ rv.append("Exists")
+ return rv
+
@staticmethod
@transaction.commit_on_success
def prune_orphans():
@@ -316,6 +312,9 @@ class Entries(models.Model):
cursor.execute('delete from reports_entries where not exists (select rei.id from reports_entries_interactions rei where rei.entry_id = reports_entries.id)')
transaction.set_dirty()
+ class Meta:
+ unique_together = ("name", "kind")
+
class Entries_interactions(models.Model):
"""Define the relation between the reason, the interaction and the entry."""
@@ -343,10 +342,52 @@ class Performance(models.Model):
transaction.set_dirty()
-class InternalDatabaseVersion(models.Model):
- """Object that tell us to witch version is the database."""
- version = models.IntegerField()
- updated = models.DateTimeField(auto_now_add=True)
+class Group(models.Model):
+ """
+ Groups extracted from interactions
+
+ name - The group name
+
+ TODO - Most of this is for future use
+ TODO - set a default group
+ """
+
+ name = models.CharField(max_length=255, unique=True)
+ profile = models.BooleanField(default=False)
+ public = models.BooleanField(default=False)
+ category = models.CharField(max_length=1024, blank=True)
+ comment = models.TextField(blank=True)
+
+ groups = models.ManyToManyField("self", symmetrical=False)
+ bundles = models.ManyToManyField("Bundle")
+
+ def __unicode__(self):
+ return self.name
+
+
+class Bundle(models.Model):
+ """
+ Bundles extracted from interactions
+
+ name - The bundle name
+ """
+
+ name = models.CharField(max_length=255, unique=True)
+
+ def __unicode__(self):
+ return self.name
+
+
+class InteractionMetadata(models.Model):
+ """
+ InteractionMetadata
+
+ Hold extra data associated with the client and interaction
+ """
+
+ interaction = models.OneToOneField(Interaction, primary_key=True, related_name='metadata')
+ profile = models.ForeignKey(Group, related_name="+")
+ groups = models.ManyToManyField(Group)
+ bundles = models.ManyToManyField(Bundle)
+
- def __str__(self):
- return "version %d updated the %s" % (self.version, self.updated.isoformat())
diff --git a/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql b/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql
deleted file mode 100644
index 28e785450..000000000
--- a/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-CREATE VIEW reports_current_interactions AS SELECT x.client_id AS client_id, reports_interaction.id AS interaction_id FROM (select client_id, MAX(timestamp) as timer FROM reports_interaction GROUP BY client_id) x, reports_interaction WHERE reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer;
-
-create index reports_interaction_client_id on reports_interaction (client_id);
-create index reports_client_current_interaction_id on reports_client (current_interaction_id);
-create index reports_performance_interaction_performance_id on reports_performance_interaction (performance_id);
-create index reports_interaction_timestamp on reports_interaction (timestamp);
-create index reports_performance_interation_interaction_id on reports_performance_interaction (interaction_id);
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
index 842de36f0..9a5ef651c 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
@@ -20,6 +20,9 @@ document.write(getCalendarStyles());
{% if not timestamp %}Rendered at {% now "Y-m-d H:i" %} | {% else %}View as of {{ timestamp|date:"Y-m-d H:i" }} | {% endif %}{% spaceless %}
<a id='cal_link' name='cal_link' href='#' onclick='showCalendar(); return false;'
>[change]</a>
- <form method='post' action='{{ path }}' id='cal_form' name='cal_form'><input id='cal_date' name='cal_date' type='hidden' value=''/></form>
+ <form method='post' action='{{ path }}' id='cal_form' name='cal_form'>
+ <input id='cal_date' name='cal_date' type='hidden' value=''/>
+ <input name='op' type='hidden' value='timeview'/>
+ </form>
{% endspaceless %}
{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
index f541c0d2b..3fa482a19 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
@@ -62,6 +62,7 @@
<li>Entries Configured</li>
</ul>
<ul class='menu-level2'>
+ <li><a href="{% url reports_common_problems %}">Common problems</a></li>
<li><a href="{% url reports_item_list "bad" %}">Bad</a></li>
<li><a href="{% url reports_item_list "modified" %}">Modified</a></li>
<li><a href="{% url reports_item_list "extra" %}">Extra</a></li>
@@ -87,7 +88,7 @@
<div style='clear:both'></div>
</div><!-- document -->
<div id="footer">
- <span>Bcfg2 Version 1.2.2</span>
+ <span>Bcfg2 Version 1.2.3</span>
</div>
<div id="calendar_div" style='position:absolute; visibility:hidden; background-color:white; layer-background-color:white;'></div>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
index dd4295f21..9b86b609f 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
@@ -50,6 +50,9 @@ span.history_links a {
{% if interaction.server %}
<tr><td>Served by</td><td>{{interaction.server}}</td></tr>
{% endif %}
+ {% if interaction.metadata %}
+ <tr><td>Profile</td><td>{{interaction.metadata.profile}}</td></tr>
+ {% endif %}
{% if interaction.repo_rev_code %}
<tr><td>Revision</td><td>{{interaction.repo_rev_code}}</td></tr>
{% endif %}
@@ -60,58 +63,57 @@ span.history_links a {
{% endif %}
</table>
- {% if interaction.bad_entry_count %}
+ {% if interaction.metadata.groups.count %}
<div class='entry_list'>
- <div class='entry_list_head dirty-lineitem' onclick='javascript:toggleMe("bad_table");'>
- <h3>Bad Entries &#8212; {{ interaction.bad_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_bad_table'>[+]</div>
+ <div class='entry_list_head' onclick='javascript:toggleMe("groups_table");'>
+ <h3>Group membership</h3>
+ <div class='entry_expand_tab' id='plusminus_groups_table'>[+]</div>
</div>
- <table id='bad_table' class='entry_list'>
- {% for e in interaction.bad|sortwell %}
+ <table id='groups_table' class='entry_list' style='display: none'>
+ {% for group in interaction.metadata.groups.all %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "bad",e.id %}">
- {{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{group}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
- {% if interaction.modified_entry_count %}
+ {% if interaction.metadata.bundles.count %}
<div class='entry_list'>
- <div class='entry_list_head modified-lineitem' onclick='javascript:toggleMe("modified_table");'>
- <h3>Modified Entries &#8212; {{ interaction.modified_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_modified_table'>[+]</div>
+ <div class='entry_list_head' onclick='javascript:toggleMe("bundles_table");'>
+ <h3>Bundle membership</h3>
+ <div class='entry_expand_tab' id='plusminus_bundless_table'>[+]</div>
</div>
- <table id='modified_table' class='entry_list'>
- {% for e in interaction.modified|sortwell %}
+ <table id='bundles_table' class='entry_list' style='display: none'>
+ {% for bundle in interaction.metadata.bundles.all %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "modified",e.id %}">
- {{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{bundle}}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
- {% if interaction.extra_entry_count %}
+ {% for type, ei_list in ei_lists %}
+ {% if ei_list %}
<div class='entry_list'>
- <div class='entry_list_head extra-lineitem' onclick='javascript:toggleMe("extra_table");'>
- <h3>Extra Entries &#8212; {{ interaction.extra_entry_count }}</h3>
- <div class='entry_expand_tab' id='plusminus_extra_table'>[+]</div>
+ <div class='entry_list_head {{type}}-lineitem' onclick='javascript:toggleMe("{{type}}_table");'>
+ <h3>{{ type|capfirst }} Entries &#8212; {{ ei_list|length }}</h3>
+ <div class='entry_expand_tab' id='plusminus_{{type}}_table'>[+]</div>
</div>
- <table id='extra_table' class='entry_list'>
- {% for e in interaction.extra|sortwell %}
+ <table id='{{type}}_table' class='entry_list'>
+ {% for ei in ei_list %}
<tr class='{% cycle listview,listview_alt %}'>
- <td class='entry_list_type'>{{e.entry.kind}}:</td>
- <td><a href="{% url reports_item "extra",e.id %}">{{e.entry.name}}</a></td>
+ <td class='entry_list_type'>{{ei.entry.kind}}</td>
+ <td><a href="{% url reports_item type ei.id %}">
+ {{ei.entry.name}}</a></td>
</tr>
- {% endfor %}
+ {% endfor %}
</table>
</div>
{% endif %}
+ {% endfor %}
{% if entry_list %}
<div class="entry_list recent_history_wrapper">
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
index 84ac71d92..9be59e7d2 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
@@ -6,18 +6,18 @@
{% block content %}
<div class='client_list_box'>
-{% if entry_list %}
{% filter_navigator %}
+{% if entry_list %}
<table cellpadding="3">
<tr id='table_list_header' class='listview'>
- <td class='left_column'>Node</td>
- <td class='right_column' style='width:75px'>State</td>
- <td class='right_column_narrow'>Good</td>
- <td class='right_column_narrow'>Bad</td>
- <td class='right_column_narrow'>Modified</td>
- <td class='right_column_narrow'>Extra</td>
- <td class='right_column'>Last Run</td>
- <td class='right_column_wide'>Server</td>
+ <td class='left_column'>{% sort_link 'client' 'Node' %}</td>
+ <td class='right_column' style='width:75px'>{% sort_link 'state' 'State' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-good' 'Good' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-bad' 'Bad' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-modified' 'Modified' %}</td>
+ <td class='right_column_narrow'>{% sort_link '-extra' 'Extra' %}</td>
+ <td class='right_column'>{% sort_link 'timestamp' 'Last Run' %}</td>
+ <td class='right_column_wide'>{% sort_link 'server' 'Server' %}</td>
</tr>
{% for entry in entry_list %}
<tr class='{% cycle listview,listview_alt %}'>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
index 134e237d6..45ba20b86 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
@@ -9,6 +9,7 @@
{% block pagebanner %}Clients - Grid View{% endblock %}
{% block content %}
+{% filter_navigator %}
{% if inter_list %}
<table class='grid-view' align='center'>
{% for inter in inter_list %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html
index 5725ae577..443ec8ccb 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html
@@ -38,8 +38,8 @@
</tr>
{% endfor %}
</table>
- </div>
{% else %}
<p>No client records are available.</p>
{% endif %}
+ </div>
{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html
new file mode 100644
index 000000000..d6ad303fc
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/common.html
@@ -0,0 +1,42 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Common Problems{% endblock %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block pagebanner %}Common configuration problems{% endblock %}
+
+{% block content %}
+ <div id='threshold_box'>
+ <form method='post' action='{{ request.path }}'>
+ <span>Showing items with more then {{ threshold }} entries</span>
+ <input type='text' name='threshold' value='{{ threshold }}' maxlength='5' size='5' />
+ <input type='submit' value='Change' />
+ </form>
+ </div>
+ {% for type_name, type_list in lists %}
+ <div class='entry_list'>
+ <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ type_name }}");'>
+ <h3>{{ type_name|capfirst }} entries</h3>
+ <div class='entry_expand_tab' id='plusminus_table_{{ type_name }}'>[&ndash;]</div>
+ </div>
+ {% if type_list %}
+ <table id='table_{{ type_name }}' class='entry_list'>
+ <tr style='text-align: left'><th>Type</th><th>Name</th><th>Count</th><th>Reason</th></tr>
+ {% for entry, reason, interaction in type_list %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td>{{ entry.kind }}</td>
+ <td><a href="{% url reports_entry eid=entry.pk %}">{{ entry.name }}</a></td>
+ <td>{{ interaction|length }}</td>
+ <td><a href="{% url reports_item type=type_name pk=interaction.0 %}">{{ reason.short_list|join:"," }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>There are currently no inconsistent {{ type_name }} configuration entries.</p>
+ {% endif %}
+ </div>
+ {% endfor %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html
new file mode 100644
index 000000000..5f7579eb9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/entry_status.html
@@ -0,0 +1,30 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Entry Status{% endblock %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block pagebanner %}{{ entry.kind }} entry {{ entry.name }} status{% endblock %}
+
+{% block content %}
+{% filter_navigator %}
+{% if item_data %}
+ <div class='entry_list'>
+ <table class='entry_list'>
+ <tr style='text-align: left' ><th>Name</th><th>Timestamp</th><th>State</th><th>Reason</th></tr>
+ {% for ei, inter, reason in item_data %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=inter.client.name, pk=inter.id %}'>{{ inter.client.name }}</a></td>
+ <td style='white-space: nowrap'><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=inter.client.name, pk=inter.id %}'>{{ inter.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</a></td>
+ <td>{{ ei.get_type_display }}</td>
+ <td style='white-space: nowrap'><a href="{% url reports_item type=ei.get_type_display pk=ei.pk %}">{{ reason.short_list|join:"," }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+{% else %}
+ <p>There are currently no hosts with this configuration entry.</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
index 9b1026a08..0a92e7fc0 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
@@ -9,19 +9,21 @@
{% block pagebanner %}{{mod_or_bad|capfirst}} Element Listing{% endblock %}
{% block content %}
-{% if item_list_dict %}
- {% for kind, entries in item_list_dict.items %}
-
+{% filter_navigator %}
+{% if item_list %}
+ {% for type_name, type_data in item_list %}
<div class='entry_list'>
- <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ kind }}");'>
- <h3>{{ kind }} &#8212; {{ entries|length }}</h3>
- <div class='entry_expand_tab' id='plusminus_table_{{ kind }}'>[&ndash;]</div>
+ <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ type_name }}");'>
+ <h3>{{ type_name }} &#8212; {{ type_data|length }}</h3>
+ <div class='entry_expand_tab' id='plusminus_table_{{ type_name }}'>[&ndash;]</div>
</div>
-
- <table id='table_{{ kind }}' class='entry_list'>
- {% for e in entries %}
+ <table id='table_{{ type_name }}' class='entry_list'>
+ <tr style='text-align: left' ><th>Name</th><th>Count</th><th>Reason</th></tr>
+ {% for entry, reason, eis in type_data %}
<tr class='{% cycle listview,listview_alt %}'>
- <td><a href="{% url reports_item type=mod_or_bad,pk=e.id %}">{{e.entry.name}}</a></td>
+ <td><a href="{% url reports_entry eid=entry.pk %}">{{entry.name}}</a></td>
+ <td>{{ eis|length }}</td>
+ <td><a href="{% url reports_item type=mod_or_bad,pk=eis.0 %}">{{ reason.short_list|join:"," }}</a></td>
</tr>
{% endfor %}
</table>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
index 6fbe585ab..759415507 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
@@ -1,13 +1,25 @@
{% spaceless %}
+<div class="filter_bar">
+<form name='filter_form'>
{% if filters %}
{% for filter, filter_url in filters %}
{% if forloop.first %}
- <div class="filter_bar">Active filters (click to remove):
+ Active filters (click to remove):
{% endif %}
<a href='{{ filter_url }}'>{{ filter|capfirst }}</a>{% if not forloop.last %}, {% endif %}
{% if forloop.last %}
- </div>
+ {% if groups %}|{% endif %}
{% endif %}
{% endfor %}
{% endif %}
+{% if groups %}
+<label for="id_group">Group filter:</label>
+<select id="id_group" name="group" onchange="javascript:url=document.forms['filter_form'].group.value; if(url) { location.href=url }">
+ {% for group, group_url, selected in groups %}
+ <option label="{{group}}" value="{{group_url}}" {% if selected %}selected {% endif %}/>
+ {% endfor %}
+</select>
+{% endif %}
+</form>
+</div>
{% endspaceless %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
index ac63cda3e..894353bba 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
@@ -1,11 +1,17 @@
import sys
+from copy import copy
from django import template
+from django.conf import settings
from django.core.urlresolvers import resolve, reverse, \
Resolver404, NoReverseMatch
+from django.template.loader import get_template, \
+ get_template_from_string,TemplateDoesNotExist
from django.utils.encoding import smart_unicode, smart_str
+from django.utils.safestring import mark_safe
from datetime import datetime, timedelta
from Bcfg2.Server.Reports.utils import filter_list
+from Bcfg2.Server.Reports.reports.models import Group
register = template.Library()
@@ -115,13 +121,27 @@ def filter_navigator(context):
filters = []
for filter in filter_list:
+ if filter == 'group':
+ continue
if filter in kwargs:
myargs = kwargs.copy()
del myargs[filter]
filters.append((filter,
reverse(view, args=args, kwargs=myargs)))
filters.sort(lambda x, y: cmp(x[0], y[0]))
- return {'filters': filters}
+
+ myargs = kwargs.copy()
+ selected=True
+ if 'group' in myargs:
+ del myargs['group']
+ selected=False
+ groups = [('---', reverse(view, args=args, kwargs=myargs), selected)]
+ for group in Group.objects.values('name'):
+ myargs['group'] = group['name']
+ groups.append((group['name'], reverse(view, args=args, kwargs=myargs),
+ group['name'] == kwargs.get('group', '')))
+
+ return {'filters': filters, 'groups': groups}
except (Resolver404, NoReverseMatch, ValueError, KeyError):
pass
return dict()
@@ -242,19 +262,6 @@ def add_url_filter(parser, token):
return AddUrlFilter(filter_name, filter_value)
-@register.filter
-def sortwell(value):
- """
- Sorts a list(or evaluates queryset to list) of bad, extra, or modified items in the best
- way for presentation
- """
-
- configItems = list(value)
- configItems.sort(lambda x, y: cmp(x.entry.name, y.entry.name))
- configItems.sort(lambda x, y: cmp(x.entry.kind, y.entry.kind))
- return configItems
-
-
class MediaTag(template.Node):
def __init__(self, filter_value):
self.filter_value = filter_value
@@ -311,3 +318,98 @@ def determine_client_state(entry):
else:
thisdirty = "very-dirty-lineitem"
return thisdirty
+
+
+@register.tag(name='qs')
+def do_qs(parser, token):
+ """
+ qs tag
+
+ accepts a name value pair and inserts or replaces it in the query string
+ """
+ try:
+ tag, name, value = token.split_contents()
+ except ValueError:
+ raise TemplateSyntaxError, "%r tag requires exactly two arguments" \
+ % token.contents.split()[0]
+ return QsNode(name, value)
+
+class QsNode(template.Node):
+ def __init__(self, name, value):
+ self.name = template.Variable(name)
+ self.value = template.Variable(value)
+
+ def render(self, context):
+ try:
+ name = self.name.resolve(context)
+ value = self.value.resolve(context)
+ request = context['request']
+ qs = copy(request.GET)
+ qs[name] = value
+ return "?%s" % qs.urlencode()
+ except template.VariableDoesNotExist:
+ return ''
+ except KeyError:
+ if settings.TEMPLATE_DEBUG:
+ raise Exception, "'qs' tag requires context['request']"
+ return ''
+ except:
+ return ''
+
+
+@register.tag
+def sort_link(parser, token):
+ '''
+ Create a sort anchor tag. Reverse it if active.
+
+ {% sort_link sort_key text %}
+ '''
+ try:
+ tag, sort_key, text = token.split_contents()
+ except ValueError:
+ raise TemplateSyntaxError("%r tag requires at least four arguments" \
+ % token.split_contents()[0])
+
+ return SortLinkNode(sort_key, text)
+
+class SortLinkNode(template.Node):
+ __TMPL__ = "{% load bcfg2_tags %}<a href='{% qs 'sort' key %}'>{{ text }}</a>"
+
+ def __init__(self, sort_key, text):
+ self.sort_key = template.Variable(sort_key)
+ self.text = template.Variable(text)
+
+ def render(self, context):
+ try:
+ try:
+ sort = context['request'].GET['sort']
+ except KeyError:
+ #fall back on this
+ sort = context.get('sort', '')
+ sort_key = self.sort_key.resolve(context)
+ text = self.text.resolve(context)
+
+ # add arrows
+ try:
+ sort_base = sort_key.lstrip('-')
+ if sort[0] == '-' and sort[1:] == sort_base:
+ text = text + '&#x25BC;'
+ sort_key = sort_base
+ elif sort_base == sort:
+ text = text + '&#x25B2;'
+ sort_key = '-' + sort_base
+ except IndexError:
+ pass
+
+ context.push()
+ context['key'] = sort_key
+ context['text'] = mark_safe(text)
+ output = get_template_from_string(self.__TMPL__).render(context)
+ context.pop()
+ return output
+ except:
+ if settings.DEBUG:
+ raise
+ raise
+ return ''
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
index 36d4cf693..0d4c6501d 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
@@ -4,6 +4,8 @@ from django.utils.encoding import smart_unicode
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
+from Bcfg2.Bcfg2Py3k import u_str
+
register = template.Library()
try:
@@ -16,14 +18,6 @@ except:
colorize = False
-# py3k compatibility
-def u_str(string):
- if sys.hexversion >= 0x03000000:
- return string
- else:
- return unicode(string)
-
-
@register.filter
def syntaxhilight(value, arg="diff", autoescape=None):
"""
diff --git a/src/lib/Bcfg2/Server/Reports/reports/urls.py b/src/lib/Bcfg2/Server/Reports/reports/urls.py
index 434ce07b7..1cfe725c2 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/urls.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/urls.py
@@ -17,20 +17,23 @@ urlpatterns = patterns('Bcfg2.Server.Reports.reports',
url(r'^client/(?P<hostname>[^/]+)/(?P<pk>\d+)/?$', 'views.client_detail', name='reports_client_detail_pk'),
url(r'^client/(?P<hostname>[^/]+)/?$', 'views.client_detail', name='reports_client_detail'),
url(r'^elements/(?P<type>\w+)/(?P<pk>\d+)/?$', 'views.config_item', name='reports_item'),
+ url(r'^entry/(?P<eid>\w+)/?$', 'views.entry_status', name='reports_entry'),
)
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
*timeviewUrls(
- (r'^grid/?$', 'views.client_index', None, 'reports_grid_view'),
(r'^summary/?$', 'views.display_summary', None, 'reports_summary'),
(r'^timing/?$', 'views.display_timing', None, 'reports_timing'),
- (r'^elements/(?P<type>\w+)/?$', 'views.config_item_list', None, 'reports_item_list'),
+ (r'^common/(?P<threshold>\d+)/?$', 'views.common_problems', None, 'reports_common_problems'),
+ (r'^common/?$', 'views.common_problems', None, 'reports_common_problems'),
))
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
*filteredUrls(*timeviewUrls(
+ (r'^grid/?$', 'views.client_index', None, 'reports_grid_view'),
(r'^detailed/?$',
- 'views.client_detailed_list', None, 'reports_detailed_list')
+ 'views.client_detailed_list', None, 'reports_detailed_list'),
+ (r'^elements/(?P<type>\w+)/?$', 'views.config_item_list', None, 'reports_item_list'),
)))
urlpatterns += patterns('Bcfg2.Server.Reports.reports',
diff --git a/src/lib/Bcfg2/Server/Reports/reports/views.py b/src/lib/Bcfg2/Server/Reports/reports/views.py
index ccd71a60e..e4c38363f 100644
--- a/src/lib/Bcfg2/Server/Reports/reports/views.py
+++ b/src/lib/Bcfg2/Server/Reports/reports/views.py
@@ -13,16 +13,41 @@ from django.http import \
from django.shortcuts import render_to_response, get_object_or_404
from django.core.urlresolvers import \
resolve, reverse, Resolver404, NoReverseMatch
-from django.db import connection
+from django.db import connection, DatabaseError
+from django.db.models import Q
from Bcfg2.Server.Reports.reports.models import *
+__SORT_FIELDS__ = ( 'client', 'state', 'good', 'bad', 'modified', 'extra', \
+ 'timestamp', 'server' )
+
class PaginationError(Exception):
"""This error is raised when pagination cannot be completed."""
pass
+def _in_bulk(model, ids):
+ """
+ Short cut to fetch in bulk and trap database errors. sqlite will raise
+ a "too many SQL variables" exception if this list is too long. Try using
+ django and fetch manually if an error occurs
+
+ returns a dict of this form { id: <model instance> }
+ """
+
+ try:
+ return model.objects.in_bulk(ids)
+ except DatabaseError:
+ pass
+
+ # if objects.in_bulk fails so will obejcts.filter(pk__in=ids)
+ bulk_dict = {}
+ [bulk_dict.__setitem__(i.id, i) \
+ for i in model.objects.all() if i.id in ids]
+ return bulk_dict
+
+
def server_error(request):
"""
500 error handler.
@@ -44,7 +69,7 @@ def timeview(fn):
"""
def _handle_timeview(request, **kwargs):
"""Send any posts back."""
- if request.method == 'POST':
+ if request.method == 'POST' and request.POST.get('op', '') == 'timeview':
cal_date = request.POST['cal_date']
try:
fmt = "%Y/%m/%d"
@@ -84,6 +109,30 @@ def timeview(fn):
return _handle_timeview
+def _handle_filters(query, **kwargs):
+ """
+ Applies standard filters to a query object
+
+ Returns an updated query object
+
+ query - query object to filter
+
+ server -- Filter interactions by server
+ state -- Filter interactions by state
+ group -- Filter interactions by group
+
+ """
+ if 'state' in kwargs and kwargs['state']:
+ query = query.filter(state__exact=kwargs['state'])
+ if 'server' in kwargs and kwargs['server']:
+ query = query.filter(server__exact=kwargs['server'])
+
+ if 'group' in kwargs and kwargs['group']:
+ group = get_object_or_404(Group, name=kwargs['group'])
+ query = query.filter(metadata__groups__id=group.pk)
+ return query
+
+
def config_item(request, pk, type="bad"):
"""
Display a single entry.
@@ -121,47 +170,138 @@ def config_item(request, pk, type="bad"):
@timeview
-def config_item_list(request, type, timestamp=None):
+def config_item_list(request, type, timestamp=None, **kwargs):
"""Render a listing of affected elements"""
mod_or_bad = type.lower()
type = convert_entry_type_to_id(type)
if type < 0:
raise Http404
- current_clients = Interaction.objects.get_interaction_per_client_ids(timestamp)
- item_list_dict = {}
- seen = dict()
- for x in Entries_interactions.objects.filter(interaction__in=current_clients,
- type=type).select_related():
- if (x.entry, x.reason) in seen:
- continue
- seen[(x.entry, x.reason)] = 1
- if item_list_dict.get(x.entry.kind, None):
- item_list_dict[x.entry.kind].append(x)
- else:
- item_list_dict[x.entry.kind] = [x]
+ current_clients = Interaction.objects.interaction_per_client(timestamp)
+ current_clients = [q['id'] for q in _handle_filters(current_clients, **kwargs).values('id')]
+
+ ldata = list(Entries_interactions.objects.filter(
+ interaction__in=current_clients, type=type).values())
+ entry_ids = set([x['entry_id'] for x in ldata])
+ reason_ids = set([x['reason_id'] for x in ldata])
- for kind in item_list_dict:
- item_list_dict[kind].sort(lambda a, b: cmp(a.entry.name, b.entry.name))
+ entries = _in_bulk(Entries, entry_ids)
+ reasons = _in_bulk(Reason, reason_ids)
+
+ kind_list = {}
+ [kind_list.__setitem__(kind, {}) for kind in set([e.kind for e in entries.values()])]
+ for x in ldata:
+ kind = entries[x['entry_id']].kind
+ data_key = (x['entry_id'], x['reason_id'])
+ try:
+ kind_list[kind][data_key].append(x['id'])
+ except KeyError:
+ kind_list[kind][data_key] = [x['id']]
+
+ lists = []
+ for kind in kind_list.keys():
+ lists.append((kind, [(entries[e[0][0]], reasons[e[0][1]], e[1])
+ for e in sorted(kind_list[kind].iteritems(), key=lambda x: entries[x[0][0]].name)]))
return render_to_response('config_items/listing.html',
- {'item_list_dict': item_list_dict,
+ {'item_list': lists,
'mod_or_bad': mod_or_bad,
'timestamp': timestamp},
context_instance=RequestContext(request))
@timeview
-def client_index(request, timestamp=None):
+def entry_status(request, eid, timestamp=None, **kwargs):
+ """Render a listing of affected elements"""
+ entry = get_object_or_404(Entries, pk=eid)
+
+ current_clients = Interaction.objects.interaction_per_client(timestamp)
+ inters = {}
+ [inters.__setitem__(i.id, i) \
+ for i in _handle_filters(current_clients, **kwargs).select_related('client')]
+
+ eis = Entries_interactions.objects.filter(
+ interaction__in=inters.keys(), entry=entry)
+
+ reasons = _in_bulk(Reason, set([x.reason_id for x in eis]))
+
+ item_data = []
+ for ei in eis:
+ item_data.append((ei, inters[ei.interaction_id], reasons[ei.reason_id]))
+
+ return render_to_response('config_items/entry_status.html',
+ {'entry': entry,
+ 'item_data': item_data,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def common_problems(request, timestamp=None, threshold=None):
+ """Mine config entries"""
+
+ if request.method == 'POST':
+ try:
+ threshold = int(request.POST['threshold'])
+ view, args, kw = resolve(request.META['PATH_INFO'])
+ kw['threshold'] = threshold
+ return HttpResponseRedirect(reverse(view,
+ args=args,
+ kwargs=kw))
+ except:
+ pass
+
+ try:
+ threshold = int(threshold)
+ except:
+ threshold = 10
+
+ c_intr = Interaction.objects.get_interaction_per_client_ids(timestamp)
+ data_list = {}
+ [data_list.__setitem__(t_id, {}) \
+ for t_id, t_label in TYPE_CHOICES if t_id != TYPE_GOOD]
+ ldata = list(Entries_interactions.objects.filter(
+ interaction__in=c_intr).exclude(type=TYPE_GOOD).values())
+
+ entry_ids = set([x['entry_id'] for x in ldata])
+ reason_ids = set([x['reason_id'] for x in ldata])
+ for x in ldata:
+ type = x['type']
+ data_key = (x['entry_id'], x['reason_id'])
+ try:
+ data_list[type][data_key].append(x['id'])
+ except KeyError:
+ data_list[type][data_key] = [x['id']]
+
+ entries = _in_bulk(Entries, entry_ids)
+ reasons = _in_bulk(Reason, reason_ids)
+
+ lists = []
+ for type, type_name in TYPE_CHOICES:
+ if type == TYPE_GOOD:
+ continue
+ lists.append([type_name.lower(), [(entries[e[0][0]], reasons[e[0][1]], e[1])
+ for e in sorted(data_list[type].items(), key=lambda x: len(x[1]), reverse=True)
+ if len(e[1]) > threshold]])
+
+ return render_to_response('config_items/common.html',
+ {'lists': lists,
+ 'timestamp': timestamp,
+ 'threshold': threshold},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def client_index(request, timestamp=None, **kwargs):
"""
Render a grid view of active clients.
Keyword parameters:
- timestamp -- datetime objectto render from
+ timestamp -- datetime object to render from
"""
- list = Interaction.objects.interaction_per_client(timestamp).select_related()\
- .order_by("client__name").all()
+ list = _handle_filters(Interaction.objects.interaction_per_client(timestamp), **kwargs).\
+ select_related().order_by("client__name").all()
return render_to_response('clients/index.html',
{'inter_list': list,
@@ -177,8 +317,29 @@ def client_detailed_list(request, timestamp=None, **kwargs):
"""
+ try:
+ sort = request.GET['sort']
+ if sort[0] == '-':
+ sort_key = sort[1:]
+ else:
+ sort_key = sort
+ if not sort_key in __SORT_FIELDS__:
+ raise ValueError
+
+ if sort_key == "client":
+ kwargs['orderby'] = "%s__name" % sort
+ elif sort_key == "good":
+ kwargs['orderby'] = "%scount" % sort
+ elif sort_key in ["bad", "modified", "extra"]:
+ kwargs['orderby'] = "%s_entries" % sort
+ else:
+ kwargs['orderby'] = sort
+ kwargs['sort'] = sort
+ except (ValueError, KeyError):
+ kwargs['orderby'] = "client__name"
+ kwargs['sort'] = "client"
+
kwargs['interaction_base'] = Interaction.objects.interaction_per_client(timestamp).select_related()
- kwargs['orderby'] = "client__name"
kwargs['page_limit'] = 0
return render_history_view(request, 'clients/detailed-list.html', **kwargs)
@@ -187,13 +348,25 @@ def client_detail(request, hostname=None, pk=None):
context = dict()
client = get_object_or_404(Client, name=hostname)
if(pk == None):
- context['interaction'] = client.current_interaction
- return render_history_view(request, 'clients/detail.html', page_limit=5,
- client=client, context=context)
+ inter = client.current_interaction
+ maxdate = None
else:
- context['interaction'] = client.interactions.get(pk=pk)
- return render_history_view(request, 'clients/detail.html', page_limit=5,
- client=client, maxdate=context['interaction'].timestamp, context=context)
+ inter = client.interactions.get(pk=pk)
+ maxdate = inter.timestamp
+
+ ei = Entries_interactions.objects.filter(interaction=inter).select_related('entry').order_by('entry__kind', 'entry__name')
+ #ei = Entries_interactions.objects.filter(interaction=inter).select_related('entry')
+ #ei = sorted(Entries_interactions.objects.filter(interaction=inter).select_related('entry'),
+ # key=lambda x: (x.entry.kind, x.entry.name))
+ context['ei_lists'] = (
+ ('bad', [x for x in ei if x.type == TYPE_BAD]),
+ ('modified', [x for x in ei if x.type == TYPE_MODIFIED]),
+ ('extra', [x for x in ei if x.type == TYPE_EXTRA])
+ )
+
+ context['interaction']=inter
+ return render_history_view(request, 'clients/detail.html', page_limit=5,
+ client=client, maxdate=maxdate, context=context)
def client_manage(request):
@@ -230,9 +403,9 @@ def display_summary(request, timestamp=None):
"""
Display a summary of the bcfg2 world
"""
- query = Interaction.objects.interaction_per_client(timestamp).select_related()
- node_count = query.count()
- recent_data = query.all()
+ recent_data = Interaction.objects.interaction_per_client(timestamp) \
+ .select_related().all()
+ node_count = len(recent_data)
if not timestamp:
timestamp = datetime.now()
@@ -240,18 +413,11 @@ def display_summary(request, timestamp=None):
bad=[],
modified=[],
extra=[],
- stale=[],
- pings=[])
+ stale=[])
for node in recent_data:
if timestamp - node.timestamp > timedelta(hours=24):
collected_data['stale'].append(node)
# If stale check for uptime
- try:
- if node.client.pings.latest().status == 'N':
- collected_data['pings'].append(node)
- except Ping.DoesNotExist:
- collected_data['pings'].append(node)
- continue
if node.bad_entry_count() > 0:
collected_data['bad'].append(node)
else:
@@ -281,9 +447,6 @@ def display_summary(request, timestamp=None):
if len(collected_data['stale']) > 0:
summary_data.append(get_dict('stale',
'nodes did not run within the last 24 hours.'))
- if len(collected_data['pings']) > 0:
- summary_data.append(get_dict('pings',
- 'are down.'))
return render_to_response('displays/summary.html',
{'summary_data': summary_data, 'node_count': node_count,
@@ -299,7 +462,11 @@ def display_timing(request, timestamp=None):
for inter in inters]
for metric in Performance.objects.filter(interaction__in=list(mdict.keys())).all():
for i in metric.interaction.all():
- mdict[i][metric.metric] = metric.value
+ try:
+ mdict[i][metric.metric] = metric.value
+ except KeyError:
+ #In the unlikely event two interactions share a metric, ignore it
+ pass
return render_to_response('displays/timing.html',
{'metrics': list(mdict.values()),
'timestamp': timestamp},
@@ -324,6 +491,7 @@ def render_history_view(request, template='clients/history.html', **kwargs):
not found
server -- Filter interactions by server
state -- Filter interactions by state
+ group -- Filter interactions by group
entry_max -- Most recent interaction to display
orderby -- Sort results using this field
@@ -345,15 +513,15 @@ def render_history_view(request, template='clients/history.html', **kwargs):
# Either filter by client or limit by clients
iquery = kwargs.get('interaction_base', Interaction.objects)
if client:
- iquery = iquery.filter(client__exact=client).select_related()
+ iquery = iquery.filter(client__exact=client)
+ iquery = iquery.select_related()
if 'orderby' in kwargs and kwargs['orderby']:
iquery = iquery.order_by(kwargs['orderby'])
+ if 'sort' in kwargs:
+ context['sort'] = kwargs['sort']
- if 'state' in kwargs and kwargs['state']:
- iquery = iquery.filter(state__exact=kwargs['state'])
- if 'server' in kwargs and kwargs['server']:
- iquery = iquery.filter(server__exact=kwargs['server'])
+ iquery = _handle_filters(iquery, **kwargs)
if entry_max:
iquery = iquery.filter(timestamp__lte=entry_max)
diff --git a/src/lib/Bcfg2/Server/Reports/settings.py b/src/lib/Bcfg2/Server/Reports/settings.py
deleted file mode 100644
index 4d567f1a2..000000000
--- a/src/lib/Bcfg2/Server/Reports/settings.py
+++ /dev/null
@@ -1,161 +0,0 @@
-import django
-import sys
-
-# Compatibility import
-from Bcfg2.Bcfg2Py3k import ConfigParser
-# Django settings for bcfg2 reports project.
-c = ConfigParser.ConfigParser()
-if len(c.read(['/etc/bcfg2.conf', '/etc/bcfg2-web.conf'])) == 0:
- raise ImportError("Please check that bcfg2.conf or bcfg2-web.conf exists "
- "and is readable by your web server.")
-
-try:
- DEBUG = c.getboolean('statistics', 'web_debug')
-except:
- DEBUG = False
-
-if DEBUG:
- print("Warning: Setting web_debug to True causes extraordinary memory "
- "leaks. Only use this setting if you know what you're doing.")
-
-TEMPLATE_DEBUG = DEBUG
-
-ADMINS = (
- ('Root', 'root'),
-)
-
-MANAGERS = ADMINS
-try:
- db_engine = c.get('statistics', 'database_engine')
-except ConfigParser.NoSectionError:
- e = sys.exc_info()[1]
- raise ImportError("Failed to determine database engine: %s" % e)
-db_name = ''
-if c.has_option('statistics', 'database_name'):
- db_name = c.get('statistics', 'database_name')
-if db_engine == 'sqlite3' and db_name == '':
- db_name = "%s/etc/brpt.sqlite" % c.get('server', 'repository')
-
-DATABASES = {
- 'default': {
- 'ENGINE': "django.db.backends.%s" % db_engine,
- 'NAME': db_name
- }
-}
-
-if db_engine != 'sqlite3':
- DATABASES['default']['USER'] = c.get('statistics', 'database_user')
- DATABASES['default']['PASSWORD'] = c.get('statistics', 'database_password')
- DATABASES['default']['HOST'] = c.get('statistics', 'database_host')
- try:
- DATABASES['default']['PORT'] = c.get('statistics', 'database_port')
- except: # An empty string tells Django to use the default port.
- DATABASES['default']['PORT'] = ''
-
-if django.VERSION[0] == 1 and django.VERSION[1] < 2:
- DATABASE_ENGINE = db_engine
- DATABASE_NAME = DATABASES['default']['NAME']
- if DATABASE_ENGINE != 'sqlite3':
- DATABASE_USER = DATABASES['default']['USER']
- DATABASE_PASSWORD = DATABASES['default']['PASSWORD']
- DATABASE_HOST = DATABASES['default']['HOST']
- DATABASE_PORT = DATABASES['default']['PORT']
-
-
-# Local time zone for this installation. All choices can be found here:
-# http://docs.djangoproject.com/en/dev/ref/settings/#time-zone
-if django.VERSION[0] == 1 and django.VERSION[1] > 2:
- try:
- TIME_ZONE = c.get('statistics', 'time_zone')
- except:
- TIME_ZONE = None
-
-# Language code for this installation. All choices can be found here:
-# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
-# http://blogs.law.harvard.edu/tech/stories/storyReader$15
-LANGUAGE_CODE = 'en-us'
-
-SITE_ID = 1
-
-# Absolute path to the directory that holds media.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
-
-# URL that handles the media served from MEDIA_ROOT.
-# Example: "http://media.lawrence.com"
-MEDIA_URL = '/site_media'
-if c.has_option('statistics', 'web_prefix'):
- MEDIA_URL = c.get('statistics', 'web_prefix').rstrip('/') + MEDIA_URL
-
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
-# trailing slash.
-# Examples: "http://foo.com/media/", "/media/".
-ADMIN_MEDIA_PREFIX = '/media/'
-
-# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'eb5+y%oy-qx*2+62vv=gtnnxg1yig_odu0se5$h0hh#pc*lmo7'
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.load_template_source',
- 'django.template.loaders.app_directories.load_template_source',
-)
-
-MIDDLEWARE_CLASSES = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.middleware.doc.XViewMiddleware',
-)
-
-ROOT_URLCONF = 'Bcfg2.Server.Reports.urls'
-
-# Authentication Settings
-# Use NIS authentication backend defined in backends.py
-AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',
- 'Bcfg2.Server.Reports.backends.NISBackend')
-# The NIS group authorized to login to BCFG2's reportinvg system
-AUTHORIZED_GROUP = ''
-#create login url area:
-try:
- import django.contrib.auth
-except ImportError:
- raise ImportError('Import of Django module failed. Is Django installed?')
-django.contrib.auth.LOGIN_URL = '/login'
-
-SESSION_EXPIRE_AT_BROWSER_CLOSE = True
-
-
-
-TEMPLATE_DIRS = (
- # Put strings here, like "/home/html/django_templates".
- # Always use forward slashes, even on Windows.
- '/usr/share/python-support/python-django/django/contrib/admin/templates/',
- 'Bcfg2.Server.Reports.reports'
-)
-
-if django.VERSION[0] == 1 and django.VERSION[1] < 2:
- TEMPLATE_CONTEXT_PROCESSORS = (
- 'django.core.context_processors.auth',
- 'django.core.context_processors.debug',
- 'django.core.context_processors.i18n',
- 'django.core.context_processors.media',
- 'django.core.context_processors.request'
- )
-else:
- TEMPLATE_CONTEXT_PROCESSORS = (
- 'django.contrib.auth.context_processors.auth',
- 'django.core.context_processors.debug',
- 'django.core.context_processors.i18n',
- 'django.core.context_processors.media',
- 'django.core.context_processors.request'
- )
-
-INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.admin',
- 'Bcfg2.Server.Reports.reports'
-)
diff --git a/src/lib/Bcfg2/Server/Reports/updatefix.py b/src/lib/Bcfg2/Server/Reports/updatefix.py
deleted file mode 100644
index 192b94b61..000000000
--- a/src/lib/Bcfg2/Server/Reports/updatefix.py
+++ /dev/null
@@ -1,281 +0,0 @@
-import Bcfg2.Server.Reports.settings
-
-from django.db import connection, DatabaseError
-import django.core.management
-import logging
-import sys
-import traceback
-from Bcfg2.Server.Reports.reports.models import InternalDatabaseVersion, \
- TYPE_BAD, TYPE_MODIFIED, TYPE_EXTRA
-logger = logging.getLogger('Bcfg2.Server.Reports.UpdateFix')
-
-
-# all update function should go here
-def _merge_database_table_entries():
- cursor = connection.cursor()
- insert_cursor = connection.cursor()
- find_cursor = connection.cursor()
- cursor.execute("""
- Select name, kind from reports_bad
- union
- select name, kind from reports_modified
- union
- select name, kind from reports_extra
- """)
- # this fetch could be better done
- entries_map = {}
- for row in cursor.fetchall():
- insert_cursor.execute("insert into reports_entries (name, kind) \
- values (%s, %s)", (row[0], row[1]))
- entries_map[(row[0], row[1])] = insert_cursor.lastrowid
-
- cursor.execute("""
- Select name, kind, reason_id, interaction_id, 1 from reports_bad
- inner join reports_bad_interactions on reports_bad.id=reports_bad_interactions.bad_id
- union
- Select name, kind, reason_id, interaction_id, 2 from reports_modified
- inner join reports_modified_interactions on reports_modified.id=reports_modified_interactions.modified_id
- union
- Select name, kind, reason_id, interaction_id, 3 from reports_extra
- inner join reports_extra_interactions on reports_extra.id=reports_extra_interactions.extra_id
- """)
- for row in cursor.fetchall():
- key = (row[0], row[1])
- if entries_map.get(key, None):
- entry_id = entries_map[key]
- else:
- find_cursor.execute("Select id from reports_entries where name=%s and kind=%s", key)
- rowe = find_cursor.fetchone()
- entry_id = rowe[0]
- insert_cursor.execute("insert into reports_entries_interactions \
- (entry_id, interaction_id, reason_id, type) values (%s, %s, %s, %s)", (entry_id, row[3], row[2], row[4]))
-
-
-def _interactions_constraint_or_idx():
- """sqlite doesn't support alter tables.. or constraints"""
- cursor = connection.cursor()
- try:
- cursor.execute('alter table reports_interaction add constraint reports_interaction_20100601 unique (client_id,timestamp)')
- except:
- cursor.execute('create unique index reports_interaction_20100601 on reports_interaction (client_id,timestamp)')
-
-
-def _remove_table_column(tbl, col):
- """sqlite doesn't support deleting a column via alter table"""
- cursor = connection.cursor()
- db_engine = Bcfg2.Server.Reports.settings.DATABASES['default']['ENGINE']
- if db_engine == 'django.db.backends.mysql':
- db_name = Bcfg2.Server.Reports.settings.DATABASES['default']['NAME']
- column_exists = cursor.execute('select * from information_schema.columns '
- 'where table_schema="%s" and '
- 'table_name="%s" '
- 'and column_name="%s";' % (db_name, tbl, col))
- if not column_exists:
- # column doesn't exist
- return
- # if column exists from previous database, remove it
- cursor.execute('alter table %s '
- 'drop column %s;' % (tbl, col))
- elif db_engine == 'django.db.backends.sqlite3':
- # check if table exists
- try:
- cursor.execute('select * from sqlite_master where name=%s and type="table";' % tbl)
- except DatabaseError:
- # table doesn't exist
- return
-
- # sqlite wants us to create a new table containing the columns we want
- # and copy into it http://www.sqlite.org/faq.html#q11
- tmptbl_name = "t_backup"
- _tmptbl_create = \
-"""create temporary table "%s" (
- "id" integer NOT NULL PRIMARY KEY,
- "client_id" integer NOT NULL REFERENCES "reports_client" ("id"),
- "timestamp" datetime NOT NULL,
- "state" varchar(32) NOT NULL,
- "repo_rev_code" varchar(64) NOT NULL,
- "goodcount" integer NOT NULL,
- "totalcount" integer NOT NULL,
- "server" varchar(256) NOT NULL,
- "bad_entries" integer NOT NULL,
- "modified_entries" integer NOT NULL,
- "extra_entries" integer NOT NULL,
- UNIQUE ("client_id", "timestamp")
-);""" % tmptbl_name
- _newtbl_create = \
-"""create table "%s" (
- "id" integer NOT NULL PRIMARY KEY,
- "client_id" integer NOT NULL REFERENCES "reports_client" ("id"),
- "timestamp" datetime NOT NULL,
- "state" varchar(32) NOT NULL,
- "repo_rev_code" varchar(64) NOT NULL,
- "goodcount" integer NOT NULL,
- "totalcount" integer NOT NULL,
- "server" varchar(256) NOT NULL,
- "bad_entries" integer NOT NULL,
- "modified_entries" integer NOT NULL,
- "extra_entries" integer NOT NULL,
- UNIQUE ("client_id", "timestamp")
-);""" % tbl
- new_cols = "id,\
- client_id,\
- timestamp,\
- state,\
- repo_rev_code,\
- goodcount,\
- totalcount,\
- server,\
- bad_entries,\
- modified_entries,\
- extra_entries"
-
- delete_col = [_tmptbl_create,
- "insert into %s select %s from %s;" % (tmptbl_name, new_cols, tbl),
- "drop table %s" % tbl,
- _newtbl_create,
- "create index reports_interaction_client_id on %s (client_id);" % tbl,
- "insert into %s select %s from %s;" % (tbl, new_cols,
- tmptbl_name),
- "drop table %s;" % tmptbl_name]
-
- for sql in delete_col:
- cursor.execute(sql)
-
-
-def _populate_interaction_entry_counts():
- '''Populate up the type totals for the interaction table'''
- cursor = connection.cursor()
- count_field = {TYPE_BAD: 'bad_entries',
- TYPE_MODIFIED: 'modified_entries',
- TYPE_EXTRA: 'extra_entries'}
-
- for type in list(count_field.keys()):
- cursor.execute("select count(type), interaction_id " +
- "from reports_entries_interactions where type = %s group by interaction_id" % type)
- updates = []
- for row in cursor.fetchall():
- updates.append(row)
- try:
- cursor.executemany("update reports_interaction set " + count_field[type] + "=%s where id = %s", updates)
- except Exception:
- e = sys.exc_info()[1]
- print(e)
- cursor.close()
-
-
-# be sure to test your upgrade query before reflecting the change in the models
-# the list of function and sql command to do should go here
-_fixes = [_merge_database_table_entries,
- # this will remove unused tables
- "drop table reports_bad;",
- "drop table reports_bad_interactions;",
- "drop table reports_extra;",
- "drop table reports_extra_interactions;",
- "drop table reports_modified;",
- "drop table reports_modified_interactions;",
- "drop table reports_repository;",
- "drop table reports_metadata;",
- "alter table reports_interaction add server varchar(256) not null default 'N/A';",
- # fix revision data type to support $VCS hashes
- "alter table reports_interaction add repo_rev_code varchar(64) default '';",
- # Performance enhancements for large sites
- 'alter table reports_interaction add column bad_entries integer not null default -1;',
- 'alter table reports_interaction add column modified_entries integer not null default -1;',
- 'alter table reports_interaction add column extra_entries integer not null default -1;',
- _populate_interaction_entry_counts,
- _interactions_constraint_or_idx,
- 'alter table reports_reason add is_binary bool NOT NULL default False;',
- 'alter table reports_reason add is_sensitive bool NOT NULL default False;',
- _remove_table_column('reports_interaction', 'client_version'),
- "alter table reports_reason add unpruned varchar(1280) not null default '';",
-]
-
-# this will calculate the last possible version of the database
-lastversion = len(_fixes)
-
-
-def rollupdate(current_version):
- """function responsible to coordinates all the updates
- need current_version as integer
- """
- ret = None
- if current_version < lastversion:
- for i in range(current_version, lastversion):
- try:
- if type(_fixes[i]) == str:
- connection.cursor().execute(_fixes[i])
- else:
- _fixes[i]()
- except:
- logger.error("Failed to perform db update %s" % (_fixes[i]),
- exc_info=1)
- # since the array starts at 0 but version
- # starts at 1 we add 1 to the normal count
- ret = InternalDatabaseVersion.objects.create(version=i + 1)
- return ret
- else:
- return None
-
-
-def dosync():
- """Function to do the syncronisation for the models"""
- # try to detect if it's a fresh new database
- try:
- cursor = connection.cursor()
- # If this table goes missing,
- # don't forget to change it to the new one
- cursor.execute("Select * from reports_client")
- # if we get here with no error then the database has existing tables
- fresh = False
- except:
- logger.debug("there was an error while detecting "
- "the freshness of the database")
- #we should get here if the database is new
- fresh = True
-
- # ensure database connections are closed
- # so that the management can do its job right
- try:
- cursor.close()
- connection.close()
- except:
- # ignore any errors from missing/invalid dbs
- pass
- # Do the syncdb according to the django version
- if "call_command" in dir(django.core.management):
- # this is available since django 1.0 alpha.
- # not yet tested for full functionnality
- django.core.management.call_command("syncdb", interactive=False, verbosity=0)
- if fresh:
- django.core.management.call_command("loaddata", 'initial_version.xml', verbosity=0)
- elif "syncdb" in dir(django.core.management):
- # this exist only for django 0.96.*
- django.core.management.syncdb(interactive=False, verbosity=0)
- if fresh:
- logger.debug("loading the initial_version fixtures")
- django.core.management.load_data(fixture_labels=['initial_version'], verbosity=0)
- else:
- logger.warning("Don't forget to run syncdb")
-
-
-def update_database():
- """method to search where we are in the revision
- of the database models and update them"""
- try:
- logger.debug("Running upgrade of models to the new one")
- dosync()
- know_version = InternalDatabaseVersion.objects.order_by('-version')
- if not know_version:
- logger.debug("No version, creating initial version")
- know_version = InternalDatabaseVersion.objects.create(version=0)
- else:
- know_version = know_version[0]
- logger.debug("Presently at %s" % know_version)
- if know_version.version < lastversion:
- new_version = rollupdate(know_version.version)
- if new_version:
- logger.debug("upgraded to %s" % new_version)
- except:
- logger.error("Error while updating the database")
- for x in traceback.format_exc().splitlines():
- logger.error(x)
diff --git a/src/lib/Bcfg2/Server/Reports/utils.py b/src/lib/Bcfg2/Server/Reports/utils.py
index e0b6ead59..c47763e39 100755
--- a/src/lib/Bcfg2/Server/Reports/utils.py
+++ b/src/lib/Bcfg2/Server/Reports/utils.py
@@ -3,7 +3,7 @@ from django.conf.urls.defaults import *
import re
"""List of filters provided by filteredUrls"""
-filter_list = ('server', 'state')
+filter_list = ('server', 'state', 'group')
class BatchFetch(object):
@@ -97,6 +97,8 @@ def filteredUrls(pattern, view, kwargs=None, name=None):
tail = mtail.group(1)
pattern = pattern[:len(pattern) - len(tail)]
for filter in ('/state/(?P<state>\w+)',
+ '/group/(?P<group>[\w\-\.]+)',
+ '/group/(?P<group>[\w\-\.]+)/(?P<state>[A-Za-z]+)',
'/server/(?P<server>[\w\-\.]+)',
'/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)'):
results += [(pattern + filter + tail, view, kwargs)]