Merge lp:~saviq/charm-helpers/confg-patterns into lp:charm-helpers
- confg-patterns
- Merge into devel
Status: | Rejected |
---|---|
Rejected by: | Stuart Bishop |
Proposed branch: | lp:~saviq/charm-helpers/confg-patterns |
Merge into: | lp:charm-helpers |
Diff against target: |
391 lines (+217/-15) 3 files modified
charmhelpers/contrib/openstack/context.py (+9/-0) charmhelpers/contrib/openstack/templating.py (+96/-11) tests/contrib/openstack/test_os_templating.py (+112/-4) |
To merge this branch: | bzr merge lp:~saviq/charm-helpers/confg-patterns |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Disapprove | ||
Review via email: mp+260750@code.launchpad.net |
Commit message
[saviq] Add support for config file patterns
Description of the change
- 384. By Michał Sawicz
-
Fix lint
- 385. By Michał Sawicz
-
Fix for python3
Corey Bryant (corey.bryant) wrote : | # |
Michał Sawicz (saviq) wrote : | # |
> I know the initial target is for the horizon subordinate plugin charms
> to use this, but would you mind giving an example of how horizon would use it?
Sure. The charm would register a pattern config:
> register_
Then,
> class RouterSettingCo
> def __call__(self):
> ctxt = {
> (40, 'router'): {'DISABLED': False if config('profile') in ['cisco'] else True}
> }
> return ctxt
and, for example
> class PluginContext(
> def __call__(self):
> # ...
> ctxt = {}
> for rid in relation_
> # ...
> rdata = relation_
> ctxt[(rdata[
> 'DISABLED': rdata['DISABLED'],
> # ...
> return ctxt
This would result in generating files, say:
> /path/to/
> /path/to/
In PluginContext I can then warn about conflicts etc. Multiple subordinates could even override one another.
HTH, let me know if I should explain more of this in the docstring.
- 386. By Michał Sawicz
-
Fix a bug in pattern context generation and add some more comments
Corey Bryant (corey.bryant) wrote : | # |
> > I know the initial target is for the horizon subordinate plugin charms
> > to use this, but would you mind giving an example of how horizon would use
> it?
>
> Sure. The charm would register a pattern config:
>
> > register_
>
> Then,
>
> > class RouterSettingCo
> > def __call__(self):
> > ctxt = {
> > (40, 'router'): {'DISABLED': False if config('profile') in
> ['cisco'] else True}
> > }
> > return ctxt
>
> and, for example
> > class PluginContext(
> > def __call__(self):
> > # ...
> > ctxt = {}
> > for rid in relation_
> > # ...
> > rdata = relation_
> > ctxt[(rdata[
> rdata['DASHBOARD'],
> > 'DISABLED': rdata['DISABLED'],
> > # ...
> > return ctxt
>
> This would result in generating files, say:
>
> > /path/to/
> > /path/to/
>
> In PluginContext I can then warn about conflicts etc. Multiple subordinates
> could even override one another.
>
> HTH, let me know if I should explain more of this in the docstring.
Thanks for the explanation. I'm wondering if this can be solved without adding the pattern context code. I was just looking at neutron-
BASE_RESOURCE_MAP = OrderedDict([
(NEUTRON_CONF, {
}),
...
Can you do something similar for subordinate horizon charms? In other words, the subordinate charms would write the plugin file themselves.
For example. horizon subordinate 1 could have:
BASE_RESOURCE_MAP = OrderedDict([
(/
}),
...
and horizon subordinate 2 could have:
BASE_RESOURCE_MAP = OrderedDict([
(/
}),
...
You could make the precedence (ie. 40 and 99) more flexibly in the subordinate charm and allow them to be changed via a config option.
Michał Sawicz (saviq) wrote : | # |
@Corey,
Sure, the subordinate could write that file itself, if that's preferable. Unfortunately the remaining problem for the contrail use case is that before Juno there's no way to update HORIZON_CONFIG from the split files, so we need to amend the main settings.py file. But this isn't really related to this MP...
Michał Sawicz (saviq) wrote : | # |
I will update the horizon-contrail charm to do this, so this MP can be dropped, really.
Michał Sawicz (saviq) wrote : | # |
Hah, I can't change the status of this MP to Rejected, can you?
Stuart Bishop (stub) : | # |
Unmerged revisions
- 386. By Michał Sawicz
-
Fix a bug in pattern context generation and add some more comments
- 385. By Michał Sawicz
-
Fix for python3
- 384. By Michał Sawicz
-
Fix lint
- 383. By Michał Sawicz
-
Refactor test to disallow additional calls
- 382. By Michał Sawicz
-
Implement support for generating multiple config files based on a pattern
- 381. By Michał Sawicz
-
Add context and templating classes to support generating multiple files based on a pattern
Preview Diff
1 | === modified file 'charmhelpers/contrib/openstack/context.py' | |||
2 | --- charmhelpers/contrib/openstack/context.py 2015-04-16 19:19:18 +0000 | |||
3 | +++ charmhelpers/contrib/openstack/context.py 2015-06-01 22:06:07 +0000 | |||
4 | @@ -194,6 +194,15 @@ | |||
5 | 194 | raise NotImplementedError | 194 | raise NotImplementedError |
6 | 195 | 195 | ||
7 | 196 | 196 | ||
8 | 197 | class OSPatternContextGenerator(OSContextGenerator): | ||
9 | 198 | """Base class for pattern context generators. | ||
10 | 199 | |||
11 | 200 | __call__ should return a dictionary of { tuple: dict }, where the tuple | ||
12 | 201 | will be used as input for format() for the filename pattern as registered | ||
13 | 202 | by OSConfigRenderer.register_pattern(). | ||
14 | 203 | """ | ||
15 | 204 | |||
16 | 205 | |||
17 | 197 | class SharedDBContext(OSContextGenerator): | 206 | class SharedDBContext(OSContextGenerator): |
18 | 198 | interfaces = ['shared-db'] | 207 | interfaces = ['shared-db'] |
19 | 199 | 208 | ||
20 | 200 | 209 | ||
21 | === modified file 'charmhelpers/contrib/openstack/templating.py' | |||
22 | --- charmhelpers/contrib/openstack/templating.py 2015-01-22 06:06:03 +0000 | |||
23 | +++ charmhelpers/contrib/openstack/templating.py 2015-06-01 22:06:07 +0000 | |||
24 | @@ -14,7 +14,9 @@ | |||
25 | 14 | # You should have received a copy of the GNU Lesser General Public License | 14 | # You should have received a copy of the GNU Lesser General Public License |
26 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
27 | 16 | 16 | ||
28 | 17 | import glob | ||
29 | 17 | import os | 18 | import os |
30 | 19 | import string | ||
31 | 18 | 20 | ||
32 | 19 | import six | 21 | import six |
33 | 20 | 22 | ||
34 | @@ -25,6 +27,7 @@ | |||
35 | 25 | INFO | 27 | INFO |
36 | 26 | ) | 28 | ) |
37 | 27 | from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES | 29 | from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES |
38 | 30 | from charmhelpers.contrib.openstack.context import OSPatternContextGenerator | ||
39 | 28 | 31 | ||
40 | 29 | try: | 32 | try: |
41 | 30 | from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions | 33 | from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions |
42 | @@ -96,7 +99,7 @@ | |||
43 | 96 | else: | 99 | else: |
44 | 97 | self.contexts = contexts | 100 | self.contexts = contexts |
45 | 98 | 101 | ||
47 | 99 | self._complete_contexts = [] | 102 | self._complete_contexts = set() |
48 | 100 | 103 | ||
49 | 101 | def context(self): | 104 | def context(self): |
50 | 102 | ctxt = {} | 105 | ctxt = {} |
51 | @@ -105,9 +108,7 @@ | |||
52 | 105 | if _ctxt: | 108 | if _ctxt: |
53 | 106 | ctxt.update(_ctxt) | 109 | ctxt.update(_ctxt) |
54 | 107 | # track interfaces for every complete context. | 110 | # track interfaces for every complete context. |
58 | 108 | [self._complete_contexts.append(interface) | 111 | self._complete_contexts.update(context.interfaces) |
56 | 109 | for interface in context.interfaces | ||
57 | 110 | if interface not in self._complete_contexts] | ||
59 | 111 | return ctxt | 112 | return ctxt |
60 | 112 | 113 | ||
61 | 113 | def complete_contexts(self): | 114 | def complete_contexts(self): |
62 | @@ -120,6 +121,41 @@ | |||
63 | 120 | return self._complete_contexts | 121 | return self._complete_contexts |
64 | 121 | 122 | ||
65 | 122 | 123 | ||
66 | 124 | class OSPatternConfigTemplate(OSConfigTemplate): | ||
67 | 125 | """ | ||
68 | 126 | Associates a config pattern template with a list of context generators. | ||
69 | 127 | Responsible for constructing a template context based on those generators. | ||
70 | 128 | """ | ||
71 | 129 | def __init__(self, pattern, contexts): | ||
72 | 130 | self.pattern = pattern | ||
73 | 131 | super(OSPatternConfigTemplate, self).__init__(config_file=None, | ||
74 | 132 | contexts=contexts) | ||
75 | 133 | |||
76 | 134 | def context(self): | ||
77 | 135 | base_ctxt = {} | ||
78 | 136 | ctxt = {} | ||
79 | 137 | for context in self.contexts: | ||
80 | 138 | _ctxt = context() | ||
81 | 139 | if not _ctxt: | ||
82 | 140 | continue | ||
83 | 141 | elif isinstance(context, OSPatternContextGenerator): | ||
84 | 142 | # for each returned key initialize its context with base_ctxt | ||
85 | 143 | # if not defined before and update with new data | ||
86 | 144 | for k, v in _ctxt.items(): | ||
87 | 145 | if k not in ctxt: | ||
88 | 146 | ctxt[k] = base_ctxt.copy() | ||
89 | 147 | ctxt[k].update(v) | ||
90 | 148 | else: | ||
91 | 149 | # update the base context and all pre-existing file-specific | ||
92 | 150 | # contexts | ||
93 | 151 | base_ctxt.update(_ctxt) | ||
94 | 152 | for key in ctxt: | ||
95 | 153 | ctxt[key].update(_ctxt) | ||
96 | 154 | # track interfaces for every complete context. | ||
97 | 155 | self._complete_contexts.update(context.interfaces) | ||
98 | 156 | return ctxt | ||
99 | 157 | |||
100 | 158 | |||
101 | 123 | class OSConfigRenderer(object): | 159 | class OSConfigRenderer(object): |
102 | 124 | """ | 160 | """ |
103 | 125 | This class provides a common templating system to be used by OpenStack | 161 | This class provides a common templating system to be used by OpenStack |
104 | @@ -132,6 +168,13 @@ | |||
105 | 132 | # import some common context generates from charmhelpers | 168 | # import some common context generates from charmhelpers |
106 | 133 | from charmhelpers.contrib.openstack import context | 169 | from charmhelpers.contrib.openstack import context |
107 | 134 | 170 | ||
108 | 171 | # or create your own | ||
109 | 172 | class SimpleContextGenerator(OSContextGenerator): | ||
110 | 173 | def __call__(): | ||
111 | 174 | return { | ||
112 | 175 | 'key': 'value' | ||
113 | 176 | } | ||
114 | 177 | |||
115 | 135 | # Create a renderer object for a specific OS release. | 178 | # Create a renderer object for a specific OS release. |
116 | 136 | configs = OSConfigRenderer(templates_dir='/tmp/templates', | 179 | configs = OSConfigRenderer(templates_dir='/tmp/templates', |
117 | 137 | openstack_release='folsom') | 180 | openstack_release='folsom') |
118 | @@ -142,12 +185,31 @@ | |||
119 | 142 | configs.register(config_file='/etc/nova/api-paste.ini', | 185 | configs.register(config_file='/etc/nova/api-paste.ini', |
120 | 143 | contexts=[context.IdentityServiceContext()]) | 186 | contexts=[context.IdentityServiceContext()]) |
121 | 144 | configs.register(config_file='/etc/haproxy/haproxy.conf', | 187 | configs.register(config_file='/etc/haproxy/haproxy.conf', |
123 | 145 | contexts=[context.HAProxyContext()]) | 188 | contexts=[context.HAProxyContext(), |
124 | 189 | SimpleContextGenerator()]) | ||
125 | 146 | # write out a single config | 190 | # write out a single config |
126 | 147 | configs.write('/etc/nova/nova.conf') | 191 | configs.write('/etc/nova/nova.conf') |
127 | 148 | # write out all registered configs | 192 | # write out all registered configs |
128 | 149 | configs.write_all() | 193 | configs.write_all() |
129 | 150 | 194 | ||
130 | 195 | Using patterns:: | ||
131 | 196 | class DashboardContextGenerator(OSPatternContextGenerator): | ||
132 | 197 | def __call__(): | ||
133 | 198 | return { | ||
134 | 199 | (40, 'router'): { 'DISABLED': True } | ||
135 | 200 | } | ||
136 | 201 | |||
137 | 202 | configs.register_pattern( | ||
138 | 203 | pattern='/usr/share/openstack-dashboard/openstack_dashboard' | ||
139 | 204 | '/enabled/_{}_juju_{}.py', | ||
140 | 205 | contexts=[ | ||
141 | 206 | DashboardContextGenerator() | ||
142 | 207 | ]) | ||
143 | 208 | # delete all files matching the pattern and | ||
144 | 209 | # write _40_juju_router.py anew | ||
145 | 210 | configs.write('/usr/share/openstack-dashboard/openstack_dashboard' | ||
146 | 211 | '/enabled/_{}_juju_{}.py') | ||
147 | 212 | |||
148 | 151 | **OpenStack Releases and template loading** | 213 | **OpenStack Releases and template loading** |
149 | 152 | 214 | ||
150 | 153 | When the object is instantiated, it is associated with a specific OS | 215 | When the object is instantiated, it is associated with a specific OS |
151 | @@ -219,6 +281,15 @@ | |||
152 | 219 | contexts=contexts) | 281 | contexts=contexts) |
153 | 220 | log('Registered config file: %s' % config_file, level=INFO) | 282 | log('Registered config file: %s' % config_file, level=INFO) |
154 | 221 | 283 | ||
155 | 284 | def register_pattern(self, pattern, contexts): | ||
156 | 285 | """ | ||
157 | 286 | Register a config file name pattern with a list of context generators | ||
158 | 287 | to be called during rendering. Use standard format() specification. | ||
159 | 288 | """ | ||
160 | 289 | self.templates[pattern] = OSPatternConfigTemplate(pattern=pattern, | ||
161 | 290 | contexts=contexts) | ||
162 | 291 | log('Registered config pattern: %s' % pattern, level=INFO) | ||
163 | 292 | |||
164 | 222 | def _get_tmpl_env(self): | 293 | def _get_tmpl_env(self): |
165 | 223 | if not self._tmpl_env: | 294 | if not self._tmpl_env: |
166 | 224 | loader = get_loader(self.templates_dir, self.openstack_release) | 295 | loader = get_loader(self.templates_dir, self.openstack_release) |
167 | @@ -234,7 +305,8 @@ | |||
168 | 234 | if config_file not in self.templates: | 305 | if config_file not in self.templates: |
169 | 235 | log('Config not registered: %s' % config_file, level=ERROR) | 306 | log('Config not registered: %s' % config_file, level=ERROR) |
170 | 236 | raise OSConfigException | 307 | raise OSConfigException |
172 | 237 | ctxt = self.templates[config_file].context() | 308 | ostemplate = self.templates[config_file] |
173 | 309 | ctxt = ostemplate.context() | ||
174 | 238 | 310 | ||
175 | 239 | _tmpl = os.path.basename(config_file) | 311 | _tmpl = os.path.basename(config_file) |
176 | 240 | try: | 312 | try: |
177 | @@ -253,7 +325,14 @@ | |||
178 | 253 | raise e | 325 | raise e |
179 | 254 | 326 | ||
180 | 255 | log('Rendering from template: %s' % _tmpl, level=INFO) | 327 | log('Rendering from template: %s' % _tmpl, level=INFO) |
182 | 256 | return template.render(ctxt) | 328 | |
183 | 329 | if not isinstance(ostemplate, OSPatternConfigTemplate): | ||
184 | 330 | ctxt = {(): ctxt} | ||
185 | 331 | |||
186 | 332 | renders = {} | ||
187 | 333 | for args, file_ctxt in ctxt.items(): | ||
188 | 334 | renders[args] = template.render(file_ctxt) | ||
189 | 335 | return renders | ||
190 | 257 | 336 | ||
191 | 258 | def write(self, config_file): | 337 | def write(self, config_file): |
192 | 259 | """ | 338 | """ |
193 | @@ -263,10 +342,16 @@ | |||
194 | 263 | log('Config not registered: %s' % config_file, level=ERROR) | 342 | log('Config not registered: %s' % config_file, level=ERROR) |
195 | 264 | raise OSConfigException | 343 | raise OSConfigException |
196 | 265 | 344 | ||
201 | 266 | _out = self.render(config_file) | 345 | renders = self.render(config_file) |
202 | 267 | 346 | ||
203 | 268 | with open(config_file, 'wb') as out: | 347 | files = glob.glob(''.join([t[0] + ('' if t[1] is None else '*') |
204 | 269 | out.write(_out) | 348 | for t in string.Formatter().parse(config_file)])) |
205 | 349 | for name in files: | ||
206 | 350 | os.unlink(name) | ||
207 | 351 | |||
208 | 352 | for args, render in renders.items(): | ||
209 | 353 | with open(config_file.format(*args), 'wb') as out: | ||
210 | 354 | out.write(render) | ||
211 | 270 | 355 | ||
212 | 271 | log('Wrote template %s.' % config_file, level=INFO) | 356 | log('Wrote template %s.' % config_file, level=INFO) |
213 | 272 | 357 | ||
214 | 273 | 358 | ||
215 | === modified file 'tests/contrib/openstack/test_os_templating.py' | |||
216 | --- tests/contrib/openstack/test_os_templating.py 2015-02-11 21:41:57 +0000 | |||
217 | +++ tests/contrib/openstack/test_os_templating.py 2015-06-01 22:06:07 +0000 | |||
218 | @@ -2,9 +2,11 @@ | |||
219 | 2 | import os | 2 | import os |
220 | 3 | import unittest | 3 | import unittest |
221 | 4 | 4 | ||
223 | 5 | from mock import patch, call, MagicMock | 5 | from mock import patch, call, Mock, MagicMock |
224 | 6 | 6 | ||
225 | 7 | import charmhelpers.contrib.openstack.templating as templating | 7 | import charmhelpers.contrib.openstack.templating as templating |
226 | 8 | from charmhelpers.contrib.openstack.context import OSPatternContextGenerator,\ | ||
227 | 9 | OSContextGenerator | ||
228 | 8 | 10 | ||
229 | 9 | from jinja2.exceptions import TemplateNotFound | 11 | from jinja2.exceptions import TemplateNotFound |
230 | 10 | 12 | ||
231 | @@ -15,7 +17,7 @@ | |||
232 | 15 | builtin_open = 'builtins.open' | 17 | builtin_open = 'builtins.open' |
233 | 16 | 18 | ||
234 | 17 | 19 | ||
236 | 18 | class FakeContextGenerator(object): | 20 | class FakeContextGenerator(OSContextGenerator): |
237 | 19 | interfaces = None | 21 | interfaces = None |
238 | 20 | 22 | ||
239 | 21 | def set(self, interfaces, context): | 23 | def set(self, interfaces, context): |
240 | @@ -26,6 +28,10 @@ | |||
241 | 26 | return self.context | 28 | return self.context |
242 | 27 | 29 | ||
243 | 28 | 30 | ||
244 | 31 | class FakePatternContextGenerator(FakeContextGenerator, OSPatternContextGenerator): | ||
245 | 32 | pass | ||
246 | 33 | |||
247 | 34 | |||
248 | 29 | class FakeLoader(object): | 35 | class FakeLoader(object): |
249 | 30 | def set(self, template): | 36 | def set(self, template): |
250 | 31 | self.template = template | 37 | self.template = template |
251 | @@ -55,6 +61,7 @@ | |||
252 | 55 | path = os.path.dirname(__file__) | 61 | path = os.path.dirname(__file__) |
253 | 56 | self.loader = FakeLoader() | 62 | self.loader = FakeLoader() |
254 | 57 | self.context = FakeContextGenerator() | 63 | self.context = FakeContextGenerator() |
255 | 64 | self.pattern_context = FakePatternContextGenerator() | ||
256 | 58 | 65 | ||
257 | 59 | self.addCleanup(patch.object(templating, 'apt_install').start().stop()) | 66 | self.addCleanup(patch.object(templating, 'apt_install').start().stop()) |
258 | 60 | self.addCleanup(patch.object(templating, 'log').start().stop()) | 67 | self.addCleanup(patch.object(templating, 'log').start().stop()) |
259 | @@ -167,9 +174,11 @@ | |||
260 | 167 | '''It renders template if it finds it by config file basename''' | 174 | '''It renders template if it finds it by config file basename''' |
261 | 168 | 175 | ||
262 | 169 | @patch(builtin_open) | 176 | @patch(builtin_open) |
263 | 177 | @patch('glob.glob') | ||
264 | 170 | @patch.object(templating, 'get_loader') | 178 | @patch.object(templating, 'get_loader') |
266 | 171 | def test_write_out_config(self, loader, _open): | 179 | def test_write_out_config(self, loader, _glob, _open): |
267 | 172 | '''It writes a templated config when provided a complete context''' | 180 | '''It writes a templated config when provided a complete context''' |
268 | 181 | _glob.return_value = [] | ||
269 | 173 | self.context.set(interfaces=['fooservice'], context={'foo': 'bar'}) | 182 | self.context.set(interfaces=['fooservice'], context={'foo': 'bar'}) |
270 | 174 | self.renderer.register('/tmp/foo', [self.context]) | 183 | self.renderer.register('/tmp/foo', [self.context]) |
271 | 175 | with patch.object(self.renderer, '_get_template') as _get_t: | 184 | with patch.object(self.renderer, '_get_template') as _get_t: |
272 | @@ -178,8 +187,10 @@ | |||
273 | 178 | self.renderer.write('/tmp/foo') | 187 | self.renderer.write('/tmp/foo') |
274 | 179 | _open.assert_called_with('/tmp/foo', 'wb') | 188 | _open.assert_called_with('/tmp/foo', 'wb') |
275 | 180 | 189 | ||
277 | 181 | def test_write_all(self): | 190 | @patch('glob.glob') |
278 | 191 | def test_write_all(self, _glob): | ||
279 | 182 | '''It writes out all configuration files at once''' | 192 | '''It writes out all configuration files at once''' |
280 | 193 | _glob.return_value = [] | ||
281 | 183 | self.context.set(interfaces=['fooservice'], context={'foo': 'bar'}) | 194 | self.context.set(interfaces=['fooservice'], context={'foo': 'bar'}) |
282 | 184 | self.renderer.register('/tmp/foo', [self.context]) | 195 | self.renderer.register('/tmp/foo', [self.context]) |
283 | 185 | self.renderer.register('/tmp/bar', [self.context]) | 196 | self.renderer.register('/tmp/bar', [self.context]) |
284 | @@ -192,6 +203,53 @@ | |||
285 | 192 | self.assertEquals(sorted(ex_calls), sorted(_write.call_args_list)) | 203 | self.assertEquals(sorted(ex_calls), sorted(_write.call_args_list)) |
286 | 193 | pass | 204 | pass |
287 | 194 | 205 | ||
288 | 206 | @patch(builtin_open) | ||
289 | 207 | @patch('glob.glob') | ||
290 | 208 | @patch.object(templating, 'get_loader') | ||
291 | 209 | def test_write_out_config_pattern(self, loader, _glob, _open): | ||
292 | 210 | '''It writes a templated pattern config when provided a complete context''' | ||
293 | 211 | self.pattern_context.set(interfaces=[], context={(1,): {'foo': 'bar'}, | ||
294 | 212 | (2,): {'foo': 'baz'}}) | ||
295 | 213 | self.renderer.register_pattern('/tmp/foo_{}', [self.pattern_context]) | ||
296 | 214 | _glob.return_value = [] | ||
297 | 215 | |||
298 | 216 | ex_calls = [ | ||
299 | 217 | call('/tmp/foo_1', 'wb'), | ||
300 | 218 | call('/tmp/foo_2', 'wb') | ||
301 | 219 | ] | ||
302 | 220 | |||
303 | 221 | with patch.object(self.renderer, '_get_template') as _get_t: | ||
304 | 222 | fake_tmpl = MockTemplate() | ||
305 | 223 | _get_t.return_value = fake_tmpl | ||
306 | 224 | self.renderer.write('/tmp/foo_{}') | ||
307 | 225 | self.assertEquals(sorted(ex_calls), sorted(_open.call_args_list)) | ||
308 | 226 | |||
309 | 227 | @patch(builtin_open) | ||
310 | 228 | @patch('os.unlink') | ||
311 | 229 | @patch('glob.glob') | ||
312 | 230 | @patch.object(templating, 'get_loader') | ||
313 | 231 | def test_write_out_config_pattern_clean(self, loader, _glob, _unlink, _open): | ||
314 | 232 | '''It deletes all matching files prior to writing new ones''' | ||
315 | 233 | self.pattern_context.set(interfaces=[], context={(1,): {'foo': 'bar'}}) | ||
316 | 234 | self.renderer.register_pattern('/tmp/foo_{}', [self.pattern_context]) | ||
317 | 235 | _glob.return_value = ['/tmp/foo_1', '/tmp/foo_2'] | ||
318 | 236 | |||
319 | 237 | m = Mock() | ||
320 | 238 | m.attach_mock(_open, 'open') | ||
321 | 239 | m.attach_mock(_unlink, 'unlink') | ||
322 | 240 | m.attach_mock(_glob, 'glob') | ||
323 | 241 | |||
324 | 242 | with patch.object(self.renderer, '_get_template') as _get_t: | ||
325 | 243 | fake_tmpl = MockTemplate() | ||
326 | 244 | _get_t.return_value = fake_tmpl | ||
327 | 245 | self.renderer.write('/tmp/foo_{}') | ||
328 | 246 | m.assert_has_calls([ | ||
329 | 247 | call.glob('/tmp/foo_*'), | ||
330 | 248 | call.unlink('/tmp/foo_1'), | ||
331 | 249 | call.unlink('/tmp/foo_2'), | ||
332 | 250 | call.open('/tmp/foo_1', 'wb') | ||
333 | 251 | ]) | ||
334 | 252 | |||
335 | 195 | @patch.object(templating, 'get_loader') | 253 | @patch.object(templating, 'get_loader') |
336 | 196 | def test_reset_template_loader_for_new_os_release(self, loader): | 254 | def test_reset_template_loader_for_new_os_release(self, loader): |
337 | 197 | self.loader.set('') | 255 | self.loader.set('') |
338 | @@ -282,3 +340,53 @@ | |||
339 | 282 | tmpl = templating.OSConfigTemplate(config_file='/tmp/foo', | 340 | tmpl = templating.OSConfigTemplate(config_file='/tmp/foo', |
340 | 283 | contexts=_c1) | 341 | contexts=_c1) |
341 | 284 | self.assertEquals(tmpl.contexts, [_c1]) | 342 | self.assertEquals(tmpl.contexts, [_c1]) |
342 | 343 | |||
343 | 344 | def test_generate_context(self): | ||
344 | 345 | '''Ensure context is generated correctly''' | ||
345 | 346 | def _c1(): | ||
346 | 347 | return {'a': 1, 'b': 2} | ||
347 | 348 | _c1.interfaces = [] | ||
348 | 349 | |||
349 | 350 | def _c2(): | ||
350 | 351 | return {'a': 3} | ||
351 | 352 | _c2.interfaces = [] | ||
352 | 353 | tmpl = templating.OSConfigTemplate(config_file='/tmp/foo', | ||
353 | 354 | contexts=[_c1, _c2]) | ||
354 | 355 | self.assertEqual(tmpl.context(), {'a': 3, 'b': 2}) | ||
355 | 356 | |||
356 | 357 | def test_generate_pattern_context(self): | ||
357 | 358 | class _c1(OSContextGenerator): | ||
358 | 359 | def __call__(self): | ||
359 | 360 | return {'a': 1, 'b': 2, 'c': 3, 'd': 4} | ||
360 | 361 | |||
361 | 362 | class _c2(OSPatternContextGenerator): | ||
362 | 363 | def __call__(self): | ||
363 | 364 | return { | ||
364 | 365 | ('foo',): {'a': 4}, | ||
365 | 366 | ('bar',): {'b': 5} | ||
366 | 367 | } | ||
367 | 368 | |||
368 | 369 | class _c3(OSContextGenerator): | ||
369 | 370 | def __call__(self): | ||
370 | 371 | return {'d': 6} | ||
371 | 372 | |||
372 | 373 | class _c4(OSPatternContextGenerator): | ||
373 | 374 | def __call__(self): | ||
374 | 375 | return { | ||
375 | 376 | ('bar',): {'a': 7}, | ||
376 | 377 | ('baz',): {} | ||
377 | 378 | } | ||
378 | 379 | |||
379 | 380 | class _c5(OSContextGenerator): | ||
380 | 381 | def __call__(self): | ||
381 | 382 | return {'e': 0} | ||
382 | 383 | |||
383 | 384 | tmpl = templating.OSPatternConfigTemplate(pattern='/tmp/foo_{}', | ||
384 | 385 | contexts=[_c1(), _c2(), | ||
385 | 386 | _c3(), _c4(), | ||
386 | 387 | _c5()]) | ||
387 | 388 | self.assertEqual(tmpl.context(), { | ||
388 | 389 | ('foo',): {'a': 4, 'b': 2, 'c': 3, 'd': 6, 'e': 0}, | ||
389 | 390 | ('bar',): {'a': 7, 'b': 5, 'c': 3, 'd': 6, 'e': 0}, | ||
390 | 391 | ('baz',): {'a': 1, 'b': 2, 'c': 3, 'd': 6, 'e': 0}, | ||
391 | 392 | }) |
Hi Michal,
I know the initial target is for the horizon subordinate plugin charms to use this, but would you mind giving an example of how horizon would use it?
I have a couple of comments inline below.
Corey