Set job class attributes in __init__

The job class attributes have grown to include complex data
structures that may accidentally be modified in-place.  Ensure
that each time a Job is constructed, it is initialized properly.

Nodesets were erroneously relying on this behavior, to correct
this, NodeSets must be comparable, so implement equality checks
for them.

Change-Id: I7eb22fc48f7106e963b12503c30a01fbfba27b11
diff --git a/zuul/model.py b/zuul/model.py
index 53b98a0..189244a 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -436,6 +436,15 @@
         self.name = name or ''
         self.nodes = OrderedDict()
 
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __eq__(self, other):
+        if not isinstance(other, NodeSet):
+            return False
+        return (self.name == other.name and
+                self.nodes == other.nodes)
+
     def copy(self):
         n = NodeSet(self.name)
         for name, node in self.nodes.items():
@@ -509,35 +518,35 @@
 class Job(object):
     """A Job represents the defintion of actions to perform."""
 
-    attributes = dict(
-        timeout=None,
-        # variables={},
-        nodeset=NodeSet(),
-        auth={},
-        workspace=None,
-        pre_run=None,
-        post_run=None,
-        voting=None,
-        hold_following_changes=None,
-        failure_message=None,
-        success_message=None,
-        failure_url=None,
-        success_url=None,
-        # Matchers.  These are separate so they can be individually
-        # overidden.
-        branch_matcher=None,
-        file_matcher=None,
-        irrelevant_file_matcher=None,  # skip-if
-        tags=set(),
-        mutex=None,
-        attempts=3,
-        source_project=None,
-        source_branch=None,
-        source_configrepo=None,
-        playbook=None,
-    )
-
     def __init__(self, name):
+        self.attributes = dict(
+            timeout=None,
+            # variables={},
+            nodeset=NodeSet(),
+            auth={},
+            workspace=None,
+            pre_run=[],
+            post_run=[],
+            voting=None,
+            hold_following_changes=None,
+            failure_message=None,
+            success_message=None,
+            failure_url=None,
+            success_url=None,
+            # Matchers.  These are separate so they can be individually
+            # overidden.
+            branch_matcher=None,
+            file_matcher=None,
+            irrelevant_file_matcher=None,  # skip-if
+            tags=set(),
+            mutex=None,
+            attempts=3,
+            source_project=None,
+            source_branch=None,
+            source_configrepo=None,
+            playbook=None,
+        )
+
         self.name = name
         for k, v in self.attributes.items():
             setattr(self, k, v)