Add devstack base job for zuul v3

This should be managed in the devstack repo, since it's a base job to
run devstack.

Change-Id: Iffe54fbccbccd68db08f79a1b51dd7f76dbff408
Depends-On: Ie2119f24360d56690ffd772b95a9ea6b98dd4a39
diff --git a/roles/write-devstack-local-conf/library/devstack_local_conf.py b/roles/write-devstack-local-conf/library/devstack_local_conf.py
new file mode 100644
index 0000000..4134beb
--- /dev/null
+++ b/roles/write-devstack-local-conf/library/devstack_local_conf.py
@@ -0,0 +1,185 @@
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+#
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+
+class VarGraph(object):
+    # This is based on the JobGraph from Zuul.
+
+    def __init__(self, vars):
+        self.vars = {}
+        self._varnames = set()
+        self._dependencies = {}  # dependent_var_name -> set(parent_var_names)
+        for k, v in vars.items():
+            self._varnames.add(k)
+        for k, v in vars.items():
+            self._addVar(k, str(v))
+
+    bash_var_re = re.compile(r'\$\{?(\w+)')
+    def getDependencies(self, value):
+        return self.bash_var_re.findall(value)
+
+    def _addVar(self, key, value):
+        if key in self.vars:
+            raise Exception("Variable {} already added".format(key))
+        self.vars[key] = value
+        # Append the dependency information
+        self._dependencies.setdefault(key, set())
+        try:
+            for dependency in self.getDependencies(value):
+                if dependency == key:
+                    # A variable is allowed to reference itself; no
+                    # dependency link needed in that case.
+                    continue
+                if dependency not in self._varnames:
+                    # It's not necessary to create a link for an
+                    # external variable.
+                    continue
+                # Make sure a circular dependency is never created
+                ancestor_vars = self._getParentVarNamesRecursively(
+                    dependency, soft=True)
+                ancestor_vars.add(dependency)
+                if any((key == anc_var) for anc_var in ancestor_vars):
+                    raise Exception("Dependency cycle detected in var {}".
+                                    format(key))
+                self._dependencies[key].add(dependency)
+        except Exception:
+            del self.vars[key]
+            del self._dependencies[key]
+            raise
+
+    def getVars(self):
+        ret = []
+        keys = sorted(self.vars.keys())
+        seen = set()
+        for key in keys:
+            dependencies = self.getDependentVarsRecursively(key)
+            for var in dependencies + [key]:
+                if var not in seen:
+                    ret.append((var, self.vars[var]))
+                    seen.add(var)
+        return ret
+
+    def getDependentVarsRecursively(self, parent_var):
+        dependent_vars = []
+
+        current_dependent_vars = self._dependencies[parent_var]
+        for current_var in current_dependent_vars:
+            if current_var not in dependent_vars:
+                dependent_vars.append(current_var)
+            for dep in self.getDependentVarsRecursively(current_var):
+                if dep not in dependent_vars:
+                    dependent_vars.append(dep)
+        return dependent_vars
+
+    def _getParentVarNamesRecursively(self, dependent_var, soft=False):
+        all_parent_vars = set()
+        vars_to_iterate = set([dependent_var])
+        while len(vars_to_iterate) > 0:
+            current_var = vars_to_iterate.pop()
+            current_parent_vars = self._dependencies.get(current_var)
+            if current_parent_vars is None:
+                if soft:
+                    current_parent_vars = set()
+                else:
+                    raise Exception("Dependent var {} not found: ".format(
+                                    dependent_var))
+            new_parent_vars = current_parent_vars - all_parent_vars
+            vars_to_iterate |= new_parent_vars
+            all_parent_vars |= new_parent_vars
+        return all_parent_vars
+
+
+class LocalConf(object):
+
+    def __init__(self, localrc, localconf, services, plugins):
+        self.localrc = []
+        self.meta_sections = {}
+        if plugins:
+            self.handle_plugins(plugins)
+        if services:
+            self.handle_services(services)
+        if localrc:
+            self.handle_localrc(localrc)
+        if localconf:
+            self.handle_localconf(localconf)
+
+    def handle_plugins(self, plugins):
+        for k, v in plugins.items():
+            if v:
+                self.localrc.append('enable_plugin {} {}'.format(k, v))
+
+    def handle_services(self, services):
+        for k, v in services.items():
+            if v is False:
+                self.localrc.append('disable_service {}'.format(k))
+            elif v is True:
+                self.localrc.append('enable_service {}'.format(k))
+
+    def handle_localrc(self, localrc):
+        vg = VarGraph(localrc)
+        for k, v in vg.getVars():
+            self.localrc.append('{}={}'.format(k, v))
+
+    def handle_localconf(self, localconf):
+        for phase, phase_data in localconf.items():
+            for fn, fn_data in phase_data.items():
+                ms_name = '[[{}|{}]]'.format(phase, fn)
+                ms_data = []
+                for section, section_data in fn_data.items():
+                    ms_data.append('[{}]'.format(section))
+                    for k, v in section_data.items():
+                        ms_data.append('{} = {}'.format(k, v))
+                    ms_data.append('')
+                self.meta_sections[ms_name] = ms_data
+
+    def write(self, path):
+        with open(path, 'w') as f:
+            f.write('[[local|localrc]]\n')
+            f.write('\n'.join(self.localrc))
+            f.write('\n\n')
+            for section, lines in self.meta_sections.items():
+                f.write('{}\n'.format(section))
+                f.write('\n'.join(lines))
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec=dict(
+            plugins=dict(type='dict'),
+            services=dict(type='dict'),
+            localrc=dict(type='dict'),
+            local_conf=dict(type='dict'),
+            path=dict(type='str'),
+        )
+    )
+
+    p = module.params
+    lc = LocalConf(p.get('localrc'),
+                   p.get('local_conf'),
+                   p.get('services'),
+                   p.get('plugins'))
+    lc.write(p['path'])
+
+    module.exit_json()
+
+
+from ansible.module_utils.basic import *  # noqa
+from ansible.module_utils.basic import AnsibleModule
+
+if __name__ == '__main__':
+    main()