Merge ~chad.smith/cloud-init:ubuntu/zesty into cloud-init:ubuntu/zesty

Proposed by Chad Smith on 2017-10-23
Status: Merged
Approved by: Scott Moser on 2017-10-23
Approved revision: 4c1bfc6669042db65ddd148e5c506f92ab565259
Merged at revision: 4c1bfc6669042db65ddd148e5c506f92ab565259
Proposed branch: ~chad.smith/cloud-init:ubuntu/zesty
Merge into: cloud-init:ubuntu/zesty
Diff against target: 1040 lines (+258/-289)
23 files modified
cloudinit/config/cc_lxd.py (+1/-1)
cloudinit/config/cc_ntp.py (+3/-1)
cloudinit/config/cc_resizefs.py (+13/-30)
cloudinit/config/cc_users_groups.py (+2/-1)
cloudinit/config/schema.py (+1/-1)
debian/changelog (+14/-0)
debian/patches/series (+0/-1)
dev/null (+0/-181)
doc/examples/cloud-config-user-groups.txt (+3/-3)
tests/cloud_tests/testcases/__init__.py (+7/-0)
tests/cloud_tests/testcases/base.py (+8/-4)
tests/cloud_tests/testcases/examples/including_user_groups.py (+6/-0)
tests/cloud_tests/testcases/examples/including_user_groups.yaml (+5/-2)
tests/cloud_tests/testcases/main/command_output_simple.py (+16/-0)
tests/cloud_tests/testcases/modules/ntp.yaml (+2/-2)
tests/cloud_tests/testcases/modules/user_groups.py (+6/-0)
tests/cloud_tests/testcases/modules/user_groups.yaml (+5/-2)
tests/unittests/test_handler/test_handler_lxd.py (+8/-8)
tests/unittests/test_handler/test_handler_ntp.py (+12/-11)
tests/unittests/test_handler/test_handler_resizefs.py (+57/-34)
tests/unittests/test_handler/test_schema.py (+36/-1)
tools/read-dependencies (+36/-5)
tools/run-centos (+17/-1)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-10-23
Scott Moser 2017-10-23 Pending
Review via email: mp+332671@code.launchpad.net

Description of the change

Upstream snapshot of master into zesty for SRU

To post a comment you must log in.

PASSED: Continuous integration, rev:4c1bfc6669042db65ddd148e5c506f92ab565259
https://jenkins.ubuntu.com/server/job/cloud-init-ci/435/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py
2index e6262f8..09374d2 100644
3--- a/cloudinit/config/cc_lxd.py
4+++ b/cloudinit/config/cc_lxd.py
5@@ -72,7 +72,7 @@ def handle(name, cfg, cloud, log, args):
6 type(init_cfg))
7 init_cfg = {}
8
9- bridge_cfg = lxd_cfg.get('bridge')
10+ bridge_cfg = lxd_cfg.get('bridge', {})
11 if not isinstance(bridge_cfg, dict):
12 log.warn("lxd/bridge config must be a dictionary. found a '%s'",
13 type(bridge_cfg))
14diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
15index 15ae1ec..d43d060 100644
16--- a/cloudinit/config/cc_ntp.py
17+++ b/cloudinit/config/cc_ntp.py
18@@ -100,7 +100,9 @@ def handle(name, cfg, cloud, log, _args):
19 LOG.debug(
20 "Skipping module named %s, not present or disabled by cfg", name)
21 return
22- ntp_cfg = cfg.get('ntp', {})
23+ ntp_cfg = cfg['ntp']
24+ if ntp_cfg is None:
25+ ntp_cfg = {} # Allow empty config which will install the package
26
27 # TODO drop this when validate_cloudconfig_schema is strict=True
28 if not isinstance(ntp_cfg, (dict)):
29diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py
30index f774baa..0d282e6 100644
31--- a/cloudinit/config/cc_resizefs.py
32+++ b/cloudinit/config/cc_resizefs.py
33@@ -145,25 +145,6 @@ RESIZE_FS_PRECHECK_CMDS = {
34 }
35
36
37-def rootdev_from_cmdline(cmdline):
38- found = None
39- for tok in cmdline.split():
40- if tok.startswith("root="):
41- found = tok[5:]
42- break
43- if found is None:
44- return None
45-
46- if found.startswith("/dev/"):
47- return found
48- if found.startswith("LABEL="):
49- return "/dev/disk/by-label/" + found[len("LABEL="):]
50- if found.startswith("UUID="):
51- return "/dev/disk/by-uuid/" + found[len("UUID="):]
52-
53- return "/dev/" + found
54-
55-
56 def can_skip_resize(fs_type, resize_what, devpth):
57 fstype_lc = fs_type.lower()
58 for i, func in RESIZE_FS_PRECHECK_CMDS.items():
59@@ -172,14 +153,15 @@ def can_skip_resize(fs_type, resize_what, devpth):
60 return False
61
62
63-def is_device_path_writable_block(devpath, info, log):
64- """Return True if devpath is a writable block device.
65+def maybe_get_writable_device_path(devpath, info, log):
66+ """Return updated devpath if the devpath is a writable block device.
67
68- @param devpath: Path to the root device we want to resize.
69+ @param devpath: Requested path to the root device we want to resize.
70 @param info: String representing information about the requested device.
71 @param log: Logger to which logs will be added upon error.
72
73- @returns Boolean True if block device is writable
74+ @returns devpath or updated devpath per kernel commandline if the device
75+ path is a writable block device, returns None otherwise.
76 """
77 container = util.is_container()
78
79@@ -189,12 +171,12 @@ def is_device_path_writable_block(devpath, info, log):
80 devpath = util.rootdev_from_cmdline(util.get_cmdline())
81 if devpath is None:
82 log.warn("Unable to find device '/dev/root'")
83- return False
84+ return None
85 log.debug("Converted /dev/root to '%s' per kernel cmdline", devpath)
86
87 if devpath == 'overlayroot':
88 log.debug("Not attempting to resize devpath '%s': %s", devpath, info)
89- return False
90+ return None
91
92 try:
93 statret = os.stat(devpath)
94@@ -207,7 +189,7 @@ def is_device_path_writable_block(devpath, info, log):
95 devpath, info)
96 else:
97 raise exc
98- return False
99+ return None
100
101 if not stat.S_ISBLK(statret.st_mode) and not stat.S_ISCHR(statret.st_mode):
102 if container:
103@@ -216,8 +198,8 @@ def is_device_path_writable_block(devpath, info, log):
104 else:
105 log.warn("device '%s' not a block device. cannot resize: %s" %
106 (devpath, info))
107- return False
108- return True
109+ return None
110+ return devpath # The writable block devpath
111
112
113 def handle(name, cfg, _cloud, log, args):
114@@ -242,8 +224,9 @@ def handle(name, cfg, _cloud, log, args):
115 info = "dev=%s mnt_point=%s path=%s" % (devpth, mount_point, resize_what)
116 log.debug("resize_info: %s" % info)
117
118- if not is_device_path_writable_block(devpth, info, log):
119- return
120+ devpth = maybe_get_writable_device_path(devpth, info, log)
121+ if not devpth:
122+ return # devpath was not a writable block device
123
124 resizer = None
125 if can_skip_resize(fs_type, resize_what, devpth):
126diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py
127index b80d1d3..f363000 100644
128--- a/cloudinit/config/cc_users_groups.py
129+++ b/cloudinit/config/cc_users_groups.py
130@@ -15,7 +15,8 @@ options, see the ``Including users and groups`` config example.
131 Groups to add to the system can be specified as a list under the ``groups``
132 key. Each entry in the list should either contain a the group name as a string,
133 or a dictionary with the group name as the key and a list of users who should
134-be members of the group as the value.
135+be members of the group as the value. **Note**: Groups are added before users,
136+so any users in a group list must already exist on the system.
137
138 The ``users`` config key takes a list of users to configure. The first entry in
139 this list is used as the default user for the system. To preserve the standard
140diff --git a/cloudinit/config/schema.py b/cloudinit/config/schema.py
141index bb291ff..ca7d0d5 100644
142--- a/cloudinit/config/schema.py
143+++ b/cloudinit/config/schema.py
144@@ -74,7 +74,7 @@ def validate_cloudconfig_schema(config, schema, strict=False):
145 try:
146 from jsonschema import Draft4Validator, FormatChecker
147 except ImportError:
148- logging.warning(
149+ logging.debug(
150 'Ignoring schema validation. python-jsonschema is not present')
151 return
152 validator = Draft4Validator(schema, format_checker=FormatChecker())
153diff --git a/debian/changelog b/debian/changelog
154index e595586..86a8d0c 100644
155--- a/debian/changelog
156+++ b/debian/changelog
157@@ -1,3 +1,17 @@
158+cloud-init (17.1-25-g17a15f9e-0ubuntu1~17.04.1) zesty-proposed; urgency=medium
159+
160+ * New upstream snapshot.
161+ - resizefs: Fix regression when system booted with root=PARTUUID=
162+ (LP: #1725067)
163+ - tools: make yum package installation more reliable
164+ - citest: fix remaining warnings raised by integration tests.
165+ - citest: show the class actual class name in results.
166+ - ntp: fix config module schema to allow empty ntp config
167+ (LP: #1724951)
168+ - tools: disable fastestmirror if using proxy [Joshua Powers]
169+
170+ -- Chad Smith <chad.smith@canonical.com> Mon, 23 Oct 2017 14:57:46 -0600
171+
172 cloud-init (17.1-18-gd4f70470-0ubuntu1~17.04.2) zesty-proposed; urgency=medium
173
174 * cherry-pick 41152f1: schema: Log debug instead of warning when
175diff --git a/debian/patches/cpick-41152f1-schema-Log-debug-instead-of-warning-when-jsonschema-is b/debian/patches/cpick-41152f1-schema-Log-debug-instead-of-warning-when-jsonschema-is
176deleted file mode 100644
177index 6b9e784..0000000
178--- a/debian/patches/cpick-41152f1-schema-Log-debug-instead-of-warning-when-jsonschema-is
179+++ /dev/null
180@@ -1,181 +0,0 @@
181-From 41152f10ddbd8681cdac44b408038a4f23ab02df Mon Sep 17 00:00:00 2001
182-From: Scott Moser <smoser@brickies.net>
183-Date: Tue, 17 Oct 2017 16:12:59 -0400
184-Subject: [PATCH] schema: Log debug instead of warning when jsonschema is not
185- available.
186-
187-When operating in expected path, cloud-init should avoid logging with
188-warning. That causes 'WARNING' messages in /var/log/cloud-init.log.
189-By default, warnings also go to the console.
190-
191-Since jsonschema is a optional dependency, and not present on xenial
192-and zesty, cloud-init should not warn there.
193-
194-Also here:
195-* Add a test to integration tests to assert that there are no
196- warnings in /var/log/cloud-init.log.
197-* Update one integration test that did show warning and the related
198- documentation and examples.
199-
200-LP: #1724354
201----
202- cloudinit/config/cc_users_groups.py | 3 ++-
203- cloudinit/config/schema.py | 2 +-
204- doc/examples/cloud-config-user-groups.txt | 6 +++---
205- tests/cloud_tests/testcases/base.py | 4 ++++
206- tests/cloud_tests/testcases/examples/including_user_groups.py | 6 ++++++
207- tests/cloud_tests/testcases/examples/including_user_groups.yaml | 7 +++++--
208- tests/cloud_tests/testcases/modules/user_groups.py | 6 ++++++
209- tests/cloud_tests/testcases/modules/user_groups.yaml | 7 +++++--
210- 8 files changed, 32 insertions(+), 9 deletions(-)
211-
212-Index: cloud-init/cloudinit/config/cc_users_groups.py
213-===================================================================
214---- cloud-init.orig/cloudinit/config/cc_users_groups.py
215-+++ cloud-init/cloudinit/config/cc_users_groups.py
216-@@ -15,7 +15,8 @@ options, see the ``Including users and g
217- Groups to add to the system can be specified as a list under the ``groups``
218- key. Each entry in the list should either contain a the group name as a string,
219- or a dictionary with the group name as the key and a list of users who should
220--be members of the group as the value.
221-+be members of the group as the value. **Note**: Groups are added before users,
222-+so any users in a group list must already exist on the system.
223-
224- The ``users`` config key takes a list of users to configure. The first entry in
225- this list is used as the default user for the system. To preserve the standard
226-Index: cloud-init/cloudinit/config/schema.py
227-===================================================================
228---- cloud-init.orig/cloudinit/config/schema.py
229-+++ cloud-init/cloudinit/config/schema.py
230-@@ -74,7 +74,7 @@ def validate_cloudconfig_schema(config,
231- try:
232- from jsonschema import Draft4Validator, FormatChecker
233- except ImportError:
234-- logging.warning(
235-+ logging.debug(
236- 'Ignoring schema validation. python-jsonschema is not present')
237- return
238- validator = Draft4Validator(schema, format_checker=FormatChecker())
239-Index: cloud-init/doc/examples/cloud-config-user-groups.txt
240-===================================================================
241---- cloud-init.orig/doc/examples/cloud-config-user-groups.txt
242-+++ cloud-init/doc/examples/cloud-config-user-groups.txt
243-@@ -1,8 +1,8 @@
244- # Add groups to the system
245--# The following example adds the ubuntu group with members foo and bar and
246--# the group cloud-users.
247-+# The following example adds the ubuntu group with members 'root' and 'sys'
248-+# and the empty group cloud-users.
249- groups:
250-- - ubuntu: [foo,bar]
251-+ - ubuntu: [root,sys]
252- - cloud-users
253-
254- # Add users to the system. Users are added after groups are added.
255-Index: cloud-init/tests/cloud_tests/testcases/base.py
256-===================================================================
257---- cloud-init.orig/tests/cloud_tests/testcases/base.py
258-+++ cloud-init/tests/cloud_tests/testcases/base.py
259-@@ -72,6 +72,10 @@ class CloudTestCase(unittest.TestCase):
260- result = self.get_status_data(self.get_data_file('result.json'))
261- self.assertEqual(len(result['errors']), 0)
262-
263-+ def test_no_warnings_in_log(self):
264-+ """Warnings should not be found in the log."""
265-+ self.assertNotIn("WARN", self.get_data_file('cloud-init.log'))
266-+
267-
268- class PasswordListTest(CloudTestCase):
269- """Base password test case class."""
270-Index: cloud-init/tests/cloud_tests/testcases/examples/including_user_groups.py
271-===================================================================
272---- cloud-init.orig/tests/cloud_tests/testcases/examples/including_user_groups.py
273-+++ cloud-init/tests/cloud_tests/testcases/examples/including_user_groups.py
274-@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase)
275- out = self.get_data_file('user_cloudy')
276- self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
277-
278-+ def test_user_root_in_secret(self):
279-+ """Test root user is in 'secret' group."""
280-+ user, _, groups = self.get_data_file('root_groups').partition(":")
281-+ self.assertIn("secret", groups.split(),
282-+ msg="User root is not in group 'secret'")
283-+
284- # vi: ts=4 expandtab
285-Index: cloud-init/tests/cloud_tests/testcases/examples/including_user_groups.yaml
286-===================================================================
287---- cloud-init.orig/tests/cloud_tests/testcases/examples/including_user_groups.yaml
288-+++ cloud-init/tests/cloud_tests/testcases/examples/including_user_groups.yaml
289-@@ -8,7 +8,7 @@ cloud_config: |
290- #cloud-config
291- # Add groups to the system
292- groups:
293-- - secret: [foobar,barfoo]
294-+ - secret: [root]
295- - cloud-users
296-
297- # Add users to the system. Users are added after groups are added.
298-@@ -24,7 +24,7 @@ cloud_config: |
299- - name: barfoo
300- gecos: Bar B. Foo
301- sudo: ALL=(ALL) NOPASSWD:ALL
302-- groups: cloud-users
303-+ groups: [cloud-users, secret]
304- lock_passwd: true
305- - name: cloudy
306- gecos: Magic Cloud App Daemon User
307-@@ -49,5 +49,8 @@ collect_scripts:
308- user_cloudy: |
309- #!/bin/bash
310- getent passwd cloudy
311-+ root_groups: |
312-+ #!/bin/bash
313-+ groups root
314-
315- # vi: ts=4 expandtab
316-Index: cloud-init/tests/cloud_tests/testcases/modules/user_groups.py
317-===================================================================
318---- cloud-init.orig/tests/cloud_tests/testcases/modules/user_groups.py
319-+++ cloud-init/tests/cloud_tests/testcases/modules/user_groups.py
320-@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase)
321- out = self.get_data_file('user_cloudy')
322- self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
323-
324-+ def test_user_root_in_secret(self):
325-+ """Test root user is in 'secret' group."""
326-+ user, _, groups = self.get_data_file('root_groups').partition(":")
327-+ self.assertIn("secret", groups.split(),
328-+ msg="User root is not in group 'secret'")
329-+
330- # vi: ts=4 expandtab
331-Index: cloud-init/tests/cloud_tests/testcases/modules/user_groups.yaml
332-===================================================================
333---- cloud-init.orig/tests/cloud_tests/testcases/modules/user_groups.yaml
334-+++ cloud-init/tests/cloud_tests/testcases/modules/user_groups.yaml
335-@@ -7,7 +7,7 @@ cloud_config: |
336- #cloud-config
337- # Add groups to the system
338- groups:
339-- - secret: [foobar,barfoo]
340-+ - secret: [root]
341- - cloud-users
342-
343- # Add users to the system. Users are added after groups are added.
344-@@ -23,7 +23,7 @@ cloud_config: |
345- - name: barfoo
346- gecos: Bar B. Foo
347- sudo: ALL=(ALL) NOPASSWD:ALL
348-- groups: cloud-users
349-+ groups: [cloud-users, secret]
350- lock_passwd: true
351- - name: cloudy
352- gecos: Magic Cloud App Daemon User
353-@@ -48,5 +48,8 @@ collect_scripts:
354- user_cloudy: |
355- #!/bin/bash
356- getent passwd cloudy
357-+ root_groups: |
358-+ #!/bin/bash
359-+ groups root
360-
361- # vi: ts=4 expandtab
362diff --git a/debian/patches/series b/debian/patches/series
363index 4e403ce..fe91dbd 100644
364--- a/debian/patches/series
365+++ b/debian/patches/series
366@@ -1,2 +1 @@
367 stable-release-no-jsonschema-dep.patch
368-cpick-41152f1-schema-Log-debug-instead-of-warning-when-jsonschema-is
369diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
370index 9c5202f..0554d1f 100644
371--- a/doc/examples/cloud-config-user-groups.txt
372+++ b/doc/examples/cloud-config-user-groups.txt
373@@ -1,8 +1,8 @@
374 # Add groups to the system
375-# The following example adds the ubuntu group with members foo and bar and
376-# the group cloud-users.
377+# The following example adds the ubuntu group with members 'root' and 'sys'
378+# and the empty group cloud-users.
379 groups:
380- - ubuntu: [foo,bar]
381+ - ubuntu: [root,sys]
382 - cloud-users
383
384 # Add users to the system. Users are added after groups are added.
385diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py
386index 47217ce..a29a092 100644
387--- a/tests/cloud_tests/testcases/__init__.py
388+++ b/tests/cloud_tests/testcases/__init__.py
389@@ -5,6 +5,7 @@
390 import importlib
391 import inspect
392 import unittest
393+from unittest.util import strclass
394
395 from tests.cloud_tests import config
396 from tests.cloud_tests.testcases.base import CloudTestCase as base_test
397@@ -37,6 +38,12 @@ def get_suite(test_name, data, conf):
398
399 class tmp(test_class):
400
401+ _realclass = test_class
402+
403+ def __str__(self):
404+ return "%s (%s)" % (self._testMethodName,
405+ strclass(self._realclass))
406+
407 @classmethod
408 def setUpClass(cls):
409 cls.data = data
410diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
411index bb545ab..1706f59 100644
412--- a/tests/cloud_tests/testcases/base.py
413+++ b/tests/cloud_tests/testcases/base.py
414@@ -16,10 +16,6 @@ class CloudTestCase(unittest.TestCase):
415 conf = None
416 _cloud_config = None
417
418- def shortDescription(self):
419- """Prevent nose from using docstrings."""
420- return None
421-
422 @property
423 def cloud_config(self):
424 """Get the cloud-config used by the test."""
425@@ -72,6 +68,14 @@ class CloudTestCase(unittest.TestCase):
426 result = self.get_status_data(self.get_data_file('result.json'))
427 self.assertEqual(len(result['errors']), 0)
428
429+ def test_no_warnings_in_log(self):
430+ """Warnings should not be found in the log."""
431+ self.assertEqual(
432+ [],
433+ [l for l in self.get_data_file('cloud-init.log').splitlines()
434+ if 'WARN' in l],
435+ msg="'WARN' found inside cloud-init.log")
436+
437
438 class PasswordListTest(CloudTestCase):
439 """Base password test case class."""
440diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py
441index 67af527..93b7a82 100644
442--- a/tests/cloud_tests/testcases/examples/including_user_groups.py
443+++ b/tests/cloud_tests/testcases/examples/including_user_groups.py
444@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase):
445 out = self.get_data_file('user_cloudy')
446 self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
447
448+ def test_user_root_in_secret(self):
449+ """Test root user is in 'secret' group."""
450+ user, _, groups = self.get_data_file('root_groups').partition(":")
451+ self.assertIn("secret", groups.split(),
452+ msg="User root is not in group 'secret'")
453+
454 # vi: ts=4 expandtab
455diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.yaml b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
456index 0aa7ad2..469d03c 100644
457--- a/tests/cloud_tests/testcases/examples/including_user_groups.yaml
458+++ b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
459@@ -8,7 +8,7 @@ cloud_config: |
460 #cloud-config
461 # Add groups to the system
462 groups:
463- - secret: [foobar,barfoo]
464+ - secret: [root]
465 - cloud-users
466
467 # Add users to the system. Users are added after groups are added.
468@@ -24,7 +24,7 @@ cloud_config: |
469 - name: barfoo
470 gecos: Bar B. Foo
471 sudo: ALL=(ALL) NOPASSWD:ALL
472- groups: cloud-users
473+ groups: [cloud-users, secret]
474 lock_passwd: true
475 - name: cloudy
476 gecos: Magic Cloud App Daemon User
477@@ -49,5 +49,8 @@ collect_scripts:
478 user_cloudy: |
479 #!/bin/bash
480 getent passwd cloudy
481+ root_groups: |
482+ #!/bin/bash
483+ groups root
484
485 # vi: ts=4 expandtab
486diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py
487index fe4c767..857881c 100644
488--- a/tests/cloud_tests/testcases/main/command_output_simple.py
489+++ b/tests/cloud_tests/testcases/main/command_output_simple.py
490@@ -15,4 +15,20 @@ class TestCommandOutputSimple(base.CloudTestCase):
491 data.splitlines()[-1].strip())
492 # TODO: need to test that all stages redirected here
493
494+ def test_no_warnings_in_log(self):
495+ """Warnings should not be found in the log.
496+
497+ This class redirected stderr and stdout, so it expects to find
498+ a warning in cloud-init.log to that effect."""
499+ redirect_msg = 'Stdout, stderr changing to'
500+ warnings = [
501+ l for l in self.get_data_file('cloud-init.log').splitlines()
502+ if 'WARN' in l]
503+ self.assertEqual(
504+ [], [w for w in warnings if redirect_msg not in w],
505+ msg="'WARN' found inside cloud-init.log")
506+ self.assertEqual(
507+ 1, len(warnings),
508+ msg="Did not find %s in cloud-init.log" % redirect_msg)
509+
510 # vi: ts=4 expandtab
511diff --git a/tests/cloud_tests/testcases/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml
512index fbef431..2530d72 100644
513--- a/tests/cloud_tests/testcases/modules/ntp.yaml
514+++ b/tests/cloud_tests/testcases/modules/ntp.yaml
515@@ -4,8 +4,8 @@
516 cloud_config: |
517 #cloud-config
518 ntp:
519- pools: {}
520- servers: {}
521+ pools: []
522+ servers: []
523 collect_scripts:
524 ntp_installed: |
525 #!/bin/bash
526diff --git a/tests/cloud_tests/testcases/modules/user_groups.py b/tests/cloud_tests/testcases/modules/user_groups.py
527index 67af527..93b7a82 100644
528--- a/tests/cloud_tests/testcases/modules/user_groups.py
529+++ b/tests/cloud_tests/testcases/modules/user_groups.py
530@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase):
531 out = self.get_data_file('user_cloudy')
532 self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
533
534+ def test_user_root_in_secret(self):
535+ """Test root user is in 'secret' group."""
536+ user, _, groups = self.get_data_file('root_groups').partition(":")
537+ self.assertIn("secret", groups.split(),
538+ msg="User root is not in group 'secret'")
539+
540 # vi: ts=4 expandtab
541diff --git a/tests/cloud_tests/testcases/modules/user_groups.yaml b/tests/cloud_tests/testcases/modules/user_groups.yaml
542index 71cc9da..22b5d70 100644
543--- a/tests/cloud_tests/testcases/modules/user_groups.yaml
544+++ b/tests/cloud_tests/testcases/modules/user_groups.yaml
545@@ -7,7 +7,7 @@ cloud_config: |
546 #cloud-config
547 # Add groups to the system
548 groups:
549- - secret: [foobar,barfoo]
550+ - secret: [root]
551 - cloud-users
552
553 # Add users to the system. Users are added after groups are added.
554@@ -23,7 +23,7 @@ cloud_config: |
555 - name: barfoo
556 gecos: Bar B. Foo
557 sudo: ALL=(ALL) NOPASSWD:ALL
558- groups: cloud-users
559+ groups: [cloud-users, secret]
560 lock_passwd: true
561 - name: cloudy
562 gecos: Magic Cloud App Daemon User
563@@ -48,5 +48,8 @@ collect_scripts:
564 user_cloudy: |
565 #!/bin/bash
566 getent passwd cloudy
567+ root_groups: |
568+ #!/bin/bash
569+ groups root
570
571 # vi: ts=4 expandtab
572diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py
573index f132a77..e0d9ab6 100644
574--- a/tests/unittests/test_handler/test_handler_lxd.py
575+++ b/tests/unittests/test_handler/test_handler_lxd.py
576@@ -5,17 +5,16 @@ from cloudinit.sources import DataSourceNoCloud
577 from cloudinit import (distros, helpers, cloud)
578 from cloudinit.tests import helpers as t_help
579
580-import logging
581-
582 try:
583 from unittest import mock
584 except ImportError:
585 import mock
586
587-LOG = logging.getLogger(__name__)
588
589+class TestLxd(t_help.CiTestCase):
590+
591+ with_logs = True
592
593-class TestLxd(t_help.TestCase):
594 lxd_cfg = {
595 'lxd': {
596 'init': {
597@@ -41,7 +40,7 @@ class TestLxd(t_help.TestCase):
598 def test_lxd_init(self, mock_util):
599 cc = self._get_cloud('ubuntu')
600 mock_util.which.return_value = True
601- cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, [])
602+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
603 self.assertTrue(mock_util.which.called)
604 init_call = mock_util.subp.call_args_list[0][0][0]
605 self.assertEqual(init_call,
606@@ -55,7 +54,8 @@ class TestLxd(t_help.TestCase):
607 cc = self._get_cloud('ubuntu')
608 cc.distro = mock.MagicMock()
609 mock_util.which.return_value = None
610- cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, [])
611+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
612+ self.assertNotIn('WARN', self.logs.getvalue())
613 self.assertTrue(cc.distro.install_packages.called)
614 install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
615 self.assertEqual(sorted(install_pkg), ['lxd', 'zfs'])
616@@ -64,7 +64,7 @@ class TestLxd(t_help.TestCase):
617 def test_no_init_does_nothing(self, mock_util):
618 cc = self._get_cloud('ubuntu')
619 cc.distro = mock.MagicMock()
620- cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, LOG, [])
621+ cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, self.logger, [])
622 self.assertFalse(cc.distro.install_packages.called)
623 self.assertFalse(mock_util.subp.called)
624
625@@ -72,7 +72,7 @@ class TestLxd(t_help.TestCase):
626 def test_no_lxd_does_nothing(self, mock_util):
627 cc = self._get_cloud('ubuntu')
628 cc.distro = mock.MagicMock()
629- cc_lxd.handle('cc_lxd', {'package_update': True}, cc, LOG, [])
630+ cc_lxd.handle('cc_lxd', {'package_update': True}, cc, self.logger, [])
631 self.assertFalse(cc.distro.install_packages.called)
632 self.assertFalse(mock_util.subp.called)
633
634diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
635index 4f29124..3abe578 100644
636--- a/tests/unittests/test_handler/test_handler_ntp.py
637+++ b/tests/unittests/test_handler/test_handler_ntp.py
638@@ -293,23 +293,24 @@ class TestNtp(FilesystemMockingTestCase):
639
640 def test_ntp_handler_schema_validation_allows_empty_ntp_config(self):
641 """Ntp schema validation allows for an empty ntp: configuration."""
642- invalid_config = {'ntp': {}}
643+ valid_empty_configs = [{'ntp': {}}, {'ntp': None}]
644 distro = 'ubuntu'
645 cc = self._get_cloud(distro)
646 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
647 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
648 stream.write(NTP_TEMPLATE)
649- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
650- cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
651+ for valid_empty_config in valid_empty_configs:
652+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
653+ cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, [])
654+ with open(ntp_conf) as stream:
655+ content = stream.read()
656+ default_pools = [
657+ "{0}.{1}.pool.ntp.org".format(x, distro)
658+ for x in range(0, cc_ntp.NR_POOL_SERVERS)]
659+ self.assertEqual(
660+ "servers []\npools {0}\n".format(default_pools),
661+ content)
662 self.assertNotIn('Invalid config:', self.logs.getvalue())
663- with open(ntp_conf) as stream:
664- content = stream.read()
665- default_pools = [
666- "{0}.{1}.pool.ntp.org".format(x, distro)
667- for x in range(0, cc_ntp.NR_POOL_SERVERS)]
668- self.assertEqual(
669- "servers []\npools {0}\n".format(default_pools),
670- content)
671
672 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")
673 def test_ntp_handler_schema_validation_warns_non_string_item_type(self):
674diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
675index 3e5d436..29d5574 100644
676--- a/tests/unittests/test_handler/test_handler_resizefs.py
677+++ b/tests/unittests/test_handler/test_handler_resizefs.py
678@@ -1,9 +1,9 @@
679 # This file is part of cloud-init. See LICENSE file for license information.
680
681 from cloudinit.config.cc_resizefs import (
682- can_skip_resize, handle, is_device_path_writable_block,
683- rootdev_from_cmdline)
684+ can_skip_resize, handle, maybe_get_writable_device_path)
685
686+from collections import namedtuple
687 import logging
688 import textwrap
689
690@@ -138,47 +138,48 @@ class TestRootDevFromCmdline(CiTestCase):
691 invalid_cases = [
692 'BOOT_IMAGE=/adsf asdfa werasef root adf', 'BOOT_IMAGE=/adsf', '']
693 for case in invalid_cases:
694- self.assertIsNone(rootdev_from_cmdline(case))
695+ self.assertIsNone(util.rootdev_from_cmdline(case))
696
697 def test_rootdev_from_cmdline_with_root_startswith_dev(self):
698 """Return the cmdline root when the path starts with /dev."""
699 self.assertEqual(
700- '/dev/this', rootdev_from_cmdline('asdf root=/dev/this'))
701+ '/dev/this', util.rootdev_from_cmdline('asdf root=/dev/this'))
702
703 def test_rootdev_from_cmdline_with_root_without_dev_prefix(self):
704 """Add /dev prefix to cmdline root when the path lacks the prefix."""
705- self.assertEqual('/dev/this', rootdev_from_cmdline('asdf root=this'))
706+ self.assertEqual(
707+ '/dev/this', util.rootdev_from_cmdline('asdf root=this'))
708
709 def test_rootdev_from_cmdline_with_root_with_label(self):
710 """When cmdline root contains a LABEL, our root is disk/by-label."""
711 self.assertEqual(
712 '/dev/disk/by-label/unique',
713- rootdev_from_cmdline('asdf root=LABEL=unique'))
714+ util.rootdev_from_cmdline('asdf root=LABEL=unique'))
715
716 def test_rootdev_from_cmdline_with_root_with_uuid(self):
717 """When cmdline root contains a UUID, our root is disk/by-uuid."""
718 self.assertEqual(
719 '/dev/disk/by-uuid/adsfdsaf-adsf',
720- rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf'))
721+ util.rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf'))
722
723
724-class TestIsDevicePathWritableBlock(CiTestCase):
725+class TestMaybeGetDevicePathAsWritableBlock(CiTestCase):
726
727 with_logs = True
728
729- def test_is_device_path_writable_block_false_on_overlayroot(self):
730+ def test_maybe_get_writable_device_path_none_on_overlayroot(self):
731 """When devpath is overlayroot (on MAAS), is_dev_writable is False."""
732 info = 'does not matter'
733- is_writable = wrap_and_call(
734+ devpath = wrap_and_call(
735 'cloudinit.config.cc_resizefs.util',
736 {'is_container': {'return_value': False}},
737- is_device_path_writable_block, 'overlayroot', info, LOG)
738- self.assertFalse(is_writable)
739+ maybe_get_writable_device_path, 'overlayroot', info, LOG)
740+ self.assertIsNone(devpath)
741 self.assertIn(
742 "Not attempting to resize devpath 'overlayroot'",
743 self.logs.getvalue())
744
745- def test_is_device_path_writable_block_warns_missing_cmdline_root(self):
746+ def test_maybe_get_writable_device_path_warns_missing_cmdline_root(self):
747 """When root does not exist isn't in the cmdline, log warning."""
748 info = 'does not matter'
749
750@@ -190,43 +191,43 @@ class TestIsDevicePathWritableBlock(CiTestCase):
751 exists_mock_path = 'cloudinit.config.cc_resizefs.os.path.exists'
752 with mock.patch(exists_mock_path) as m_exists:
753 m_exists.return_value = False
754- is_writable = wrap_and_call(
755+ devpath = wrap_and_call(
756 'cloudinit.config.cc_resizefs.util',
757 {'is_container': {'return_value': False},
758 'get_mount_info': {'side_effect': fake_mount_info},
759 'get_cmdline': {'return_value': 'BOOT_IMAGE=/vmlinuz.efi'}},
760- is_device_path_writable_block, '/dev/root', info, LOG)
761- self.assertFalse(is_writable)
762+ maybe_get_writable_device_path, '/dev/root', info, LOG)
763+ self.assertIsNone(devpath)
764 logs = self.logs.getvalue()
765 self.assertIn("WARNING: Unable to find device '/dev/root'", logs)
766
767- def test_is_device_path_writable_block_does_not_exist(self):
768+ def test_maybe_get_writable_device_path_does_not_exist(self):
769 """When devpath does not exist, a warning is logged."""
770 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
771- is_writable = wrap_and_call(
772+ devpath = wrap_and_call(
773 'cloudinit.config.cc_resizefs.util',
774 {'is_container': {'return_value': False}},
775- is_device_path_writable_block, '/I/dont/exist', info, LOG)
776- self.assertFalse(is_writable)
777+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
778+ self.assertIsNone(devpath)
779 self.assertIn(
780 "WARNING: Device '/I/dont/exist' did not exist."
781 ' cannot resize: %s' % info,
782 self.logs.getvalue())
783
784- def test_is_device_path_writable_block_does_not_exist_in_container(self):
785+ def test_maybe_get_writable_device_path_does_not_exist_in_container(self):
786 """When devpath does not exist in a container, log a debug message."""
787 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
788- is_writable = wrap_and_call(
789+ devpath = wrap_and_call(
790 'cloudinit.config.cc_resizefs.util',
791 {'is_container': {'return_value': True}},
792- is_device_path_writable_block, '/I/dont/exist', info, LOG)
793- self.assertFalse(is_writable)
794+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
795+ self.assertIsNone(devpath)
796 self.assertIn(
797 "DEBUG: Device '/I/dont/exist' did not exist in container."
798 ' cannot resize: %s' % info,
799 self.logs.getvalue())
800
801- def test_is_device_path_writable_block_raises_oserror(self):
802+ def test_maybe_get_writable_device_path_raises_oserror(self):
803 """When unexpected OSError is raises by os.stat it is reraised."""
804 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
805 with self.assertRaises(OSError) as context_manager:
806@@ -234,41 +235,63 @@ class TestIsDevicePathWritableBlock(CiTestCase):
807 'cloudinit.config.cc_resizefs',
808 {'util.is_container': {'return_value': True},
809 'os.stat': {'side_effect': OSError('Something unexpected')}},
810- is_device_path_writable_block, '/I/dont/exist', info, LOG)
811+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
812 self.assertEqual(
813 'Something unexpected', str(context_manager.exception))
814
815- def test_is_device_path_writable_block_non_block(self):
816+ def test_maybe_get_writable_device_path_non_block(self):
817 """When device is not a block device, emit warning return False."""
818 fake_devpath = self.tmp_path('dev/readwrite')
819 util.write_file(fake_devpath, '', mode=0o600) # read-write
820 info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
821
822- is_writable = wrap_and_call(
823+ devpath = wrap_and_call(
824 'cloudinit.config.cc_resizefs.util',
825 {'is_container': {'return_value': False}},
826- is_device_path_writable_block, fake_devpath, info, LOG)
827- self.assertFalse(is_writable)
828+ maybe_get_writable_device_path, fake_devpath, info, LOG)
829+ self.assertIsNone(devpath)
830 self.assertIn(
831 "WARNING: device '{0}' not a block device. cannot resize".format(
832 fake_devpath),
833 self.logs.getvalue())
834
835- def test_is_device_path_writable_block_non_block_on_container(self):
836+ def test_maybe_get_writable_device_path_non_block_on_container(self):
837 """When device is non-block device in container, emit debug log."""
838 fake_devpath = self.tmp_path('dev/readwrite')
839 util.write_file(fake_devpath, '', mode=0o600) # read-write
840 info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
841
842- is_writable = wrap_and_call(
843+ devpath = wrap_and_call(
844 'cloudinit.config.cc_resizefs.util',
845 {'is_container': {'return_value': True}},
846- is_device_path_writable_block, fake_devpath, info, LOG)
847- self.assertFalse(is_writable)
848+ maybe_get_writable_device_path, fake_devpath, info, LOG)
849+ self.assertIsNone(devpath)
850 self.assertIn(
851 "DEBUG: device '{0}' not a block device in container."
852 ' cannot resize'.format(fake_devpath),
853 self.logs.getvalue())
854
855+ def test_maybe_get_writable_device_path_returns_cmdline_root(self):
856+ """When root device is UUID in kernel commandline, update devpath."""
857+ # XXX Long-term we want to use FilesystemMocking test to avoid
858+ # touching os.stat.
859+ FakeStat = namedtuple(
860+ 'FakeStat', ['st_mode', 'st_size', 'st_mtime']) # minimal def.
861+ info = 'dev=/dev/root mnt_point=/ path=/does/not/matter'
862+ devpath = wrap_and_call(
863+ 'cloudinit.config.cc_resizefs',
864+ {'util.get_cmdline': {'return_value': 'asdf root=UUID=my-uuid'},
865+ 'util.is_container': False,
866+ 'os.path.exists': False, # /dev/root doesn't exist
867+ 'os.stat': {
868+ 'return_value': FakeStat(25008, 0, 1)} # char block device
869+ },
870+ maybe_get_writable_device_path, '/dev/root', info, LOG)
871+ self.assertEqual('/dev/disk/by-uuid/my-uuid', devpath)
872+ self.assertIn(
873+ "DEBUG: Converted /dev/root to '/dev/disk/by-uuid/my-uuid'"
874+ " per kernel cmdline",
875+ self.logs.getvalue())
876+
877
878 # vi: ts=4 expandtab
879diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
880index b8fc893..648573f 100644
881--- a/tests/unittests/test_handler/test_schema.py
882+++ b/tests/unittests/test_handler/test_schema.py
883@@ -4,11 +4,12 @@ from cloudinit.config.schema import (
884 CLOUD_CONFIG_HEADER, SchemaValidationError, annotated_cloudconfig_file,
885 get_schema_doc, get_schema, validate_cloudconfig_file,
886 validate_cloudconfig_schema, main)
887-from cloudinit.util import write_file
888+from cloudinit.util import subp, write_file
889
890 from cloudinit.tests.helpers import CiTestCase, mock, skipIf
891
892 from copy import copy
893+import os
894 from six import StringIO
895 from textwrap import dedent
896 from yaml import safe_load
897@@ -364,4 +365,38 @@ class MainTest(CiTestCase):
898 self.assertIn(
899 'Valid cloud-config file {0}'.format(myyaml), m_stdout.getvalue())
900
901+
902+class CloudTestsIntegrationTest(CiTestCase):
903+ """Validate all cloud-config yaml schema provided in integration tests.
904+
905+ It is less expensive to have unittests validate schema of all cloud-config
906+ yaml provided to integration tests, than to run an integration test which
907+ raises Warnings or errors on invalid cloud-config schema.
908+ """
909+
910+ @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")
911+ def test_all_integration_test_cloud_config_schema(self):
912+ """Validate schema of cloud_tests yaml files looking for warnings."""
913+ schema = get_schema()
914+ testsdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
915+ integration_testdir = os.path.sep.join(
916+ [testsdir, 'cloud_tests', 'testcases'])
917+ errors = []
918+ out, _ = subp(['find', integration_testdir, '-name', '*yaml'])
919+ for filename in out.splitlines():
920+ test_cfg = safe_load(open(filename))
921+ cloud_config = test_cfg.get('cloud_config')
922+ if cloud_config:
923+ cloud_config = safe_load(
924+ cloud_config.replace("#cloud-config\n", ""))
925+ try:
926+ validate_cloudconfig_schema(
927+ cloud_config, schema, strict=True)
928+ except SchemaValidationError as e:
929+ errors.append(
930+ '{0}: {1}'.format(
931+ filename, e))
932+ if errors:
933+ raise AssertionError(', '.join(errors))
934+
935 # vi: ts=4 expandtab syntax=python
936diff --git a/tools/read-dependencies b/tools/read-dependencies
937index 2a64868..421f470 100755
938--- a/tools/read-dependencies
939+++ b/tools/read-dependencies
940@@ -30,9 +30,35 @@ DISTRO_PKG_TYPE_MAP = {
941 'suse': 'suse'
942 }
943
944-DISTRO_INSTALL_PKG_CMD = {
945+MAYBE_RELIABLE_YUM_INSTALL = [
946+ 'sh', '-c',
947+ """
948+ error() { echo "$@" 1>&2; }
949+ n=0; max=10;
950+ bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
951+ while n=$(($n+1)); do
952+ error ":: running $bcmd $* [$n/$max]"
953+ $bcmd "$@"
954+ r=$?
955+ [ $r -eq 0 ] && break
956+ [ $n -ge $max ] && { error "gave up on $bcmd"; exit $r; }
957+ nap=$(($n*5))
958+ error ":: failed [$r] ($n/$max). sleeping $nap."
959+ sleep $nap
960+ done
961+ error ":: running yum install --cacheonly --assumeyes $*"
962+ yum install --cacheonly --assumeyes "$@"
963+ """,
964+ 'reliable-yum-install']
965+
966+DRY_DISTRO_INSTALL_PKG_CMD = {
967 'centos': ['yum', 'install', '--assumeyes'],
968 'redhat': ['yum', 'install', '--assumeyes'],
969+}
970+
971+DISTRO_INSTALL_PKG_CMD = {
972+ 'centos': MAYBE_RELIABLE_YUM_INSTALL,
973+ 'redhat': MAYBE_RELIABLE_YUM_INSTALL,
974 'debian': ['apt', 'install', '-y'],
975 'ubuntu': ['apt', 'install', '-y'],
976 'opensuse': ['zypper', 'install'],
977@@ -80,8 +106,8 @@ def get_parser():
978 help='Additionally install continuous integration system packages '
979 'required for build and test automation.')
980 parser.add_argument(
981- '-v', '--python-version', type=str, dest='python_version', default=None,
982- choices=["2", "3"],
983+ '-v', '--python-version', type=str, dest='python_version',
984+ default=None, choices=["2", "3"],
985 help='Override the version of python we want to generate system '
986 'package dependencies for. Defaults to the version of python '
987 'this script is called with')
988@@ -219,10 +245,15 @@ def pkg_install(pkg_list, distro, test_distro=False, dry_run=False):
989 '(dryrun)' if dry_run else '', ' '.join(pkg_list)))
990 install_cmd = []
991 if dry_run:
992- install_cmd.append('echo')
993+ install_cmd.append('echo')
994 if os.geteuid() != 0:
995 install_cmd.append('sudo')
996- install_cmd.extend(DISTRO_INSTALL_PKG_CMD[distro])
997+
998+ cmd = DISTRO_INSTALL_PKG_CMD[distro]
999+ if dry_run and distro in DRY_DISTRO_INSTALL_PKG_CMD:
1000+ cmd = DRY_DISTRO_INSTALL_PKG_CMD[distro]
1001+ install_cmd.extend(cmd)
1002+
1003 if distro in ['centos', 'redhat']:
1004 # CentOS and Redhat need epel-release to access oauthlib and jsonschema
1005 subprocess.check_call(install_cmd + ['epel-release'])
1006diff --git a/tools/run-centos b/tools/run-centos
1007index d44d514..d58ef3e 100755
1008--- a/tools/run-centos
1009+++ b/tools/run-centos
1010@@ -123,7 +123,22 @@ prep() {
1011 return 0
1012 fi
1013 error "Installing prep packages: ${needed}"
1014- yum install --assumeyes ${needed}
1015+ set -- $needed
1016+ local n max r
1017+ n=0; max=10;
1018+ bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
1019+ while n=$(($n+1)); do
1020+ error ":: running $bcmd $* [$n/$max]"
1021+ $bcmd "$@"
1022+ r=$?
1023+ [ $r -eq 0 ] && break
1024+ [ $n -ge $max ] && { error "gave up on $bcmd"; exit $r; }
1025+ nap=$(($n*5))
1026+ error ":: failed [$r] ($n/$max). sleeping $nap."
1027+ sleep $nap
1028+ done
1029+ error ":: running yum install --cacheonly --assumeyes $*"
1030+ yum install --cacheonly --assumeyes "$@"
1031 }
1032
1033 start_container() {
1034@@ -153,6 +168,7 @@ start_container() {
1035 if [ ! -z "${http_proxy-}" ]; then
1036 debug 1 "configuring proxy ${http_proxy}"
1037 inside "$name" sh -c "echo proxy=$http_proxy >> /etc/yum.conf"
1038+ inside "$name" sed -i s/enabled=1/enabled=0/ /etc/yum/pluginconf.d/fastestmirror.conf
1039 fi
1040 }
1041

Subscribers

People subscribed via source and target branches