blob: 2f97d0e35543a6608feec01542057abc097b6774 [file] [log] [blame]
Monty Taylor36ddea32017-10-02 10:05:17 -05001# Copyright (C) 2017 Red Hat, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12# implied.
13#
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
James E. Blair6f27fca2017-11-21 17:05:43 -080017import os
Monty Taylor36ddea32017-10-02 10:05:17 -050018import re
19
20
James E. Blair6f27fca2017-11-21 17:05:43 -080021class DependencyGraph(object):
Monty Taylor36ddea32017-10-02 10:05:17 -050022 # This is based on the JobGraph from Zuul.
23
James E. Blair6f27fca2017-11-21 17:05:43 -080024 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
75class VarGraph(DependencyGraph):
Monty Taylor36ddea32017-10-02 10:05:17 -050076 def __init__(self, vars):
James E. Blair6f27fca2017-11-21 17:05:43 -080077 super(VarGraph, self).__init__()
Monty Taylor36ddea32017-10-02 10:05:17 -050078 self.vars = {}
79 self._varnames = set()
Monty Taylor36ddea32017-10-02 10:05:17 -050080 for k, v in vars.items():
81 self._varnames.add(k)
82 for k, v in vars.items():
83 self._addVar(k, str(v))
84
85 bash_var_re = re.compile(r'\$\{?(\w+)')
86 def getDependencies(self, value):
87 return self.bash_var_re.findall(value)
88
89 def _addVar(self, key, value):
90 if key in self.vars:
91 raise Exception("Variable {} already added".format(key))
92 self.vars[key] = value
93 # Append the dependency information
James E. Blair6f27fca2017-11-21 17:05:43 -080094 dependencies = set()
95 for dependency in self.getDependencies(value):
96 if dependency == key:
97 # A variable is allowed to reference itself; no
98 # dependency link needed in that case.
99 continue
100 if dependency not in self._varnames:
101 # It's not necessary to create a link for an
102 # external variable.
103 continue
104 dependencies.add(dependency)
Monty Taylor36ddea32017-10-02 10:05:17 -0500105 try:
James E. Blair6f27fca2017-11-21 17:05:43 -0800106 self.add(key, dependencies)
Monty Taylor36ddea32017-10-02 10:05:17 -0500107 except Exception:
108 del self.vars[key]
Monty Taylor36ddea32017-10-02 10:05:17 -0500109 raise
110
111 def getVars(self):
112 ret = []
113 keys = sorted(self.vars.keys())
114 seen = set()
115 for key in keys:
James E. Blair6f27fca2017-11-21 17:05:43 -0800116 dependencies = self.getDependenciesRecursively(key)
Monty Taylor36ddea32017-10-02 10:05:17 -0500117 for var in dependencies + [key]:
118 if var not in seen:
119 ret.append((var, self.vars[var]))
120 seen.add(var)
121 return ret
122
Monty Taylor36ddea32017-10-02 10:05:17 -0500123
James E. Blair6f27fca2017-11-21 17:05:43 -0800124class PluginGraph(DependencyGraph):
125 def __init__(self, base_dir, plugins):
126 super(PluginGraph, self).__init__()
127 # The dependency trees expressed by all the plugins we found
128 # (which may be more than those the job is using).
129 self._plugin_dependencies = {}
130 self.loadPluginNames(base_dir)
Monty Taylor36ddea32017-10-02 10:05:17 -0500131
James E. Blair6f27fca2017-11-21 17:05:43 -0800132 self.plugins = {}
133 self._pluginnames = set()
134 for k, v in plugins.items():
135 self._pluginnames.add(k)
136 for k, v in plugins.items():
137 self._addPlugin(k, str(v))
138
139 def loadPluginNames(self, base_dir):
140 if base_dir is None:
141 return
142 git_roots = []
143 for root, dirs, files in os.walk(base_dir):
144 if '.git' not in dirs:
145 continue
146 # Don't go deeper than git roots
147 dirs[:] = []
148 git_roots.append(root)
149 for root in git_roots:
150 devstack = os.path.join(root, 'devstack')
151 if not (os.path.exists(devstack) and os.path.isdir(devstack)):
152 continue
153 settings = os.path.join(devstack, 'settings')
154 if not (os.path.exists(settings) and os.path.isfile(settings)):
155 continue
156 self.loadDevstackPluginInfo(settings)
157
Jens Harbott0b855002018-12-19 12:20:51 +0000158 define_re = re.compile(r'^define_plugin\s+(\S+).*')
159 require_re = re.compile(r'^plugin_requires\s+(\S+)\s+(\S+).*')
James E. Blair6f27fca2017-11-21 17:05:43 -0800160 def loadDevstackPluginInfo(self, fn):
161 name = None
162 reqs = set()
163 with open(fn) as f:
164 for line in f:
165 m = self.define_re.match(line)
166 if m:
167 name = m.group(1)
168 m = self.require_re.match(line)
169 if m:
170 if name == m.group(1):
171 reqs.add(m.group(2))
172 if name and reqs:
173 self._plugin_dependencies[name] = reqs
174
175 def getDependencies(self, value):
176 return self._plugin_dependencies.get(value, [])
177
178 def _addPlugin(self, key, value):
179 if key in self.plugins:
180 raise Exception("Plugin {} already added".format(key))
181 self.plugins[key] = value
182 # Append the dependency information
183 dependencies = set()
184 for dependency in self.getDependencies(key):
185 if dependency == key:
186 continue
187 dependencies.add(dependency)
188 try:
189 self.add(key, dependencies)
190 except Exception:
191 del self.plugins[key]
192 raise
193
194 def getPlugins(self):
195 ret = []
196 keys = sorted(self.plugins.keys())
197 seen = set()
198 for key in keys:
199 dependencies = self.getDependenciesRecursively(key)
200 for plugin in dependencies + [key]:
201 if plugin not in seen:
202 ret.append((plugin, self.plugins[plugin]))
203 seen.add(plugin)
204 return ret
Monty Taylor36ddea32017-10-02 10:05:17 -0500205
206
207class LocalConf(object):
208
James E. Blair6f27fca2017-11-21 17:05:43 -0800209 def __init__(self, localrc, localconf, base_services, services, plugins,
Luigi Toscano70d043d2019-03-12 22:25:44 +0100210 base_dir, projects, project, tempest_plugins):
Monty Taylor36ddea32017-10-02 10:05:17 -0500211 self.localrc = []
Luigi Toscano70d043d2019-03-12 22:25:44 +0100212 self.warnings = []
Monty Taylor36ddea32017-10-02 10:05:17 -0500213 self.meta_sections = {}
James E. Blair6f27fca2017-11-21 17:05:43 -0800214 self.plugin_deps = {}
215 self.base_dir = base_dir
James E. Blaire1edde32018-03-02 15:05:14 +0000216 self.projects = projects
James E. Blair8e5f8c22018-06-15 10:10:35 -0700217 self.project = project
Luigi Toscano70d043d2019-03-12 22:25:44 +0100218 self.tempest_plugins = tempest_plugins
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000219 if services or base_services:
220 self.handle_services(base_services, services or {})
James E. Blaire1edde32018-03-02 15:05:14 +0000221 self.handle_localrc(localrc)
Luigi Toscano610927f2019-02-26 18:39:51 +0100222 # Plugins must be the last items in localrc, otherwise
223 # the configuration lines which follows them in the file are
224 # not applied to the plugins (for example, the value of DEST.)
225 if plugins:
226 self.handle_plugins(plugins)
Monty Taylor36ddea32017-10-02 10:05:17 -0500227 if localconf:
228 self.handle_localconf(localconf)
229
230 def handle_plugins(self, plugins):
James E. Blair6f27fca2017-11-21 17:05:43 -0800231 pg = PluginGraph(self.base_dir, plugins)
232 for k, v in pg.getPlugins():
Monty Taylor36ddea32017-10-02 10:05:17 -0500233 if v:
234 self.localrc.append('enable_plugin {} {}'.format(k, v))
235
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000236 def handle_services(self, base_services, services):
237 enable_base_services = services.pop('base', True)
238 if enable_base_services and base_services:
239 self.localrc.append('ENABLED_SERVICES={}'.format(
240 ",".join(base_services)))
241 else:
Andrea Frittoli (andreaf)55511702017-11-30 15:49:39 +0000242 self.localrc.append('disable_all_services')
Monty Taylor36ddea32017-10-02 10:05:17 -0500243 for k, v in services.items():
244 if v is False:
245 self.localrc.append('disable_service {}'.format(k))
246 elif v is True:
247 self.localrc.append('enable_service {}'.format(k))
248
249 def handle_localrc(self, localrc):
James E. Blaire1edde32018-03-02 15:05:14 +0000250 lfg = False
Luigi Toscano70d043d2019-03-12 22:25:44 +0100251 tp = False
James E. Blaire1edde32018-03-02 15:05:14 +0000252 if localrc:
253 vg = VarGraph(localrc)
254 for k, v in vg.getVars():
Jens Harbott7f0b4f32019-04-01 11:43:28 +0000255 # Avoid double quoting
256 if len(v) and v[0]=='"':
257 self.localrc.append('{}={}'.format(k, v))
258 else:
259 self.localrc.append('{}="{}"'.format(k, v))
James E. Blaire1edde32018-03-02 15:05:14 +0000260 if k == 'LIBS_FROM_GIT':
261 lfg = True
Luigi Toscano70d043d2019-03-12 22:25:44 +0100262 elif k == 'TEMPEST_PLUGINS':
263 tp = True
James E. Blaire1edde32018-03-02 15:05:14 +0000264
James E. Blair8e5f8c22018-06-15 10:10:35 -0700265 if not lfg and (self.projects or self.project):
James E. Blaire1edde32018-03-02 15:05:14 +0000266 required_projects = []
James E. Blair8e5f8c22018-06-15 10:10:35 -0700267 if self.projects:
268 for project_name, project_info in self.projects.items():
269 if project_info.get('required'):
270 required_projects.append(project_info['short_name'])
271 if self.project:
272 if self.project['short_name'] not in required_projects:
273 required_projects.append(self.project['short_name'])
James E. Blaire1edde32018-03-02 15:05:14 +0000274 if required_projects:
275 self.localrc.append('LIBS_FROM_GIT={}'.format(
276 ','.join(required_projects)))
Monty Taylor36ddea32017-10-02 10:05:17 -0500277
Luigi Toscano70d043d2019-03-12 22:25:44 +0100278 if self.tempest_plugins:
279 if not tp:
280 tp_dirs = []
281 for tempest_plugin in self.tempest_plugins:
282 tp_dirs.append(os.path.join(self.base_dir, tempest_plugin))
283 self.localrc.append('TEMPEST_PLUGINS="{}"'.format(
284 ' '.join(tp_dirs)))
285 else:
286 self.warnings.append('TEMPEST_PLUGINS already defined ({}),'
287 'requested value {} ignored'.format(
288 tp, self.tempest_plugins))
289
290
Monty Taylor36ddea32017-10-02 10:05:17 -0500291 def handle_localconf(self, localconf):
292 for phase, phase_data in localconf.items():
293 for fn, fn_data in phase_data.items():
294 ms_name = '[[{}|{}]]'.format(phase, fn)
295 ms_data = []
296 for section, section_data in fn_data.items():
297 ms_data.append('[{}]'.format(section))
298 for k, v in section_data.items():
299 ms_data.append('{} = {}'.format(k, v))
300 ms_data.append('')
301 self.meta_sections[ms_name] = ms_data
302
303 def write(self, path):
304 with open(path, 'w') as f:
305 f.write('[[local|localrc]]\n')
306 f.write('\n'.join(self.localrc))
307 f.write('\n\n')
308 for section, lines in self.meta_sections.items():
309 f.write('{}\n'.format(section))
310 f.write('\n'.join(lines))
311
312
313def main():
314 module = AnsibleModule(
315 argument_spec=dict(
316 plugins=dict(type='dict'),
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000317 base_services=dict(type='list'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500318 services=dict(type='dict'),
319 localrc=dict(type='dict'),
320 local_conf=dict(type='dict'),
James E. Blair6f27fca2017-11-21 17:05:43 -0800321 base_dir=dict(type='path'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500322 path=dict(type='str'),
James E. Blaire1edde32018-03-02 15:05:14 +0000323 projects=dict(type='dict'),
James E. Blair8e5f8c22018-06-15 10:10:35 -0700324 project=dict(type='dict'),
Luigi Toscano70d043d2019-03-12 22:25:44 +0100325 tempest_plugins=dict(type='list'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500326 )
327 )
328
329 p = module.params
330 lc = LocalConf(p.get('localrc'),
331 p.get('local_conf'),
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000332 p.get('base_services'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500333 p.get('services'),
James E. Blair6f27fca2017-11-21 17:05:43 -0800334 p.get('plugins'),
James E. Blaire1edde32018-03-02 15:05:14 +0000335 p.get('base_dir'),
James E. Blair8e5f8c22018-06-15 10:10:35 -0700336 p.get('projects'),
Luigi Toscano70d043d2019-03-12 22:25:44 +0100337 p.get('project'),
338 p.get('tempest_plugins'))
Monty Taylor36ddea32017-10-02 10:05:17 -0500339 lc.write(p['path'])
340
Luigi Toscano70d043d2019-03-12 22:25:44 +0100341 module.exit_json(warnings=lc.warnings)
Monty Taylor36ddea32017-10-02 10:05:17 -0500342
343
James E. Blair6f27fca2017-11-21 17:05:43 -0800344try:
345 from ansible.module_utils.basic import * # noqa
346 from ansible.module_utils.basic import AnsibleModule
347except ImportError:
348 pass
Monty Taylor36ddea32017-10-02 10:05:17 -0500349
350if __name__ == '__main__':
351 main()