diff options
author | Tim Laszlo <tim.laszlo@gmail.com> | 2010-10-14 15:28:58 -0500 |
---|---|---|
committer | Sol Jerome <sol.jerome@gmail.com> | 2010-11-02 20:35:17 -0500 |
commit | 2435042c73537a368cda92a4a01c9009d3aaffbe (patch) | |
tree | 4cf551ce778394d93b54bc02c8c5502b1ad7e433 /src/lib/Server | |
parent | feed7f02aeda0b8abe4a3a521e6ab13081c84232 (diff) | |
download | bcfg2-2435042c73537a368cda92a4a01c9009d3aaffbe.tar.gz bcfg2-2435042c73537a368cda92a4a01c9009d3aaffbe.tar.bz2 bcfg2-2435042c73537a368cda92a4a01c9009d3aaffbe.zip |
web reports: new skin
Diffstat (limited to 'src/lib/Server')
34 files changed, 1773 insertions, 1079 deletions
diff --git a/src/lib/Server/Plugin.py b/src/lib/Server/Plugin.py index 95569e3ac..0458a4ce0 100644 --- a/src/lib/Server/Plugin.py +++ b/src/lib/Server/Plugin.py @@ -33,8 +33,8 @@ info_regex = re.compile( \ 'encoding:(\s)*(?P<encoding>\w+)|' + 'group:(\s)*(?P<group>\S+)|' + 'important:(\s)*(?P<important>\S+)|' + - 'mtime:(\s)*(?P<mtime>\w+)|' + - 'owner:(\s)*(?P<owner>\S+)|' + + 'mtime:(\s)*(?P<mtime>\w+)$' + + '^owner:(\s)*(?P<owner>\S+)|' + 'paranoid:(\s)*(?P<paranoid>\S+)|' + 'perms:(\s)*(?P<perms>\w+)|') diff --git a/src/lib/Server/Reports/reports/models.py b/src/lib/Server/Reports/reports/models.py index 5468420f6..1963a9090 100644 --- a/src/lib/Server/Reports/reports/models.py +++ b/src/lib/Server/Reports/reports/models.py @@ -28,26 +28,30 @@ TYPE_CHOICES = ( (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='now'): - '''returns a set of clients that have been created and have not yet been + 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 - string formatted in the fashion: 2006-01-01 00:00:00''' + datetime object.""" - if timestamp == 'now': + if timestamp == None: timestamp = datetime.now() + elif not isinstance(timestamp, datetime): + raise ValueError, 'Expected a datetime object' else: - print timestamp try: timestamp = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6]) except ValueError: - return self.filter(expiration__lt=timestamp, creation__gt=timestamp); - ''' - - this is a really hacky way to return an empty QuerySet - - this should return Client.objects.none() in Django - development version. - ''' + return self.none() return self.filter(Q(expiration__gt=timestamp) | Q(expiration__isnull=True), creation__lt=timestamp) @@ -81,33 +85,64 @@ class Ping(models.Model): get_latest_by = 'endtime' class InteractiveManager(models.Manager): - """Manages interactions objects. + """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. + """ - Returns most recent interaction as of specified timestamp in format: - '2006-01-01 00:00:00' or 'now' or None->'now' + return 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) + + """ - """ - def interaction_per_client(self, maxdate = None): - """Returns the most recent interactions for clients as of a date. - FIXME - check the dates passed in. + 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 != 'now': - sql = sql + " where timestamp < '%s' " % maxdate + 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''' - return self.none() - return self.filter(id__in = [item[0] for item in cursor.fetchall()]) - + pass + return [] class Interaction(models.Model): """Models each reconfiguration operation interaction between client and server.""" @@ -213,6 +248,7 @@ class Interaction(models.Model): pass class Meta: get_latest_by = 'timestamp' + ordering = ['-timestamp'] unique_together = ("client", "timestamp") class Reason(models.Model): @@ -267,42 +303,6 @@ class Entries_interactions(models.Model): interaction = models.ForeignKey(Interaction) type = models.IntegerField(choices=TYPE_CHOICES) -class PerformanceManager(models.Manager): - """ - Provides ability to effectively query for performance information - It is possible this should move to the view - - """ - #Date format for maxdate: '2006-01-01 00:00:00' - def performance_per_client(self, maxdate = None): - from django.db import connection - cursor = connection.cursor() - if (maxdate == 'now' or maxdate == None): - cursor.execute("SELECT reports_client.name, reports_performance.metric, reports_performance.value "+ - "FROM reports_performance, reports_performance_interaction, reports_client WHERE ( "+ - "reports_client.current_interaction_id = reports_performance_interaction.interaction_id AND "+ - "reports_performance.id = reports_performance_interaction.performance_id)") - else: - cursor.execute("select reports_client.name, reports_performance.metric, "+ - "reports_performance.value from (Select reports_interaction.client_id as client_id, "+ - "MAX(reports_interaction.timestamp) as timestamp from reports_interaction where "+ - "timestamp < %s GROUP BY reports_interaction.client_id) x, reports_client, "+ - "reports_interaction, reports_performance, reports_performance_interaction where "+ - "reports_client.id = x.client_id AND x.timestamp = reports_interaction.timestamp AND "+ - "x.client_id = reports_interaction.client_id AND reports_performance.id = "+ - "reports_performance_interaction.performance_id AND "+ - "reports_performance_interaction.interaction_id = reports_interaction.id", [maxdate]) - - results = {} - for row in cursor.fetchall(): - try: - results[row[0]].__setitem__(row[1], row[2]) - except KeyError: - results[row[0]] = {row[1]:row[2]} - - return results - -#performance metrics, models a performance-metric-item class Performance(models.Model): """Object representing performance data for any interaction.""" interaction = models.ManyToManyField(Interaction, related_name="performance_items") @@ -319,8 +319,6 @@ class Performance(models.Model): 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() - objects = PerformanceManager() - class InternalDatabaseVersion(models.Model): """Object that tell us to witch version is the database.""" version = models.IntegerField() diff --git a/src/lib/Server/Reports/reports/models.py.orig b/src/lib/Server/Reports/reports/models.py.orig new file mode 100644 index 000000000..5468420f6 --- /dev/null +++ b/src/lib/Server/Reports/reports/models.py.orig @@ -0,0 +1,330 @@ +"""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'), +) +class ClientManager(models.Manager): + """Extended client manager functions.""" + def active(self, timestamp='now'): + '''returns a set of clients that have been created and have not yet been + expired as of optional timestmamp argument. Timestamp should be a + string formatted in the fashion: 2006-01-01 00:00:00''' + + if timestamp == 'now': + timestamp = datetime.now() + else: + print timestamp + try: + timestamp = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6]) + except ValueError: + return self.filter(expiration__lt=timestamp, creation__gt=timestamp); + ''' + - this is a really hacky way to return an empty QuerySet + - this should return Client.objects.none() in Django + development version. + ''' + + 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. + + Returns most recent interaction as of specified timestamp in format: + '2006-01-01 00:00:00' or 'now' or None->'now' + + """ + def interaction_per_client(self, maxdate = None): + """Returns the most recent interactions for clients as of a date. + FIXME - check the dates passed in. + + """ + from django.db import connection + cursor = connection.cursor() + + sql = 'select reports_interaction.id, x.client_id from (select client_id, MAX(timestamp) ' + \ + 'as timer from reports_interaction' + if maxdate != 'now': + sql = sql + " where timestamp < '%s' " % maxdate + sql = sql + ' GROUP BY client_id) x, reports_interaction where ' + \ + 'reports_interaction.client_id = x.client_id AND reports_interaction.timestamp = x.timer' + try: + cursor.execute(sql) + except: + '''FIXME - really need some error hadling''' + return self.none() + return self.filter(id__in = [item[0] for item in cursor.fetchall()]) + + +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' + 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) + 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 PerformanceManager(models.Manager): + """ + Provides ability to effectively query for performance information + It is possible this should move to the view + + """ + #Date format for maxdate: '2006-01-01 00:00:00' + def performance_per_client(self, maxdate = None): + from django.db import connection + cursor = connection.cursor() + if (maxdate == 'now' or maxdate == None): + cursor.execute("SELECT reports_client.name, reports_performance.metric, reports_performance.value "+ + "FROM reports_performance, reports_performance_interaction, reports_client WHERE ( "+ + "reports_client.current_interaction_id = reports_performance_interaction.interaction_id AND "+ + "reports_performance.id = reports_performance_interaction.performance_id)") + else: + cursor.execute("select reports_client.name, reports_performance.metric, "+ + "reports_performance.value from (Select reports_interaction.client_id as client_id, "+ + "MAX(reports_interaction.timestamp) as timestamp from reports_interaction where "+ + "timestamp < %s GROUP BY reports_interaction.client_id) x, reports_client, "+ + "reports_interaction, reports_performance, reports_performance_interaction where "+ + "reports_client.id = x.client_id AND x.timestamp = reports_interaction.timestamp AND "+ + "x.client_id = reports_interaction.client_id AND reports_performance.id = "+ + "reports_performance_interaction.performance_id AND "+ + "reports_performance_interaction.interaction_id = reports_interaction.id", [maxdate]) + + results = {} + for row in cursor.fetchall(): + try: + results[row[0]].__setitem__(row[1], row[2]) + except KeyError: + results[row[0]] = {row[1]:row[2]} + + return results + +#performance metrics, models a performance-metric-item +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() + + objects = PerformanceManager() + +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/Server/Reports/reports/models.py.rej b/src/lib/Server/Reports/reports/models.py.rej new file mode 100644 index 000000000..c8e1b2229 --- /dev/null +++ b/src/lib/Server/Reports/reports/models.py.rej @@ -0,0 +1,15 @@ +*************** +*** 1,6 **** + """Django models for Bcfg2 reports.""" + from django.db import models +- from django.db.models import Q + from datetime import datetime, timedelta + from time import strptime + +--- 1,6 ---- + """Django models for Bcfg2 reports.""" + from django.db import models ++ from django.db.models import Q, Max + from datetime import datetime, timedelta + from time import strptime + diff --git a/src/lib/Server/Reports/reports/templates/404.html b/src/lib/Server/Reports/reports/templates/404.html new file mode 100644 index 000000000..168bd9fec --- /dev/null +++ b/src/lib/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/Server/Reports/reports/templates/base-timeview.html b/src/lib/Server/Reports/reports/templates/base-timeview.html new file mode 100644 index 000000000..d0617cde7 --- /dev/null +++ b/src/lib/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/Server/Reports/reports/templates/base.html b/src/lib/Server/Reports/reports/templates/base.html index d42e26960..64c105e34 100644 --- a/src/lib/Server/Reports/reports/templates/base.html +++ b/src/lib/Server/Reports/reports/templates/base.html @@ -1,55 +1,93 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<?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> - <link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/boxypastel.css" /> - <link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/base.css" /> - <script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/main.js"></script> - {% block extra_header_info %}{% endblock %} +<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="{{ MEDIA_URL }}/bcfg2_base.css" media="all" /> +<script type="text/javascript" src="{{ MEDIA_URL }}/bcfg2.js"></script> +<script type="text/javascript" src="{{ MEDIA_URL }}/date.js"></script> +<script type="text/javascript" src="{{ MEDIA_URL }}/AnchorPosition.js"></script> +<script type="text/javascript" src="{{ MEDIA_URL }}/CalendarPopup.js"></script> +<script type="text/javascript" src="{{ MEDIA_URL }}/PopupWindow.js"></script> +{% block extra_header_info %}{% endblock %} + </head> +<body onload="{% block body_onload %}{% endblock %}"> -<body> <div id="header"> - <div id="branding"> - <h1>Bcfg2 Reporting System</h1> + <a href="http://trac.mcs.anl.gov/projects/bcfg2"><img src='{{ MEDIA_URL }}/bcfg2_logo.png' + height='115' width='300' alt='Bcfg2' style='float:left; height: 115px' /></a> </div> - <div id="user-tools">...Change is Coming...</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 id="content-main"> - <div id="sidebar"> - {% block sidebar %} - <ul class="sidebar"> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../" class="sidebar">Home</a></li> - <li> - <a href="{% url Bcfg2.Server.Reports.reports.views.client_index %}" class="sidebar">Clients</a> - <ul class="sidebar-level2"> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.client_detailed_list %}" class="sidebar">Detailed List</a></li> - </ul> - </li> - <li> - <a href="{% url Bcfg2.Server.Reports.reports.views.display_index %}" class="sidebar">Displays</a> - <ul class="sidebar-level2"> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_sys_view %}" class="sidebar">System</a></li> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_summary %}" class="sidebar">Summary</a></li> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.display_timing %}" class="sidebar">Timing</a></li> - </ul> - </li> - <li> - <span class="sidebar">Config Items</span> - <ul class="sidebar-level2"> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.bad_item_index %}" class="sidebar">Bad</a></li> - <li><a href="{% url Bcfg2.Server.Reports.reports.views.modified_item_index %}" class="sidebar">Modified</a></li> - </ul> - </li> + <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://trac.mcs.anl.gov/projects/bcfg2">Homepage</a></li> + <li><a href="http://doc.bcfg2.fourkitchens.com/index.html">Documentation</a></li> </ul> {% endblock %} + </div></div><!-- sidemenu --> + <div style='clear:both'></div> +</div><!-- document --> + <div id="footer"> + <span>Bcfg2 Version 1.1.0</span> </div> - <div id="container"> - {% block pagebanner %}{% endblock %} - {% block content %}{% endblock %} - </div> - </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/Server/Reports/reports/templates/clients/client-nodebox.html b/src/lib/Server/Reports/reports/templates/clients/client-nodebox.html deleted file mode 100644 index 8dbd01d9a..000000000 --- a/src/lib/Server/Reports/reports/templates/clients/client-nodebox.html +++ /dev/null @@ -1,63 +0,0 @@ -{% load django_templating_sigh %} -{% if client %} - <a name="{{client.name}}"></a> - <div class="nodebox"> - <span class="notebox">Time Ran: {{interaction.timestamp}}</span> - <!--<span class="configbox">(-Insert Profile Name Here-)</span>--> - - <table class="invisitable"> - <tr><td width="43%"><h2>Node: <span class="nodename"> - <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=client.name, pk=client.current_interaction.id %}">{{client.name}}</a></span></h2></td> - <td width="23%"> - {% if interaction.repo_rev_code %}Revision: {{interaction.repo_rev_code}}{% endif %} - </td> - <td width="33%"><div class="statusborder"> - <div class="greenbar" style="width: {{interaction.percentgood}}%;"> </div> - <div class="redbar" style="width: {{interaction.percentbad}}%;"> </div> - </div> - </td></tr> - </table> - {% if interaction.isclean %} - <div class="clean"> - <span class="nodelisttitle">Node is clean; Everything has been satisfactorily configured.</span> - </div> - {% endif %} - {% if interaction.isstale %} - <div class="warning"> - <span class="nodelisttitle">This node did not run within the last 24 hours-- it may be out of date.</span> - </div> - {% endif %} - {% if interaction.bad %} - <div class="bad"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-bad');" title="Click to expand" class="commentLink">{{interaction.bad.count}}</a> items did not verify and are considered Dirty.<br /></span> - <div class="items" id="{{client.name}}-bad"><ul class="plain"> - {% for bad in interaction.bad|sortwell %} - <li><strong>{{bad.entry.kind}}: </strong><tt><a href="{% url Bcfg2.Server.Reports.reports.views.config_item_bad bad.id%}">{{bad.entry.name}}</a></tt></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if interaction.modified %} - <div class="modified"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-modified');" title="Click to expand" class="commentLink">{{interaction.modified.count}}</a> items were modified in the last run.<br /></span> - <div class="items" id="{{client.name}}-modified"><ul class="plain"> - {% for modified in interaction.modified|sortwell %} - <li><strong>{{modified.entry.kind}}: </strong><tt><a href="{% url Bcfg2.Server.Reports.reports.views.config_item_modified modified.id %}">{{modified.entry.name}}</a></tt></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if interaction.extra %} - <div class="extra"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('{{client.name}}-extra');" title="Click to expand" class="commentLink">{{interaction.extra.count}}</a> extra configuration elements on the node.<br /></span> - <div class="items" id="{{client.name}}-extra"><ul class="plain"> - {% for extra in interaction.extra|sortwell %} - <li><strong>{{extra.entry.kind}}: </strong><tt>{{extra.entry.name}}</tt></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - </div> -{% else %} - <p>No record could be found for this client.</p> -{% endif %} diff --git a/src/lib/Server/Reports/reports/templates/clients/detail.html b/src/lib/Server/Reports/reports/templates/clients/detail.html index 77f505804..efd5f9e00 100644 --- a/src/lib/Server/Reports/reports/templates/clients/detail.html +++ b/src/lib/Server/Reports/reports/templates/clients/detail.html @@ -1,17 +1,127 @@ {% extends "base.html" %} +{% load bcfg2_tags %} -{% block title %}Info for: {{client.name}}{% endblock %} +{% 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 %} -<h2>Client Status Detail page for {{client.name}}</h2><br/> -<b>Select time: </b> -<select name=quick onChange="MM_jumpMenu('parent',this,0)"> - {% for i in client.interactions.all %} - <option {% ifequal i.id interaction.id %}selected {% endifequal %} value="{% url Bcfg2.Server.Reports.reports.views.client_detail hostname=client.name, pk=i.id %}"> {{i.timestamp}} + <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 + <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 — 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'> + <div class='entry_expand_tab' onclick='javascript:toggleMe("bad_table");'>[+]</div> + <h3>Bad Entries — {{ interaction.bad_entry_count }}</h3> + </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'> + <div class='entry_expand_tab' onclick='javascript:toggleMe("modified_table");'>[+]</div> + <h3>Modified Entries — {{ interaction.modified_entry_count }}</h3> + </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'> + <div class='entry_expand_tab' onclick='javascript:toggleMe("extra_table");'>[+]</div> + <h3>Extra Entries — {{ interaction.extra_entry_count }}</h3> + </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 %} -</select> - -<a href="{% url Bcfg2.Server.Reports.reports.views.client_manage hostname=client.name %}">Manage</a> {{client.name}} options.<br/> + </table> + </div> + {% endif %} -{% include "clients/client-nodebox.html" %} + {% 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/Server/Reports/reports/templates/clients/detailed-list.html b/src/lib/Server/Reports/reports/templates/clients/detailed-list.html index 5a1352cff..0c1fae8d5 100644 --- a/src/lib/Server/Reports/reports/templates/clients/detailed-list.html +++ b/src/lib/Server/Reports/reports/templates/clients/detailed-list.html @@ -1,57 +1,15 @@ -{% extends "base.html" %} +{% extends "base-timeview.html" %} +{% load bcfg2_tags %} -{% block title %}Detailed Client Listing{% endblock %} - -{% block extra_header_info %} -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script type="text/javascript">var cal = new CalendarPopup();</script> -<style type="text/css"> -#client_list_header { - font-weight: bold; - border-bottom:1px solid; - /*color: #333366;*/ -} -/*#client_list_box { - min-width:875px; -}*/ -.listview { - padding-top:3px; - padding-bottom:3px; -} -.listview_alt { - background:#f1ffc9; - padding-top:3px; - padding-bottom:3px; -} -</style> -{% endblock%} - -{% block pagebanner %} - <div class="header"> - <h1>Detailed Client List</h1> - </div> - <br/> -{% endblock %} +{% block title %}Bcfg2 - Detailed Client Listing{% endblock %} +{% block pagebanner %}Clients - Detailed View{% endblock %} {% block content %} -<div> -<form name="timestamp-select" action='{{ path }}' method='get'> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@ -<input type="text" name="time" value="{{timestamp_time}}" size="8" /> -<a href="#" onclick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" id="anchor1">Calendar</a> -<input type="button" value="Go" onclick="document.forms['timestamp-select'].submit();"/> - | <input type="button" name="now" value="Now" onclick="location.href='{{ path }}';"/> -</span><br/><br/> -</form> -</div> - -<div id='client_list_box'> +<div class='client_list_box'> {% if entry_list %} + {% filter_navigator %} <table cellpadding="3"> - <tr id='client_list_header' class='listview'> + <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> @@ -61,30 +19,19 @@ <td class='right_column'>Last Run</td> <td class='right_column_wide'>Server</td> </tr> - {% for client,entry,stale in entry_list %} + {% 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=client, pk=entry.id %}'>{{ client }}</a></td> - <td class='right_column' style='width:75px'><a href= - {% if server %} - '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=server,state=entry.state %}{{ qsa }}' - {% else %} - '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list state=entry.state %}{{ qsa }}' - {% endif %} - {% ifequal entry.state 'dirty' %}style='background:#FF6A6A'{% endifequal %}>{{ entry.state }}</a></td> + <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 stale %}style='background:#FF6A6A'{% endif %}>{{ entry.timestamp|date:"Y-m-d H:i" }}</span></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= - {% if state %} - '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=entry.server,state=state %}{{ qsa }}' - {% else %} - '{% url Bcfg2.Server.Reports.reports.views.client_detailed_list server=entry.server %}{{ qsa }}' - {% endif %} - >{{ entry.server }}</a> + <a href='{% add_url_filter server=entry.server %}'>{{ entry.server }}</a> {% else %} {% endif %} diff --git a/src/lib/Server/Reports/reports/templates/clients/history.html b/src/lib/Server/Reports/reports/templates/clients/history.html new file mode 100644 index 000000000..01d4ec2f4 --- /dev/null +++ b/src/lib/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/Server/Reports/reports/templates/clients/index.html b/src/lib/Server/Reports/reports/templates/clients/index.html index cfb8a6c83..e0c0d2d7a 100644 --- a/src/lib/Server/Reports/reports/templates/clients/index.html +++ b/src/lib/Server/Reports/reports/templates/clients/index.html @@ -1,56 +1,33 @@ -{% extends "base.html" %} +{% extends "base-timeview.html" %} {% block extra_header_info %} -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/sorttable.js"></script> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script> {% endblock%} -{% block title %}Client Index Listing{% endblock %} +{% block title %}Bcfg2 - Client Grid View{% endblock %} -{% block pagebanner %} - <div class="header"> - <h1>Clients List</h1> - </div> - <br/> -{% endblock %} +{% block pagebanner %}Clients - Grid View{% endblock %} {% block content %} -<div> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -</span> -<form name="timestamp-select" action="{{path}}" method="get"> -<span class="mini-date"> -<input type="text" name="date1" value="{{timestamp_date}}" size=10 />@ -<input type="text" name="time" value="{{timestamp_time}}" size=8 /> -<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" ID="anchor1">Calendar</A> -<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.client_index %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" /> - | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.client_index %}';"/> -</span></form> -<br/><br/><br/></div> {% if inter_list %} -<table><tr><td valign="top"> - <ul style="list-style-type:none;"> - {% for client,inter in inter_list %} - <li><div class="{{inter.state}}-lineitem"> - <a href="{% spaceless %}{% ifequal timestamp 'now' %} - {% url Bcfg2.Server.Reports.reports.views.client_detail client %} + <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 %} - {% url Bcfg2.Server.Reports.reports.views.client_detail client,inter.id %} - {% endifequal %} - {% endspaceless %}">{{ client }}</a> - </div></li> - {% ifequal half_list forloop.counter0 %} - </ul> -</td><td valign="top"> - <ul style="list-style-type:none;"> - {% endifequal %} + {% if forloop.counter|divisibleby:"4" %}</tr><tr>{% endif %} + {% endif %} {% endfor %} - </ul> -</tr></table> + </table> {% else %} <p>No client records are available.</p> {% endif %} diff --git a/src/lib/Server/Reports/reports/templates/clients/manage.html b/src/lib/Server/Reports/reports/templates/clients/manage.html index 61f0fe017..5725ae577 100644 --- a/src/lib/Server/Reports/reports/templates/clients/manage.html +++ b/src/lib/Server/Reports/reports/templates/clients/manage.html @@ -1,29 +1,45 @@ {% extends "base.html" %} + {% block extra_header_info %} -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script> {% endblock%} -{% block title %}{{client.name}}{% endblock %} -{% block content %} -<h2>Client Options Management page for {{client.name}}</h2><br/> -<p>Client status detail page: <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail client.name %}">{{client.name}}</a>.</p> -<p>Hosts may be prevented from showing up in the reporting system if they have been retired, are no longer managed by bcfg2 :(, etc. </p> -<b>Select deactivation date: </b> -<div> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -</span> -<form name="timestamp-select" action="{% url Bcfg2.Server.Reports.reports.views.client_manage client.name %}" method="post"> -<span class="mini-date"> -<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@ -<input type="text" name="time" value="{{timestamp_time}}" size="8" /> -<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" ID="anchor1">Calendar</a> -<input type="submit" value="Submit"> -</span></form> -<br/><br/><br/></div> -<br/><br/> -<p>{{message}}</p> +{% 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/Server/Reports/reports/templates/config_items/index.html b/src/lib/Server/Reports/reports/templates/config_items/index.html deleted file mode 100644 index 04083344c..000000000 --- a/src/lib/Server/Reports/reports/templates/config_items/index.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends "base.html" %} - -{% load syntax_coloring %} - -{% block extra_header_info %} -<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/syntax-coloring.css" /> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script> -{% endblock%} -{% block title %}Configuration Element Details{% endblock %} - -{% block pagebanner %} - <div class="header"> - <h1>Configuration Element Details</h1> - </div> - <br/> -{% endblock %} - -{% block content %} - -{% ifequal mod_or_bad "bad" %} -<div class="bad"> -<h2>Bad {{item.entry.kind}}: {{item.entry.name}}</h2> -</div> -{% else %} -<div class="modified"> -<h2>Modified {{item.entry.kind}}: {{item.entry.name}}</h2> -</div> -{% endifequal %} -<center> -<table border=1 padding=0 > -<tr><th>Reason</th><th>Current Status</th><th>Specified in bcfg2</th></tr> -{% if item.reason.current_owner %} -<tr><td align="right"><b>Owner: </b></td><td>{{item.reason.current_owner}}</td><td>{{item.reason.owner}}</td></tr> -{% endif %}{% if item.reason.current_group %} -<tr><td align="right"><b>Group: </b></td><td>{{item.reason.current_group}}</td><td>{{item.reason.group}}</td></tr> -{% endif %}{% if item.reason.current_perms %} -<tr><td align="right"><b>Permissions: </b></td><td>{{item.reason.current_perms}}</td><td>{{item.reason.perms}}</td></tr> -{% endif %}{% if item.reason.current_status %} -<tr><td align="right"><b>Status: </b></td><td>{{item.reason.current_status}}</td><td>{{item.reason.status}}</td></tr> -{% endif %}{% if item.reason.current_to %} -<tr><td align="right"><b>Link Destination: </b></td><td>{{item.reason.current_to}}</td><td>{{item.reason.to}}</td></tr> -{% endif %}{% if item.reason.current_version %} -<tr><td align="right"><b>Version: </b></td><td>{{item.reason.current_version}}</td><td>{{item.reason.version}}</td></tr> -{% endif %}{% if not item.reason.current_exists %} -<tr><td align="right"><b>Existence: </b></td><td colspan=2>This item does not currently exist on the host but is specified to exist in the configuration.</td></tr> -{% endif %}{% if item.reason.current_diff %} -<tr><td align="right"><b>NDiff: </b></td><td colspan=2><pre>{{item.reason.current_diff|syntaxhilight:"diff"}}</pre></td></tr> -{% endif %} -</table></center> -<hr/> -<div> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -</span> -<form name="timestamp-select" action="{{path}}" method="get"> -<span class="mini-date"> -<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@ -<input type="text" name="time" value="{{timestamp_time}}" size="8" /> -<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" ID="anchor1">Calendar</A> -{% ifequal mod_or_bad "modified" %} - <input type="button" - name="go" - value="Go" - onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id%}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" /> - | <input type="button" - name="now" - value="Now" - onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id %}';"/> -{% else %} - <input type="button" - name="go" - value="Go" - onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;"/> - | <input type="button" - name="now" - value="Now" - onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id %}';"/> -{% endifequal %} -</span></form> -<br/><br/><br/></div> -{% if associated_client_list %} - <p>The following clients had this problem as of {{timestamp_date}}@{{timestamp_time}}:</p> - {% for client in associated_client_list %} - <a href="{% url Bcfg2.Server.Reports.reports.views.client_detail client.name %}">{{client.name}}</a><br/> - {% endfor %} - <br /> - <br /> -{% else %} - <p>No Clients had this problem at {{timestamp}}</p> -{% endif %} - - - - - - - -{% endblock %} diff --git a/src/lib/Server/Reports/reports/templates/config_items/item.html b/src/lib/Server/Reports/reports/templates/config_items/item.html new file mode 100644 index 000000000..41474922b --- /dev/null +++ b/src/lib/Server/Reports/reports/templates/config_items/item.html @@ -0,0 +1,109 @@ +{% 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 %} + <div class='entry_list'> + <div class='entry_list_head'> + <h3>Incorrect file contents</h3> + </div> + <div class='diff_wrapper'> + {{ item.reason.current_diff|syntaxhilight }} + </div> + </div> + {% endif %} + + + <div class='entry_list'> + <div class='entry_list_head'> + <h3>Occurances 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/Server/Reports/reports/templates/config_items/listing.html b/src/lib/Server/Reports/reports/templates/config_items/listing.html index 64a60e506..572249470 100644 --- a/src/lib/Server/Reports/reports/templates/config_items/listing.html +++ b/src/lib/Server/Reports/reports/templates/config_items/listing.html @@ -1,50 +1,32 @@ -{% extends "base.html" %} -{% load django_templating_sigh %} +{% extends "base-timeview.html" %} +{% load bcfg2_tags %} -{% block extra_header_info %} -<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/tabview/assets/tabview.css" /> -<link rel="stylesheet" type="text/css" href="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/round_tabs.css" /> - -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/yahoo/yahoo.js"></script> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/event/event.js"></script> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/dom/dom.js"></script> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/yui/tabview/tabview.js"></script> -<script type="text/javascript"> -YAHOO.example.init = function( ){ - var tabView = new YAHOO.widget.TabView( { id: 'demo' } ); - {% for item_list in item_list_pseudodict %} - tabView.addTab( new YAHOO.widget.Tab({ - label: '{{item_list.0}}', - content: '<p><ul style="list-style-type:none;">{% for item in item_list.1|sortwell %}<li><strong>{{item.entry.kind}}: <'+'/strong><tt>{% ifequal mod_or_bad "modified" %}<a href="{%url Bcfg2.Server.Reports.reports.views.config_item_modified eyedee=item.id%}">{{item.entry.name}}<'+'/a>{% else %}<a href="{%url Bcfg2.Server.Reports.reports.views.config_item_bad eyedee=item.id%}">{{item.entry.name}}<'+'/a>{% endifequal %}<'+'/tt><'+'/li>{% endfor %}<'+'/ul><'+'/p>', - active: 'True' - })); - {% endfor %} +{% block title %}Bcfg2 - Element Listing{% endblock %} - YAHOO.util.Event.onContentReady('tabview', function() { - tabView.appendTo(this); /* append to #doc */ - }); - -}; -YAHOO.example.init(); +{% block extra_header_info %} +{% endblock%} -</script> -<style type="text/css"> -#demo .yui-content { padding:1em; } /* pad content container */ -</style> -{% endblock %} +{% block pagebanner %}{{mod_or_bad|capfirst}} Element Listing{% endblock %} -{% block title %}{{mod_or_bad|capfirst}} Item Listing{% endblock %} +{% block content %} +{% if item_list_dict %} + {% for kind, entries in item_list_dict.items %} -{% block pagebanner %} - <div class="header"> - <h1>{{mod_or_bad|capfirst}} Configuration Elements</h1> + <div class='entry_list'> + <div class='entry_list_head element_list_head'> + <div class='entry_expand_tab' onclick='javascript:toggleMe("table_{{ kind }}");'>[+]</div> + <h3>{{ kind }} — {{ entries|length }}</h3> </div> - <br/> -{% endblock %} -{% block content %} -{% if item_list_pseudodict %} -<div id="tabview"></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 %} diff --git a/src/lib/Server/Reports/reports/templates/displays/index.html b/src/lib/Server/Reports/reports/templates/displays/index.html deleted file mode 100644 index c078539b6..000000000 --- a/src/lib/Server/Reports/reports/templates/displays/index.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Display Index Listing{% endblock %} -{% block pagebanner %} - <div class="header"> - <h1>BCFG Display Index</h1> - {% comment %} <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>{% endcomment %} - </div> - <br/> -{% endblock %} - -{% block content %} -<ul> -<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_sys_view %}">System View</a></li> -<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_summary %}">Summary Only</a></li> -<li><a href="{% url Bcfg2.Server.Reports.reports.views.display_timing %}">Timing</a></li> -</ul> -{% endblock %} diff --git a/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html b/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html deleted file mode 100644 index 60f97eadc..000000000 --- a/src/lib/Server/Reports/reports/templates/displays/summary-block-direct-links.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "displays/summary-block.html" %} -{% block linkprefix1 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} -{% block linkprefix2 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} -{% block linkprefix3 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} -{% block linkprefix4 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} -{% block linkprefix5 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %} -{% block linkprefix6 %}{% url Bcfg2.Server.Reports.reports.views.client_index %}{% endblock %}
\ No newline at end of file diff --git a/src/lib/Server/Reports/reports/templates/displays/summary-block.html b/src/lib/Server/Reports/reports/templates/displays/summary-block.html deleted file mode 100644 index 060ff0fa1..000000000 --- a/src/lib/Server/Reports/reports/templates/displays/summary-block.html +++ /dev/null @@ -1,90 +0,0 @@ -{% load django_templating_sigh %} - - <div class="nodebox"> - <h2>Summary:</h2> - <p class="indented">{{client_list|length }} Nodes were included in your report.</p> - {% if clean_client_list %} - <div class="clean"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('goodsummary');" title="Click to Expand" class="commentLink">{{clean_client_list|length}}</a> nodes are clean.<br /></span> - <div class="items" id="goodsummary"><ul class="plain"> - {% for client in clean_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix1 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if bad_client_list %} - <div class="bad"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('badsummary');" title="Click to Expand" class="commentLink">{{bad_client_list|length}}</a> nodes are bad.<br /></span> - <div class="items" id="badsummary"><ul class="plain"> - {% for client in bad_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix2 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if modified_client_list %} - <div class="modified"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('modifiedsummary');" title="Click to Expand" class="commentLink">{{modified_client_list|length}}</a> nodes were modified in the previous run.<br /></span> - <div class="items" id="modifiedsummary"><ul class="plain"> - {% for client in modified_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix3 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if extra_client_list %} - <div class="extra"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('extrasummary');" title="Click to Expand" class="commentLink">{{extra_client_list|length}}</a> nodes have extra configuration. (includes both good and bad nodes)<br /></span> - <div class="items" id="extrasummary"><ul class="plain"> - {% for client in extra_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix4 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if stale_up_client_list %} - <div class="warning"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('vstalesummary');" title="Click to Expand" class="commentLink">{{stale_up_client_list|length}}</a> nodes did not run within the last 24 hours but were pingable.<br /></span> - <div class="items" id="vstalesummary"><ul class="plain"> - {% for client in stale_up_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix5 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if stale_all_client_list %} - <div class="all-warning"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('stalesummary');" title="Click to Expand" class="commentLink">{{stale_all_client_list|length}}</a> nodes did not run within the last 24 hours. (includes nodes up and down)<br /></span> - <div class="items" id="stalesummary"><ul class="plain"> - {% for client in stale_all_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="{% block linkprefix6 %}#{% endblock %}{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - {% if down_client_list %} - <div class="down"> - <span class="nodelisttitle"><a href="javascript:toggleLayer('unpingablesummary');" title="Click to Expand" class="commentLink">{{down_client_list|length}}</a> nodes were down.<br /></span> - <div class="items" id="unpingablesummary"><ul class="plain"> - {% for client in down_client_list|sortname %} - {% set_interaction "foo" %} - <li><b>Node: </b> - <tt><a href="#{{client.name}}">{{client.name}}</a></tt><span class="mini-date">{{interaction.timestamp}}</span></li> - {% endfor %} - </ul></div> - </div> - {% endif %} - </div> diff --git a/src/lib/Server/Reports/reports/templates/displays/summary.html b/src/lib/Server/Reports/reports/templates/displays/summary.html index 29cbb22d7..0124f635d 100644 --- a/src/lib/Server/Reports/reports/templates/displays/summary.html +++ b/src/lib/Server/Reports/reports/templates/displays/summary.html @@ -1,31 +1,42 @@ -{% extends "base.html" %} +{% 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" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script> +<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 title %}Display Index Listing{% endblock %} -{% block pagebanner %} - <div class="header"> - <h1>BCFG Clients Summary</h1> - <span class="notebox">Report Run @ {% now "F j, Y P"%}</span> - </div> - <br/> -{% endblock %} {% block content %} -<div> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -</span> -<form name="timestamp-select" action="{{path}" method="get"> -<span class="mini-date"> -<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@ -<input type="text" name="time" value="{{timestamp_time}}" size="8" /> -<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" ID="anchor1">Calendar</A> -<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_summary %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" /> - | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_summary %}';"/> -</span></form> -<br/><br/><br/></div> - {% include "displays/summary-block-direct-links.html" %} + <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'> + <div class='entry_expand_tab' onclick='javascript:toggleMe("table_{{ summary.name }}");'>[+]</div> + <h3>{{ summary.nodes|length }} {{ summary.label }}</h3> + </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/Server/Reports/reports/templates/displays/sys_view.html b/src/lib/Server/Reports/reports/templates/displays/sys_view.html deleted file mode 100644 index 1298059bf..000000000 --- a/src/lib/Server/Reports/reports/templates/displays/sys_view.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} -{% load django_templating_sigh %} - -{% block title %}System-View Display{% endblock %} -{% block pagebanner %} - <div class="header"> - <h1>Grand System View</h1> - <span class="notebox">Report Run @ {% now "F j, Y P"%}</span> - </div> - <br/> -{% endblock %} -{% block content %} -<center><h2>This view is deprecated and will be removed soon.</h2><br/>Please use the "Summary" view and drill down instead.</center> - - {% include "displays/summary-block.html" %} - {% for client in client_list %} - {% set_interaction "foo" %} - {% include "clients/client-nodebox.html" %} - {% endfor %} -{% endblock %} diff --git a/src/lib/Server/Reports/reports/templates/displays/timing.html b/src/lib/Server/Reports/reports/templates/displays/timing.html index 32ddab464..47accb2cb 100644 --- a/src/lib/Server/Reports/reports/templates/displays/timing.html +++ b/src/lib/Server/Reports/reports/templates/displays/timing.html @@ -1,54 +1,38 @@ -{% extends "base.html" %} +{% extends "base-timeview.html" %} +{% load bcfg2_tags %} + +{% block title %}Bcfg2 - Performance Metrics{% endblock %} +{% block pagebanner %}Performance Metrics{% endblock %} + {% block extra_header_info %} -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/sorttable.js"></script> -<script type="text/javascript" src="{% url Bcfg2.Server.Reports.reports.views.client_index %}../site_media/CalendarPopup.js"></script> -<script language="JavaScript" type="text/javascript">var cal = new CalendarPopup();</script> {% endblock%} -{% block title %}Display Index Listing{% endblock %} {% block content %} - <div class="header"> - <h1>BCFG Performance Timings</h1> - <span class="notebox">Report Run @ {% now "F j, Y P"%}</span> - </div> - <br/> -<div> -<span class="mini-date"> -<b>Enter date or use calendar popup: </b> -</span> -<form name="timestamp-select" action="{{path}}" method="get"> -<span class="mini-date"> -<input type="text" name="date1" value="{{timestamp_date}}" size="10" />@ -<input type="text" name="time" value="{{timestamp_time}}" size="8" /> -<a href="#" onClick="cal.select(document.forms['timestamp-select'].date1,'anchor1','yyyy-MM-dd'); return false;" - name="anchor1" ID="anchor1">Calendar</A> -<input type="button" name="go" value="Go" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_timing %}'+document.forms['timestamp-select'].date1.value+'@'+document.forms['timestamp-select'].time.value;" /> - | <input type="button" name="now" value="Now" onClick="location.href='{% url Bcfg2.Server.Reports.reports.views.display_timing %}';"/> -</span></form> -<br/><br/><br/></div> - <center> - <table id="t1" class="sortable"> - <tr> - <th class="sortable">Hostname</th> - <th class="sortable">Parse</th> - <th class="sortable">Probe</th> - <th class="sortable">Inventory</th> - <th class="sortable">Install</th> - <th class="sortable">Config</th> - <th class="sortable">Total</th> +<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 dict_unit in stats_list %} - <tr> - <td class="sortable"><a href="{% url Bcfg2.Server.Reports.reports.views.client_detail dict_unit.name%}/">{{dict_unit.name}}</a></td> - <td class="sortable">{{dict_unit.parse}}</td> - <td class="sortable">{{dict_unit.probe}}</td> - <td class="sortable">{{dict_unit.inventory}}</td> - <td class="sortable">{{dict_unit.install}}</td> - <td class="sortable">{{dict_unit.config}}</td> - <td class="sortable">{{dict_unit.total}}</td> + {% 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> - </center> + {% else %} + <p>No metric data available</p> + {% endif %} +</div> {% endblock %} diff --git a/src/lib/Server/Reports/reports/templates/index.html b/src/lib/Server/Reports/reports/templates/index.html deleted file mode 100644 index 002a3f770..000000000 --- a/src/lib/Server/Reports/reports/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} - -{% block pagebanner %} - <div class="header"> - <h1>BCFG Reports</h1> - {% comment %} <span class="notebox">Report Run @ {% now "F j, Y P"%}</span>{% endcomment %} - </div> - <br/> -{% endblock %} -{% block content %} -<h1>Welcome to the Bcfg2 Reporting System</h1> -<p> -Please use the links at the left to navigate. -</p> -{% endblock %} diff --git a/src/lib/Server/Reports/reports/templates/widgets/filter_bar.html b/src/lib/Server/Reports/reports/templates/widgets/filter_bar.html new file mode 100644 index 000000000..6b57baf6a --- /dev/null +++ b/src/lib/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/Server/Reports/reports/templates/widgets/interaction_list.inc b/src/lib/Server/Reports/reports/templates/widgets/interaction_list.inc new file mode 100644 index 000000000..8f2dec1dc --- /dev/null +++ b/src/lib/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 %} + + {% endif %} + </td> + </tr> + {% endfor %} + </table> +</div> diff --git a/src/lib/Server/Reports/reports/templates/widgets/page_bar.html b/src/lib/Server/Reports/reports/templates/widgets/page_bar.html new file mode 100644 index 000000000..aa0def83e --- /dev/null +++ b/src/lib/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 }}">< Prev</a><span> </span>{% endif %} + {% if first_page %}<a href="{{ first_page }}">1</a><span> ... </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> ... </span><a href="{{ last_page }}">{{ total_pages }}</a><span> </span>{% endif %} + {% if next_page %}<a href="{{ next_page }}">Next ></a><span> </span>{% endif %} + |{% for limit, limit_url in page_limits %} <a href="{{ limit_url }}">{{ limit }}</a>{% endfor %} + </div> + {% else %} + <span> </span> + {% endif %} +{% endfor %} +{% endspaceless %} +<!-- {{ path }} --> diff --git a/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py b/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py new file mode 100644 index 000000000..2c27aab04 --- /dev/null +++ b/src/lib/Server/Reports/reports/templatetags/bcfg2_tags.py @@ -0,0 +1,239 @@ +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'].path + total_pages = int(context['total_pages']) + records_per_page = int(context['records_per_page']) + except KeyError, e: + return fragment + except ValueError, e: + 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: + 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'].path + 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'].path + 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, rm: + link = reverse(self.fallback_view, args=None, + kwargs={ filter_name: filter_value }) + except NoReverseMatch, rm: + raise rm + except (Resolver404, ValueError), e: + 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 + diff --git a/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py b/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py deleted file mode 100644 index c0d05d2c1..000000000 --- a/src/lib/Server/Reports/reports/templatetags/django_templating_sigh.py +++ /dev/null @@ -1,41 +0,0 @@ -from django import template -#from Bcfg2.Server.Reports.reports.models import Client, Interaction, Bad, Modified, Extra - -register = template.Library() - -def set_interaction(parser, token): - try: - # Splitting by None == splitting by spaces. - tag_name, format_string = token.contents.split(None, 1) - except ValueError: - raise template.TemplateSyntaxError, "%r tag requires an argument" % token.contents[0] - if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): - raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name - return SetInteraction(format_string[1:-1]) - -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 -def sortname(value): - "sorts a list( or evaluates queryset) by name" - configItems = list(value) - configItems.sort(lambda x,y: cmp(x.name, y.name)) - return configItems - -class SetInteraction(template.Node): - def __init__(self, times): - self.times = times#do soemthing to select different interaction with host? - def render(self, context): - try: - context['interaction'] = context['client_interaction_dict'][context['client'].id] - except:#I don't fully know what the implications of this are. - pass - return '' - -register.tag('set_interaction', set_interaction) -register.filter('sortwell', sortwell) -register.filter('sortname', sortname) diff --git a/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py b/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py index 083b83a73..43dafb262 100644 --- a/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py +++ b/src/lib/Server/Reports/reports/templatetags/syntax_coloring.py @@ -1,4 +1,7 @@ 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() @@ -11,15 +14,28 @@ try: except: colorize = False -def syntaxhilight(value, arg="diff"): - '''Returns a syntax-hilighted version of Code; requires code/language arguments''' +@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'<style type="text/css">' \ + + smart_unicode(HtmlFormatter().get_style_defs('.highlight')) \ + + u'</style>' + lexer = get_lexer_by_name(arg) - return highlight(value, lexer, HtmlFormatter()) + output += highlight(value, lexer, HtmlFormatter()) + return mark_safe(output) except: return value else: - return value + return mark_safe(u'<div class="note-box">Tip: Install pygments for highlighting</div><pre>%s</pre>' % value) +syntaxhilight.needs_autoescape = True -register.filter('syntaxhilight', syntaxhilight) diff --git a/src/lib/Server/Reports/reports/urls.py b/src/lib/Server/Reports/reports/urls.py new file mode 100644 index 000000000..9970d26a1 --- /dev/null +++ b/src/lib/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>\S+)/(?P<pk>\d+)/?$', 'views.client_detail', name='reports_client_detail_pk'), + url(r'^client/(?P<hostname>\S+)/?$', '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>[\w\-\.]+)/?$', + '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/Server/Reports/reports/views.py b/src/lib/Server/Reports/reports/views.py index d159dcd43..64617ce70 100644 --- a/src/lib/Server/Reports/reports/views.py +++ b/src/lib/Server/Reports/reports/views.py @@ -1,354 +1,379 @@ -# Create your views here. -#from django.shortcuts import get_object_or_404, render_to_response -from django.template import Context, loader -from django.http import HttpResponseRedirect, HttpResponse +""" +Report views + +Functions to handle all of the reporting views. +""" +from django.template import Context, RequestContext, loader +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseServerError, Http404 from django.shortcuts import render_to_response, get_object_or_404 -from Bcfg2.Server.Reports.reports.models import Client, Interaction, Entries, Entries_interactions, Performance, Reason -from Bcfg2.Server.Reports.reports.models import TYPE_BAD, TYPE_MODIFIED, TYPE_EXTRA -from datetime import datetime, timedelta -from time import strptime +from django.core.urlresolvers import resolve, reverse, Resolver404, NoReverseMatch from django.db import connection from django.db.backends import util -from django.contrib.auth.decorators import login_required -def index(request): - return render_to_response('index.html') +from Bcfg2.Server.Reports.reports.models import * +from datetime import datetime, timedelta +from time import strptime +import sys -def config_item_modified(request, eyedee =None, timestamp = 'now', type=TYPE_MODIFIED): - #if eyedee = None, dump with a 404 - timestamp = timestamp.replace("@"," ") - if type == TYPE_MODIFIED: - mod_or_bad = "modified" - else: - mod_or_bad = "bad" - - item = get_object_or_404(Entries_interactions, id=eyedee) - #if everything is blank except current_exists, do something special - cursor = connection.cursor() - if timestamp == 'now': - cursor.execute("select client_id from reports_interaction, reports_entries_interactions, reports_client "+ - "WHERE reports_client.current_interaction_id = reports_entries_interactions.interaction_id "+ - "AND reports_entries_interactions.interaction_id = reports_interaction.id "+ - "AND reports_entries_interactions.entry_id = %s " + - "AND reports_entries_interactions.reason_id = %s", [item.entry.id, item.reason.id]) - associated_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()]) - else: - interact_queryset = Interaction.objects.interaction_per_client(timestamp) - interactionlist = [] - [interactionlist.append(x.id) for x in interact_queryset] - if not interactionlist == []: - cursor.execute("select client_id from reports_interaction, reports_entries_interactions, reports_client "+ - "WHERE reports_entries_interactions.interaction_id IN %s "+ - "AND reports_entries_interactions.interaction_id = reports_interaction.id "+ - "AND reports_entries_interactions.entry_id = %s " + - "AND reports_entries_interactions.reason_id = %s ", [interactionlist, item.entry_id, item.reason.id]) - associated_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()]) - else: - associated_client_list = [] +class PaginationError(Exception): + """This error is raised when pagination cannot be completed.""" + pass - if timestamp == 'now': - timestamp = datetime.now().isoformat('@') +def server_error(request): + """ + 500 error handler. - return render_to_response('config_items/index.html', {'item':item, - 'mod_or_bad':mod_or_bad, - 'associated_client_list':associated_client_list, - 'timestamp' : timestamp, - 'timestamp_date' : timestamp[:10], - 'timestamp_time' : timestamp[11:19]}) + 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 config_item_bad(request, eyedee = None, timestamp = 'now'): - return config_item_modified(request, eyedee, timestamp, TYPE_BAD) +def timeview(fn): + """ + Setup a timeview view -def bad_item_index(request, timestamp = 'now', type=TYPE_BAD): - timestamp = timestamp.replace("@"," ") - if type == TYPE_BAD: - mod_or_bad = "bad" - else: - mod_or_bad = "modified" + 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.path) + 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 = [c.current_interaction - for c in Client.objects.active(timestamp)] + current_clients = Interaction.objects.get_interaction_per_client_ids(timestamp) item_list_dict = {} - for x in Entries_interactions.objects.select_related().filter(interaction__in=current_clients, type=type).distinct(): + 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] - item_list_pseudodict = item_list_dict.items() - if timestamp == 'now': - timestamp = datetime.now().isoformat('@') + 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_pseudodict':item_list_pseudodict, + return render_to_response('config_items/listing.html', {'item_list_dict':item_list_dict, 'mod_or_bad':mod_or_bad, - 'timestamp' : timestamp, - 'timestamp_date' : timestamp[:10], - 'timestamp_time' : timestamp[11:19]}) -def modified_item_index(request, timestamp = 'now'): - return bad_item_index(request, timestamp, TYPE_MODIFIED) - -def client_index(request, timestamp = 'now'): - timestamp = timestamp.replace("@"," ") - - c_dict = dict() - [c_dict.__setitem__(cl.id,cl.name) for cl in Client.objects.active(timestamp).order_by('name')] - - list = [] - for inter in Interaction.objects.interaction_per_client(timestamp): - if inter.client_id in c_dict: - list.append([c_dict[inter.client_id], inter]) - list.sort(lambda a,b: cmp(a[0], b[0])) - half_list = len(list) / 2 - - if timestamp == 'now': - timestamp = datetime.now().isoformat('@') + '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, - 'half_list': half_list, - 'timestamp' : timestamp, - 'timestamp_date' : timestamp[:10], - 'timestamp_time' : timestamp[11:19]}) + { 'inter_list': list, 'timestamp' : timestamp}, + context_instance=RequestContext(request)) -def client_detailed_list(request, **kwargs): +@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. Somewhat clunky now that dates are allowed. + filters to be passed in. """ - context = dict(path=request.path) - timestamp = 'now' - entry_max = datetime.now() - if request.GET: - context['qsa']='?%s' % request.GET.urlencode() - if request.GET.has_key('date1') and request.GET.has_key('time'): - timestamp = "%s %s" % (request.GET['date1'],request.GET['time']) - entry_max = datetime(*strptime(timestamp, "%Y-%m-%d %H:%M:%S")[0:6]) - client_list = Client.objects.active(timestamp).order_by('name') - if timestamp == 'now': - timestamp = datetime.now().isoformat('@') - context['timestamp_date'] = timestamp[:10] - context['timestamp_time'] = timestamp[11:19] - - interactions = Interaction.objects.interaction_per_client(timestamp) - if 'state' in kwargs and kwargs['state']: - context['state'] = kwargs['state'] - interactions=interactions.filter(state__exact=kwargs['state']) - if 'server' in kwargs and kwargs['server']: - interactions=interactions.filter(server__exact=kwargs['server']) - context['server'] = kwargs['server'] - # build the entry list from available clients - c_dict = dict() - [c_dict.__setitem__(cl.id,cl.name) for cl in client_list] - - entry_list = [] - for inter in interactions: - if inter.client_id in c_dict: - entry_list.append([c_dict[inter.client_id], inter, \ - entry_max - inter.timestamp > timedelta(hours=24)]) - entry_list.sort(lambda a,b: cmp(a[0], b[0])) - ''' - if(datetime.now()-self.timestamp > timedelta(hours=25) ): - return True - else: - return False - ''' - - context['entry_list'] = entry_list - return render_to_response('clients/detailed-list.html', context) + 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): - #SETUP error pages for when you specify a client or interaction that doesn't exist + context = dict() client = get_object_or_404(Client, name=hostname) if(pk == None): - interaction = client.current_interaction + context['interaction'] = client.current_interaction + return render_history_view(request, 'clients/detail.html', page_limit=5, + client=client, context=context) else: - interaction = client.interactions.get(pk=pk)#can this be a get object or 404? - return render_to_response('clients/detail.html', {'client': client, 'interaction': interaction}) + 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, hostname = None): - #SETUP error pages for when you specify a client or interaction that doesn't exist - client = get_object_or_404(Client, name=hostname) - currenttime = datetime.now().isoformat('@') - if client.expiration != None: - message = ("This client currently has an expiration date of %s. " - "Reports after %s will not include data for this host " - "You may change this if you wish by selecting a new " - "time, earlier or later." - % (client.expiration, client.expiration)) - else: - message = ("This client is currently active and displayed. You " - "may choose a date after which this client will no " - "longer appear in reports.") +def client_manage(request): + """Manage client expiration""" + message = '' if request.method == 'POST': - date = request.POST['date1'] - time = request.POST['time'] try: - timestamp = datetime(*(strptime(date+"@"+time, "%Y-%m-%d@%H:%M:%S")[0:6])) - except ValueError: - timestamp = None - if timestamp == None: - message = "Invalid removal date, please try again using the format: yyyy-mm-dd hh:mm:ss." - else: - client.expiration = timestamp + 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 = "Expiration for client set to %s." % client.expiration - return render_to_response('clients/manage.html', {'client': client, 'message': message, - 'timestamp_date' : currenttime[:10], - 'timestamp_time' : currenttime[11:19]}) - -def display_sys_view(request, timestamp = 'now'): - client_lists = prepare_client_lists(request, timestamp) - return render_to_response('displays/sys_view.html', client_lists) - -def display_summary(request, timestamp = 'now'): - - client_lists = prepare_client_lists(request, timestamp) - #this returns timestamp and the timestamp parts too - return render_to_response('displays/summary.html', client_lists) - -def display_timing(request, timestamp = 'now'): - #We're going to send a list of dictionaries. Each dictionary will be a row in the table - #+------+-------+----------------+-----------+---------+----------------+-------+ - #| name | parse | probe download | inventory | install | cfg dl & parse | total | - #+------+-------+----------------+-----------+---------+----------------+-------+ - client_list = Client.objects.active(timestamp.replace("@"," ")).order_by('name') - stats_list = [] - - if not timestamp == 'now': - results = Performance.objects.performance_per_client(timestamp.replace("@"," ")) + message = "%s is now active." % client_name else: - results = Performance.objects.performance_per_client() - timestamp = datetime.now().isoformat('@') - - for client in client_list:#Go explicitly to an interaction ID! (new item in dictionary) + 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: - d = results[client.name] - except KeyError: - d = {} + 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=mdict.keys()).all(): + for i in metric.interaction.all(): + mdict[i][metric.metric] = metric.value + return render_to_response('displays/timing.html', + {'metrics': 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 - dict_unit = {} - try: - dict_unit["name"] = client.name #node name - except: - dict_unit["name"] = "n/a" - try: - dict_unit["parse"] = round(d["config_parse"] - d["config_download"], 4) #parse - except: - dict_unit["parse"] = "n/a" - try: - dict_unit["probe"] = round(d["probe_upload"] - d["start"], 4) #probe - except: - dict_unit["probe"] = "n/a" - try: - dict_unit["inventory"] = round(d["inventory"] - d["initialization"], 4) #inventory - except: - dict_unit["inventory"] = "n/a" - try: - dict_unit["install"] = round(d["install"] - d["inventory"], 4) #install - except: - dict_unit["install"] = "n/a" - try: - dict_unit["config"] = round(d["config_parse"] - d["probe_upload"], 4)#config download & parse - except: - dict_unit["config"] = "n/a" + """ + + 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: - dict_unit["total"] = round(d["finished"] - d["start"], 4) #total - except: - dict_unit["total"] = "n/a" - - stats_list.append(dict_unit) - - return render_to_response('displays/timing.html', {'client_list': client_list, - 'stats_list': stats_list, - 'timestamp' : timestamp, - 'timestamp_date' : timestamp[:10], - 'timestamp_time' : timestamp[11:19]}) - -def display_index(request): - return render_to_response('displays/index.html') - -def prepare_client_lists(request, timestamp = 'now'): - #I suggest we implement "expiration" here. - - timestamp = timestamp.replace("@"," ") - #client_list = Client.objects.all().order_by('name')#change this to order by interaction's state - client_interaction_dict = {} - clean_client_list = [] - bad_client_list = [] - extra_client_list = [] - modified_client_list = [] - stale_up_client_list = [] - #stale_all_client_list = [] - down_client_list = [] - - cursor = connection.cursor() - - interact_queryset = Interaction.objects.interaction_per_client(timestamp) - # or you can specify a time like this: '2007-01-01 00:00:00' - [client_interaction_dict.__setitem__(x.client_id, x) for x in interact_queryset] - client_list = Client.objects.active(timestamp).filter(id__in=client_interaction_dict.keys()).order_by('name') - - [clean_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=[y.client_id for y in interact_queryset.filter(state='clean')])] - [bad_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=[y.client_id for y in interact_queryset.filter(state='dirty')])] - - client_ping_dict = {} - [client_ping_dict.__setitem__(x,'Y') for x in client_interaction_dict.keys()]#unless we know otherwise... + rec_start, rec_end = prepare_paginated_list(request, context, iquery, page, max_results) + except PaginationError, page_error: + 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: - cursor.execute("select reports_ping.status, x.client_id from (select client_id, MAX(endtime) "+ - "as timer from reports_ping GROUP BY client_id) x, reports_ping where "+ - "reports_ping.client_id = x.client_id AND reports_ping.endtime = x.timer") - [client_ping_dict.__setitem__(x[1], x[0]) for x in cursor.fetchall()] + total_pages = (nitems / int(max_results)) + 1 except: - pass #This is to fix problems when you have only zero records returned - - client_down_ids = [y for y in client_ping_dict.keys() if client_ping_dict[y]=='N'] - if not client_down_ids == []: - [down_client_list.append(x) for x in Client.objects.active(timestamp).filter(id__in=client_down_ids)] - - if (timestamp == 'now' or timestamp == None): - cursor.execute("select client_id, MAX(timestamp) as timestamp from reports_interaction GROUP BY client_id") - results = cursor.fetchall() - for x in results: - if type(x[1]) == type("") or type(x[1]) == type(u""): - ts = util.typecast_timestamp(x[1]) - else: - ts = x[1] - stale_all_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in results if datetime.now() - ts > timedelta(days=1)]) - else: - cursor.execute("select client_id, timestamp, MAX(timestamp) as timestamp from reports_interaction "+ - "WHERE timestamp < %s GROUP BY client_id", [timestamp]) - t = strptime(timestamp,"%Y-%m-%d %H:%M:%S") - datetimestamp = datetime(t[0], t[1], t[2], t[3], t[4], t[5]) - results = cursor.fetchall() - for x in results: - if type(x[1]) == type(""): - x[1] = util.typecast_timestamp(x[1]) - stale_all_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in results if datetimestamp - x[1] > timedelta(days=1)]) - - [stale_up_client_list.append(x) for x in stale_all_client_list if not client_ping_dict[x.id]=='N'] + total_pages = 1 + if page > total_pages: + # If we passed beyond the end send back + try: + view, args, kwargs = resolve(request.path) + kwargs['page_number'] = total_pages + raise PaginationError, HttpResponseRedirect( reverse(view, kwargs=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)) - - cursor.execute("SELECT reports_client.id FROM reports_client, reports_interaction, reports_entries_interactions WHERE reports_client.id = reports_interaction.client_id AND reports_client.current_interaction_id = reports_entries_interactions.interaction_id and reports_entries_interactions.type=%s GROUP BY reports_client.id", [TYPE_MODIFIED]) - modified_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()]) - - cursor.execute("SELECT reports_client.id FROM reports_client, reports_interaction, reports_entries_interactions WHERE reports_client.id = reports_interaction.client_id AND reports_client.current_interaction_id = reports_entries_interactions.interaction_id and reports_entries_interactions.type=%s GROUP BY reports_client.id", [TYPE_EXTRA]) - extra_client_list = Client.objects.active(timestamp).filter(id__in=[x[0] for x in cursor.fetchall()]) - - if timestamp == 'now': - timestamp = datetime.now().isoformat('@') - - return {'client_list': client_list, - 'client_interaction_dict':client_interaction_dict, - 'clean_client_list': clean_client_list, - 'bad_client_list': bad_client_list, - 'extra_client_list': extra_client_list, - 'modified_client_list': modified_client_list, - 'stale_up_client_list': stale_up_client_list, - 'stale_all_client_list': stale_all_client_list, - 'down_client_list': down_client_list, - 'timestamp' : timestamp, - 'timestamp_date' : timestamp[:10], - 'timestamp_time' : timestamp[11:19]} diff --git a/src/lib/Server/Reports/settings.py b/src/lib/Server/Reports/settings.py index 59d29114d..81220c0e3 100644 --- a/src/lib/Server/Reports/settings.py +++ b/src/lib/Server/Reports/settings.py @@ -1,3 +1,5 @@ +import django + # Django settings for bcfg2 reports project. from ConfigParser import ConfigParser, NoSectionError, NoOptionError c = ConfigParser() @@ -62,7 +64,9 @@ MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. # Example: "http://media.lawrence.com" -MEDIA_URL = '' +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. @@ -109,9 +113,26 @@ 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/', - '/usr/share/bcfg2/Reports/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', diff --git a/src/lib/Server/Reports/urls.py b/src/lib/Server/Reports/urls.py index e1326b5ea..5d298c974 100644 --- a/src/lib/Server/Reports/urls.py +++ b/src/lib/Server/Reports/urls.py @@ -1,4 +1,7 @@ from django.conf.urls.defaults import * +from django.http import HttpResponsePermanentRedirect + +handler500 = 'Bcfg2.Server.Reports.reports.views.server_error' from ConfigParser import ConfigParser, NoSectionError, NoOptionError c = ConfigParser() @@ -9,56 +12,15 @@ c.read(['/etc/bcfg2.conf', '/etc/bcfg2-web.conf']) # web_prefix_root is a workaround for the index if c.has_option('statistics', 'web_prefix'): web_prefix = c.get('statistics', 'web_prefix').lstrip('/') - web_prefix_root = web_prefix else: web_prefix = '' - web_prefix_root = '/' urlpatterns = patterns('', - # Example: - # (r'^%sBcfg2.Server.Reports/' % web_prefix, include('Bcfg2.Server.Reports.apps.foo.urls.foo')), - (r'^%s*$' % web_prefix_root,'Bcfg2.Server.Reports.reports.views.index'), - - (r'^%sclients-detailed/state/(?P<state>\w+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'), - (r'^%sclients-detailed/server/(?P<server>[\w\-\.]+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'), - (r'^%sclients-detailed/server/(?P<server>[\w\-\.]+)/(?P<state>[A-Za-z]+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'), - (r'^%sclients-detailed/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_detailed_list'), - (r'^%sclients/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'), - (r'^%sclients/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'), - (r'^%sclients/(?P<hostname>\S+)/(?P<pk>\d+)/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'), - (r'^%sclients/(?P<hostname>\S+)/manage/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_manage'), - (r'^%sclients/(?P<hostname>\S+)/$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'), - (r'^%sclients/(?P<hostname>\S+)$' % web_prefix, 'Bcfg2.Server.Reports.reports.views.client_detail'), - #hack because hostnames have periods and we still want to append slash - (r'^%sclients/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.client_index'), - (r'^%sdisplays/sys-view/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_sys_view'), - (r'^%sdisplays/sys-view/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_sys_view'), - (r'^%sdisplays/summary/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_summary'), - (r'^%sdisplays/summary/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_summary'), - (r'^%sdisplays/timing/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_timing'), - (r'^%sdisplays/timing/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_timing'), - (r'^%sdisplays/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.display_index'), - - (r'^%selements/modified/(?P<eyedee>\d+)/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_modified'), - (r'^%selements/modified/(?P<eyedee>\d+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_modified'), - (r'^%selements/modified/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01]\ - [0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.modified_item_index'), - (r'^%selements/modified/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.modified_item_index'), - (r'^%selements/bad/(?P<eyedee>\d+)/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01][0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_bad'), - (r'^%selements/bad/(?P<eyedee>\d+)/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.config_item_bad'), - (r'^%selements/bad/(?P<timestamp>(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])@([01]\ - [0-9]|2[0-3]):([0-5][0-9]|60):([0-5][0-9]|60))/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.bad_item_index'), - (r'^%selements/bad/$' % web_prefix,'Bcfg2.Server.Reports.reports.views.bad_item_index'), + (r'^%s' % web_prefix, include('Bcfg2.Server.Reports.reports.urls')) ) - # Uncomment this for admin: - #(r'^%sadmin/' % web_prefix, include('django.contrib.admin.urls')), - - -## Uncomment this section if using authentication -#urlpatterns += patterns('', -# (r'^%slogin/$' % web_prefix, 'django.contrib.auth.views.login', -# {'template_name': 'auth/login.html'}), -# (r'^%slogout/$' % web_prefix, 'django.contrib.auth.views.logout', -# {'template_name': 'auth/logout.html'}) -# ) +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/Server/Reports/utils.py b/src/lib/Server/Reports/utils.py index 2ef21e446..b74f09e74 100755 --- a/src/lib/Server/Reports/utils.py +++ b/src/lib/Server/Reports/utils.py @@ -1,7 +1,13 @@ -'''Helper functions for reports''' +"""Helper functions for reports""" +from Bcfg2.Server.Reports.reports.models import TYPE_CHOICES +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''' + """Fetch Django objects in smaller batches to save memory""" def __init__(self, obj, step=10000): self.count = 0 @@ -15,8 +21,8 @@ class BatchFetch(object): return self def next(self): - '''Return the next object from our array and fetch from the - database when needed''' + """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: @@ -28,3 +34,83 @@ class BatchFetch(object): 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 + |