Change-Id: I81302e8988fe6498fea9f08ed66f5d0cc1fce161
| ... | ... |
@@ -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() |