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