Merge ~rjschwei/cloud-init:addZyppRepos into cloud-init:master

Proposed by Robert Schweikert
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)
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

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) 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.
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.

review: Needs Fixing
Revision history for this message
Scott Moser (smoser) wrote :

oh, and you'll want to enable the config module in config/cloud.cfg.tmpl.

Revision history for this message
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...here..

rather than
  zypp_repos:
    stuff_here
  zypp_config:
    stuff here....

Revision history for this message
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.

Revision history for this message
Robert Schweikert (rjschwei) wrote :

> oh, and you'll want to enable the config module in config/cloud.cfg.tmpl.

Yes, now that the tmpl stuff is merged I will rebase and add that. I didn't want to create merg conflict for myself ;)

Revision history for this message
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.

Revision history for this message
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

~rjschwei/cloud-init:addZyppRepos updated
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

Revision history for this message
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.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:ab3a0e9057319580b73fcbd4ac89d8d9277bda35
https://jenkins.ubuntu.com/server/job/cloud-init-ci/346/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/346/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
~rjschwei/cloud-init:addZyppRepos updated
3476d6a... by Robert Schweikert

- Add SUSE repos
  + Update repo description and configuration format
  + Unit testing
  + Complete schema definition

Revision history for this message
Robert Schweikert (rjschwei) wrote :

Ready for another review.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:3476d6ab76922accc0c8ca9466f935e0aed609e3
https://jenkins.ubuntu.com/server/job/cloud-init-ci/350/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/350/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
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.

~rjschwei/cloud-init:addZyppRepos updated
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

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:d93870abb9668863c91ebd897ac6fd56ebc219c4
https://jenkins.ubuntu.com/server/job/cloud-init-ci/351/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/351/rebuild

review: Needs Fixing (continuous-integration)
~rjschwei/cloud-init:addZyppRepos updated
4de284c... by Robert Schweikert

- Style fixes

Revision history for this message
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

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:4de284ca40c57e1f6a4a09f0136a856f19c94c6c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/356/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/356/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
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/config/cc_zypper_add_repo.py

returns clean on my machine. My a difference between py3 flake 8 and py 2 flake8?

~rjschwei/cloud-init:addZyppRepos updated
b550f2d... by Robert Schweikert

- fix alpha order for importing

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:b550f2dfd944d89e967c9d72829fba0748575ac3
https://jenkins.ubuntu.com/server/job/cloud-init-ci/362/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/362/rebuild

review: Needs Fixing (continuous-integration)
~rjschwei/cloud-init:addZyppRepos updated
e4ed9ec... by Robert Schweikert

- import alpha order part 2

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:e4ed9ec76a82fb1168b8e92b4ea8a98463b0acfa
https://jenkins.ubuntu.com/server/job/cloud-init-ci/363/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/363/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
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://paste.ubuntu.com/25658611/

Revision history for this message
Robert Schweikert (rjschwei) wrote :

If http://paste.ubuntu.com/25658611/ is what it takes to get this merged I'll make the changes, however, I do not like the idea of having

_write_repos call _write_repository_configs

and having _write_repository_configs be responsible for multiple things as indicated in the doc string by the "and". IMHO, any time an implementation has an "and" as part of it's documentation that smells like side effect programming which IMHO is not a good practice.

But as I said, whatever it takes to get it merged and to avoid me carrying a patch in the SUSE package.

~rjschwei/cloud-init:addZyppRepos updated
a4d7d5a... by Robert Schweikert

- more "pythonic"

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:a4d7d5adefe2e411d709069f7cd539014fccb285
https://jenkins.ubuntu.com/server/job/cloud-init-ci/366/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/366/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Robert, that's not what it takes to get the approval, but rather a quick set of suggestions/opinions on parts of the structure of the code.

I admit my quick pass at this review needs validation and/or discussion/rejection. Feel free to reject parts as undesireable.

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.

Revision history for this message
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.

Revision history for this message
Chad Smith (chad.smith) wrote :

[1] Couple flakes:

cloudinit/config/cc_zypper_add_repo.py:15:1: H306: imports not in alphabetical order (cloudinit.util, cloudinit.config.schema.get_schema_doc)
tests/unittests/test_handler/test_handler_zypper_add_repo.py:18:1: F401 'tempfile' imported but unused.

[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.

review: Approve
~rjschwei/cloud-init:addZyppRepos updated
9485f74... by Robert Schweikert

- Test improvements for zypper repo add

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:9485f742ee4083d01d9ad7f50842f628176c58db
https://jenkins.ubuntu.com/server/job/cloud-init-ci/373/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/373/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_zypper_add_repo.py b/cloudinit/config/cc_zypper_add_repo.py
2new file mode 100644
3index 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
227diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl
228index 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 %}
241diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/test_handler/test_handler_zypper_add_repo.py
242new file mode 100644
243index 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
484diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
485index 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(

Subscribers

People subscribed via source and target branches