summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Reports
diff options
context:
space:
mode:
authorSol Jerome <sol.jerome@gmail.com>2012-03-24 11:20:07 -0500
committerSol Jerome <sol.jerome@gmail.com>2012-03-24 11:20:07 -0500
commitdab1d03d81c538966d03fb9318a4588a9e803b44 (patch)
treef51e27fa55887e9fb961766805fe43f0da56c5b9 /src/lib/Bcfg2/Server/Reports
parent5cd6238df496a3cea178e4596ecd87967cce1ce6 (diff)
downloadbcfg2-dab1d03d81c538966d03fb9318a4588a9e803b44.tar.gz
bcfg2-dab1d03d81c538966d03fb9318a4588a9e803b44.tar.bz2
bcfg2-dab1d03d81c538966d03fb9318a4588a9e803b44.zip
Allow to run directly from a git checkout (#1037)
Signed-off-by: Sol Jerome <sol.jerome@gmail.com>
Diffstat (limited to 'src/lib/Bcfg2/Server/Reports')
-rw-r--r--src/lib/Bcfg2/Server/Reports/__init__.py1
-rw-r--r--src/lib/Bcfg2/Server/Reports/backends.py34
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/importscript.py310
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/manage.py11
-rw-r--r--src/lib/Bcfg2/Server/Reports/nisauth.py44
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/__init__.py1
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml39
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/models.py343
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/sql/client.sql9
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/404.html8
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html25
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/base.html95
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html127
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html46
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/history.html20
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html34
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html45
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/item.html115
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html33
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/displays/summary.html42
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/displays/timing.html38
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html13
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/widgets/interaction_list.inc38
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templates/widgets/page_bar.html23
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/__init__.py0
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py276
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py49
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/urls.py55
-rw-r--r--src/lib/Bcfg2/Server/Reports/reports/views.py415
-rw-r--r--src/lib/Bcfg2/Server/Reports/settings.py161
-rw-r--r--src/lib/Bcfg2/Server/Reports/updatefix.py190
-rw-r--r--src/lib/Bcfg2/Server/Reports/urls.py14
-rwxr-xr-xsrc/lib/Bcfg2/Server/Reports/utils.py124
33 files changed, 2778 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Server/Reports/__init__.py b/src/lib/Bcfg2/Server/Reports/__init__.py
new file mode 100644
index 000000000..bdf908f4a
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/__init__.py
@@ -0,0 +1 @@
+__all__ = ['manage', 'nisauth', 'reports', 'settings', 'backends', 'urls', 'importscript']
diff --git a/src/lib/Bcfg2/Server/Reports/backends.py b/src/lib/Bcfg2/Server/Reports/backends.py
new file mode 100644
index 000000000..85241932f
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/backends.py
@@ -0,0 +1,34 @@
+from django.contrib.auth.models import User
+from nisauth import *
+
+
+class NISBackend(object):
+
+ def authenticate(self, username=None, password=None):
+ try:
+ print("start nis authenticate")
+ n = nisauth(username, password)
+ temp_pass = User.objects.make_random_password(100)
+ nis_user = dict(username=username,
+ )
+
+ user_session_obj = dict(email=username,
+ first_name=None,
+ last_name=None,
+ uid=n.uid)
+ user, created = User.objects.get_or_create(username=username)
+
+ return user
+
+ except NISAUTHError:
+ e = sys.exc_info()[1]
+ print(e)
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ e = sys.exc_info()[1]
+ print(e)
+ return None
diff --git a/src/lib/Bcfg2/Server/Reports/importscript.py b/src/lib/Bcfg2/Server/Reports/importscript.py
new file mode 100755
index 000000000..cbdf019f5
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/importscript.py
@@ -0,0 +1,310 @@
+#! /usr/bin/env python
+"""
+Imports statistics.xml and clients.xml files in to database backend for
+new statistics engine
+"""
+
+import binascii
+import os
+import sys
+try:
+ import Bcfg2.Server.Reports.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_name = os.path.basename(project_directory)
+sys.path.append(os.path.join(project_directory, '..'))
+project_module = __import__(project_name, '', '', [''])
+sys.path.pop()
+# Set DJANGO_SETTINGS_MODULE appropriately.
+os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % project_name
+
+from Bcfg2.Server.Reports.reports.models import *
+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
+import logging
+import Bcfg2.Logger
+import platform
+
+# Compatibility import
+from Bcfg2.Bcfg2Py3k import ConfigParser
+
+
+def build_reason_kwargs(r_ent, encoding, logger):
+ binary_file = False
+ sensitive_file = False
+ if r_ent.get('sensitive') in ['true', 'True']:
+ sensitive_file = True
+ rc_diff = ''
+ elif r_ent.get('current_bfile', False):
+ binary_file = True
+ rc_diff = r_ent.get('current_bfile')
+ if len(rc_diff) > 1024 * 1024:
+ rc_diff = ''
+ elif len(rc_diff) == 0:
+ # 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'))
+ elif r_ent.get('current_diff', False):
+ rc_diff = r_ent.get('current_diff')
+ else:
+ rc_diff = ''
+ if not binary_file:
+ try:
+ rc_diff = rc_diff.decode(encoding)
+ except:
+ logger.error("Reason isn't %s encoded, cannot decode it" % encoding)
+ rc_diff = ''
+ return dict(owner=r_ent.get('owner', default=""),
+ current_owner=r_ent.get('current_owner', default=""),
+ group=r_ent.get('group', default=""),
+ current_group=r_ent.get('current_group', default=""),
+ perms=r_ent.get('perms', default=""),
+ current_perms=r_ent.get('current_perms', default=""),
+ status=r_ent.get('status', default=""),
+ current_status=r_ent.get('current_status', default=""),
+ to=r_ent.get('to', default=""),
+ current_to=r_ent.get('current_to', default=""),
+ version=r_ent.get('version', default=""),
+ current_version=r_ent.get('current_version', default=""),
+ current_exists=r_ent.get('current_exists', default="True").capitalize() == "True",
+ current_diff=rc_diff,
+ is_binary=binary_file,
+ is_sensitive=sensitive_file)
+
+
+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')]
+
+ 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',
+ default="unknown"),
+ repo_rev_code=statistics.get('revision',
+ default="unknown"),
+ client_version=statistics.get('client_version',
+ 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
+ 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()
+
+ if vlevel > 1:
+ logger.info("---------------PINGDATA SYNCED---------------------")
+
+ #Clients are consistent
+
+if __name__ == '__main__':
+ from sys import argv
+ verb = 0
+ cpath = "/etc/bcfg2.conf"
+ clientpath = False
+ statpath = False
+ syslog = False
+
+ try:
+ opts, args = getopt(argv[1:], "hvudc:s:CS", ["help",
+ "verbose",
+ "updates",
+ "debug",
+ "clients=",
+ "stats=",
+ "config=",
+ "syslog"])
+ 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))
+ 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("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
+ if o in ["-C", "--config"]:
+ cpath = a
+
+ if o in ("-v", "--verbose"):
+ verb = 1
+ if o in ("-u", "--updates"):
+ verb = 2
+ if o in ("-d", "--debug"):
+ verb = 3
+ if o in ("-c", "--clients"):
+ clientspath = a
+
+ if o in ("-s", "--stats"):
+ statpath = a
+ if o in ("-S", "--syslog"):
+ syslog = True
+
+ logger = logging.getLogger('importscript.py')
+ logging.getLogger().setLevel(logging.INFO)
+ Bcfg2.Logger.setup_logging('importscript.py',
+ True,
+ syslog)
+
+ cf = ConfigParser.ConfigParser()
+ cf.read([cpath])
+
+ if not statpath:
+ try:
+ statpath = "%s/etc/statistics.xml" % cf.get('server', 'repository')
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ print("Could not read bcfg2.conf; exiting")
+ raise SystemExit(1)
+ try:
+ statsdata = XML(open(statpath).read())
+ except (IOError, XMLSyntaxError):
+ print("StatReports: Failed to parse %s" % (statpath))
+ raise SystemExit(1)
+
+ try:
+ encoding = cf.get('components', 'encoding')
+ 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
+ # Be sure the database is ready for new schema
+ update_database()
+ load_stats(clientsdata,
+ statsdata,
+ encoding,
+ verb,
+ logger,
+ quick=q,
+ location=platform.node())
diff --git a/src/lib/Bcfg2/Server/Reports/manage.py b/src/lib/Bcfg2/Server/Reports/manage.py
new file mode 100755
index 000000000..858bddeca
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/src/lib/Bcfg2/Server/Reports/nisauth.py b/src/lib/Bcfg2/Server/Reports/nisauth.py
new file mode 100644
index 000000000..b3e37113b
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/nisauth.py
@@ -0,0 +1,44 @@
+import crypt
+import nis
+from Bcfg2.Server.Reports.settings import AUTHORIZED_GROUP
+
+"""Checks with NIS to see if the current user is in the support group"""
+
+
+class NISAUTHError(Exception):
+ """NISAUTHError is raised when somehting goes boom."""
+ pass
+
+
+class nisauth(object):
+ group_test = False
+ samAcctName = None
+ distinguishedName = None
+ sAMAccountName = None
+ telephoneNumber = None
+ title = None
+ memberOf = None
+ department = None # this will be a list
+ mail = None
+ extensionAttribute1 = None # badgenumber
+ badge_no = None
+ uid = None
+
+ def __init__(self, login, passwd=None):
+ """get user profile from NIS"""
+ try:
+ p = nis.match(login, 'passwd.byname').split(":")
+ print(p)
+ except:
+ raise NISAUTHError('username')
+ # check user password using crypt and 2 character salt from passwd file
+ if p[1] == crypt.crypt(passwd, p[1][:2]):
+ # check to see if user is in valid support groups
+ # will have to include these groups in a settings file eventually
+ if not login in nis.match(AUTHORIZED_GROUP,
+ 'group.byname').split(':')[-1].split(','):
+ raise NISAUTHError('group')
+ self.uid = p[2]
+ print(self.uid)
+ else:
+ raise NISAUTHError('password')
diff --git a/src/lib/Bcfg2/Server/Reports/reports/__init__.py b/src/lib/Bcfg2/Server/Reports/reports/__init__.py
new file mode 100644
index 000000000..ccdce8943
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/__init__.py
@@ -0,0 +1 @@
+__all__ = ['templatetags']
diff --git a/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml b/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
new file mode 100644
index 000000000..919265d48
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/fixtures/initial_version.xml
@@ -0,0 +1,39 @@
+<?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>
+</django-objects>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/models.py b/src/lib/Bcfg2/Server/Reports/reports/models.py
new file mode 100644
index 000000000..870239641
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/models.py
@@ -0,0 +1,343 @@
+"""Django models for Bcfg2 reports."""
+from django.db import models
+from django.db import connection, transaction
+from django.db.models import Q
+from datetime import datetime, timedelta
+from time import strptime
+
+KIND_CHOICES = (
+ #These are the kinds of config elements
+ ('Package', 'Package'),
+ ('Path', 'directory'),
+ ('Path', 'file'),
+ ('Path', 'permissions'),
+ ('Path', 'symlink'),
+ ('Service', 'Service'),
+)
+PING_CHOICES = (
+ #These are possible ping states
+ ('Up (Y)', 'Y'),
+ ('Down (N)', 'N')
+)
+TYPE_BAD = 1
+TYPE_MODIFIED = 2
+TYPE_EXTRA = 3
+
+TYPE_CHOICES = (
+ (TYPE_BAD, 'Bad'),
+ (TYPE_MODIFIED, 'Modified'),
+ (TYPE_EXTRA, 'Extra'),
+)
+
+
+def convert_entry_type_to_id(type_name):
+ """Convert a entry type to its entry id"""
+ for e_id, e_name in TYPE_CHOICES:
+ if e_name.lower() == type_name.lower():
+ return e_id
+ return -1
+
+
+class ClientManager(models.Manager):
+ """Extended client manager functions."""
+ def active(self, timestamp=None):
+ """returns a set of clients that have been created and have not
+ yet been expired as of optional timestmamp argument. Timestamp
+ should be a datetime object."""
+
+ if timestamp == None:
+ timestamp = datetime.now()
+ elif not isinstance(timestamp, datetime):
+ raise ValueError('Expected a datetime object')
+ else:
+ try:
+ timestamp = datetime(*strptime(timestamp,
+ "%Y-%m-%d %H:%M:%S")[0:6])
+ except ValueError:
+ return self.none()
+
+ return self.filter(Q(expiration__gt=timestamp) | Q(expiration__isnull=True),
+ creation__lt=timestamp)
+
+
+class Client(models.Model):
+ """Object representing every client we have seen stats for."""
+ creation = models.DateTimeField(auto_now_add=True)
+ name = models.CharField(max_length=128,)
+ current_interaction = models.ForeignKey('Interaction',
+ null=True, blank=True,
+ related_name="parent_client")
+ expiration = models.DateTimeField(blank=True, null=True)
+
+ def __str__(self):
+ return self.name
+
+ objects = ClientManager()
+
+ class Admin:
+ 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
+
+ Arguments:
+ maxdate -- datetime object. Most recent date to pull. (dafault None)
+ active_only -- Include only active clients (default True)
+
+ """
+
+ if maxdate and not isinstance(maxdate, datetime):
+ raise ValueError('Expected a datetime object')
+ return self.filter(id__in=self.get_interaction_per_client_ids(maxdate, active_only))
+
+ def get_interaction_per_client_ids(self, maxdate=None, active_only=True):
+ """
+ Returns the ids of most recent interactions for clients as of a date.
+
+ Arguments:
+ maxdate -- datetime object. Most recent date to pull. (dafault None)
+ active_only -- Include only active clients (default True)
+
+ """
+ from django.db import connection
+ cursor = connection.cursor()
+ cfilter = "expiration is null"
+
+ sql = 'select reports_interaction.id, x.client_id from (select client_id, MAX(timestamp) ' + \
+ 'as timer from reports_interaction'
+ if maxdate:
+ if not isinstance(maxdate, datetime):
+ raise ValueError('Expected a datetime object')
+ sql = sql + " where timestamp <= '%s' " % maxdate
+ cfilter = "(expiration is null or expiration > '%s') and creation <= '%s'" % (maxdate, maxdate)
+ sql = sql + ' GROUP BY client_id) x, reports_interaction where ' + \
+ 'reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer'
+ if active_only:
+ sql = sql + " and x.client_id in (select id from reports_client where %s)" % \
+ cfilter
+ try:
+ cursor.execute(sql)
+ return [item[0] for item in cursor.fetchall()]
+ except:
+ '''FIXME - really need some error hadling'''
+ 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
+ state = models.CharField(max_length=32) # good/bad/modified/etc
+ repo_rev_code = models.CharField(max_length=64) # repo revision at time of interaction
+ client_version = models.CharField(max_length=32) # Client Version
+ goodcount = models.IntegerField() # of good config-items
+ totalcount = models.IntegerField() # of total config-items
+ server = models.CharField(max_length=256) # Name of the server used for the interaction
+ bad_entries = models.IntegerField(default=-1)
+ modified_entries = models.IntegerField(default=-1)
+ extra_entries = models.IntegerField(default=-1)
+
+ def __str__(self):
+ return "With " + self.client.name + " @ " + self.timestamp.isoformat()
+
+ def percentgood(self):
+ if not self.totalcount == 0:
+ return (self.goodcount / float(self.totalcount)) * 100
+ else:
+ return 0
+
+ def percentbad(self):
+ if not self.totalcount == 0:
+ return ((self.totalcount - self.goodcount) / (float(self.totalcount))) * 100
+ else:
+ return 0
+
+ def isclean(self):
+ if (self.bad_entry_count() == 0 and self.goodcount == self.totalcount):
+ return True
+ else:
+ return False
+
+ def isstale(self):
+ if (self == self.client.current_interaction): # Is Mostrecent
+ if(datetime.now() - self.timestamp > timedelta(hours=25)):
+ return True
+ else:
+ return False
+ else:
+ #Search for subsequent Interaction for this client
+ #Check if it happened more than 25 hrs ago.
+ if (self.client.interactions.filter(timestamp__gt=self.timestamp)
+ .order_by('timestamp')[0].timestamp -
+ self.timestamp > timedelta(hours=25)):
+ return True
+ else:
+ return False
+
+ def save(self):
+ super(Interaction, self).save() # call the real save...
+ self.client.current_interaction = self.client.interactions.latest()
+ self.client.save() # save again post update
+
+ def delete(self):
+ '''Override the default delete. Allows us to remove Performance items'''
+ pitems = list(self.performance_items.all())
+ super(Interaction, self).delete()
+ for perf in pitems:
+ if perf.interaction.count() == 0:
+ perf.delete()
+
+ def badcount(self):
+ return self.totalcount - self.goodcount
+
+ def bad(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_BAD)
+
+ def bad_entry_count(self):
+ """Number of bad entries. Store the count in the interation field to save db queries."""
+ if self.bad_entries < 0:
+ self.bad_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_BAD).count()
+ self.save()
+ return self.bad_entries
+
+ def modified(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_MODIFIED)
+
+ def modified_entry_count(self):
+ """Number of modified entries. Store the count in the interation field to save db queries."""
+ if self.modified_entries < 0:
+ self.modified_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_MODIFIED).count()
+ self.save()
+ return self.modified_entries
+
+ def extra(self):
+ return Entries_interactions.objects.select_related().filter(interaction=self, type=TYPE_EXTRA)
+
+ def extra_entry_count(self):
+ """Number of extra entries. Store the count in the interation field to save db queries."""
+ if self.extra_entries < 0:
+ self.extra_entries = Entries_interactions.objects.filter(interaction=self, type=TYPE_EXTRA).count()
+ self.save()
+ return self.extra_entries
+
+ objects = InteractiveManager()
+
+ class Admin:
+ list_display = ('client', 'timestamp', 'state')
+ list_filter = ['client', 'timestamp']
+ pass
+
+ class Meta:
+ get_latest_by = 'timestamp'
+ ordering = ['-timestamp']
+ unique_together = ("client", "timestamp")
+
+
+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)
+ current_exists = models.BooleanField() # False means its missing. Default True
+ current_diff = models.TextField(max_length=1280, blank=True)
+ is_binary = models.BooleanField(default=False)
+ is_sensitive = models.BooleanField(default=False)
+
+ def _str_(self):
+ return "Reason"
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ cursor.execute('delete from reports_reason where not exists (select rei.id from reports_entries_interactions rei where rei.reason_id = reports_reason.id)')
+ transaction.set_dirty()
+
+
+class Entries(models.Model):
+ """Contains all the entries feed by the client."""
+ name = models.CharField(max_length=128, db_index=True)
+ kind = models.CharField(max_length=16, choices=KIND_CHOICES, db_index=True)
+
+ def __str__(self):
+ return self.name
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ 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 Entries_interactions(models.Model):
+ """Define the relation between the reason, the interaction and the entry."""
+ entry = models.ForeignKey(Entries)
+ reason = models.ForeignKey(Reason)
+ interaction = models.ForeignKey(Interaction)
+ type = models.IntegerField(choices=TYPE_CHOICES)
+
+
+class Performance(models.Model):
+ """Object representing performance data for any interaction."""
+ interaction = models.ManyToManyField(Interaction, related_name="performance_items")
+ metric = models.CharField(max_length=128)
+ value = models.DecimalField(max_digits=32, decimal_places=16)
+
+ def __str__(self):
+ return self.metric
+
+ @staticmethod
+ @transaction.commit_on_success
+ def prune_orphans():
+ '''Prune oprhaned rows... no good way to use the ORM'''
+ cursor = connection.cursor()
+ cursor.execute('delete from reports_performance where not exists (select ri.id from reports_performance_interaction ri where ri.performance_id = reports_performance.id)')
+ 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)
+
+ 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
new file mode 100644
index 000000000..8c63754c9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/sql/client.sql
@@ -0,0 +1,9 @@
+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_extra_interactions_client_id on reports_extra_interactions(interaction_id);
+create index reports_modified_interactions_client_id on reports_modified_interactions(interaction_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); \ No newline at end of file
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/404.html b/src/lib/Bcfg2/Server/Reports/reports/templates/404.html
new file mode 100644
index 000000000..168bd9fec
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/404.html
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% block title %}Bcfg2 - Page not found{% endblock %}
+{% block fullcontent %}
+<h2>Page not found</h2>
+<p>
+The page or object requested could not be found.
+</p>
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
new file mode 100644
index 000000000..842de36f0
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base-timeview.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block timepiece %}
+<script type="text/javascript">
+function showCalendar() {
+ var cal = new CalendarPopup("calendar_div");
+ cal.showYearNavigation();
+ cal.select(document.forms['cal_form'].cal_date,'cal_link',
+ 'yyyy/MM/dd' {% if timestamp %}, '{{ timestamp|date:"Y/m/d" }}'{% endif %} );
+ return false;
+}
+function bcfg2_check_date() {
+ var new_date = document.getElementById('cal_date').value;
+ if(new_date) {
+ document.cal_form.submit();
+ }
+}
+document.write(getCalendarStyles());
+</script>
+{% 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>
+{% endspaceless %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/base.html b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
new file mode 100644
index 000000000..f541c0d2b
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/base.html
@@ -0,0 +1,95 @@
+{% load bcfg2_tags %}
+
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>{% block title %}Bcfg2 Reporting System{% endblock %}</title>
+
+<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+<meta http-equiv="Content-language" content="en" />
+<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
+<meta name="robots" content="noindex, nofollow" />
+<meta http-equiv="cache-control" content="no-cache" />
+
+<link rel="stylesheet" type="text/css" href="{% to_media_url bcfg2_base.css %}" media="all" />
+<script type="text/javascript" src="{% to_media_url bcfg2.js %}"></script>
+<script type="text/javascript" src="{% to_media_url date.js %}"></script>
+<script type="text/javascript" src="{% to_media_url AnchorPosition.js %}"></script>
+<script type="text/javascript" src="{% to_media_url CalendarPopup.js %}"></script>
+<script type="text/javascript" src="{% to_media_url PopupWindow.js %}"></script>
+{% block extra_header_info %}{% endblock %}
+
+</head>
+<body onload="{% block body_onload %}{% endblock %}">
+
+ <div id="header">
+ <a href="http://bcfg2.org"><img src='{% to_media_url bcfg2_logo.png %}'
+ height='115' width='300' alt='Bcfg2' style='float:left; height: 115px' /></a>
+ </div>
+
+<div id="document">
+ <div id="content"><div id="contentwrapper">
+ {% block fullcontent %}
+ <div class='page_name'>
+ <h1>{% block pagebanner %}Page Banner{% endblock %}</h1>
+ <div id="timepiece">{% block timepiece %}Rendered at {% now "Y-m-d H:i" %}{% endblock %}</div>
+ </div>
+ <div class='detail_wrapper'>
+ {% block content %}{% endblock %}
+ </div>
+ {% endblock %}
+ </div></div><!-- content -->
+ <div id="sidemenucontainer"><div id="sidemenu">
+ {% block sidemenu %}
+ <ul class='menu-level1'>
+ <li>Overview</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="{% url reports_summary %}">Summary</a></li>
+ <li><a href="{% url reports_history %}">Recent Interactions</a></li>
+ <li><a href="{% url reports_timing %}">Timing</a></li>
+ </ul>
+ <ul class='menu-level1'>
+ <li>Clients</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="{% url reports_grid_view %}">Grid View</a></li>
+ <li><a href="{% url reports_detailed_list %}">Detailed List</a></li>
+ <li><a href="{% url reports_client_manage %}">Manage</a></li>
+ </ul>
+ <ul class='menu-level1'>
+ <li>Entries Configured</li>
+ </ul>
+ <ul class='menu-level2'>
+ <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>
+ </ul>
+{% comment %}
+ TODO
+ <ul class='menu-level1'>
+ <li>Entry Types</li>
+ </ul>
+ <ul class='menu-level2'>
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Package</a></li>
+ <li><a href="#">Path</a></li>
+ <li><a href="#">Service</a></li>
+ </ul>
+{% endcomment %}
+ <ul class='menu-level1'>
+ <li><a href="http://bcfg2.org">Homepage</a></li>
+ <li><a href="http://docs.bcfg2.org">Documentation</a></li>
+ </ul>
+ {% endblock %}
+ </div></div><!-- sidemenu -->
+ <div style='clear:both'></div>
+</div><!-- document -->
+ <div id="footer">
+ <span>Bcfg2 Version 1.2.2</span>
+ </div>
+
+<div id="calendar_div" style='position:absolute; visibility:hidden; background-color:white; layer-background-color:white;'></div>
+</body>
+</html>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
new file mode 100644
index 000000000..dd4295f21
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detail.html
@@ -0,0 +1,127 @@
+{% extends "base.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Client {{client.name}}{% endblock %}
+
+{% block extra_header_info %}
+<style type="text/css">
+.node_data {
+ border: 1px solid #98DBCC;
+ margin: 10px;
+ padding-left: 18px;
+}
+.node_data td {
+ padding: 1px 20px 1px 2px;
+}
+span.history_links {
+ font-size: 90%;
+ margin-left: 50px;
+}
+span.history_links a {
+ font-size: 90%;
+}
+</style>
+{% endblock %}
+
+{% block body_onload %}javascript:clientdetailload(){% endblock %}
+
+{% block pagebanner %}Client Details{% endblock %}
+
+{% block content %}
+ <div class='detail_header'>
+ <h2>{{client.name}}</h2>
+ <a href='{% url reports_client_manage %}#{{ client.name }}'>[manage]</a>
+ <span class='history_links'><a href="{% url reports_client_history client.name %}">View History</a> | Jump to&nbsp;
+ <select id="quick" name="quick" onchange="javascript:pageJump('quick');">
+ <option value="" selected="selected">--- Time ---</option>
+ {% for i in client.interactions.all|slice:":25" %}
+ <option value="{% url reports_client_detail_pk hostname=client.name, pk=i.id %}">{{i.timestamp}}</option>
+ {% endfor %}
+ </select></span>
+ </div>
+
+ {% if interaction.isstale %}
+ <div class="warningbox">
+ This node did not run within the last 24 hours &#8212; it may be out of date.
+ </div>
+ {% endif %}
+ <table class='node_data'>
+ <tr><td>Timestamp</td><td>{{interaction.timestamp}}</td></tr>
+ {% if interaction.server %}
+ <tr><td>Served by</td><td>{{interaction.server}}</td></tr>
+ {% endif %}
+ {% if interaction.repo_rev_code %}
+ <tr><td>Revision</td><td>{{interaction.repo_rev_code}}</td></tr>
+ {% endif %}
+ <tr><td>State</td><td class='{{interaction.state}}-lineitem'>{{interaction.state|capfirst}}</td></tr>
+ <tr><td>Managed entries</td><td>{{interaction.totalcount}}</td></tr>
+ {% if not interaction.isclean %}
+ <tr><td>Deviation</td><td>{{interaction.percentbad|floatformat:"3"}}%</td></tr>
+ {% endif %}
+ </table>
+
+ {% if interaction.bad_entry_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>
+ <table id='bad_table' class='entry_list'>
+ {% for e in interaction.bad|sortwell %}
+ <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>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+
+ {% if interaction.modified_entry_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>
+ <table id='modified_table' class='entry_list'>
+ {% for e in interaction.modified|sortwell %}
+ <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>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+
+ {% if interaction.extra_entry_count %}
+ <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>
+ <table id='extra_table' class='entry_list'>
+ {% for e in interaction.extra|sortwell %}
+ <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>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endif %}
+
+ {% if entry_list %}
+ <div class="entry_list recent_history_wrapper">
+ <div class="entry_list_head" style="border-bottom: 2px solid #98DBCC;">
+ <h4 style="display: inline"><a href="{% url reports_client_history client.name %}">Recent Interactions</a></h4>
+ </div>
+ <div class='recent_history_box'>
+ {% include "widgets/interaction_list.inc" %}
+ <div style='padding-left: 5px'><a href="{% url reports_client_history client.name %}">more...</a></div>
+ </div>
+ </div>
+ {% endif %}
+{% endblock %}
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
new file mode 100644
index 000000000..0c1fae8d5
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/detailed-list.html
@@ -0,0 +1,46 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Detailed Client Listing{% endblock %}
+{% block pagebanner %}Clients - Detailed View{% endblock %}
+
+{% block content %}
+<div class='client_list_box'>
+{% if entry_list %}
+ {% filter_navigator %}
+ <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>
+ </tr>
+ {% for entry in entry_list %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='left_column'><a href='{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=entry.client.name, pk=entry.id %}'>{{ entry.client.name }}</a></td>
+ <td class='right_column' style='width:75px'><a href='{% add_url_filter state=entry.state %}'
+ {% ifequal entry.state 'dirty' %}class='dirty-lineitem'{% endifequal %}>{{ entry.state }}</a></td>
+ <td class='right_column_narrow'>{{ entry.goodcount }}</td>
+ <td class='right_column_narrow'>{{ entry.bad_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.modified_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.extra_entry_count }}</td>
+ <td class='right_column'><span {% if entry.timestamp|isstale:entry_max %}class='dirty-lineitem'{% endif %}>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</span></td>
+ <td class='right_column_wide'>
+ {% if entry.server %}
+ <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a>
+ {% else %}
+ &nbsp;
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
+</div>
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/history.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/history.html
new file mode 100644
index 000000000..01d4ec2f4
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/history.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Interaction History{% endblock %}
+{% block pagebanner %}Interaction history{% if client %} for {{ client.name }}{% endif %}{% endblock %}
+
+{% block extra_header_info %}
+{% endblock %}
+
+{% block content %}
+<div class='client_list_box'>
+{% if entry_list %}
+ {% filter_navigator %}
+ {% include "widgets/interaction_list.inc" %}
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
+</div>
+{% page_navigator %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
new file mode 100644
index 000000000..e0c0d2d7a
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/index.html
@@ -0,0 +1,34 @@
+{% extends "base-timeview.html" %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block title %}Bcfg2 - Client Grid View{% endblock %}
+
+{% block pagebanner %}Clients - Grid View{% endblock %}
+
+{% block content %}
+
+{% if inter_list %}
+ <table class='grid-view' align='center'>
+ {% for inter in inter_list %}
+ {% if forloop.first %}<tr>{% endif %}
+ <td class="{{inter.state}}-lineitem">
+ <a href="{% spaceless %}{% if not timestamp %}
+ {% url reports_client_detail inter.client.name %}
+ {% else %}
+ {% url reports_client_detail_pk inter.client.name,inter.id %}
+ {% endif %}
+ {% endspaceless %}">{{ inter.client.name }}</a>
+ </td>
+ {% if forloop.last %}
+ </tr>
+ {% else %}
+ {% if forloop.counter|divisibleby:"4" %}</tr><tr>{% endif %}
+ {% endif %}
+ {% endfor %}
+ </table>
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html
new file mode 100644
index 000000000..5725ae577
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/clients/manage.html
@@ -0,0 +1,45 @@
+{% extends "base.html" %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block title %}Bcfg2 - Manage Clients{% endblock %}
+
+{% block pagebanner %}Clients - Manage{% endblock %}
+
+{% block content %}
+<div class='client_list_box'>
+ {% if message %}
+ <div class="warningbox">{{ message }}</div>
+ {% endif %}
+{% if clients %}
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td class='left_column'>Node</td>
+ <td class='right_column'>Expiration</td>
+ <td class='right_column_narrow'>Manage</td>
+ </tr>
+ {% for client in clients %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><span id="{{ client.name }}"> </span>
+ <span id="ttag-{{ client.name }}"> </span>
+ <span id="s-ttag-{{ client.name }}"> </span>
+ <a href="{% url reports_client_detail client.name %}">{{ client.name }}</a></td>
+ <td>{% firstof client.expiration 'Active' %}</td>
+ <td>
+ <form method="post" action="{% url reports_client_manage %}">
+ <div> {# here for no reason other then to validate #}
+ <input type="hidden" name="client_name" value="{{ client.name }}" />
+ <input type="hidden" name="client_action" value="{% if client.expiration %}unexpire{% else %}expire{% endif %}" />
+ <input type="submit" value="{% if client.expiration %}Activate{% else %}Expire Now{% endif %}" />
+ </div>
+ </form>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+{% else %}
+ <p>No client records are available.</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/item.html b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/item.html
new file mode 100644
index 000000000..cc99ef503
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/item.html
@@ -0,0 +1,115 @@
+{% extends "base.html" %}
+{% load syntax_coloring %}
+
+
+{% block title %}Bcfg2 - Element Details{% endblock %}
+
+
+{% block extra_header_info %}
+<style type="text/css">
+#table_list_header {
+ font-size: 100%;
+}
+table.entry_list {
+ width: auto;
+}
+div.information_wrapper {
+ margin: 15px;
+}
+div.diff_wrapper {
+ overflow: auto;
+}
+div.entry_list h3 {
+ font-size: 90%;
+ padding: 5px;
+}
+</style>
+{% endblock%}
+
+{% block pagebanner %}Element Details{% endblock %}
+
+{% block content %}
+ <div class='detail_header'>
+ <h3>{{mod_or_bad|capfirst}} {{item.entry.kind}}: {{item.entry.name}}</h3>
+ </div>
+
+ <div class="information_wrapper">
+
+ {% if isextra %}
+ <p>This item exists on the host but is not defined in the configuration.</p>
+ {% endif %}
+
+ {% if not item.reason.current_exists %}
+ <div class="warning">This item does not currently exist on the host but is specified to exist in the configuration.</div>
+ {% endif %}
+
+ {% if item.reason.current_owner or item.reason.current_group or item.reason.current_perms or item.reason.current_status or item.reason.current_status or item.reason.current_to or item.reason.current_version %}
+ <table class='entry_list'>
+ <tr id='table_list_header'>
+ <td style='text-align: right;'>Problem Type</td><td>Expected</td><td style='border-bottom: 1px solid #98DBCC;'>Found</td></tr>
+ {% if item.reason.current_owner %}
+ <tr><td style='text-align: right'><b>Owner</b></td><td>{{item.reason.owner}}</td>
+ <td>{{item.reason.current_owner}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_group %}
+ <tr><td style='text-align: right'><b>Group</b></td><td>{{item.reason.group}}</td>
+ <td>{{item.reason.current_group}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_perms %}
+ <tr><td style='text-align: right'><b>Permissions</b></td><td>{{item.reason.perms}}</td>
+ <td>{{item.reason.current_perms}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_status %}
+ <tr><td style='text-align: right'><b>Status</b></td><td>{{item.reason.status}}</td>
+ <td>{{item.reason.current_status}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_to %}
+ <tr><td style='text-align: right'><b>Symlink Target</b></td><td>{{item.reason.to}}</td>
+ <td>{{item.reason.current_to}}</td></tr>
+ {% endif %}
+ {% if item.reason.current_version %}
+ <tr><td style='text-align: right'><b>Package Version</b></td><td>{{item.reason.version|cut:"("|cut:")"}}</td>
+ <td>{{item.reason.current_version|cut:"("|cut:")"}}</td></tr>
+ {% endif %}
+ </table>
+ {% endif %}
+
+ {% if item.reason.current_diff or item.reason.is_sensitive %}
+ <div class='entry_list'>
+ <div class='entry_list_head'>
+ {% if item.reason.is_sensitive %}
+ <h3>File contents unavailable, as they might contain sensitive data.</h3>
+ {% else %}
+ <h3>Incorrect file contents</h3>
+ {% endif %}
+ </div>
+ {% if not item.reason.is_sensitive %}
+ <div class='diff_wrapper'>
+ {{ item.reason.current_diff|syntaxhilight }}
+ </div>
+ {% endif %}
+ </div>
+ {% endif %}
+
+
+ <div class='entry_list'>
+ <div class='entry_list_head'>
+ <h3>Occurences on {{ timestamp|date:"Y-m-d" }}</h3>
+ </div>
+ {% if associated_list %}
+ <table class="entry_list" cellpadding="3">
+ {% for inter in associated_list %}
+ <tr><td><a href="{% url reports_client_detail inter.client.name %}"
+ >{{inter.client.name}}</a></td>
+ <td><a href="{% url reports_client_detail_pk hostname=inter.client.name,pk=inter.id %}"
+ >{{inter.timestamp}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>Missing client list</p>
+ {% endif %}
+ </div>
+
+ </div><!-- information_wrapper -->
+{% 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
new file mode 100644
index 000000000..9b1026a08
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/config_items/listing.html
@@ -0,0 +1,33 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Element Listing{% endblock %}
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block pagebanner %}{{mod_or_bad|capfirst}} Element Listing{% endblock %}
+
+{% block content %}
+{% if item_list_dict %}
+ {% for kind, entries in item_list_dict.items %}
+
+ <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>
+
+ <table id='table_{{ kind }}' class='entry_list'>
+ {% for e in entries %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href="{% url reports_item type=mod_or_bad,pk=e.id %}">{{e.entry.name}}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
+{% else %}
+ <p>There are currently no inconsistent configuration entries.</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/displays/summary.html b/src/lib/Bcfg2/Server/Reports/reports/templates/displays/summary.html
new file mode 100644
index 000000000..b9847cf96
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/displays/summary.html
@@ -0,0 +1,42 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Client Summary{% endblock %}
+{% block pagebanner %}Clients - Summary{% endblock %}
+
+{% block body_onload %}javascript:hide_table_array(hide_tables){% endblock %}
+
+{% block extra_header_info %}
+<script type="text/javascript">
+var hide_tables = new Array({{ summary_data|length }});
+{% for summary in summary_data %}
+hide_tables[{{ forloop.counter0 }}] = "table_{{ summary.name }}";
+{% endfor %}
+</script>
+{% endblock%}
+
+{% block content %}
+ <div class='detail_header'>
+ <h2>{{ node_count }} nodes reporting in</h2>
+ </div>
+{% if summary_data %}
+ {% for summary in summary_data %}
+ <div class='entry_list'>
+ <div class='entry_list_head element_list_head' onclick='javascript:toggleMe("table_{{ summary.name }}");'>
+ <h3>{{ summary.nodes|length }} {{ summary.label }}</h3>
+ <div class='entry_expand_tab' id='plusminus_table_{{ summary.name }}'>[+]</div>
+ </div>
+
+ <table id='table_{{ summary.name }}' class='entry_list'>
+ {% for node in summary.nodes|sort_interactions_by_name %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a href="{% url reports_client_detail_pk hostname=node.client.name,pk=node.id %}">{{ node.client.name }}</a></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
+{% else %}
+ <p>No data to report on</p>
+{% endif %}
+{% endblock %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/displays/timing.html b/src/lib/Bcfg2/Server/Reports/reports/templates/displays/timing.html
new file mode 100644
index 000000000..ff775ded5
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/displays/timing.html
@@ -0,0 +1,38 @@
+{% extends "base-timeview.html" %}
+{% load bcfg2_tags %}
+
+{% block title %}Bcfg2 - Performance Metrics{% endblock %}
+{% block pagebanner %}Performance Metrics{% endblock %}
+
+
+{% block extra_header_info %}
+{% endblock%}
+
+{% block content %}
+<div class='client_list_box'>
+ {% if metrics %}
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td>Name</td>
+ <td>Parse</td>
+ <td>Probe</td>
+ <td>Inventory</td>
+ <td>Install</td>
+ <td>Config</td>
+ <td>Total</td>
+ </tr>
+ {% for metric in metrics|dictsort:"name" %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td><a style='font-size: 100%'
+ href="{% url reports_client_detail hostname=metric.name %}">{{ metric.name }}</a></td>
+ {% for mitem in metric|build_metric_list %}
+ <td>{{ mitem }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </table>
+ {% else %}
+ <p>No metric data available</p>
+ {% endif %}
+</div>
+{% endblock %}
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
new file mode 100644
index 000000000..6fbe585ab
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/filter_bar.html
@@ -0,0 +1,13 @@
+{% spaceless %}
+{% if filters %}
+{% for filter, filter_url in filters %}
+ {% if forloop.first %}
+ <div class="filter_bar">Active filters (click to remove):
+ {% endif %}
+ <a href='{{ filter_url }}'>{{ filter|capfirst }}</a>{% if not forloop.last %}, {% endif %}
+ {% if forloop.last %}
+ </div>
+ {% endif %}
+{% endfor %}
+{% endif %}
+{% endspaceless %}
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/interaction_list.inc b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/interaction_list.inc
new file mode 100644
index 000000000..8f2dec1dc
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/interaction_list.inc
@@ -0,0 +1,38 @@
+{% load bcfg2_tags %}
+<div class='interaction_history_widget'>
+ <table cellpadding="3">
+ <tr id='table_list_header' class='listview'>
+ <td class='left_column'>Timestamp</td>
+ {% if not client %}
+ <td class='right_column_wide'>Client</td>
+ {% endif %}
+ <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_wide'>Server</td>
+ </tr>
+ {% for entry in entry_list %}
+ <tr class='{% cycle listview,listview_alt %}'>
+ <td class='left_column'><a href='{% url reports_client_detail_pk hostname=entry.client.name, pk=entry.id %}'>{{ entry.timestamp|date:"Y-m-d\&\n\b\s\p\;H:i"|safe }}</a></td>
+ {% if not client %}
+ <td class='right_column_wide'><a href='{% add_url_filter hostname=entry.client.name %}'>{{ entry.client.name }}</a></td>
+ {% endif %}
+ <td class='right_column' style='width:75px'><a href='{% add_url_filter state=entry.state %}'
+ {% ifequal entry.state 'dirty' %}class='dirty-lineitem'{% endifequal %}>{{ entry.state }}</a></td>
+ <td class='right_column_narrow'>{{ entry.goodcount }}</td>
+ <td class='right_column_narrow'>{{ entry.bad_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.modified_entry_count }}</td>
+ <td class='right_column_narrow'>{{ entry.extra_entry_count }}</td>
+ <td class='right_column_wide'>
+ {% if entry.server %}
+ <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a>
+ {% else %}
+ &nbsp;
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+</div>
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/page_bar.html b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/page_bar.html
new file mode 100644
index 000000000..aa0def83e
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templates/widgets/page_bar.html
@@ -0,0 +1,23 @@
+{% spaceless %}
+{% for page, page_url in pager %}
+ {% if forloop.first %}
+ <div class="page_bar">
+ {% if prev_page %}<a href="{{ prev_page }}">&lt; Prev</a><span>&nbsp;</span>{% endif %}
+ {% if first_page %}<a href="{{ first_page }}">1</a><span>&nbsp;...&nbsp;</span>{% endif %}
+ {% endif %}
+ {% ifequal page current_page %}
+ <span class='nav_bar_current'>{{ page }}</span>
+ {% else %}
+ <a href="{{ page_url }}">{{ page }}</a>
+ {% endifequal %}
+ {% if forloop.last %}
+ {% if last_page %}<span>&nbsp;...&nbsp;</span><a href="{{ last_page }}">{{ total_pages }}</a><span>&nbsp;</span>{% endif %}
+ {% if next_page %}<a href="{{ next_page }}">Next &gt;</a><span>&nbsp;</span>{% endif %}
+ |{% for limit, limit_url in page_limits %}&nbsp;<a href="{{ limit_url }}">{{ limit }}</a>{% endfor %}
+ </div>
+ {% else %}
+ <span>&nbsp;</span>
+ {% endif %}
+{% endfor %}
+{% endspaceless %}
+<!-- {{ path }} -->
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/__init__.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/__init__.py
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
new file mode 100644
index 000000000..f738f7bdd
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/bcfg2_tags.py
@@ -0,0 +1,276 @@
+from django import template
+from django.core.urlresolvers import resolve, reverse, Resolver404, NoReverseMatch
+from django.utils.encoding import smart_unicode, smart_str
+from datetime import datetime, timedelta
+from Bcfg2.Server.Reports.utils import filter_list
+
+register = template.Library()
+
+__PAGE_NAV_LIMITS__ = (10, 25, 50, 100)
+
+@register.inclusion_tag('widgets/page_bar.html', takes_context=True)
+def page_navigator(context):
+ """
+ Creates paginated links.
+
+ Expects the context to be a RequestContext and views.prepare_paginated_list()
+ to have populated page information.
+ """
+ fragment = dict()
+ try:
+ path = context['request'].META['PATH_INFO']
+ total_pages = int(context['total_pages'])
+ records_per_page = int(context['records_per_page'])
+ except KeyError:
+ return fragment
+ except ValueError:
+ return fragment
+
+ if total_pages < 2:
+ return {}
+
+ try:
+ view, args, kwargs = resolve(path)
+ current_page = int(kwargs.get('page_number',1))
+ fragment['current_page'] = current_page
+ fragment['page_number'] = current_page
+ fragment['total_pages'] = total_pages
+ fragment['records_per_page'] = records_per_page
+ if current_page > 1:
+ kwargs['page_number'] = current_page - 1
+ fragment['prev_page'] = reverse(view, args=args, kwargs=kwargs)
+ if current_page < total_pages:
+ kwargs['page_number'] = current_page + 1
+ fragment['next_page'] = reverse(view, args=args, kwargs=kwargs)
+
+ view_range = 5
+ if total_pages > view_range:
+ pager_start = current_page - 2
+ pager_end = current_page + 2
+ if pager_start < 1:
+ pager_end += (1 - pager_start)
+ pager_start = 1
+ if pager_end > total_pages:
+ pager_start -= (pager_end - total_pages)
+ pager_end = total_pages
+ else:
+ pager_start = 1
+ pager_end = total_pages
+
+ if pager_start > 1:
+ kwargs['page_number'] = 1
+ fragment['first_page'] = reverse(view, args=args, kwargs=kwargs)
+ if pager_end < total_pages:
+ kwargs['page_number'] = total_pages
+ fragment['last_page'] = reverse(view, args=args, kwargs=kwargs)
+
+ pager = []
+ for page in range(pager_start, int(pager_end) + 1):
+ kwargs['page_number'] = page
+ pager.append( (page, reverse(view, args=args, kwargs=kwargs)) )
+
+ kwargs['page_number'] = 1
+ page_limits = []
+ for limit in __PAGE_NAV_LIMITS__:
+ kwargs['page_limit'] = limit
+ page_limits.append( (limit, reverse(view, args=args, kwargs=kwargs)) )
+ # resolver doesn't like this
+ del kwargs['page_number']
+ del kwargs['page_limit']
+ page_limits.append( ('all', reverse(view, args=args, kwargs=kwargs) + "|all") )
+
+ fragment['pager'] = pager
+ fragment['page_limits'] = page_limits
+
+ except Resolver404:
+ path = "404"
+ except NoReverseMatch:
+ nr = sys.exc_info()[1]
+ path = "NoReverseMatch: %s" % nr
+ except ValueError:
+ path = "ValueError"
+ #FIXME - Handle these
+
+ fragment['path'] = path
+ return fragment
+
+@register.inclusion_tag('widgets/filter_bar.html', takes_context=True)
+def filter_navigator(context):
+ try:
+ path = context['request'].META['PATH_INFO']
+ view, args, kwargs = resolve(path)
+
+ # Strip any page limits and numbers
+ if 'page_number' in kwargs:
+ del kwargs['page_number']
+ if 'page_limit' in kwargs:
+ del kwargs['page_limit']
+
+ filters = []
+ for filter in filter_list:
+ 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 }
+ except (Resolver404, NoReverseMatch, ValueError, KeyError):
+ pass
+ return dict()
+
+def _subtract_or_na(mdict, x, y):
+ """
+ Shortcut for build_metric_list
+ """
+ try:
+ return round(mdict[x] - mdict[y], 4)
+ except:
+ return "n/a"
+
+@register.filter
+def build_metric_list(mdict):
+ """
+ Create a list of metric table entries
+
+ Moving this here it simplify the view. Should really handle the case where these
+ are missing...
+ """
+ td_list = []
+ # parse
+ td_list.append( _subtract_or_na(mdict, 'config_parse', 'config_download'))
+ #probe
+ td_list.append( _subtract_or_na(mdict, 'probe_upload', 'start'))
+ #inventory
+ td_list.append( _subtract_or_na(mdict, 'inventory', 'initialization'))
+ #install
+ td_list.append( _subtract_or_na(mdict, 'install', 'inventory'))
+ #cfg download & parse
+ td_list.append( _subtract_or_na(mdict, 'config_parse', 'probe_upload'))
+ #total
+ td_list.append( _subtract_or_na(mdict, 'finished', 'start'))
+ return td_list
+
+@register.filter
+def isstale(timestamp, entry_max=None):
+ """
+ Check for a stale timestamp
+
+ Compares two timestamps and returns True if the
+ difference is greater then 24 hours.
+ """
+ if not entry_max:
+ entry_max = datetime.now()
+ return entry_max - timestamp > timedelta(hours=24)
+
+@register.filter
+def sort_interactions_by_name(value):
+ """
+ Sort an interaction list by client name
+ """
+ inters = list(value)
+ inters.sort(lambda a,b: cmp(a.client.name, b.client.name))
+ return inters
+
+class AddUrlFilter(template.Node):
+ def __init__(self, filter_name, filter_value):
+ self.filter_name = filter_name
+ self.filter_value = filter_value
+ self.fallback_view = 'Bcfg2.Server.Reports.reports.views.render_history_view'
+
+ def render(self, context):
+ link = '#'
+ try:
+ path = context['request'].META['PATH_INFO']
+ view, args, kwargs = resolve(path)
+ filter_value = self.filter_value.resolve(context, True)
+ if filter_value:
+ filter_name = smart_str(self.filter_name)
+ filter_value = smart_unicode(filter_value)
+ kwargs[filter_name] = filter_value
+ # These two don't make sense
+ if filter_name == 'server' and 'hostname' in kwargs:
+ del kwargs['hostname']
+ elif filter_name == 'hostname' and 'server' in kwargs:
+ del kwargs['server']
+ try:
+ link = reverse(view, args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ link = reverse(self.fallback_view, args=None,
+ kwargs={ filter_name: filter_value })
+ except NoReverseMatch:
+ rm = sys.exc_info()[1]
+ raise rm
+ except (Resolver404, ValueError):
+ pass
+ return link
+
+@register.tag
+def add_url_filter(parser, token):
+ """
+ Return a url with the filter added to the current view.
+
+ Takes a new filter and resolves the current view with the new filter
+ applied. Resolves to Bcfg2.Server.Reports.reports.views.client_history
+ by default.
+
+ {% add_url_filter server=interaction.server %}
+ """
+ try:
+ tag_name, filter_pair = token.split_contents()
+ filter_name, filter_value = filter_pair.split('=', 1)
+ filter_name = filter_name.strip()
+ filter_value = parser.compile_filter(filter_value)
+ except ValueError:
+ raise template.TemplateSyntaxError("%r tag requires exactly one argument" % token.contents.split()[0])
+ if not filter_name or not filter_value:
+ raise template.TemplateSyntaxError("argument should be a filter=value pair")
+
+ 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
+
+ def render(self, context):
+ base = context['MEDIA_URL']
+ try:
+ request = context['request']
+ try:
+ base = request.environ['bcfg2.media_url']
+ except:
+ if request.path != request.META['PATH_INFO']:
+ offset = request.path.find(request.META['PATH_INFO'])
+ if offset > 0:
+ base = "%s/%s" % (request.path[:offset], \
+ context['MEDIA_URL'].strip('/'))
+ except:
+ pass
+ return "%s/%s" % (base, self.filter_value)
+
+@register.tag
+def to_media_url(parser, token):
+ """
+ Return a url relative to the media_url.
+
+ {% to_media_url /bcfg2.css %}
+ """
+ try:
+ tag_name, filter_value = token.split_contents()
+ filter_value = parser.compile_filter(filter_value)
+ except ValueError:
+ raise template.TemplateSyntaxError("%r tag requires exactly one argument" % token.contents.split()[0])
+
+ return MediaTag(filter_value)
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
new file mode 100644
index 000000000..2e30125f9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/templatetags/syntax_coloring.py
@@ -0,0 +1,49 @@
+import sys
+from django import template
+from django.utils.encoding import smart_unicode, smart_str
+from django.utils.html import conditional_escape
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name
+ from pygments.formatters import HtmlFormatter
+ colorize = True
+
+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):
+ """
+ Returns a syntax-hilighted version of Code; requires code/language arguments
+ """
+
+ if autoescape:
+ value = conditional_escape(value)
+ arg = conditional_escape(arg)
+
+ if colorize:
+ try:
+ output = u_str('<style type="text/css">') \
+ + smart_unicode(HtmlFormatter().get_style_defs('.highlight')) \
+ + u_str('</style>')
+
+ lexer = get_lexer_by_name(arg)
+ output += highlight(value, lexer, HtmlFormatter())
+ return mark_safe(output)
+ except:
+ return value
+ else:
+ return mark_safe(u_str('<div class="note-box">Tip: Install pygments for highlighting</div><pre>%s</pre>') % value)
+syntaxhilight.needs_autoescape = True
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/urls.py b/src/lib/Bcfg2/Server/Reports/reports/urls.py
new file mode 100644
index 000000000..434ce07b7
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/urls.py
@@ -0,0 +1,55 @@
+from django.conf.urls.defaults import *
+from django.core.urlresolvers import reverse, NoReverseMatch
+from django.http import HttpResponsePermanentRedirect
+from Bcfg2.Server.Reports.utils import filteredUrls, paginatedUrls, timeviewUrls
+
+def newRoot(request):
+ try:
+ grid_view = reverse('reports_grid_view')
+ except NoReverseMatch:
+ grid_view = '/grid'
+ return HttpResponsePermanentRedirect(grid_view)
+
+urlpatterns = patterns('Bcfg2.Server.Reports.reports',
+ (r'^$', newRoot),
+
+ url(r'^manage/?$', 'views.client_manage', name='reports_client_manage'),
+ 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'),
+)
+
+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'),
+))
+
+urlpatterns += patterns('Bcfg2.Server.Reports.reports',
+ *filteredUrls(*timeviewUrls(
+ (r'^detailed/?$',
+ 'views.client_detailed_list', None, 'reports_detailed_list')
+)))
+
+urlpatterns += patterns('Bcfg2.Server.Reports.reports',
+ *paginatedUrls( *filteredUrls(
+ (r'^history/?$',
+ 'views.render_history_view', None, 'reports_history'),
+ (r'^history/(?P<hostname>[^/|]+)/?$',
+ 'views.render_history_view', None, 'reports_client_history'),
+)))
+
+ # Uncomment this for admin:
+ #(r'^admin/', include('django.contrib.admin.urls')),
+
+
+## Uncomment this section if using authentication
+#urlpatterns += patterns('',
+# (r'^login/$', 'django.contrib.auth.views.login',
+# {'template_name': 'auth/login.html'}),
+# (r'^logout/$', 'django.contrib.auth.views.logout',
+# {'template_name': 'auth/logout.html'})
+# )
+
diff --git a/src/lib/Bcfg2/Server/Reports/reports/views.py b/src/lib/Bcfg2/Server/Reports/reports/views.py
new file mode 100644
index 000000000..ccd71a60e
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/reports/views.py
@@ -0,0 +1,415 @@
+"""
+Report views
+
+Functions to handle all of the reporting views.
+"""
+from datetime import datetime, timedelta
+import sys
+from time import strptime
+
+from django.template import Context, RequestContext
+from django.http import \
+ HttpResponse, HttpResponseRedirect, HttpResponseServerError, Http404
+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 Bcfg2.Server.Reports.reports.models import *
+
+
+class PaginationError(Exception):
+ """This error is raised when pagination cannot be completed."""
+ pass
+
+
+def server_error(request):
+ """
+ 500 error handler.
+
+ For now always return the debug response. Mailing isn't appropriate here.
+
+ """
+ from django.views import debug
+ return debug.technical_500_response(request, *sys.exc_info())
+
+
+def timeview(fn):
+ """
+ Setup a timeview view
+
+ Handles backend posts from the calendar and converts date pieces
+ into a 'timestamp' parameter
+
+ """
+ def _handle_timeview(request, **kwargs):
+ """Send any posts back."""
+ if request.method == 'POST':
+ cal_date = request.POST['cal_date']
+ try:
+ fmt = "%Y/%m/%d"
+ if cal_date.find(' ') > -1:
+ fmt += " %H:%M"
+ timestamp = datetime(*strptime(cal_date, fmt)[0:6])
+ view, args, kw = resolve(request.META['PATH_INFO'])
+ kw['year'] = "%0.4d" % timestamp.year
+ kw['month'] = "%02.d" % timestamp.month
+ kw['day'] = "%02.d" % timestamp.day
+ if cal_date.find(' ') > -1:
+ kw['hour'] = timestamp.hour
+ kw['minute'] = timestamp.minute
+ return HttpResponseRedirect(reverse(view,
+ args=args,
+ kwargs=kw))
+ except KeyError:
+ pass
+ except:
+ pass
+ # FIXME - Handle this
+
+ """Extract timestamp from args."""
+ timestamp = None
+ try:
+ timestamp = datetime(int(kwargs.pop('year')),
+ int(kwargs.pop('month')),
+ int(kwargs.pop('day')), int(kwargs.pop('hour', 0)),
+ int(kwargs.pop('minute', 0)), 0)
+ kwargs['timestamp'] = timestamp
+ except KeyError:
+ pass
+ except:
+ raise
+ return fn(request, **kwargs)
+
+ return _handle_timeview
+
+
+def config_item(request, pk, type="bad"):
+ """
+ Display a single entry.
+
+ Dispalys information about a single entry.
+
+ """
+ item = get_object_or_404(Entries_interactions, id=pk)
+ timestamp = item.interaction.timestamp
+ time_start = item.interaction.timestamp.replace(hour=0,
+ minute=0,
+ second=0,
+ microsecond=0)
+ time_end = time_start + timedelta(days=1)
+
+ todays_data = Interaction.objects.filter(timestamp__gte=time_start,
+ timestamp__lt=time_end)
+ shared_entries = Entries_interactions.objects.filter(entry=item.entry,
+ reason=item.reason,
+ type=item.type,
+ interaction__in=[x['id']\
+ for x in todays_data.values('id')])
+
+ associated_list = Interaction.objects.filter(id__in=[x['interaction']\
+ for x in shared_entries.values('interaction')])\
+ .order_by('client__name', 'timestamp').select_related().all()
+
+ return render_to_response('config_items/item.html',
+ {'item': item,
+ 'isextra': item.type == TYPE_EXTRA,
+ 'mod_or_bad': type,
+ 'associated_list': associated_list,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def config_item_list(request, type, timestamp=None):
+ """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]
+
+ for kind in item_list_dict:
+ item_list_dict[kind].sort(lambda a, b: cmp(a.entry.name, b.entry.name))
+
+ return render_to_response('config_items/listing.html',
+ {'item_list_dict': item_list_dict,
+ 'mod_or_bad': mod_or_bad,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def client_index(request, timestamp=None):
+ """
+ Render a grid view of active clients.
+
+ Keyword parameters:
+ timestamp -- datetime objectto render from
+
+ """
+ list = Interaction.objects.interaction_per_client(timestamp).select_related()\
+ .order_by("client__name").all()
+
+ return render_to_response('clients/index.html',
+ {'inter_list': list,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def client_detailed_list(request, timestamp=None, **kwargs):
+ """
+ Provides a more detailed list view of the clients. Allows for extra
+ filters to be passed in.
+
+ """
+
+ 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)
+
+
+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)
+ 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)
+
+
+def client_manage(request):
+ """Manage client expiration"""
+ message = ''
+ if request.method == 'POST':
+ try:
+ client_name = request.POST.get('client_name', None)
+ client_action = request.POST.get('client_action', None)
+ client = Client.objects.get(name=client_name)
+ if client_action == 'expire':
+ client.expiration = datetime.now()
+ client.save()
+ message = "Expiration for %s set to %s." % \
+ (client_name, client.expiration.strftime("%Y-%m-%d %H:%M:%S"))
+ elif client_action == 'unexpire':
+ client.expiration = None
+ client.save()
+ message = "%s is now active." % client_name
+ else:
+ message = "Missing action"
+ except Client.DoesNotExist:
+ if not client_name:
+ client_name = "<none>"
+ message = "Couldn't find client \"%s\"" % client_name
+
+ return render_to_response('clients/manage.html',
+ {'clients': Client.objects.order_by('name').all(), 'message': message},
+ context_instance=RequestContext(request))
+
+
+@timeview
+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()
+ if not timestamp:
+ timestamp = datetime.now()
+
+ collected_data = dict(clean=[],
+ bad=[],
+ modified=[],
+ extra=[],
+ stale=[],
+ pings=[])
+ 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:
+ collected_data['clean'].append(node)
+ if node.modified_entry_count() > 0:
+ collected_data['modified'].append(node)
+ if node.extra_entry_count() > 0:
+ collected_data['extra'].append(node)
+
+ # label, header_text, node_list
+ summary_data = []
+ get_dict = lambda name, label: {'name': name,
+ 'nodes': collected_data[name],
+ 'label': label}
+ if len(collected_data['clean']) > 0:
+ summary_data.append(get_dict('clean',
+ 'nodes are clean.'))
+ if len(collected_data['bad']) > 0:
+ summary_data.append(get_dict('bad',
+ 'nodes are bad.'))
+ if len(collected_data['modified']) > 0:
+ summary_data.append(get_dict('modified',
+ 'nodes were modified.'))
+ if len(collected_data['extra']) > 0:
+ summary_data.append(get_dict('extra',
+ 'nodes have extra configurations.'))
+ 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,
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+@timeview
+def display_timing(request, timestamp=None):
+ mdict = dict()
+ inters = Interaction.objects.interaction_per_client(timestamp).select_related().all()
+ [mdict.__setitem__(inter, {'name': inter.client.name}) \
+ 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
+ return render_to_response('displays/timing.html',
+ {'metrics': list(mdict.values()),
+ 'timestamp': timestamp},
+ context_instance=RequestContext(request))
+
+
+def render_history_view(request, template='clients/history.html', **kwargs):
+ """
+ Provides a detailed history of a clients interactions.
+
+ Renders a detailed history of a clients interactions. Allows for various
+ filters and settings. Automatically sets pagination data into the context.
+
+ Keyword arguments:
+ interaction_base -- Interaction QuerySet to build on
+ (default Interaction.objects)
+ context -- Additional context data to render with
+ page_number -- Page to display (default 1)
+ page_limit -- Number of results per page, if 0 show all (default 25)
+ client -- Client object to render
+ hostname -- Client hostname to lookup and render. Returns a 404 if
+ not found
+ server -- Filter interactions by server
+ state -- Filter interactions by state
+ entry_max -- Most recent interaction to display
+ orderby -- Sort results using this field
+
+ """
+
+ context = kwargs.get('context', dict())
+ max_results = int(kwargs.get('page_limit', 25))
+ page = int(kwargs.get('page_number', 1))
+
+ client = kwargs.get('client', None)
+ if not client and 'hostname' in kwargs:
+ client = get_object_or_404(Client, name=kwargs['hostname'])
+ if client:
+ context['client'] = client
+
+ entry_max = kwargs.get('maxdate', None)
+ context['entry_max'] = entry_max
+
+ # 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()
+
+ if 'orderby' in kwargs and kwargs['orderby']:
+ iquery = iquery.order_by(kwargs['orderby'])
+
+ 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'])
+
+ if entry_max:
+ iquery = iquery.filter(timestamp__lte=entry_max)
+
+ if max_results < 0:
+ max_results = 1
+ entry_list = []
+ if max_results > 0:
+ try:
+ rec_start, rec_end = prepare_paginated_list(request,
+ context,
+ iquery,
+ page,
+ max_results)
+ except PaginationError:
+ page_error = sys.exc_info()[1]
+ if isinstance(page_error[0], HttpResponse):
+ return page_error[0]
+ return HttpResponseServerError(page_error)
+ context['entry_list'] = iquery.all()[rec_start:rec_end]
+ else:
+ context['entry_list'] = iquery.all()
+
+ return render_to_response(template, context,
+ context_instance=RequestContext(request))
+
+
+def prepare_paginated_list(request, context, paged_list, page=1, max_results=25):
+ """
+ Prepare context and slice an object for pagination.
+ """
+ if max_results < 1:
+ raise PaginationError("Max results less then 1")
+ if paged_list == None:
+ raise PaginationError("Invalid object")
+
+ try:
+ nitems = paged_list.count()
+ except TypeError:
+ nitems = len(paged_list)
+
+ rec_start = (page - 1) * int(max_results)
+ try:
+ total_pages = (nitems / int(max_results)) + 1
+ except:
+ total_pages = 1
+ if page > total_pages:
+ # If we passed beyond the end send back
+ try:
+ view, args, kwargs = resolve(request.META['PATH_INFO'])
+ kwargs['page_number'] = total_pages
+ raise PaginationError(HttpResponseRedirect(reverse(view,
+ kwards=kwargs)))
+ except (Resolver404, NoReverseMatch, ValueError):
+ raise "Accessing beyond last page. Unable to resolve redirect."
+
+ context['total_pages'] = total_pages
+ context['records_per_page'] = max_results
+ return (rec_start, rec_start + int(max_results))
diff --git a/src/lib/Bcfg2/Server/Reports/settings.py b/src/lib/Bcfg2/Server/Reports/settings.py
new file mode 100644
index 000000000..c8ceb5d88
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/settings.py
@@ -0,0 +1,161 @@
+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
+try:
+ TIME_ZONE = c.get('statistics', 'time_zone')
+except:
+ if django.VERSION[0] == 1 and django.VERSION[1] > 2:
+ 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
new file mode 100644
index 000000000..c6593fb9c
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/updatefix.py
@@ -0,0 +1,190 @@
+import Bcfg2.Server.Reports.settings
+
+from django.db import connection
+import django.core.management
+import logging
+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 _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;',
+]
+
+# 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 array start at 0 but version start 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 then 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 connection are close, so that the management can do it's 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():
+ ''' methode 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/urls.py b/src/lib/Bcfg2/Server/Reports/urls.py
new file mode 100644
index 000000000..d7ff1eee5
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/urls.py
@@ -0,0 +1,14 @@
+from django.conf.urls.defaults import *
+from django.http import HttpResponsePermanentRedirect
+
+handler500 = 'Bcfg2.Server.Reports.reports.views.server_error'
+
+urlpatterns = patterns('',
+ (r'^', include('Bcfg2.Server.Reports.reports.urls'))
+)
+
+#urlpatterns += patterns("django.views",
+# url(r"media/(?P<path>.*)$", "static.serve", {
+# "document_root": '/Users/tlaszlo/svn/bcfg2/reports/site_media/',
+# })
+#)
diff --git a/src/lib/Bcfg2/Server/Reports/utils.py b/src/lib/Bcfg2/Server/Reports/utils.py
new file mode 100755
index 000000000..e0b6ead59
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Reports/utils.py
@@ -0,0 +1,124 @@
+"""Helper functions for reports"""
+from django.conf.urls.defaults import *
+import re
+
+"""List of filters provided by filteredUrls"""
+filter_list = ('server', 'state')
+
+
+class BatchFetch(object):
+ """Fetch Django objects in smaller batches to save memory"""
+
+ def __init__(self, obj, step=10000):
+ self.count = 0
+ self.block_count = 0
+ self.obj = obj
+ self.data = None
+ self.step = step
+ self.max = obj.count()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ """Provide compatibility with python < 3.0"""
+ return self.__next__()
+
+ def __next__(self):
+ """Return the next object from our array and fetch from the
+ database when needed"""
+ if self.block_count + self.count - self.step == self.max:
+ raise StopIteration
+ if self.block_count == 0 or self.count == self.step:
+ # Without list() this turns into LIMIT 1 OFFSET x queries
+ self.data = list(self.obj.all()[self.block_count: \
+ (self.block_count + self.step)])
+ self.block_count += self.step
+ self.count = 0
+ self.count += 1
+ return self.data[self.count - 1]
+
+
+def generateUrls(fn):
+ """
+ Parse url tuples and send to functions.
+
+ Decorator for url generators. Handles url tuple parsing
+ before the actual function is called.
+ """
+ def url_gen(*urls):
+ results = []
+ for url_tuple in urls:
+ if isinstance(url_tuple, (list, tuple)):
+ results += fn(*url_tuple)
+ else:
+ raise ValueError("Unable to handle compiled urls")
+ return results
+ return url_gen
+
+
+@generateUrls
+def paginatedUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a group of url tuples and adds paginated urls.
+
+ Extends a url tuple to include paginated urls.
+ Currently doesn't handle url() compiled patterns.
+
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ results += [(pattern + "/(?P<page_number>\d+)" + tail, view, kwargs)]
+ results += [(pattern + "/(?P<page_number>\d+)\|(?P<page_limit>\d+)" +
+ tail, view, kwargs)]
+ if not kwargs:
+ kwargs = dict()
+ kwargs['page_limit'] = 0
+ results += [(pattern + "/?\|(?P<page_limit>all)" + tail, view, kwargs)]
+ return results
+
+
+@generateUrls
+def filteredUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a url and adds filtered urls.
+
+ Extends a url tuple to include filtered view urls. Currently doesn't
+ handle url() compiled patterns.
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ for filter in ('/state/(?P<state>\w+)',
+ '/server/(?P<server>[\w\-\.]+)',
+ '/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)'):
+ results += [(pattern + filter + tail, view, kwargs)]
+ return results
+
+
+@generateUrls
+def timeviewUrls(pattern, view, kwargs=None, name=None):
+ """
+ Takes a url and adds timeview urls
+
+ Extends a url tuple to include filtered view urls. Currently doesn't
+ handle url() compiled patterns.
+ """
+ results = [(pattern, view, kwargs, name)]
+ tail = ''
+ mtail = re.search('(/+\+?\\*?\??\$?)$', pattern)
+ if mtail:
+ tail = mtail.group(1)
+ pattern = pattern[:len(pattern) - len(tail)]
+ for filter in ('/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/' + \
+ '(?P<hour>\d\d)-(?P<minute>\d\d)',
+ '/(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'):
+ results += [(pattern + filter + tail, view, kwargs)]
+ return results