blob: 9728fef37bbe8dd6df70582bbf4c2cf7b2ed0aa7 [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
158 define_re = re.compile(r'^define_plugin\s+(\w+).*')
159 require_re = re.compile(r'^plugin_requires\s+(\w+)\s+(\w+).*')
160 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,
James E. Blaire1edde32018-03-02 15:05:14 +0000210 base_dir, projects):
Monty Taylor36ddea32017-10-02 10:05:17 -0500211 self.localrc = []
212 self.meta_sections = {}
James E. Blair6f27fca2017-11-21 17:05:43 -0800213 self.plugin_deps = {}
214 self.base_dir = base_dir
James E. Blaire1edde32018-03-02 15:05:14 +0000215 self.projects = projects
Monty Taylor36ddea32017-10-02 10:05:17 -0500216 if plugins:
217 self.handle_plugins(plugins)
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000218 if services or base_services:
219 self.handle_services(base_services, services or {})
James E. Blaire1edde32018-03-02 15:05:14 +0000220 self.handle_localrc(localrc)
Monty Taylor36ddea32017-10-02 10:05:17 -0500221 if localconf:
222 self.handle_localconf(localconf)
223
224 def handle_plugins(self, plugins):
James E. Blair6f27fca2017-11-21 17:05:43 -0800225 pg = PluginGraph(self.base_dir, plugins)
226 for k, v in pg.getPlugins():
Monty Taylor36ddea32017-10-02 10:05:17 -0500227 if v:
228 self.localrc.append('enable_plugin {} {}'.format(k, v))
229
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000230 def handle_services(self, base_services, services):
231 enable_base_services = services.pop('base', True)
232 if enable_base_services and base_services:
233 self.localrc.append('ENABLED_SERVICES={}'.format(
234 ",".join(base_services)))
235 else:
Andrea Frittoli (andreaf)55511702017-11-30 15:49:39 +0000236 self.localrc.append('disable_all_services')
Monty Taylor36ddea32017-10-02 10:05:17 -0500237 for k, v in services.items():
238 if v is False:
239 self.localrc.append('disable_service {}'.format(k))
240 elif v is True:
241 self.localrc.append('enable_service {}'.format(k))
242
243 def handle_localrc(self, localrc):
James E. Blaire1edde32018-03-02 15:05:14 +0000244 lfg = False
245 if localrc:
246 vg = VarGraph(localrc)
247 for k, v in vg.getVars():
248 self.localrc.append('{}={}'.format(k, v))
249 if k == 'LIBS_FROM_GIT':
250 lfg = True
251
252 if not lfg and self.projects:
253 required_projects = []
254 for project_name, project_info in self.projects.items():
255 if project_info.get('required'):
256 required_projects.append(project_info['short_name'])
257 if required_projects:
258 self.localrc.append('LIBS_FROM_GIT={}'.format(
259 ','.join(required_projects)))
Monty Taylor36ddea32017-10-02 10:05:17 -0500260
261 def handle_localconf(self, localconf):
262 for phase, phase_data in localconf.items():
263 for fn, fn_data in phase_data.items():
264 ms_name = '[[{}|{}]]'.format(phase, fn)
265 ms_data = []
266 for section, section_data in fn_data.items():
267 ms_data.append('[{}]'.format(section))
268 for k, v in section_data.items():
269 ms_data.append('{} = {}'.format(k, v))
270 ms_data.append('')
271 self.meta_sections[ms_name] = ms_data
272
273 def write(self, path):
274 with open(path, 'w') as f:
275 f.write('[[local|localrc]]\n')
276 f.write('\n'.join(self.localrc))
277 f.write('\n\n')
278 for section, lines in self.meta_sections.items():
279 f.write('{}\n'.format(section))
280 f.write('\n'.join(lines))
281
282
283def main():
284 module = AnsibleModule(
285 argument_spec=dict(
286 plugins=dict(type='dict'),
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000287 base_services=dict(type='list'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500288 services=dict(type='dict'),
289 localrc=dict(type='dict'),
290 local_conf=dict(type='dict'),
James E. Blair6f27fca2017-11-21 17:05:43 -0800291 base_dir=dict(type='path'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500292 path=dict(type='str'),
James E. Blaire1edde32018-03-02 15:05:14 +0000293 projects=dict(type='dict'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500294 )
295 )
296
297 p = module.params
298 lc = LocalConf(p.get('localrc'),
299 p.get('local_conf'),
Andrea Frittoli (andreaf)7d444652017-12-01 17:36:38 +0000300 p.get('base_services'),
Monty Taylor36ddea32017-10-02 10:05:17 -0500301 p.get('services'),
James E. Blair6f27fca2017-11-21 17:05:43 -0800302 p.get('plugins'),
James E. Blaire1edde32018-03-02 15:05:14 +0000303 p.get('base_dir'),
304 p.get('projects'))
Monty Taylor36ddea32017-10-02 10:05:17 -0500305 lc.write(p['path'])
306
307 module.exit_json()
308
309
James E. Blair6f27fca2017-11-21 17:05:43 -0800310try:
311 from ansible.module_utils.basic import * # noqa
312 from ansible.module_utils.basic import AnsibleModule
313except ImportError:
314 pass
Monty Taylor36ddea32017-10-02 10:05:17 -0500315
316if __name__ == '__main__':
317 main()