Change-Id: I81302e8988fe6498fea9f08ed66f5d0cc1fce161
James E. Blair authored on 2017/11/22 10:05:43... | ... |
@@ -14,16 +14,69 @@ |
14 | 14 |
# See the License for the specific language governing permissions and |
15 | 15 |
# limitations under the License. |
16 | 16 |
|
17 |
+import os |
|
17 | 18 |
import re |
18 | 19 |
|
19 | 20 |
|
20 |
-class VarGraph(object): |
|
21 |
+class DependencyGraph(object): |
|
21 | 22 |
# This is based on the JobGraph from Zuul. |
22 | 23 |
|
24 |
+ def __init__(self): |
|
25 |
+ self._names = set() |
|
26 |
+ self._dependencies = {} # dependent_name -> set(parent_names) |
|
27 |
+ |
|
28 |
+ def add(self, name, dependencies): |
|
29 |
+ # Append the dependency information |
|
30 |
+ self._dependencies.setdefault(name, set()) |
|
31 |
+ try: |
|
32 |
+ for dependency in dependencies: |
|
33 |
+ # Make sure a circular dependency is never created |
|
34 |
+ ancestors = self._getParentNamesRecursively( |
|
35 |
+ dependency, soft=True) |
|
36 |
+ ancestors.add(dependency) |
|
37 |
+ if name in ancestors: |
|
38 |
+ raise Exception("Dependency cycle detected in {}". |
|
39 |
+ format(name)) |
|
40 |
+ self._dependencies[name].add(dependency) |
|
41 |
+ except Exception: |
|
42 |
+ del self._dependencies[name] |
|
43 |
+ raise |
|
44 |
+ |
|
45 |
+ def getDependenciesRecursively(self, parent): |
|
46 |
+ dependencies = [] |
|
47 |
+ |
|
48 |
+ current_dependencies = self._dependencies[parent] |
|
49 |
+ for current in current_dependencies: |
|
50 |
+ if current not in dependencies: |
|
51 |
+ dependencies.append(current) |
|
52 |
+ for dep in self.getDependenciesRecursively(current): |
|
53 |
+ if dep not in dependencies: |
|
54 |
+ dependencies.append(dep) |
|
55 |
+ return dependencies |
|
56 |
+ |
|
57 |
+ def _getParentNamesRecursively(self, dependent, soft=False): |
|
58 |
+ all_parent_items = set() |
|
59 |
+ items_to_iterate = set([dependent]) |
|
60 |
+ while len(items_to_iterate) > 0: |
|
61 |
+ current_item = items_to_iterate.pop() |
|
62 |
+ current_parent_items = self._dependencies.get(current_item) |
|
63 |
+ if current_parent_items is None: |
|
64 |
+ if soft: |
|
65 |
+ current_parent_items = set() |
|
66 |
+ else: |
|
67 |
+ raise Exception("Dependent item {} not found: ".format( |
|
68 |
+ dependent)) |
|
69 |
+ new_parent_items = current_parent_items - all_parent_items |
|
70 |
+ items_to_iterate |= new_parent_items |
|
71 |
+ all_parent_items |= new_parent_items |
|
72 |
+ return all_parent_items |
|
73 |
+ |
|
74 |
+ |
|
75 |
+class VarGraph(DependencyGraph): |
|
23 | 76 |
def __init__(self, vars): |
77 |
+ super(VarGraph, self).__init__() |
|
24 | 78 |
self.vars = {} |
25 | 79 |
self._varnames = set() |
26 |
- self._dependencies = {} # dependent_var_name -> set(parent_var_names) |
|
27 | 80 |
for k, v in vars.items(): |
28 | 81 |
self._varnames.add(k) |
29 | 82 |
for k, v in vars.items(): |
... | ... |
@@ -38,28 +91,21 @@ class VarGraph(object): |
38 | 38 |
raise Exception("Variable {} already added".format(key)) |
39 | 39 |
self.vars[key] = value |
40 | 40 |
# Append the dependency information |
41 |
- self._dependencies.setdefault(key, set()) |
|
41 |
+ dependencies = set() |
|
42 |
+ for dependency in self.getDependencies(value): |
|
43 |
+ if dependency == key: |
|
44 |
+ # A variable is allowed to reference itself; no |
|
45 |
+ # dependency link needed in that case. |
|
46 |
+ continue |
|
47 |
+ if dependency not in self._varnames: |
|
48 |
+ # It's not necessary to create a link for an |
|
49 |
+ # external variable. |
|
50 |
+ continue |
|
51 |
+ dependencies.add(dependency) |
|
42 | 52 |
try: |
43 |
- for dependency in self.getDependencies(value): |
|
44 |
- if dependency == key: |
|
45 |
- # A variable is allowed to reference itself; no |
|
46 |
- # dependency link needed in that case. |
|
47 |
- continue |
|
48 |
- if dependency not in self._varnames: |
|
49 |
- # It's not necessary to create a link for an |
|
50 |
- # external variable. |
|
51 |
- continue |
|
52 |
- # Make sure a circular dependency is never created |
|
53 |
- ancestor_vars = self._getParentVarNamesRecursively( |
|
54 |
- dependency, soft=True) |
|
55 |
- ancestor_vars.add(dependency) |
|
56 |
- if any((key == anc_var) for anc_var in ancestor_vars): |
|
57 |
- raise Exception("Dependency cycle detected in var {}". |
|
58 |
- format(key)) |
|
59 |
- self._dependencies[key].add(dependency) |
|
53 |
+ self.add(key, dependencies) |
|
60 | 54 |
except Exception: |
61 | 55 |
del self.vars[key] |
62 |
- del self._dependencies[key] |
|
63 | 56 |
raise |
64 | 57 |
|
65 | 58 |
def getVars(self): |
... | ... |
@@ -67,48 +113,105 @@ class VarGraph(object): |
67 | 67 |
keys = sorted(self.vars.keys()) |
68 | 68 |
seen = set() |
69 | 69 |
for key in keys: |
70 |
- dependencies = self.getDependentVarsRecursively(key) |
|
70 |
+ dependencies = self.getDependenciesRecursively(key) |
|
71 | 71 |
for var in dependencies + [key]: |
72 | 72 |
if var not in seen: |
73 | 73 |
ret.append((var, self.vars[var])) |
74 | 74 |
seen.add(var) |
75 | 75 |
return ret |
76 | 76 |
|
77 |
- def getDependentVarsRecursively(self, parent_var): |
|
78 |
- dependent_vars = [] |
|
79 |
- |
|
80 |
- current_dependent_vars = self._dependencies[parent_var] |
|
81 |
- for current_var in current_dependent_vars: |
|
82 |
- if current_var not in dependent_vars: |
|
83 |
- dependent_vars.append(current_var) |
|
84 |
- for dep in self.getDependentVarsRecursively(current_var): |
|
85 |
- if dep not in dependent_vars: |
|
86 |
- dependent_vars.append(dep) |
|
87 |
- return dependent_vars |
|
88 |
- |
|
89 |
- def _getParentVarNamesRecursively(self, dependent_var, soft=False): |
|
90 |
- all_parent_vars = set() |
|
91 |
- vars_to_iterate = set([dependent_var]) |
|
92 |
- while len(vars_to_iterate) > 0: |
|
93 |
- current_var = vars_to_iterate.pop() |
|
94 |
- current_parent_vars = self._dependencies.get(current_var) |
|
95 |
- if current_parent_vars is None: |
|
96 |
- if soft: |
|
97 |
- current_parent_vars = set() |
|
98 |
- else: |
|
99 |
- raise Exception("Dependent var {} not found: ".format( |
|
100 |
- dependent_var)) |
|
101 |
- new_parent_vars = current_parent_vars - all_parent_vars |
|
102 |
- vars_to_iterate |= new_parent_vars |
|
103 |
- all_parent_vars |= new_parent_vars |
|
104 |
- return all_parent_vars |
|
77 |
+ |
|
78 |
+class PluginGraph(DependencyGraph): |
|
79 |
+ def __init__(self, base_dir, plugins): |
|
80 |
+ super(PluginGraph, self).__init__() |
|
81 |
+ # The dependency trees expressed by all the plugins we found |
|
82 |
+ # (which may be more than those the job is using). |
|
83 |
+ self._plugin_dependencies = {} |
|
84 |
+ self.loadPluginNames(base_dir) |
|
85 |
+ |
|
86 |
+ self.plugins = {} |
|
87 |
+ self._pluginnames = set() |
|
88 |
+ for k, v in plugins.items(): |
|
89 |
+ self._pluginnames.add(k) |
|
90 |
+ for k, v in plugins.items(): |
|
91 |
+ self._addPlugin(k, str(v)) |
|
92 |
+ |
|
93 |
+ def loadPluginNames(self, base_dir): |
|
94 |
+ if base_dir is None: |
|
95 |
+ return |
|
96 |
+ git_roots = [] |
|
97 |
+ for root, dirs, files in os.walk(base_dir): |
|
98 |
+ if '.git' not in dirs: |
|
99 |
+ continue |
|
100 |
+ # Don't go deeper than git roots |
|
101 |
+ dirs[:] = [] |
|
102 |
+ git_roots.append(root) |
|
103 |
+ for root in git_roots: |
|
104 |
+ devstack = os.path.join(root, 'devstack') |
|
105 |
+ if not (os.path.exists(devstack) and os.path.isdir(devstack)): |
|
106 |
+ continue |
|
107 |
+ settings = os.path.join(devstack, 'settings') |
|
108 |
+ if not (os.path.exists(settings) and os.path.isfile(settings)): |
|
109 |
+ continue |
|
110 |
+ self.loadDevstackPluginInfo(settings) |
|
111 |
+ |
|
112 |
+ define_re = re.compile(r'^define_plugin\s+(\w+).*') |
|
113 |
+ require_re = re.compile(r'^plugin_requires\s+(\w+)\s+(\w+).*') |
|
114 |
+ def loadDevstackPluginInfo(self, fn): |
|
115 |
+ name = None |
|
116 |
+ reqs = set() |
|
117 |
+ with open(fn) as f: |
|
118 |
+ for line in f: |
|
119 |
+ m = self.define_re.match(line) |
|
120 |
+ if m: |
|
121 |
+ name = m.group(1) |
|
122 |
+ m = self.require_re.match(line) |
|
123 |
+ if m: |
|
124 |
+ if name == m.group(1): |
|
125 |
+ reqs.add(m.group(2)) |
|
126 |
+ if name and reqs: |
|
127 |
+ self._plugin_dependencies[name] = reqs |
|
128 |
+ |
|
129 |
+ def getDependencies(self, value): |
|
130 |
+ return self._plugin_dependencies.get(value, []) |
|
131 |
+ |
|
132 |
+ def _addPlugin(self, key, value): |
|
133 |
+ if key in self.plugins: |
|
134 |
+ raise Exception("Plugin {} already added".format(key)) |
|
135 |
+ self.plugins[key] = value |
|
136 |
+ # Append the dependency information |
|
137 |
+ dependencies = set() |
|
138 |
+ for dependency in self.getDependencies(key): |
|
139 |
+ if dependency == key: |
|
140 |
+ continue |
|
141 |
+ dependencies.add(dependency) |
|
142 |
+ try: |
|
143 |
+ self.add(key, dependencies) |
|
144 |
+ except Exception: |
|
145 |
+ del self.plugins[key] |
|
146 |
+ raise |
|
147 |
+ |
|
148 |
+ def getPlugins(self): |
|
149 |
+ ret = [] |
|
150 |
+ keys = sorted(self.plugins.keys()) |
|
151 |
+ seen = set() |
|
152 |
+ for key in keys: |
|
153 |
+ dependencies = self.getDependenciesRecursively(key) |
|
154 |
+ for plugin in dependencies + [key]: |
|
155 |
+ if plugin not in seen: |
|
156 |
+ ret.append((plugin, self.plugins[plugin])) |
|
157 |
+ seen.add(plugin) |
|
158 |
+ return ret |
|
105 | 159 |
|
106 | 160 |
|
107 | 161 |
class LocalConf(object): |
108 | 162 |
|
109 |
- def __init__(self, localrc, localconf, base_services, services, plugins): |
|
163 |
+ def __init__(self, localrc, localconf, base_services, services, plugins, |
|
164 |
+ base_dir): |
|
110 | 165 |
self.localrc = [] |
111 | 166 |
self.meta_sections = {} |
167 |
+ self.plugin_deps = {} |
|
168 |
+ self.base_dir = base_dir |
|
112 | 169 |
if plugins: |
113 | 170 |
self.handle_plugins(plugins) |
114 | 171 |
if services or base_services: |
... | ... |
@@ -119,7 +222,8 @@ class LocalConf(object): |
119 | 119 |
self.handle_localconf(localconf) |
120 | 120 |
|
121 | 121 |
def handle_plugins(self, plugins): |
122 |
- for k, v in plugins.items(): |
|
122 |
+ pg = PluginGraph(self.base_dir, plugins) |
|
123 |
+ for k, v in pg.getPlugins(): |
|
123 | 124 |
if v: |
124 | 125 |
self.localrc.append('enable_plugin {} {}'.format(k, v)) |
125 | 126 |
|
... | ... |
@@ -171,6 +275,7 @@ def main(): |
171 | 171 |
services=dict(type='dict'), |
172 | 172 |
localrc=dict(type='dict'), |
173 | 173 |
local_conf=dict(type='dict'), |
174 |
+ base_dir=dict(type='path'), |
|
174 | 175 |
path=dict(type='str'), |
175 | 176 |
) |
176 | 177 |
) |
... | ... |
@@ -180,14 +285,18 @@ def main(): |
180 | 180 |
p.get('local_conf'), |
181 | 181 |
p.get('base_services'), |
182 | 182 |
p.get('services'), |
183 |
- p.get('plugins')) |
|
183 |
+ p.get('plugins'), |
|
184 |
+ p.get('base_dir')) |
|
184 | 185 |
lc.write(p['path']) |
185 | 186 |
|
186 | 187 |
module.exit_json() |
187 | 188 |
|
188 | 189 |
|
189 |
-from ansible.module_utils.basic import * # noqa |
|
190 |
-from ansible.module_utils.basic import AnsibleModule |
|
190 |
+try: |
|
191 |
+ from ansible.module_utils.basic import * # noqa |
|
192 |
+ from ansible.module_utils.basic import AnsibleModule |
|
193 |
+except ImportError: |
|
194 |
+ pass |
|
191 | 195 |
|
192 | 196 |
if __name__ == '__main__': |
193 | 197 |
main() |
194 | 198 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,166 @@ |
0 |
+# Copyright (C) 2017 Red Hat, Inc. |
|
1 |
+# |
|
2 |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
|
3 |
+# you may not use this file except in compliance with the License. |
|
4 |
+# You may obtain a copy of the License at |
|
5 |
+# |
|
6 |
+# http://www.apache.org/licenses/LICENSE-2.0 |
|
7 |
+# |
|
8 |
+# Unless required by applicable law or agreed to in writing, software |
|
9 |
+# distributed under the License is distributed on an "AS IS" BASIS, |
|
10 |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|
11 |
+# implied. |
|
12 |
+# |
|
13 |
+# See the License for the specific language governing permissions and |
|
14 |
+# limitations under the License. |
|
15 |
+ |
|
16 |
+import os |
|
17 |
+import shutil |
|
18 |
+import tempfile |
|
19 |
+import unittest |
|
20 |
+ |
|
21 |
+from devstack_local_conf import LocalConf |
|
22 |
+from collections import OrderedDict |
|
23 |
+ |
|
24 |
+class TestDevstackLocalConf(unittest.TestCase): |
|
25 |
+ def setUp(self): |
|
26 |
+ self.tmpdir = tempfile.mkdtemp() |
|
27 |
+ |
|
28 |
+ def tearDown(self): |
|
29 |
+ shutil.rmtree(self.tmpdir) |
|
30 |
+ |
|
31 |
+ def test_plugins(self): |
|
32 |
+ "Test that plugins without dependencies work" |
|
33 |
+ localrc = {'test_localrc': '1'} |
|
34 |
+ local_conf = {'install': |
|
35 |
+ {'nova.conf': |
|
36 |
+ {'main': |
|
37 |
+ {'test_conf': '2'}}}} |
|
38 |
+ services = {'cinder': True} |
|
39 |
+ # We use ordereddict here to make sure the plugins are in the |
|
40 |
+ # *wrong* order for testing. |
|
41 |
+ plugins = OrderedDict([ |
|
42 |
+ ('bar', 'git://git.openstack.org/openstack/bar-plugin'), |
|
43 |
+ ('foo', 'git://git.openstack.org/openstack/foo-plugin'), |
|
44 |
+ ('baz', 'git://git.openstack.org/openstack/baz-plugin'), |
|
45 |
+ ]) |
|
46 |
+ p = dict(localrc=localrc, |
|
47 |
+ local_conf=local_conf, |
|
48 |
+ base_services=[], |
|
49 |
+ services=services, |
|
50 |
+ plugins=plugins, |
|
51 |
+ base_dir='./test', |
|
52 |
+ path=os.path.join(self.tmpdir, 'test.local.conf')) |
|
53 |
+ lc = LocalConf(p.get('localrc'), |
|
54 |
+ p.get('local_conf'), |
|
55 |
+ p.get('base_services'), |
|
56 |
+ p.get('services'), |
|
57 |
+ p.get('plugins'), |
|
58 |
+ p.get('base_dir')) |
|
59 |
+ lc.write(p['path']) |
|
60 |
+ |
|
61 |
+ plugins = [] |
|
62 |
+ with open(p['path']) as f: |
|
63 |
+ for line in f: |
|
64 |
+ if line.startswith('enable_plugin'): |
|
65 |
+ plugins.append(line.split()[1]) |
|
66 |
+ self.assertEqual(['bar', 'baz', 'foo'], plugins) |
|
67 |
+ |
|
68 |
+ def test_plugin_deps(self): |
|
69 |
+ "Test that plugins with dependencies work" |
|
70 |
+ os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', 'devstack')) |
|
71 |
+ os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', '.git')) |
|
72 |
+ os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', 'devstack')) |
|
73 |
+ os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', '.git')) |
|
74 |
+ with open(os.path.join( |
|
75 |
+ self.tmpdir, |
|
76 |
+ 'foo-plugin', 'devstack', 'settings'), 'w') as f: |
|
77 |
+ f.write('define_plugin foo\n') |
|
78 |
+ with open(os.path.join( |
|
79 |
+ self.tmpdir, |
|
80 |
+ 'bar-plugin', 'devstack', 'settings'), 'w') as f: |
|
81 |
+ f.write('define_plugin bar\n') |
|
82 |
+ f.write('plugin_requires bar foo\n') |
|
83 |
+ |
|
84 |
+ localrc = {'test_localrc': '1'} |
|
85 |
+ local_conf = {'install': |
|
86 |
+ {'nova.conf': |
|
87 |
+ {'main': |
|
88 |
+ {'test_conf': '2'}}}} |
|
89 |
+ services = {'cinder': True} |
|
90 |
+ # We use ordereddict here to make sure the plugins are in the |
|
91 |
+ # *wrong* order for testing. |
|
92 |
+ plugins = OrderedDict([ |
|
93 |
+ ('bar', 'git://git.openstack.org/openstack/bar-plugin'), |
|
94 |
+ ('foo', 'git://git.openstack.org/openstack/foo-plugin'), |
|
95 |
+ ]) |
|
96 |
+ p = dict(localrc=localrc, |
|
97 |
+ local_conf=local_conf, |
|
98 |
+ base_services=[], |
|
99 |
+ services=services, |
|
100 |
+ plugins=plugins, |
|
101 |
+ base_dir=self.tmpdir, |
|
102 |
+ path=os.path.join(self.tmpdir, 'test.local.conf')) |
|
103 |
+ lc = LocalConf(p.get('localrc'), |
|
104 |
+ p.get('local_conf'), |
|
105 |
+ p.get('base_services'), |
|
106 |
+ p.get('services'), |
|
107 |
+ p.get('plugins'), |
|
108 |
+ p.get('base_dir')) |
|
109 |
+ lc.write(p['path']) |
|
110 |
+ |
|
111 |
+ plugins = [] |
|
112 |
+ with open(p['path']) as f: |
|
113 |
+ for line in f: |
|
114 |
+ if line.startswith('enable_plugin'): |
|
115 |
+ plugins.append(line.split()[1]) |
|
116 |
+ self.assertEqual(['foo', 'bar'], plugins) |
|
117 |
+ |
|
118 |
+ def test_plugin_circular_deps(self): |
|
119 |
+ "Test that plugins with circular dependencies fail" |
|
120 |
+ os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', 'devstack')) |
|
121 |
+ os.makedirs(os.path.join(self.tmpdir, 'foo-plugin', '.git')) |
|
122 |
+ os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', 'devstack')) |
|
123 |
+ os.makedirs(os.path.join(self.tmpdir, 'bar-plugin', '.git')) |
|
124 |
+ with open(os.path.join( |
|
125 |
+ self.tmpdir, |
|
126 |
+ 'foo-plugin', 'devstack', 'settings'), 'w') as f: |
|
127 |
+ f.write('define_plugin foo\n') |
|
128 |
+ f.write('plugin_requires foo bar\n') |
|
129 |
+ with open(os.path.join( |
|
130 |
+ self.tmpdir, |
|
131 |
+ 'bar-plugin', 'devstack', 'settings'), 'w') as f: |
|
132 |
+ f.write('define_plugin bar\n') |
|
133 |
+ f.write('plugin_requires bar foo\n') |
|
134 |
+ |
|
135 |
+ localrc = {'test_localrc': '1'} |
|
136 |
+ local_conf = {'install': |
|
137 |
+ {'nova.conf': |
|
138 |
+ {'main': |
|
139 |
+ {'test_conf': '2'}}}} |
|
140 |
+ services = {'cinder': True} |
|
141 |
+ # We use ordereddict here to make sure the plugins are in the |
|
142 |
+ # *wrong* order for testing. |
|
143 |
+ plugins = OrderedDict([ |
|
144 |
+ ('bar', 'git://git.openstack.org/openstack/bar-plugin'), |
|
145 |
+ ('foo', 'git://git.openstack.org/openstack/foo-plugin'), |
|
146 |
+ ]) |
|
147 |
+ p = dict(localrc=localrc, |
|
148 |
+ local_conf=local_conf, |
|
149 |
+ base_services=[], |
|
150 |
+ services=services, |
|
151 |
+ plugins=plugins, |
|
152 |
+ base_dir=self.tmpdir, |
|
153 |
+ path=os.path.join(self.tmpdir, 'test.local.conf')) |
|
154 |
+ with self.assertRaises(Exception): |
|
155 |
+ lc = LocalConf(p.get('localrc'), |
|
156 |
+ p.get('local_conf'), |
|
157 |
+ p.get('base_services'), |
|
158 |
+ p.get('services'), |
|
159 |
+ p.get('plugins'), |
|
160 |
+ p.get('base_dir')) |
|
161 |
+ lc.write(p['path']) |
|
162 |
+ |
|
163 |
+ |
|
164 |
+if __name__ == '__main__': |
|
165 |
+ unittest.main() |