src/modules/client/history.py
changeset 569 48f7e9d2b1ac
parent 556 1c3526ca7b9e
child 593 f4305c5f2602
--- a/src/modules/client/history.py	Tue Oct 07 10:20:20 2008 -0700
+++ b/src/modules/client/history.py	Tue Oct 07 15:41:50 2008 -0500
@@ -59,6 +59,42 @@
 # Operations that are discarded, not saved, when recorded by history.
 DISCARDED_OPERATIONS = ["contents", "info", "list"]
 
+class __HistoryException(Exception):
+        """Private base exception class for all History exceptions."""
+        def __init__(self, *args):
+                Exception.__init__(self, *args)
+                self.data = args[0]
+
+        def __str__(self):
+                return str(self.data)
+
+class HistoryLoadException(__HistoryException):
+        """Used to indicate that an unexpected error occurred while loading
+        History operation information.
+
+        The first argument should be an exception object related to the
+        error encountered.
+        """
+        pass
+
+class HistoryStoreException(__HistoryException):
+        """Used to indicate that an unexpected error occurred while storing
+        History operation information.
+
+        The first argument should be an exception object related to the
+        error encountered.
+        """
+        pass
+
+class HistoryPurgeException(__HistoryException):
+        """Used to indicate that an unexpected error occurred while purging
+        History operation information.
+
+        The first argument should be an exception object related to the
+        error encountered.
+        """
+        pass
+
 class _HistoryOperation(object):
         """A _HistoryOperation object is a representation of data about an
         operation that a pkg(5) client has performed.  This class is private
@@ -77,6 +113,20 @@
 
                 return object.__setattr__(self, name, value)
 
+        def __str__(self):
+                return """\
+Operation Name: %s
+Operation Result: %s
+Operation Start Time: %s
+Operation End Time: %s
+Operation Start State:
+%s
+Operation End State:
+%s
+Operation User: %s (%s)
+""" % (self.name, self.result, self.start_time, self.end_time,
+    self.start_state, self.end_state, self.username, self.userid)
+
         # All "time" values should be in UTC, using ISO 8601 as the format.
         # Name of the operation performed (e.g. install, image-update, etc.).
         name = None
@@ -205,7 +255,7 @@
                                 self.__save()
 
                         # Discard it now that it is no longer needed.
-                        ops.pop()
+                        del ops[-1]
 
         def __init__(self, root_dir=".", filename=None):
                 """'root_dir' should be the path of the directory where the
@@ -221,6 +271,10 @@
                 if filename:
                         self.__load(filename)
 
+        def __str__(self):
+                ops = self.__operations
+                return "\n".join([str(op["operation"]) for op in ops])
+
         @property
         def path(self):
                 """The directory where history files will be written to or
@@ -320,25 +374,35 @@
                 # Ensure all previous information is discarded.
                 self.clear()
 
-                pathname = os.path.join(self.path, filename)
-                d = xmini.parse(pathname)
-                root = d.documentElement
-                for cnode in root.childNodes:
-                        if cnode.nodeName == "client":
-                                self.__load_client_data(cnode)
-                        elif cnode.nodeName == "operation":
-                                # Operations load differently due to the stack.
-                                self.__operations.append({
-                                    "pathname": pathname,
-                                    "operation": self.__load_operation_data(
-                                        cnode)
-                                    })
-                return True
+                try:
+                        pathname = os.path.join(self.path, filename)
+                        d = xmini.parse(pathname)
+                        root = d.documentElement
+                        for cnode in root.childNodes:
+                                if cnode.nodeName == "client":
+                                        self.__load_client_data(cnode)
+                                elif cnode.nodeName == "operation":
+                                        # Operations load differently due to
+                                        # the stack.
+                                        self.__operations.append({
+                                            "pathname": pathname,
+                                            "operation":
+                                                self.__load_operation_data(
+                                                cnode)
+                                            })
+                except KeyboardInterrupt:
+                        raise
+                except Exception, e:
+                        raise HistoryLoadException(e)
 
         def __serialize_client_data(self, d):
                 """Internal function used to serialize current client data
                 using the supplied 'd' (xml.dom.minidom) object.
                 """
+
+                assert self.client_name is not None
+                assert self.client_version is not None
+
                 root = d.documentElement
                 client = d.createElement("client")
                 client.setAttribute("name", self.client_name)
@@ -358,6 +422,16 @@
                 """Internal function used to serialize current operation data
                 using the supplied 'd' (xml.dom.minidom) object.
                 """
+
+                if self.operation_userid is None:
+                        raise HistoryStoreException("Unable to determine the "
+                            "id of the user that performed the current "
+                            "operation; unable to store history information.")
+                elif self.operation_username is None:
+                        raise HistoryStoreException("Unable to determine the "
+                            "username of the user that performed the current "
+                            "operation; unable to store history information.")
+
                 root = d.documentElement
                 op = d.createElement("operation")
                 op.setAttribute("name", self.operation_name)
@@ -409,41 +483,66 @@
                 # operations possibly occuring within the same second (but not
                 # microsecond).
                 pathname = self.pathname
-                for i in range(1, 10):
+                for i in range(1, 100):
                         try:
                                 f = os.fdopen(os.open(pathname,
                                     os.O_CREAT|os.O_EXCL|os.O_WRONLY), "w")
                                 d.writexml(f,
                                     encoding=sys.getdefaultencoding())
                                 f.close()
-                                return True
-                        except (OSError, IOError), e:
+                                return
+                        except EnvironmentError, e:
                                 if e.errno == errno.EEXIST:
                                         name, ext = os.path.splitext(
                                             os.path.basename(pathname))
-                                        name, seq = name.split("-", 1)
+                                        name = name.split("-", 1)[0]
                                         # Pick the next name in our sequence
                                         # and try again.
                                         pathname = os.path.join(self.path,
                                             "%s-%02d%s" % (name, i + 1, ext))
                                         continue
+                                elif e.errno not in (errno.EROFS,
+                                    errno.EACCES):
+                                        # Ignore read-only file system and
+                                        # access errors as it isn't critical
+                                        # to the image that this data is
+                                        # written.
+                                        raise HistoryStoreException(
+                                            e)
+                        except KeyboardInterrupt:
                                 raise
-
-                return False
-
+                        except Exception, e:
+                                raise HistoryStoreException(e)
+        
         def purge(self):
-                """Removes self.path (including its contents).
+                """Removes all history information by deleting the directory
+                indicated by the value self.path and then creates a new history
+                entry to record that this purge occurred.
                 """
                 self.operation_name = "purge-history"
                 try:
-                        self.operation_name = "purge-history"
                         shutil.rmtree(self.path)
-                except IOError, e:
-                        if e.errno == errno.EACCES:
-                                # XXX inform the user how to resolve this?
-                                # No point in attempting to end the operation
-                                # as it will likely fail during write as well.
-                                raise
+                except KeyboardInterrupt:
+                        raise
+                except Exception, e:
+                        raise HistoryPurgeException(e)
                 else:
                         self.operation_result = RESULT_SUCCEEDED
 
+        def abort(self, result):
+                """Intended to be used by the client during top-level error
+                handling to indicate that an unrecoverable error occurred
+                during the current operation(s).  This allows History to end
+                all of the current operations properly and handle any possible
+                errors that might be encountered in History itself.
+                """
+                try:
+                        # Ensure that all operations in the current stack are
+                        # ended properly.
+                        while self.operation_name:
+                                self.operation_result = result
+                except HistoryStoreException:
+                        # Ignore storage errors as it's likely that whatever
+                        # caused the client to abort() also caused the storage
+                        # of the history information to fail.
+                        return