From 4c9eba6b66e93acca71aae2eacc18d00b38e88fa Mon Sep 17 00:00:00 2001
From: Stephen Soltesz <soltesz@cs.princeton.edu>
Date: Wed, 25 Mar 2009 17:47:08 +0000
Subject: [PATCH] update to db model.  now uses automatic history on updates:
 acts_as_versioned() updated files that used old '*Sync' and old messy
 Findbad*Records.

---
 bootman.py                                    |  8 ++-
 findbad.py                                    | 22 +++---
 findbadpcu.py                                 | 21 +++---
 monitor/common.py                             |  8 +--
 monitor/database/info/findbad.py              | 72 ++++++++++---------
 monitor/database/info/history.py              |  7 ++
 monitor/reboot.py                             |  2 +-
 monitor/scanapi.py                            | 33 +++++----
 monitor/wrapper/emailTxt.py                   | 12 +++-
 nodebad.py                                    | 40 +++++++----
 nodegroups.py                                 |  2 +-
 nodeinfo.py                                   |  2 +-
 nodequery.py                                  |  4 +-
 pcubad.py                                     |  2 +-
 web/MonitorWeb/monitorweb/controllers.py      | 21 +++---
 web/MonitorWeb/monitorweb/templates/links.py  |  2 +
 .../monitorweb/templates/pcuview.kid          |  7 +-
 17 files changed, 154 insertions(+), 111 deletions(-)

diff --git a/bootman.py b/bootman.py
index 640f9ee..0cd88ec 100755
--- a/bootman.py
+++ b/bootman.py
@@ -82,11 +82,11 @@ class NodeConnection:
 			print "   ERROR:", x
 			print "   Possibly, unable to find valid configuration file"
 
-		if bm_continue and self.config and not self.config.quiet:
+		if bm_continue:
 			for key in bm.VARS.keys():
 				print key, " == ", bm.VARS[key]
 		else:
-			if self.config and not self.config.quiet: print "   Unable to read Node Configuration"
+			print "   Unable to read Node Configuration"
 		
 
 	def compare_and_repair_nodekeys(self):
@@ -308,7 +308,8 @@ def reboot(hostname, config=None, forced_action=None):
 	# NOTE: Nothing works if the bootcd is REALLY old.
 	#       So, this is the first step.
 	fbnode = FindbadNodeRecord.get_latest_by(hostname=hostname).to_dict()
-	if fbnode['category'] == "OLDBOOTCD":
+	print fbnode.keys()
+	if fbnode['observed_category'] == "OLDBOOTCD":
 		print "...NOTIFY OWNER TO UPDATE BOOTCD!!!"
 		args = {}
 		args['hostname_list'] = "    %s" % hostname
@@ -594,6 +595,7 @@ def reboot(hostname, config=None, forced_action=None):
 			# actual solution appears to involve removing the bad files, and
 			# continually trying to boot the node.
 			"bminit-cfg-auth-getplc-update-installinit-validate-rebuildinitrd-netcfg-disk-update4-update3-update3-implementerror-update-debug-done",
+			"bminit-cfg-auth-getplc-installinit-validate-exception-bmexceptmount-exception-noinstall-update-debug-done",
 			]:
 		sequences.update({n : "restart_bootmanager_rins"})
 
diff --git a/findbad.py b/findbad.py
index f01f31f..12b0080 100755
--- a/findbad.py
+++ b/findbad.py
@@ -12,7 +12,7 @@ from monitor.util import file
 from pcucontrol.util import command
 from monitor import config
 
-from monitor.database.info.model import FindbadNodeRecordSync, FindbadNodeRecord, session
+from monitor.database.info.model import FindbadNodeRecord, session
 
 from monitor.sources import comon
 from monitor.wrapper import plc, plccache
@@ -53,9 +53,10 @@ def checkAndRecordState(l_nodes, cohash):
 
 	# CREATE all the work requests
 	for nodename in l_nodes:
-		fbnodesync = FindbadNodeRecordSync.findby_or_create(hostname=nodename, if_new_set={'round':0})
-		node_round   = fbnodesync.round
-		fbnodesync.flush()
+		#fbnodesync = FindbadNodeRecordSync.findby_or_create(hostname=nodename, if_new_set={'round':0})
+		#node_round   = fbnodesync.round
+		node_round = global_round - 1
+		#fbnodesync.flush()
 
 		if node_round < global_round or config.force:
 			# recreate node stats when refreshed
@@ -86,16 +87,16 @@ def checkAndRecordState(l_nodes, cohash):
 			print "All results collected."
 			break
 
-	print FindbadNodeRecordSync.query.count()
+	#print FindbadNodeRecordSync.query.count()
 	print FindbadNodeRecord.query.count()
 	session.flush()
 
 def main():
 	global global_round
 
-	fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
-													if_new_set={'round' : global_round})
-	global_round = fbsync.round
+	#fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
+	#												if_new_set={'round' : global_round})
+	#global_round = fbsync.round
 
 	if config.increment:
 		# update global round number to force refreshes across all nodes
@@ -145,8 +146,9 @@ def main():
 
 	if config.increment:
 		# update global round number to force refreshes across all nodes
-		fbsync.round = global_round
-		fbsync.flush()
+		#fbsync.round = global_round
+		#fbsync.flush()
+		pass
 
 	return 0
 
diff --git a/findbadpcu.py b/findbadpcu.py
index d00d7f7..c6e6a8f 100755
--- a/findbadpcu.py
+++ b/findbadpcu.py
@@ -14,7 +14,7 @@ import threading
 
 import monitor
 from monitor import config
-from monitor.database.info.model import FindbadPCURecordSync, FindbadPCURecord, session
+from monitor.database.info.model import FindbadPCURecord, session
 from monitor import database
 from monitor import util 
 from monitor.wrapper import plc, plccache
@@ -43,10 +43,11 @@ def checkPCUs(l_pcus, cohash):
 	# CREATE all the work requests
 	for pcuname in l_pcus:
 		pcu_id = int(pcuname)
-		fbnodesync = FindbadPCURecordSync.findby_or_create(plc_pcuid=pcu_id, if_new_set={'round' : 0})
-		fbnodesync.flush()
+		#fbnodesync = FindbadPCURecordSync.findby_or_create(plc_pcuid=pcu_id, if_new_set={'round' : 0})
+		#fbnodesync.flush()
 
-		node_round   = fbnodesync.round
+		#node_round   = fbnodesync.round
+		node_round   = global_round - 1
 		if node_round < global_round or config.force:
 			# recreate node stats when refreshed
 			#print "%s" % nodename
@@ -75,7 +76,7 @@ def checkPCUs(l_pcus, cohash):
 			print "All results collected."
 			break
 
-	print FindbadPCURecordSync.query.count()
+	#print FindbadPCURecordSync.query.count()
 	print FindbadPCURecord.query.count()
 	session.flush()
 
@@ -86,10 +87,10 @@ def main():
 	l_pcus = plccache.l_pcus
 	cohash = {}
 
-	fbsync = FindbadPCURecordSync.findby_or_create(plc_pcuid=0, 
-											if_new_set={'round' : global_round})
+	#fbsync = FindbadPCURecordSync.findby_or_create(plc_pcuid=0, 
+											#if_new_set={'round' : global_round})
 
-	global_round = fbsync.round
+	#global_round = fbsync.round
 	api = plc.getAuthAPI()
 
 	if config.site is not None:
@@ -139,8 +140,8 @@ def main():
 
 	if config.increment:
 		# update global round number to force refreshes across all nodes
-		fbsync.round = global_round
-		fbsync.flush()
+		#fbsync.round = global_round
+		#fbsync.flush()
 		session.flush()
 
 	return 0
diff --git a/monitor/common.py b/monitor/common.py
index 6f88051..0f6dd40 100644
--- a/monitor/common.py
+++ b/monitor/common.py
@@ -224,17 +224,17 @@ def email_exception(content=None):
 
 def changed_lessthan(last_changed, days):
 	if datetime.now() - last_changed <= timedelta(days):
-		print "last changed less than %s" % timedelta(days)
+		#print "last changed less than %s" % timedelta(days)
 		return True
 	else:
-		print "last changed more than %s" % timedelta(days)
+		#print "last changed more than %s" % timedelta(days)
 		return False
 
 def changed_greaterthan(last_changed, days):
 	if datetime.now() - last_changed > timedelta(days):
-		print "last changed more than %s" % timedelta(days)
+		#print "last changed more than %s" % timedelta(days)
 		return True
 	else:
-		print "last changed less than %s" % timedelta(days)
+		#print "last changed less than %s" % timedelta(days)
 		return False
 	
diff --git a/monitor/database/info/findbad.py b/monitor/database/info/findbad.py
index 66859b1..b437842 100644
--- a/monitor/database/info/findbad.py
+++ b/monitor/database/info/findbad.py
@@ -4,54 +4,58 @@ from elixir import String, Integer as Int, DateTime, PickleType, Boolean
 from datetime import datetime,timedelta
 import elixir
 import traceback
+from elixir.ext.versioned import *
 
 from monitor.database.dborm import mon_metadata, mon_session
 __metadata__ = mon_metadata
 __session__  = mon_session
 
 
-class FindbadNodeRecordSync(Entity):
-	hostname = Field(String(250),primary_key=True) #,alternateMethodName='by_hostname')
-	round    = Field(Int,default=0)
+#class FindbadNodeRecordSync(Entity):
+#	hostname = Field(String(250),primary_key=True) #,alternateMethodName='by_hostname')
+#	round    = Field(Int,default=0)
 	
-class FindbadPCURecordSync(Entity):
-	plc_pcuid = Field(Int,primary_key=True) #,alternateMethodName='by_pcuid')
-	round     = Field(Int,default=0)
+#class FindbadPCURecordSync(Entity):
+#	plc_pcuid = Field(Int,primary_key=True) #,alternateMethodName='by_pcuid')
+#	round     = Field(Int,default=0)
 
 class FindbadNodeRecord(Entity):
 	@classmethod
 	def get_all_latest(cls):
-		fbsync = FindbadNodeRecordSync.get_by(hostname="global")
-		if fbsync:
-			return cls.query.filter_by(round=fbsync.round)
-		else:
-			return []
+		return cls.query.all()
+		#fbsync = FindbadNodeRecordSync.get_by(hostname="global")
+		#if fbsync:
+		#	return cls.query.filter_by(round=fbsync.round)
+		#else:
+		#	return []
 
 	@classmethod
 	def get_latest_by(cls, **kwargs):
-		fbsync = FindbadNodeRecordSync.get_by(hostname="global")
-		if fbsync:
-			kwargs['round'] = fbsync.round
-			return cls.query.filter_by(**kwargs).order_by(FindbadNodeRecord.date_checked.desc())
-		else:
-			return []
+		return cls.query.filter_by(**kwargs).first()
+		#fbsync = FindbadNodeRecordSync.get_by(hostname="global")
+		#if fbsync:
+		#	kwargs['round'] = fbsync.round
+		#	return cls.query.filter_by(**kwargs).order_by(FindbadNodeRecord.date_checked.desc())
+		#else:
+		#	return []
 
 	@classmethod
 	def get_latest_n_by(cls, n=3, **kwargs):
-		fbsync = FindbadNodeRecordSync.get_by(hostname="global")
-		kwargs['round'] = fbsync.round
-		ret = []
-		for i in range(0,n):
-			kwargs['round'] = kwargs['round'] - i
-			f = cls.query.filter_by(**kwargs).first()
-			if f:
-				ret.append(f)
-		return ret
+		return cls.query.filter_by(**kwargs)
+		#fbsync = FindbadNodeRecordSync.get_by(hostname="global")
+		#kwargs['round'] = fbsync.round
+		#ret = []
+		#for i in range(0,n):
+		#	kwargs['round'] = kwargs['round'] - i
+		#	f = cls.query.filter_by(**kwargs).first()
+		#	if f:
+		#		ret.append(f)
+		#return ret
 
 # ACCOUNTING
 	date_checked = Field(DateTime,default=datetime.now)
 	round = Field(Int,default=0)
-	hostname = Field(String,default=None)
+	hostname = Field(String,primary_key=True,default=None)
 	loginbase = Field(String)
 
 # INTERNAL
@@ -79,23 +83,19 @@ class FindbadNodeRecord(Entity):
 	observed_category = Field(String,default=None)
 	observed_status = Field(String,default=None)
 
+	acts_as_versioned(ignore=['date_checked'])
 	# NOTE: this is the child relation
 	#action = ManyToOne('ActionRecord', required=False)
 
 class FindbadPCURecord(Entity):
 	@classmethod
 	def get_all_latest(cls):
-		fbsync = FindbadPCURecordSync.get_by(plc_pcuid=0)
-		if fbsync:
-			return cls.query.filter_by(round=fbsync.round)
-		else:
-			return []
+		return cls.query.all()
 
 	@classmethod
 	def get_latest_by(cls, **kwargs):
-		fbsync = FindbadPCURecordSync.get_by(plc_pcuid=0)
-		kwargs['round'] = fbsync.round
-		return cls.query.filter_by(**kwargs).order_by(FindbadPCURecord.date_checked.desc())
+		return cls.query.filter_by(**kwargs)
+
 # ACCOUNTING
 	date_checked = Field(DateTime)
 	round = Field(Int,default=0)
@@ -110,3 +110,5 @@ class FindbadPCURecord(Entity):
 # INTERNAL
 # INFERRED
 	reboot_trial_status = Field(String)
+
+	acts_as_versioned(ignore=['date_checked'])
diff --git a/monitor/database/info/history.py b/monitor/database/info/history.py
index 6f04c44..3c5842a 100644
--- a/monitor/database/info/history.py
+++ b/monitor/database/info/history.py
@@ -1,6 +1,8 @@
 from elixir import Entity, Field, OneToMany, ManyToOne, ManyToMany
 from elixir import options_defaults, using_options, setup_all
 from elixir import String, Integer as Int, DateTime, Boolean
+from elixir.ext.versioned import *
+
 from datetime import datetime,timedelta
 
 from monitor.database.dborm import mon_metadata, mon_session
@@ -13,6 +15,7 @@ class HistoryNodeRecord(Entity):
 	last_checked = Field(DateTime,default=datetime.now)
 	last_changed = Field(DateTime,default=datetime.now)
 	status = Field(String,default="unknown")
+	acts_as_versioned(ignore=['last_changed', 'last_checked'])
 
 	@classmethod
 	def by_hostname(cls, hostname):
@@ -28,10 +31,13 @@ class HistoryPCURecord(Entity):
 	last_valid = Field(DateTime,default=None)
 	valid  = Field(String,default="unknown")
 
+	acts_as_versioned(ignore=['last_changed', 'last_checked'])
+
 	@classmethod
 	def by_pcuid(cls, pcuid):
 		return cls.query.filter_by(pcuid=pcuid).first()
 
+
 class HistorySiteRecord(Entity):
 	loginbase = Field(String(250),primary_key=True)
 
@@ -57,6 +63,7 @@ class HistorySiteRecord(Entity):
 
 	penalty_level   = Field(Int, default=0)
 	penalty_applied = Field(Boolean, default=False)
+	acts_as_versioned(ignore=['last_changed', 'last_checked'])
 
 	@classmethod
 	def by_loginbase(cls, loginbase):
diff --git a/monitor/reboot.py b/monitor/reboot.py
index 289fb47..15d5c52 100755
--- a/monitor/reboot.py
+++ b/monitor/reboot.py
@@ -45,7 +45,7 @@ def get_pcu_values(pcu_id):
 	from monitor.database.info.model import FindbadPCURecord
 	print "pcuid: %s" % pcu_id
 	try:
-		pcurec = FindbadPCURecord.get_latest_by(plc_pcuid=pcu_id).first()
+		pcurec = FindbadPCURecord.get_latest_by(plc_pcuid=pcu_id)
 		if pcurec:
 			values = pcurec.to_dict()
 		else:
diff --git a/monitor/scanapi.py b/monitor/scanapi.py
index e1a09e8..963822d 100644
--- a/monitor/scanapi.py
+++ b/monitor/scanapi.py
@@ -112,7 +112,7 @@ class ScanInterface(object):
 	syncclass = None
 	primarykey = 'hostname'
 
-	def __init__(self, round):
+	def __init__(self, round=1):
 		self.round = round
 		self.count = 1
 
@@ -133,22 +133,24 @@ class ScanInterface(object):
 		try:
 			if values is None:
 				return
-
-			fbnodesync = self.syncclass.findby_or_create(
-												if_new_set={'round' : self.round},
+			
+			if self.syncclass:
+				fbnodesync = self.syncclass.findby_or_create(
+												#if_new_set={'round' : self.round},
 												**{ self.primarykey : nodename})
 			# NOTE: This code will either add a new record for the new self.round, 
 			# 	OR it will find the previous value, and update it with new information.
 			#	The data that is 'lost' is not that important, b/c older
 			#	history still exists.  
 			fbrec = self.recordclass.findby_or_create(
-						**{'round':self.round, self.primarykey:nodename})
+						**{ self.primarykey:nodename})
 
 			fbrec.set( **values ) 
 
 			fbrec.flush()
-			fbnodesync.round = self.round
-			fbnodesync.flush()
+			if self.syncclass:
+				fbnodesync.round = self.round
+				fbnodesync.flush()
 
 			print "%d %s %s" % (self.count, nodename, values)
 			self.count += 1
@@ -160,7 +162,8 @@ class ScanInterface(object):
 
 class ScanNodeInternal(ScanInterface):
 	recordclass = FindbadNodeRecord
-	syncclass = FindbadNodeRecordSync
+	#syncclass = FindbadNodeRecordSync
+	syncclass = None
 	primarykey = 'hostname'
 
 	def collectNMAP(self, nodename, cohash):
@@ -375,9 +378,9 @@ EOF				""")
 		return (nodename, values)
 
 def internalprobe(hostname):
-	fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
-													if_new_set={'round' : 1})
-	scannode = ScanNodeInternal(fbsync.round)
+	#fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
+	#												if_new_set={'round' : 1})
+	scannode = ScanNodeInternal() # fbsync.round)
 	try:
 		(nodename, values) = scannode.collectInternal(hostname, {})
 		scannode.record(None, (nodename, values))
@@ -388,9 +391,9 @@ def internalprobe(hostname):
 		return False
 
 def externalprobe(hostname):
-	fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
-													if_new_set={'round' : 1})
-	scannode = ScanNodeInternal(fbsync.round)
+	#fbsync = FindbadNodeRecordSync.findby_or_create(hostname="global", 
+	#												if_new_set={'round' : 1})
+	scannode = ScanNodeInternal() # fbsync.round)
 	try:
 		(nodename, values) = scannode.collectNMAP(hostname, {})
 		scannode.record(None, (nodename, values))
@@ -402,7 +405,7 @@ def externalprobe(hostname):
 
 class ScanPCU(ScanInterface):
 	recordclass = FindbadPCURecord
-	syncclass = FindbadPCURecordSync
+	syncclass = None
 	primarykey = 'plc_pcuid'
 
 	def collectInternal(self, pcuname, cohash):
diff --git a/monitor/wrapper/emailTxt.py b/monitor/wrapper/emailTxt.py
index 675068a..385ac63 100644
--- a/monitor/wrapper/emailTxt.py
+++ b/monitor/wrapper/emailTxt.py
@@ -231,12 +231,20 @@ This notice is simply to test whether notices work.
 
 Thank you very much for your help!
 	""")
-	offline_notice=("""Host %(hostname)s is offline""",
+	retry_bootman=("""Running BootManager on %(hostname)s""",
 	"""
 This notice is simply to let you know that:
     %(hostname)s
 
-is offline and or non-operational.  Please investigate, thank you very much for your help!
+appears stuck in a debug mode.  To try to correct this, we're trying to rerun BootManager.py.  
+If any action is needed from you, you will recieve additional notices.  Thank you!
+	""")
+	down_notice=("""Host %(hostname)s is down""",
+	"""
+This notice is simply to let you know that:
+    %(hostname)s
+
+is down, disconnected from the network and/or non-operational.  Please investigate, thank you very much for your help!
 	""")
 
 	clear_penalty=("""All penalties have been cleared from site %(loginbase)s""",
diff --git a/nodebad.py b/nodebad.py
index 90c3be0..a0490e4 100755
--- a/nodebad.py
+++ b/nodebad.py
@@ -44,35 +44,49 @@ def check_node_state(rec, node):
 		boot_state = "unknown"
 		last_contact = None
 
-	if node_state == 'DOWN' and ( node.status == 'online' or node.status == 'good' ):
+	# NOTE: 'DOWN' and 'DEBUG'  are temporary states, so only need
+	# 			'translations' into the node.status state
+	#		'BOOT' is a permanent state, but we want it to have a bit of
+	#			hysteresis (less than 0.5 days)
+
+	#################################################################3
+	# "Translate" the findbad states into nodebad status.
+
+	if node_state == 'DOWN' and ( node.status != 'offline' and node.status != 'down' ) and boot_state != 'disable' :
 		print "changed status from %s to offline" % node.status
 		node.status = 'offline'
 		node.last_changed = datetime.now()
 
-	if node_state == 'BOOT' and changed_lessthan(node.last_changed, 0.5) and node.status != 'online':
+	if node_state == 'DEBUG' and node.status != 'monitordebug':
+		print "changed status from %s to monitordebug" % (node.status)
+		node.status = "monitordebug"
+		node.last_changed = datetime.now()
+
+	if node_state == 'BOOT' and node.status != 'online' and node.status != 'good':
 		print "changed status from %s to online" % node.status
 		node.status = 'online'
 		node.last_changed = datetime.now()
 
+	#################################################################3
+	# Switch temporary hystersis states into their 'firm' states.
+
 	if node.status == 'online' and changed_greaterthan(node.last_changed, 0.5):
-		#send thank you notice, or on-line notice.
 		print "changed status from %s to good" % node.status
 		node.status = 'good'
 		# NOTE: do not reset last_changed, or you lose how long it's been up.
 
-	#if node.status == 'offline' and changed_greaterthan(node.last_changed, 1): #  and pcu.status == 'good' 
-	#	# attempt reboots
-	#	pass
-	#if node.status == 'offline' and changed_greaterthan(node.last_changed, 1.5): # and node.has_pcu
-	#	# send PCU failure message
-	#	pass
-
 	if node.status == 'offline' and changed_greaterthan(node.last_changed, 2):
 		print "changed status from %s to down" % node.status
-		# send down node notice
 		node.status = 'down'
-		node.last_changed = datetime.now()
+		# NOTE: do not reset last_changed, or you lose how long it's been down.
+
+	if node.status == 'monitordebug' and changed_greaterthan(node.last_changed, 14):
+		print "changed status from %s to down" % node.status
+		node.status = 'down'
+		# NOTE: do not reset last_changed, or you lose how long it's been down.
+		#node.last_changed = datetime.now()
 
+	# extreme cases of offline nodes
 	if ( boot_state == 'disabled' or last_contact == None ) and \
 			changed_greaterthan(node.last_changed, 2*30) and \
 			node.status != 'down':
@@ -92,7 +106,7 @@ def checkAndRecordState(l_nodes, l_plcnodes):
 
 		try:
 			# Find the most recent record
-			noderec = FindbadNodeRecord.query.filter(FindbadNodeRecord.hostname==nodename).order_by(FindbadNodeRecord.date_checked.desc()).first()
+			noderec = FindbadNodeRecord.get_latest_by(hostname=nodename)
 		except:
 			print "COULD NOT FIND %s" % nodename
 			import traceback
diff --git a/nodegroups.py b/nodegroups.py
index d6beb54..056f5b8 100755
--- a/nodegroups.py
+++ b/nodegroups.py
@@ -121,7 +121,7 @@ def main():
 		i = 1
 		for node in nodelist:
 			print "%-2d" % i, 
-			fbrec = FindbadNodeRecord.query.filter(FindbadNodeRecord.hostname==node['hostname']).order_by(FindbadNodeRecord.date_checked.desc()).first()
+			fbrec = FindbadNodeRecord.get_latest_by(hostname=node['hostname'])
 			fbdata = fbrec.to_dict()
 			print nodegroup_display(node, fbdata, config)
 			i += 1
diff --git a/nodeinfo.py b/nodeinfo.py
index 3248707..e599d24 100755
--- a/nodeinfo.py
+++ b/nodeinfo.py
@@ -7,7 +7,7 @@ from monitor import *
 from monitor import util
 from monitor import parser as parsermodule
 
-from monitor import database
+from monitor.database.info.model import *
 from monitor import reboot
 
 import time
diff --git a/nodequery.py b/nodequery.py
index 7a53df0..1f41ceb 100755
--- a/nodequery.py
+++ b/nodequery.py
@@ -16,7 +16,7 @@ import string
 from monitor.wrapper import plc, plccache
 api = plc.getAuthAPI()
 
-from monitor.database.info.model import FindbadNodeRecordSync, FindbadNodeRecord, FindbadPCURecord, session
+from monitor.database.info.model import FindbadNodeRecord, FindbadPCURecord, session
 from monitor import util
 from monitor import config
 
@@ -412,7 +412,7 @@ def main():
 
 		try:
 			# Find the most recent record
-			fb_noderec = FindbadNodeRecord.query.filter(FindbadNodeRecord.hostname==node).order_by(FindbadNodeRecord.date_checked.desc()).first()
+			fb_noderec = FindbadNodeRecord.get_latest_by(hostname=node) 
 		except:
 			print traceback.print_exc()
 			pass
diff --git a/pcubad.py b/pcubad.py
index 5d14475..0ce1a9e 100755
--- a/pcubad.py
+++ b/pcubad.py
@@ -104,7 +104,7 @@ def checkAndRecordState(l_pcus, l_plcpcus):
 
 		try:
 			# Find the most recent record
-			pcurec = FindbadPCURecord.query.filter(FindbadPCURecord.plc_pcuid==pcuname).order_by(FindbadPCURecord.date_checked.desc()).first()
+			pcurec = FindbadPCURecord.query.filter(FindbadPCURecord.plc_pcuid==pcuname).first()
 		except:
 			print "COULD NOT FIND FB record for %s" % reboot.pcu_name(d_pcu)
 			import traceback
diff --git a/web/MonitorWeb/monitorweb/controllers.py b/web/MonitorWeb/monitorweb/controllers.py
index 617c46a..1178aa1 100644
--- a/web/MonitorWeb/monitorweb/controllers.py
+++ b/web/MonitorWeb/monitorweb/controllers.py
@@ -292,12 +292,14 @@ class Root(controllers.RootController):
 			exceptions = data['exceptions']
 
 		if loginbase:
-			actions = ActionRecord.query.filter_by(loginbase=loginbase).order_by(ActionRecord.date_created.desc())
+			actions = ActionRecord.query.filter_by(loginbase=loginbase
+							).filter(ActionRecord.date_created >= datetime.now() - timedelta(7)
+							).order_by(ActionRecord.date_created.desc())
 			actions = [ a for a in actions ]
 			sitequery = [HistorySiteRecord.by_loginbase(loginbase)]
 			pcus = {}
 			for plcnode in site_lb2hn[loginbase]:
-				for node in FindbadNodeRecord.get_latest_by(hostname=plcnode['hostname']):
+					node = FindbadNodeRecord.get_latest_by(hostname=plcnode['hostname'])
 					# NOTE: reformat some fields.
 					prep_node_for_display(node)
 					nodequery += [node]
@@ -311,18 +313,17 @@ class Root(controllers.RootController):
 
 		if pcuid and hostname is None:
 			print "pcuid: %s" % pcuid
-			for pcu in FindbadPCURecord.get_latest_by(plc_pcuid=pcuid):
-				# NOTE: count filter
-				prep_pcu_for_display(pcu)
-				pcuquery += [pcu]
+			pcu = FindbadPCURecord.get_latest_by(plc_pcuid=pcuid)
+			# NOTE: count filter
+			prep_pcu_for_display(pcu)
+			pcuquery += [pcu]
 			if 'site_id' in pcu.plc_pcu_stats:
 				sitequery = [HistorySiteRecord.by_loginbase(pcu.loginbase)]
 				
 			if 'nodenames' in pcu.plc_pcu_stats:
 				for nodename in pcu.plc_pcu_stats['nodenames']: 
 					print "query for %s" % nodename
-					q = FindbadNodeRecord.get_latest_by(hostname=nodename)
-					node = q.first()
+					node = FindbadNodeRecord.get_latest_by(hostname=nodename)
 					print "%s" % node.port_status
 					print "%s" % node.to_dict()
 					print "%s" % len(q.all())
@@ -331,13 +332,13 @@ class Root(controllers.RootController):
 						nodequery += [node]
 
 		if hostname and pcuid is None:
-			for node in FindbadNodeRecord.get_latest_by(hostname=hostname):
+				node = FindbadNodeRecord.get_latest_by(hostname=hostname)
 				# NOTE: reformat some fields.
 				prep_node_for_display(node)
 				sitequery = [node.site]
 				nodequery += [node]
 				if node.plc_pcuid: 	# not None
-					pcu = FindbadPCURecord.get_latest_by(plc_pcuid=node.plc_pcuid).first()
+					pcu = FindbadPCURecord.get_latest_by(plc_pcuid=node.plc_pcuid)
 					prep_pcu_for_display(pcu)
 					pcuquery += [pcu]
 			
diff --git a/web/MonitorWeb/monitorweb/templates/links.py b/web/MonitorWeb/monitorweb/templates/links.py
index 6b47bb1..2bc6917 100644
--- a/web/MonitorWeb/monitorweb/templates/links.py
+++ b/web/MonitorWeb/monitorweb/templates/links.py
@@ -2,6 +2,8 @@ from monitor import config
 import turbogears as tg
 import urllib
 
+def plc_mail_uri(ticketid):
+	return config.RT_WEB_SERVER + "/Ticket/Display.html?id=" + str(ticketid)
 def plc_node_uri(hostname):
 	return "https://" + config.PLC_WWW_HOSTNAME + "/db/nodes/index.php?nodepattern=" + str(hostname)
 def plc_site_uri(loginbase):
diff --git a/web/MonitorWeb/monitorweb/templates/pcuview.kid b/web/MonitorWeb/monitorweb/templates/pcuview.kid
index a39810e..694fc4d 100644
--- a/web/MonitorWeb/monitorweb/templates/pcuview.kid
+++ b/web/MonitorWeb/monitorweb/templates/pcuview.kid
@@ -194,7 +194,7 @@ from links import *
 		<div id="status_block" class="flash"
             py:if="value_of('tg_flash', None)" py:content="tg_flash"></div>
 
-	<h4>Recent Actions</h4>
+	<h4>Actions Over the Last Week</h4>
 		<p py:if="actions and len(actions) == 0">
 			There are no recent actions taken for this site.
 		</p>
@@ -223,8 +223,9 @@ from links import *
 					</td>
 					<!--td py : content="diff_time(mktime(node.date_checked.timetuple()))"></td-->
 					<td py:content="act.action_type"></td>
-					<td py:content="act.message_id"></td>
-					<td py:content="act.error_string"></td>
+					<td><a class="ext-link" href="${plc_mail_uri(act.message_id)}">
+							<span class="icon">${act.message_id}</span></a></td>
+					<td><pre py:content="act.error_string"></pre></td>
 				</tr>
 			</tbody>
 		</table>
-- 
2.47.0