Add secret top-level config object

This adds secrets as a top-level config object, including a new
custom YAML tag to indicate encrypted data.

It also adds a script which encrypts data for use in tests.

Change-Id: I92a6bc048874f8aa4ebe0dd27180b253bede7370
diff --git a/zuul/model.py b/zuul/model.py
index 9c395c4..e929f8b 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -515,6 +515,35 @@
         self.state_time = data['state_time']
 
 
+class Secret(object):
+    """A collection of private data.
+
+    In configuration, Secrets are collections of private data in
+    key-value pair format.  They are defined as top-level
+    configuration objects and then referenced by Jobs.
+
+    """
+
+    def __init__(self, name):
+        self.name = name
+        # The secret data may or may not be encrypted.  This attribute
+        # is named 'secret_data' to make it easy to search for and
+        # spot where it is directly used.
+        self.secret_data = {}
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __eq__(self, other):
+        if not isinstance(other, Secret):
+            return False
+        return (self.name == other.name and
+                self.secret_data == other.secret_data)
+
+    def __repr__(self):
+        return '<Secret %s>' % (self.name,)
+
+
 class SourceContext(object):
     """A reference to the branch of a project in configuration.
 
@@ -2132,6 +2161,7 @@
         self.project_templates = []
         self.projects = {}
         self.nodesets = []
+        self.secrets = []
 
     def copy(self):
         r = UnparsedTenantConfig()
@@ -2140,6 +2170,7 @@
         r.project_templates = copy.deepcopy(self.project_templates)
         r.projects = copy.deepcopy(self.projects)
         r.nodesets = copy.deepcopy(self.nodesets)
+        r.secrets = copy.deepcopy(self.secrets)
         return r
 
     def extend(self, conf):
@@ -2150,6 +2181,7 @@
             for k, v in conf.projects.items():
                 self.projects.setdefault(k, []).extend(v)
             self.nodesets.extend(conf.nodesets)
+            self.secrets.extend(conf.secrets)
             return
 
         if not isinstance(conf, list):
@@ -2178,6 +2210,8 @@
                 self.pipelines.append(value)
             elif key == 'nodeset':
                 self.nodesets.append(value)
+            elif key == 'secret':
+                self.secrets.append(value)
             else:
                 raise Exception("Configuration item `%s` not recognized "
                                 "(when parsing %s)" %
@@ -2200,6 +2234,7 @@
         # inherit from the reference definition.
         self.jobs = {'noop': [Job('noop')]}
         self.nodesets = {}
+        self.secrets = {}
 
     def getJob(self, name):
         if name in self.jobs:
@@ -2233,6 +2268,11 @@
             raise Exception("NodeSet %s already defined" % (nodeset.name,))
         self.nodesets[nodeset.name] = nodeset
 
+    def addSecret(self, secret):
+        if secret.name in self.secrets:
+            raise Exception("Secret %s already defined" % (secret.name,))
+        self.secrets[secret.name] = secret
+
     def addPipeline(self, pipeline):
         self.pipelines[pipeline.name] = pipeline