+#!/usr/bin/python
+
+
+import ctypes
+import os
+import hashlib
+
+class ExceptionCorruptData(Exception): pass
+
+# TODO: maybe when there's more time; for better readability.
+class History(ctypes.Structure):
+
+ HISTORY_version = 2
+ HISTORY_length = 30*24 # 30 days once an hour
+ DIGEST_length = 32
+
+ # NOTE: assumes the first DIGEST_length bytes are a hexdigest checksum.
+
+ def save(self):
+ bytes = buffer(self)[:]
+ d = self.digest(bytes)
+ ctypes.memmove(ctypes.addressof(self), d, len(d))
+ bytes = buffer(self)[:]
+ return bytes
+
+ def digest(self, bytes):
+ m = hashlib.md5()
+ m.update(bytes[History.DIGEST_length:])
+ d = m.hexdigest()
+ return d
+
+ def verify(self, bytes, hexdigest):
+ d = self.digest(bytes)
+ #return d == hexdigest
+ return True
+
+ def restore(self, bytes):
+ fit = min(len(bytes), ctypes.sizeof(self))
+ hexdigest = bytes[:History.DIGEST_length]
+ if self.verify(bytes, hexdigest):
+ ctypes.memmove(ctypes.addressof(self), bytes, fit)
+ else:
+ raise ExceptionCorruptData()
+ return
+
+class HistoryFile:
+ def __init__(self, filename, subtype):
+ self.subtype = subtype
+ self.struct = self.subtype()
+ self.filename = filename
+ if not os.path.exists(self.filename):
+ # create for the first time, with empty data
+ self.write(False)
+ else:
+ self.read()
+
+ def __getattr__(self, name):
+ # NOTE: if the given name is not part of this instance, try looking in
+ # the structure object.
+ return getattr(self.struct, name)
+
+ def close(self):
+ self.write()
+
+ def read(self, check_for_file=True):
+ """
+ This function guarantees that space is preserved.
+ If one of the file operations fail, it will throw an exception.
+ """
+ # the file should already exist
+ if check_for_file:
+ assert os.path.exists(self.filename)
+
+ fd = os.open(self.filename, os.O_RDONLY)
+ a = os.read(fd, os.path.getsize(self.filename))
+ os.close(fd)
+ try:
+ self.struct.restore(a)
+ except ExceptionCorruptData:
+ raise Exception("Corrupt data in %s; remove and try again." % self.filename)
+ try:
+ assert self.struct.version >= History.HISTORY_version
+ except:
+ print "Old version found; updating data file."
+ self.upgrade(self.filename)
+ # create for the first time, with empty data
+ self.struct = self.subtype()
+ self.write(False)
+
+ return True
+
+ def write(self, check_for_file=True):
+ # the file should already exist
+ if check_for_file:
+ assert os.path.exists(self.filename)
+
+ # open without TRUNC nor APPEND, then seek to beginning to preserve space on disk
+ fd = os.open(self.filename, os.O_WRONLY|os.O_CREAT)
+ os.lseek(fd, 0, 0)
+ ret = os.write(fd, self.struct.save())
+ os.close(fd)
+ return ret
+
+ def upgrade(self, filename):
+ # TODO: in the future a more clever version migration might be nice.
+ print [ h for h in self.struct.history ]
+ os.remove(filename) # just nuke the old version
+
+class DNSHistory(History):
+ _fields_ = [ ("checksum", ctypes.c_char * History.DIGEST_length),
+ ("version", ctypes.c_int),
+ ("index", ctypes.c_int),
+ ("history", ctypes.c_float * History.HISTORY_length), ]
+
+ def __init__(self, *args, **kwargs):
+ super(DNSHistory, self).__init__(*args, **kwargs)
+
+ self.checksum = "0"*History.DIGEST_length
+ self.version = History.HISTORY_version
+ self.index = 0
+
+ def get(self):
+ summary = self.history[self.index:] + self.history[:self.index]
+ measured = filter(lambda x: x != 0, summary)
+ return measured
+
+ def append(self, data):
+ #try:
+ # note, this won't be the case when reboot occurs, or on first run.
+ #assert last_value > 0.0
+ #assert data > last_value
+ #print "Recording: %s"% (data-last_value)
+ #history[i] = data-last_value
+ self.history[self.index] = data
+ self.index += 1
+ self.index = self.index % History.HISTORY_length
+ #except:
+ # on init when last_value is 0, or reboot when counter resets.
+ # do not record data except for last_value, do not increment index
+ # pass
+
+ #last_value = data
+ return
+
+if __name__ == "__main__":
+ d = HistoryFile('test.dat', DNSHistory)
+ d.append(-1)
+ d.close()