7f0b4f3001
When [0] introduced quoting all arguments, it broke existing consumers that already quote their value themselves. Fix this by avoiding to add additional quotes to the value when it already starts with a double quote. [0] https://review.openstack.org/636078 Change-Id: I92146e04731efc6dcc632ae6c3a7c374e783cdba Closes-Bug: 1822453
352 lines
12 KiB
Python
352 lines
12 KiB
Python
# 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()
|