Various updates; Add logAxis patch for graphite; Add namespaces to summary scripts.
[myops.git] / web / collect / client / history.py
diff --git a/web/collect/client/history.py b/web/collect/client/history.py
new file mode 100755 (executable)
index 0000000..6e040fc
--- /dev/null
@@ -0,0 +1,148 @@
+#!/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()