Merge ~rjschwei/cloud-init:addZyppRepos into cloud-init:master
- Git
- lp:~rjschwei/cloud-init
- addZyppRepos
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Scott Moser | ||||
Approved revision: | 9485f742ee4083d01d9ad7f50842f628176c58db | ||||
Merged at revision: | cc1475d07b9d0727012634ee9c7a914d67b051f5 | ||||
Proposed branch: | ~rjschwei/cloud-init:addZyppRepos | ||||
Merge into: | cloud-init:master | ||||
Diff against target: |
502 lines (+467/-1) 4 files modified
cloudinit/config/cc_zypper_add_repo.py (+220/-0) config/cloud.cfg.tmpl (+3/-0) tests/unittests/test_handler/test_handler_zypper_add_repo.py (+237/-0) tests/unittests/test_handler/test_schema.py (+7/-1) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Approve | ||
Server Team CI bot | continuous-integration | Needs Fixing | |
Chad Smith | Approve | ||
Review via email: mp+331149@code.launchpad.net |
Commit message
suse: Support addition of zypp repos.
This adds support to cloud-config for adding zypp repos.
Description of the change
Support addition of zypp repos
Scott Moser (smoser) wrote : | # |
oh, and you'll want to enable the config module in config/
Scott Moser (smoser) wrote : | # |
Oh, and one other thing:
zypp_repos
Can we make the top level key just 'zypp' (or zypper?) i dont know which is correct, but we're really trying to get to fewer top level keys rather.
zypp:
repos:
stuff.
rather than
zypp_repos:
stuff_here
zypp_config:
stuff here....
Robert Schweikert (rjschwei) wrote : | # |
> This seems generally fine. And we can take it, but i'd like to do so after the
> 17.1 release.
> I have a few requests:
>
> a.) please add schema (see runcmd as an example). Chad has been workign on
> adding schema in places. It helps docs and general cloud-init usage.
OK, having some trouble to figure out the info needed, what I am missing is the details for
'properties': {....}
> I dont' want to sound rude, thanks for your contribution, and we will get this
> in, just want to do things in the way we're moving to rather than like the
> older code that we're fixing up as we go.
Not rude, thanks for looking at this so quickly especially while you are trying to get out a release. Agreed new code should conform to where everything is going, not to where things have been.
Robert Schweikert (rjschwei) wrote : | # |
> oh, and you'll want to enable the config module in config/
Yes, now that the tmpl stuff is merged I will rebase and add that. I didn't want to create merg conflict for myself ;)
Robert Schweikert (rjschwei) wrote : | # |
> Oh, and one other thing:
> zypp_repos
>
> Can we make the top level key just 'zypp' (or zypper?) i dont know which is
> correct, but we're really trying to get to fewer top level keys rather.
> zypp:
> repos:
> stuff...here..
>
> rather than
> zypp_repos:
> stuff_here
> zypp_config:
> stuff here....
zypper would be correct but I don't think I get what you are asking.
Robert Schweikert (rjschwei) wrote : | # |
> > Oh, and one other thing:
> > zypp_repos
> >
> > Can we make the top level key just 'zypp' (or zypper?) i dont know which is
> > correct, but we're really trying to get to fewer top level keys rather.
> > zypp:
> > repos:
> > stuff...here..
> >
> > rather than
> > zypp_repos:
> > stuff_here
> > zypp_config:
> > stuff here....
>
> zypper would be correct but I don't think I get what you are asking.
Never mind, got it now, duh
- c82c116... by Robert Schweikert
-
- Zypper repo handling
+ Rename config option from zypp_add_repo to zypper_add_repo
+ Include setting the configuration
+ Provide schema data for validation
+ Change top level configuration setting to zypper - ab3a0e9... by Robert Schweikert
-
- Run zypper repo setup by default on SUSE distros
Robert Schweikert (rjschwei) wrote : | # |
@smoser
Can you take another look once the release is out the door. Still need to re-visit the test but I wanted to make sure the code is going in the right direction before I start implementing the test.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:ab3a0e90573
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) : | # |
Chad Smith (chad.smith) : | # |
- 3476d6a... by Robert Schweikert
-
- Add SUSE repos
+ Update repo description and configuration format
+ Unit testing
+ Complete schema definition
Robert Schweikert (rjschwei) wrote : | # |
Ready for another review.
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:3476d6ab769
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
just one comment in l ine. and note the bot didn't like something.
i'll review further a bit later.
- d93870a... by Robert Schweikert
-
- Improve example doc for zypper_add_repo
- Change id to cc_zypper_add_repo to match naming convention
- Add new schema definition to schema test
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:d93870abb96
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 4de284c... by Robert Schweikert
-
- Style fixes
Robert Schweikert (rjschwei) wrote : | # |
@smoser OK, one more try, not sure why the style slipped through make style-check. Anyway ready for your review. Any chance you can get to this in the next couple of days? Thanks
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:4de284ca40c
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Robert Schweikert (rjschwei) wrote : | # |
I do not get the test failure, meaning why it shows up in the bot.
Locally:
> flake8 --version
3.3.0 (mccabe: 0.6.1, naming: 0.4.1, pycodestyle: 2.3.1, pyflakes: 1.5.0) CPython 2.7.13 on Linux
> flake8 cloudinit/
returns clean on my machine. My a difference between py3 flake 8 and py 2 flake8?
- b550f2d... by Robert Schweikert
-
- fix alpha order for importing
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:b550f2dfd94
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- e4ed9ec... by Robert Schweikert
-
- import alpha order part 2
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:e4ed9ec76a8
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
Thanks for these branches Robert. Here's a minor set of patches with some of the comments I suggested. I hadn't gotten over to the unit tests yet, but I'm hoping to get that for you tomorrow morning.
Some changes suggested.
http://
Robert Schweikert (rjschwei) wrote : | # |
If http://
_write_repos call _write_
and having _write_
But as I said, whatever it takes to get it merged and to avoid me carrying a patch in the SUSE package.
- a4d7d5a... by Robert Schweikert
-
- more "pythonic"
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:a4d7d5adefe
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Chad Smith (chad.smith) wrote : | # |
Robert, that's not what it takes to get the approval, but rather a quick set of suggestions/
I admit my quick pass at this review needs validation and/or discussion/
I generally agree on your aversion to the multi-function methods, though there was a tension when I saw the code using configobj but not using configobj.write. It feels like the code was forcing functional separation when the configobj we created can already do this work for us. I don't really know how to resolve that tension, which is why I threw together the hacky patch.
Admitedly, I hadn't looked deeply enough at this branch to get the unit tests passing yet, but there are some things that felt like they rubbed. I had wanted to get you the unrefined pastebin to get the conversation started. I'll spend more time on this this morning when I start my day to see if there is an approach that makes more sense.
Robert Schweikert (rjschwei) wrote : | # |
I think at this point if we have follow ups for style, refinement lets nail that down in a follow up request. Dito for the tests.
Chad Smith (chad.smith) wrote : | # |
[1] Couple flakes:
cloudinit/
tests/unittests
[2] Minor tweak on existing unit tests in your setup method, use self.tmp_dir() as you have in other tests and drop the shutil import (and addClennup)
Per any ConfigObj comments, I think we generally have to deal w/ ConfigParser -> ConfigObj moves throughout cloudinit, so we might make any changes related to that at another time.
thanks again for this.
- 9485f74... by Robert Schweikert
-
- Test improvements for zypper repo add
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:9485f742ee4
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
changing my review. Thanks for patience Robert. I'll fix your flake8 error, and then make sure this runs in tox and then accept.
Thanks.
Preview Diff
1 | diff --git a/cloudinit/config/cc_zypper_add_repo.py b/cloudinit/config/cc_zypper_add_repo.py |
2 | new file mode 100644 |
3 | index 0000000..2cc5517 |
4 | --- /dev/null |
5 | +++ b/cloudinit/config/cc_zypper_add_repo.py |
6 | @@ -0,0 +1,220 @@ |
7 | +# |
8 | +# Copyright (C) 2017 SUSE LLC. |
9 | +# |
10 | +# This file is part of cloud-init. See LICENSE file for license information. |
11 | + |
12 | +"""zypper_add_repo: Add zyper repositories to the system""" |
13 | + |
14 | + |
15 | +import configobj |
16 | +import os |
17 | + |
18 | + |
19 | +from cloudinit import log as logging |
20 | +from cloudinit import util |
21 | +from cloudinit.config.schema import get_schema_doc |
22 | +from cloudinit.settings import PER_ALWAYS |
23 | +from six import string_types |
24 | +from textwrap import dedent |
25 | + |
26 | +distros = ['opensuse', 'sles'] |
27 | + |
28 | +schema = { |
29 | + 'id': 'cc_zypper_add_repo', |
30 | + 'name': 'ZypperAddRepo', |
31 | + 'title': 'Configure zypper behavior and add zypper repositories', |
32 | + 'description': dedent("""\ |
33 | + Configure zypper behavior by modifying /etc/zypp/zypp.conf. The |
34 | + configuration writer is "dumb" and will simply append the provided |
35 | + configuration options to the configuration file. Option settings |
36 | + that may be duplicate will be resolved by the way the zypp.conf file |
37 | + is parsed. The file is in INI format. |
38 | + Add repositories to the system. No validation is performed on the |
39 | + repository file entries, it is assumed the user is familiar with |
40 | + the zypper repository file format."""), |
41 | + 'distros': distros, |
42 | + 'examples': [dedent("""\ |
43 | + zypper: |
44 | + repos: |
45 | + - id: opensuse-oss |
46 | + name: os-oss |
47 | + baseurl: http://dl.opensuse.org/dist/leap/v/repo/oss/ |
48 | + enabled: 1 |
49 | + autorefresh: 1 |
50 | + - id: opensuse-oss-update |
51 | + name: os-oss-up |
52 | + baseurl: http://dl.opensuse.org/dist/leap/v/update |
53 | + # any setting per |
54 | + # https://en.opensuse.org/openSUSE:Standards_RepoInfo |
55 | + # enable and autorefresh are on by default |
56 | + config: |
57 | + reposdir: /etc/zypp/repos.dir |
58 | + servicesdir: /etc/zypp/services.d |
59 | + download.use_deltarpm: true |
60 | + # any setting in /etc/zypp/zypp.conf |
61 | + """)], |
62 | + 'frequency': PER_ALWAYS, |
63 | + 'type': 'object', |
64 | + 'properties': { |
65 | + 'zypper': { |
66 | + 'type': 'object', |
67 | + 'properties': { |
68 | + 'repos': { |
69 | + 'type': 'array', |
70 | + 'items': { |
71 | + 'type': 'object', |
72 | + 'properties': { |
73 | + 'id': { |
74 | + 'type': 'string', |
75 | + 'description': dedent("""\ |
76 | + The unique id of the repo, used when |
77 | + writing |
78 | + /etc/zypp/repos.d/<id>.repo.""") |
79 | + }, |
80 | + 'baseurl': { |
81 | + 'type': 'string', |
82 | + 'format': 'uri', # built-in format type |
83 | + 'description': 'The base repositoy URL' |
84 | + } |
85 | + }, |
86 | + 'required': ['id', 'baseurl'], |
87 | + 'additionalProperties': True |
88 | + }, |
89 | + 'minItems': 1 |
90 | + }, |
91 | + 'config': { |
92 | + 'type': 'object', |
93 | + 'description': dedent("""\ |
94 | + Any supported zypo.conf key is written to |
95 | + /etc/zypp/zypp.conf'""") |
96 | + } |
97 | + }, |
98 | + 'required': [], |
99 | + 'minProperties': 1, # Either config or repo must be provided |
100 | + 'additionalProperties': False, # only repos and config allowed |
101 | + } |
102 | + } |
103 | +} |
104 | + |
105 | +__doc__ = get_schema_doc(schema) # Supplement python help() |
106 | + |
107 | +LOG = logging.getLogger(__name__) |
108 | + |
109 | + |
110 | +def _canonicalize_id(repo_id): |
111 | + repo_id = repo_id.replace(" ", "_") |
112 | + return repo_id |
113 | + |
114 | + |
115 | +def _format_repo_value(val): |
116 | + if isinstance(val, bool): |
117 | + # zypp prefers 1/0 |
118 | + return 1 if val else 0 |
119 | + if isinstance(val, (list, tuple)): |
120 | + return "\n ".join([_format_repo_value(v) for v in val]) |
121 | + if not isinstance(val, string_types): |
122 | + return str(val) |
123 | + return val |
124 | + |
125 | + |
126 | +def _format_repository_config(repo_id, repo_config): |
127 | + to_be = configobj.ConfigObj() |
128 | + to_be[repo_id] = {} |
129 | + # Do basic translation of the items -> values |
130 | + for (k, v) in repo_config.items(): |
131 | + # For now assume that people using this know the format |
132 | + # of zypper repos and don't verify keys/values further |
133 | + to_be[repo_id][k] = _format_repo_value(v) |
134 | + lines = to_be.write() |
135 | + return "\n".join(lines) |
136 | + |
137 | + |
138 | +def _write_repos(repos, repo_base_path): |
139 | + """Write the user-provided repo definition files |
140 | + @param repos: A list of repo dictionary objects provided by the user's |
141 | + cloud config. |
142 | + @param repo_base_path: The directory path to which repo definitions are |
143 | + written. |
144 | + """ |
145 | + |
146 | + if not repos: |
147 | + return |
148 | + valid_repos = {} |
149 | + for index, user_repo_config in enumerate(repos): |
150 | + # Skip on absent required keys |
151 | + missing_keys = set(['id', 'baseurl']).difference(set(user_repo_config)) |
152 | + if missing_keys: |
153 | + LOG.warning( |
154 | + "Repo config at index %d is missing required config keys: %s", |
155 | + index, ",".join(missing_keys)) |
156 | + continue |
157 | + repo_id = user_repo_config.get('id') |
158 | + canon_repo_id = _canonicalize_id(repo_id) |
159 | + repo_fn_pth = os.path.join(repo_base_path, "%s.repo" % (canon_repo_id)) |
160 | + if os.path.exists(repo_fn_pth): |
161 | + LOG.info("Skipping repo %s, file %s already exists!", |
162 | + repo_id, repo_fn_pth) |
163 | + continue |
164 | + elif repo_id in valid_repos: |
165 | + LOG.info("Skipping repo %s, file %s already pending!", |
166 | + repo_id, repo_fn_pth) |
167 | + continue |
168 | + |
169 | + # Do some basic key formatting |
170 | + repo_config = dict( |
171 | + (k.lower().strip().replace("-", "_"), v) |
172 | + for k, v in user_repo_config.items() |
173 | + if k and k != 'id') |
174 | + |
175 | + # Set defaults if not present |
176 | + for field in ['enabled', 'autorefresh']: |
177 | + if field not in repo_config: |
178 | + repo_config[field] = '1' |
179 | + |
180 | + valid_repos[repo_id] = (repo_fn_pth, repo_config) |
181 | + |
182 | + for (repo_id, repo_data) in valid_repos.items(): |
183 | + repo_blob = _format_repository_config(repo_id, repo_data[-1]) |
184 | + util.write_file(repo_data[0], repo_blob) |
185 | + |
186 | + |
187 | +def _write_zypp_config(zypper_config): |
188 | + """Write to the default zypp configuration file /etc/zypp/zypp.conf""" |
189 | + if not zypper_config: |
190 | + return |
191 | + zypp_config = '/etc/zypp/zypp.conf' |
192 | + zypp_conf_content = util.load_file(zypp_config) |
193 | + new_settings = ['# Added via cloud.cfg'] |
194 | + for setting, value in zypper_config.items(): |
195 | + if setting == 'configdir': |
196 | + msg = 'Changing the location of the zypper configuration is ' |
197 | + msg += 'not supported, skipping "configdir" setting' |
198 | + LOG.warning(msg) |
199 | + continue |
200 | + if value: |
201 | + new_settings.append('%s=%s' % (setting, value)) |
202 | + if len(new_settings) > 1: |
203 | + new_config = zypp_conf_content + '\n'.join(new_settings) |
204 | + else: |
205 | + new_config = zypp_conf_content |
206 | + util.write_file(zypp_config, new_config) |
207 | + |
208 | + |
209 | +def handle(name, cfg, _cloud, log, _args): |
210 | + zypper_section = cfg.get('zypper') |
211 | + if not zypper_section: |
212 | + LOG.debug(("Skipping module named %s," |
213 | + " no 'zypper' relevant configuration found"), name) |
214 | + return |
215 | + repos = zypper_section.get('repos') |
216 | + if not repos: |
217 | + LOG.debug(("Skipping module named %s," |
218 | + " no 'repos' configuration found"), name) |
219 | + return |
220 | + zypper_config = zypper_section.get('config', {}) |
221 | + repo_base_path = zypper_config.get('reposdir', '/etc/zypp/repos.d/') |
222 | + |
223 | + _write_zypp_config(zypper_config) |
224 | + _write_repos(repos, repo_base_path) |
225 | + |
226 | +# vi: ts=4 expandtab |
227 | diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl |
228 | index 50e3bd8..32de9c9 100644 |
229 | --- a/config/cloud.cfg.tmpl |
230 | +++ b/config/cloud.cfg.tmpl |
231 | @@ -84,6 +84,9 @@ cloud_config_modules: |
232 | - apt-pipelining |
233 | - apt-configure |
234 | {% endif %} |
235 | +{% if variant in ["suse"] %} |
236 | + - zypper-add-repo |
237 | +{% endif %} |
238 | {% if variant not in ["freebsd"] %} |
239 | - ntp |
240 | {% endif %} |
241 | diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/test_handler/test_handler_zypper_add_repo.py |
242 | new file mode 100644 |
243 | index 0000000..315c2a5 |
244 | --- /dev/null |
245 | +++ b/tests/unittests/test_handler/test_handler_zypper_add_repo.py |
246 | @@ -0,0 +1,237 @@ |
247 | +# This file is part of cloud-init. See LICENSE file for license information. |
248 | + |
249 | +import glob |
250 | +import os |
251 | + |
252 | +from cloudinit.config import cc_zypper_add_repo |
253 | +from cloudinit import util |
254 | + |
255 | +from cloudinit.tests import helpers |
256 | +from cloudinit.tests.helpers import mock |
257 | + |
258 | +try: |
259 | + from configparser import ConfigParser |
260 | +except ImportError: |
261 | + from ConfigParser import ConfigParser |
262 | +import logging |
263 | +from six import StringIO |
264 | + |
265 | +LOG = logging.getLogger(__name__) |
266 | + |
267 | + |
268 | +class TestConfig(helpers.FilesystemMockingTestCase): |
269 | + def setUp(self): |
270 | + super(TestConfig, self).setUp() |
271 | + self.tmp = self.tmp_dir() |
272 | + self.zypp_conf = 'etc/zypp/zypp.conf' |
273 | + |
274 | + def test_bad_repo_config(self): |
275 | + """Config has no baseurl, no file should be written""" |
276 | + cfg = { |
277 | + 'repos': [ |
278 | + { |
279 | + 'id': 'foo', |
280 | + 'name': 'suse-test', |
281 | + 'enabled': '1' |
282 | + }, |
283 | + ] |
284 | + } |
285 | + self.patchUtils(self.tmp) |
286 | + cc_zypper_add_repo._write_repos(cfg['repos'], '/etc/zypp/repos.d') |
287 | + self.assertRaises(IOError, util.load_file, |
288 | + "/etc/zypp/repos.d/foo.repo") |
289 | + |
290 | + def test_write_repos(self): |
291 | + """Verify valid repos get written""" |
292 | + cfg = self._get_base_config_repos() |
293 | + root_d = self.tmp_dir() |
294 | + cc_zypper_add_repo._write_repos(cfg['zypper']['repos'], root_d) |
295 | + repos = glob.glob('%s/*.repo' % root_d) |
296 | + expected_repos = ['testing-foo.repo', 'testing-bar.repo'] |
297 | + if len(repos) != 2: |
298 | + assert 'Number of repos written is "%d" expected 2' % len(repos) |
299 | + for repo in repos: |
300 | + repo_name = os.path.basename(repo) |
301 | + if repo_name not in expected_repos: |
302 | + assert 'Found repo with name "%s"; unexpected' % repo_name |
303 | + # Validation that the content gets properly written is in another test |
304 | + |
305 | + def test_write_repo(self): |
306 | + """Verify the content of a repo file""" |
307 | + cfg = { |
308 | + 'repos': [ |
309 | + { |
310 | + 'baseurl': 'http://foo', |
311 | + 'name': 'test-foo', |
312 | + 'id': 'testing-foo' |
313 | + }, |
314 | + ] |
315 | + } |
316 | + root_d = self.tmp_dir() |
317 | + cc_zypper_add_repo._write_repos(cfg['repos'], root_d) |
318 | + contents = util.load_file("%s/testing-foo.repo" % root_d) |
319 | + parser = ConfigParser() |
320 | + parser.readfp(StringIO(contents)) |
321 | + expected = { |
322 | + 'testing-foo': { |
323 | + 'name': 'test-foo', |
324 | + 'baseurl': 'http://foo', |
325 | + 'enabled': '1', |
326 | + 'autorefresh': '1' |
327 | + } |
328 | + } |
329 | + for section in expected: |
330 | + self.assertTrue(parser.has_section(section), |
331 | + "Contains section {0}".format(section)) |
332 | + for k, v in expected[section].items(): |
333 | + self.assertEqual(parser.get(section, k), v) |
334 | + |
335 | + def test_config_write(self): |
336 | + """Write valid configuration data""" |
337 | + cfg = { |
338 | + 'config': { |
339 | + 'download.deltarpm': 'False', |
340 | + 'reposdir': 'foo' |
341 | + } |
342 | + } |
343 | + root_d = self.tmp_dir() |
344 | + helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'}) |
345 | + self.reRoot(root_d) |
346 | + cc_zypper_add_repo._write_zypp_config(cfg['config']) |
347 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
348 | + contents = util.load_file(cfg_out) |
349 | + expected = [ |
350 | + '# Zypp config', |
351 | + '# Added via cloud.cfg', |
352 | + 'download.deltarpm=False', |
353 | + 'reposdir=foo' |
354 | + ] |
355 | + for item in contents.split('\n'): |
356 | + if item not in expected: |
357 | + self.assertIsNone(item) |
358 | + |
359 | + @mock.patch('cloudinit.log.logging') |
360 | + def test_config_write_skip_configdir(self, mock_logging): |
361 | + """Write configuration but skip writing 'configdir' setting""" |
362 | + cfg = { |
363 | + 'config': { |
364 | + 'download.deltarpm': 'False', |
365 | + 'reposdir': 'foo', |
366 | + 'configdir': 'bar' |
367 | + } |
368 | + } |
369 | + root_d = self.tmp_dir() |
370 | + helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'}) |
371 | + self.reRoot(root_d) |
372 | + cc_zypper_add_repo._write_zypp_config(cfg['config']) |
373 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
374 | + contents = util.load_file(cfg_out) |
375 | + expected = [ |
376 | + '# Zypp config', |
377 | + '# Added via cloud.cfg', |
378 | + 'download.deltarpm=False', |
379 | + 'reposdir=foo' |
380 | + ] |
381 | + for item in contents.split('\n'): |
382 | + if item not in expected: |
383 | + self.assertIsNone(item) |
384 | + # Not finding teh right path for mocking :( |
385 | + # assert mock_logging.warning.called |
386 | + |
387 | + def test_empty_config_section_no_new_data(self): |
388 | + """When the config section is empty no new data should be written to |
389 | + zypp.conf""" |
390 | + cfg = self._get_base_config_repos() |
391 | + cfg['zypper']['config'] = None |
392 | + root_d = self.tmp_dir() |
393 | + helpers.populate_dir(root_d, {self.zypp_conf: '# No data'}) |
394 | + self.reRoot(root_d) |
395 | + cc_zypper_add_repo._write_zypp_config(cfg.get('config', {})) |
396 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
397 | + contents = util.load_file(cfg_out) |
398 | + self.assertEqual(contents, '# No data') |
399 | + |
400 | + def test_empty_config_value_no_new_data(self): |
401 | + """When the config section is not empty but there are no values |
402 | + no new data should be written to zypp.conf""" |
403 | + cfg = self._get_base_config_repos() |
404 | + cfg['zypper']['config'] = { |
405 | + 'download.deltarpm': None |
406 | + } |
407 | + root_d = self.tmp_dir() |
408 | + helpers.populate_dir(root_d, {self.zypp_conf: '# No data'}) |
409 | + self.reRoot(root_d) |
410 | + cc_zypper_add_repo._write_zypp_config(cfg.get('config', {})) |
411 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
412 | + contents = util.load_file(cfg_out) |
413 | + self.assertEqual(contents, '# No data') |
414 | + |
415 | + def test_handler_full_setup(self): |
416 | + """Test that the handler ends up calling the renderers""" |
417 | + cfg = self._get_base_config_repos() |
418 | + cfg['zypper']['config'] = { |
419 | + 'download.deltarpm': 'False', |
420 | + } |
421 | + root_d = self.tmp_dir() |
422 | + os.makedirs('%s/etc/zypp/repos.d' % root_d) |
423 | + helpers.populate_dir(root_d, {self.zypp_conf: '# Zypp config\n'}) |
424 | + self.reRoot(root_d) |
425 | + cc_zypper_add_repo.handle('zypper_add_repo', cfg, None, LOG, []) |
426 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
427 | + contents = util.load_file(cfg_out) |
428 | + expected = [ |
429 | + '# Zypp config', |
430 | + '# Added via cloud.cfg', |
431 | + 'download.deltarpm=False', |
432 | + ] |
433 | + for item in contents.split('\n'): |
434 | + if item not in expected: |
435 | + self.assertIsNone(item) |
436 | + repos = glob.glob('%s/etc/zypp/repos.d/*.repo' % root_d) |
437 | + expected_repos = ['testing-foo.repo', 'testing-bar.repo'] |
438 | + if len(repos) != 2: |
439 | + assert 'Number of repos written is "%d" expected 2' % len(repos) |
440 | + for repo in repos: |
441 | + repo_name = os.path.basename(repo) |
442 | + if repo_name not in expected_repos: |
443 | + assert 'Found repo with name "%s"; unexpected' % repo_name |
444 | + |
445 | + def test_no_config_section_no_new_data(self): |
446 | + """When there is no config section no new data should be written to |
447 | + zypp.conf""" |
448 | + cfg = self._get_base_config_repos() |
449 | + root_d = self.tmp_dir() |
450 | + helpers.populate_dir(root_d, {self.zypp_conf: '# No data'}) |
451 | + self.reRoot(root_d) |
452 | + cc_zypper_add_repo._write_zypp_config(cfg.get('config', {})) |
453 | + cfg_out = os.path.join(root_d, self.zypp_conf) |
454 | + contents = util.load_file(cfg_out) |
455 | + self.assertEqual(contents, '# No data') |
456 | + |
457 | + def test_no_repo_data(self): |
458 | + """When there is no repo data nothing should happen""" |
459 | + root_d = self.tmp_dir() |
460 | + self.reRoot(root_d) |
461 | + cc_zypper_add_repo._write_repos(None, root_d) |
462 | + content = glob.glob('%s/*' % root_d) |
463 | + self.assertEqual(len(content), 0) |
464 | + |
465 | + def _get_base_config_repos(self): |
466 | + """Basic valid repo configuration""" |
467 | + cfg = { |
468 | + 'zypper': { |
469 | + 'repos': [ |
470 | + { |
471 | + 'baseurl': 'http://foo', |
472 | + 'name': 'test-foo', |
473 | + 'id': 'testing-foo' |
474 | + }, |
475 | + { |
476 | + 'baseurl': 'http://bar', |
477 | + 'name': 'test-bar', |
478 | + 'id': 'testing-bar' |
479 | + } |
480 | + ] |
481 | + } |
482 | + } |
483 | + return cfg |
484 | diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py |
485 | index 745bb0f..b8fc893 100644 |
486 | --- a/tests/unittests/test_handler/test_schema.py |
487 | +++ b/tests/unittests/test_handler/test_schema.py |
488 | @@ -27,7 +27,13 @@ class GetSchemaTest(CiTestCase): |
489 | """Every cloudconfig module with schema is listed in allOf keyword.""" |
490 | schema = get_schema() |
491 | self.assertItemsEqual( |
492 | - ['cc_bootcmd', 'cc_ntp', 'cc_resizefs', 'cc_runcmd'], |
493 | + [ |
494 | + 'cc_bootcmd', |
495 | + 'cc_ntp', |
496 | + 'cc_resizefs', |
497 | + 'cc_runcmd', |
498 | + 'cc_zypper_add_repo' |
499 | + ], |
500 | [subschema['id'] for subschema in schema['allOf']]) |
501 | self.assertEqual('cloud-config-schema', schema['id']) |
502 | self.assertEqual( |
This seems generally fine. And we can take it, but i'd like to do so after the 17.1 release.
I have a few requests:
a.) please add schema (see runcmd as an example). Chad has been workign on adding schema in places. It helps docs and general cloud-init usage.
b.) please do less in handle. handle has an annoying interface. and its easier and more direct to unit test if you break portions out.
c.) don't use the 'log' that is passed into handle, instead import and use LOG.<verb> like other modules do. Your usage makes sense, as you copied other modules, we're just moving away from that.
I dont' want to sound rude, thanks for your contribution, and we will get this in, just want to do things in the way we're moving to rather than like the older code that we're fixing up as we go.