# 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 os import re class DependencyGraph(object): # This is based on the JobGraph from Zuul. def __init__(self): self._names = set() self._dependencies = {} # dependent_name -> set(parent_names) def add(self, name, dependencies): # Append the dependency information self._dependencies.setdefault(name, set()) try: for dependency in dependencies: # Make sure a circular dependency is never created ancestors = self._getParentNamesRecursively( dependency, soft=True) ancestors.add(dependency) if name in ancestors: raise Exception("Dependency cycle detected in {}". format(name)) self._dependencies[name].add(dependency) except Exception: del self._dependencies[name] raise def getDependenciesRecursively(self, parent): dependencies = [] current_dependencies = self._dependencies[parent] for current in current_dependencies: if current not in dependencies: dependencies.append(current) for dep in self.getDependenciesRecursively(current): if dep not in dependencies: dependencies.append(dep) return dependencies def _getParentNamesRecursively(self, dependent, soft=False): all_parent_items = set() items_to_iterate = set([dependent]) while len(items_to_iterate) > 0: current_item = items_to_iterate.pop() current_parent_items = self._dependencies.get(current_item) if current_parent_items is None: if soft: current_parent_items = set() else: raise Exception("Dependent item {} not found: ".format( dependent)) new_parent_items = current_parent_items - all_parent_items items_to_iterate |= new_parent_items all_parent_items |= new_parent_items return all_parent_items class VarGraph(DependencyGraph): def __init__(self, vars): super(VarGraph, self).__init__() self.vars = {} self._varnames = set() 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 dependencies = set() 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 dependencies.add(dependency) try: self.add(key, dependencies) except Exception: del self.vars[key] raise def getVars(self): ret = [] keys = sorted(self.vars.keys()) seen = set() for key in keys: dependencies = self.getDependenciesRecursively(key) for var in dependencies + [key]: if var not in seen: ret.append((var, self.vars[var])) seen.add(var) return ret class PluginGraph(DependencyGraph): def __init__(self, base_dir, plugins): super(PluginGraph, self).__init__() # The dependency trees expressed by all the plugins we found # (which may be more than those the job is using). self._plugin_dependencies = {} self.loadPluginNames(base_dir) self.plugins = {} self._pluginnames = set() for k, v in plugins.items(): self._pluginnames.add(k) for k, v in plugins.items(): self._addPlugin(k, str(v)) def loadPluginNames(self, base_dir): if base_dir is None: return git_roots = [] for root, dirs, files in os.walk(base_dir): if '.git' not in dirs: continue # Don't go deeper than git roots dirs[:] = [] git_roots.append(root) for root in git_roots: devstack = os.path.join(root, 'devstack') if not (os.path.exists(devstack) and os.path.isdir(devstack)): continue settings = os.path.join(devstack, 'settings') if not (os.path.exists(settings) and os.path.isfile(settings)): continue self.loadDevstackPluginInfo(settings) define_re = re.compile(r'^define_plugin\s+(\S+).*') require_re = re.compile(r'^plugin_requires\s+(\S+)\s+(\S+).*') def loadDevstackPluginInfo(self, fn): name = None reqs = set() with open(fn) as f: for line in f: m = self.define_re.match(line) if m: name = m.group(1) m = self.require_re.match(line) if m: if name == m.group(1): reqs.add(m.group(2)) if name and reqs: self._plugin_dependencies[name] = reqs def getDependencies(self, value): return self._plugin_dependencies.get(value, []) def _addPlugin(self, key, value): if key in self.plugins: raise Exception("Plugin {} already added".format(key)) self.plugins[key] = value # Append the dependency information dependencies = set() for dependency in self.getDependencies(key): if dependency == key: continue dependencies.add(dependency) try: self.add(key, dependencies) except Exception: del self.plugins[key] raise def getPlugins(self): ret = [] keys = sorted(self.plugins.keys()) seen = set() for key in keys: dependencies = self.getDependenciesRecursively(key) for plugin in dependencies + [key]: if plugin not in seen: ret.append((plugin, self.plugins[plugin])) seen.add(plugin) return ret class LocalConf(object): def __init__(self, localrc, localconf, base_services, services, plugins, base_dir, projects, project, tempest_plugins): self.localrc = [] self.warnings = [] self.meta_sections = {} self.plugin_deps = {} self.base_dir = base_dir self.projects = projects self.project = project self.tempest_plugins = tempest_plugins if services or base_services: self.handle_services(base_services, services or {}) self.handle_localrc(localrc) # Plugins must be the last items in localrc, otherwise # the configuration lines which follows them in the file are # not applied to the plugins (for example, the value of DEST.) if plugins: self.handle_plugins(plugins) if localconf: self.handle_localconf(localconf) def handle_plugins(self, plugins): pg = PluginGraph(self.base_dir, plugins) for k, v in pg.getPlugins(): if v: self.localrc.append('enable_plugin {} {}'.format(k, v)) def handle_services(self, base_services, services): enable_base_services = services.pop('base', True) if enable_base_services and base_services: self.localrc.append('ENABLED_SERVICES={}'.format( ",".join(base_services))) else: self.localrc.append('disable_all_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): lfg = False tp = False if localrc: vg = VarGraph(localrc) for k, v in vg.getVars(): # Avoid double quoting if len(v) and v[0]=='"': self.localrc.append('{}={}'.format(k, v)) else: self.localrc.append('{}="{}"'.format(k, v)) if k == 'LIBS_FROM_GIT': lfg = True elif k == 'TEMPEST_PLUGINS': tp = True if not lfg and (self.projects or self.project): required_projects = [] if self.projects: for project_name, project_info in self.projects.items(): if project_info.get('required'): required_projects.append(project_info['short_name']) if self.project: if self.project['short_name'] not in required_projects: required_projects.append(self.project['short_name']) if required_projects: self.localrc.append('LIBS_FROM_GIT={}'.format( ','.join(required_projects))) if self.tempest_plugins: if not tp: tp_dirs = [] for tempest_plugin in self.tempest_plugins: tp_dirs.append(os.path.join(self.base_dir, tempest_plugin)) self.localrc.append('TEMPEST_PLUGINS="{}"'.format( ' '.join(tp_dirs))) else: self.warnings.append('TEMPEST_PLUGINS already defined ({}),' 'requested value {} ignored'.format( tp, self.tempest_plugins)) 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'), base_services=dict(type='list'), services=dict(type='dict'), localrc=dict(type='dict'), local_conf=dict(type='dict'), base_dir=dict(type='path'), path=dict(type='str'), projects=dict(type='dict'), project=dict(type='dict'), tempest_plugins=dict(type='list'), ) ) p = module.params lc = LocalConf(p.get('localrc'), p.get('local_conf'), p.get('base_services'), p.get('services'), p.get('plugins'), p.get('base_dir'), p.get('projects'), p.get('project'), p.get('tempest_plugins')) lc.write(p['path']) module.exit_json(warnings=lc.warnings) try: from ansible.module_utils.basic import * # noqa from ansible.module_utils.basic import AnsibleModule except ImportError: pass if __name__ == '__main__': main()