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

Proposed by Chad Smith on 2017-10-23
Status: Merged
Merged at revision: b5ca0b1343faaf1a802ff3ff1c4231e1304ad770
Proposed branch: ~chad.smith/cloud-init:ubuntu/xenial
Merge into: cloud-init:ubuntu/xenial
Diff against target: 1042 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+332670@code.launchpad.net

Description of the change

Upstream snapshot or master for SRU in xenial

To post a comment you must log in.

PASSED: Continuous integration, rev:b5ca0b1343faaf1a802ff3ff1c4231e1304ad770
https://jenkins.ubuntu.com/server/job/cloud-init-ci/436/
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/436/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 5a66def..e7ffc6f 100644
155--- a/debian/changelog
156+++ b/debian/changelog
157@@ -1,3 +1,17 @@
158+cloud-init (17.1-25-g17a15f9e-0ubuntu1~16.04.1) xenial-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:54:05 -0600
171+
172 cloud-init (17.1-18-gd4f70470-0ubuntu1~16.04.2) xenial-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 8bba1e4..7e909af 100644
364--- a/debian/patches/series
365+++ b/debian/patches/series
366@@ -1,4 +1,3 @@
367 azure-use-walinux-agent.patch
368 ds-identify-behavior-xenial.patch
369 stable-release-no-jsonschema-dep.patch
370-cpick-41152f1-schema-Log-debug-instead-of-warning-when-jsonschema-is
371diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
372index 9c5202f..0554d1f 100644
373--- a/doc/examples/cloud-config-user-groups.txt
374+++ b/doc/examples/cloud-config-user-groups.txt
375@@ -1,8 +1,8 @@
376 # Add groups to the system
377-# The following example adds the ubuntu group with members foo and bar and
378-# the group cloud-users.
379+# The following example adds the ubuntu group with members 'root' and 'sys'
380+# and the empty group cloud-users.
381 groups:
382- - ubuntu: [foo,bar]
383+ - ubuntu: [root,sys]
384 - cloud-users
385
386 # Add users to the system. Users are added after groups are added.
387diff --git a/tests/cloud_tests/testcases/__init__.py b/tests/cloud_tests/testcases/__init__.py
388index 47217ce..a29a092 100644
389--- a/tests/cloud_tests/testcases/__init__.py
390+++ b/tests/cloud_tests/testcases/__init__.py
391@@ -5,6 +5,7 @@
392 import importlib
393 import inspect
394 import unittest
395+from unittest.util import strclass
396
397 from tests.cloud_tests import config
398 from tests.cloud_tests.testcases.base import CloudTestCase as base_test
399@@ -37,6 +38,12 @@ def get_suite(test_name, data, conf):
400
401 class tmp(test_class):
402
403+ _realclass = test_class
404+
405+ def __str__(self):
406+ return "%s (%s)" % (self._testMethodName,
407+ strclass(self._realclass))
408+
409 @classmethod
410 def setUpClass(cls):
411 cls.data = data
412diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py
413index bb545ab..1706f59 100644
414--- a/tests/cloud_tests/testcases/base.py
415+++ b/tests/cloud_tests/testcases/base.py
416@@ -16,10 +16,6 @@ class CloudTestCase(unittest.TestCase):
417 conf = None
418 _cloud_config = None
419
420- def shortDescription(self):
421- """Prevent nose from using docstrings."""
422- return None
423-
424 @property
425 def cloud_config(self):
426 """Get the cloud-config used by the test."""
427@@ -72,6 +68,14 @@ class CloudTestCase(unittest.TestCase):
428 result = self.get_status_data(self.get_data_file('result.json'))
429 self.assertEqual(len(result['errors']), 0)
430
431+ def test_no_warnings_in_log(self):
432+ """Warnings should not be found in the log."""
433+ self.assertEqual(
434+ [],
435+ [l for l in self.get_data_file('cloud-init.log').splitlines()
436+ if 'WARN' in l],
437+ msg="'WARN' found inside cloud-init.log")
438+
439
440 class PasswordListTest(CloudTestCase):
441 """Base password test case class."""
442diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.py b/tests/cloud_tests/testcases/examples/including_user_groups.py
443index 67af527..93b7a82 100644
444--- a/tests/cloud_tests/testcases/examples/including_user_groups.py
445+++ b/tests/cloud_tests/testcases/examples/including_user_groups.py
446@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase):
447 out = self.get_data_file('user_cloudy')
448 self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
449
450+ def test_user_root_in_secret(self):
451+ """Test root user is in 'secret' group."""
452+ user, _, groups = self.get_data_file('root_groups').partition(":")
453+ self.assertIn("secret", groups.split(),
454+ msg="User root is not in group 'secret'")
455+
456 # vi: ts=4 expandtab
457diff --git a/tests/cloud_tests/testcases/examples/including_user_groups.yaml b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
458index 0aa7ad2..469d03c 100644
459--- a/tests/cloud_tests/testcases/examples/including_user_groups.yaml
460+++ b/tests/cloud_tests/testcases/examples/including_user_groups.yaml
461@@ -8,7 +8,7 @@ cloud_config: |
462 #cloud-config
463 # Add groups to the system
464 groups:
465- - secret: [foobar,barfoo]
466+ - secret: [root]
467 - cloud-users
468
469 # Add users to the system. Users are added after groups are added.
470@@ -24,7 +24,7 @@ cloud_config: |
471 - name: barfoo
472 gecos: Bar B. Foo
473 sudo: ALL=(ALL) NOPASSWD:ALL
474- groups: cloud-users
475+ groups: [cloud-users, secret]
476 lock_passwd: true
477 - name: cloudy
478 gecos: Magic Cloud App Daemon User
479@@ -49,5 +49,8 @@ collect_scripts:
480 user_cloudy: |
481 #!/bin/bash
482 getent passwd cloudy
483+ root_groups: |
484+ #!/bin/bash
485+ groups root
486
487 # vi: ts=4 expandtab
488diff --git a/tests/cloud_tests/testcases/main/command_output_simple.py b/tests/cloud_tests/testcases/main/command_output_simple.py
489index fe4c767..857881c 100644
490--- a/tests/cloud_tests/testcases/main/command_output_simple.py
491+++ b/tests/cloud_tests/testcases/main/command_output_simple.py
492@@ -15,4 +15,20 @@ class TestCommandOutputSimple(base.CloudTestCase):
493 data.splitlines()[-1].strip())
494 # TODO: need to test that all stages redirected here
495
496+ def test_no_warnings_in_log(self):
497+ """Warnings should not be found in the log.
498+
499+ This class redirected stderr and stdout, so it expects to find
500+ a warning in cloud-init.log to that effect."""
501+ redirect_msg = 'Stdout, stderr changing to'
502+ warnings = [
503+ l for l in self.get_data_file('cloud-init.log').splitlines()
504+ if 'WARN' in l]
505+ self.assertEqual(
506+ [], [w for w in warnings if redirect_msg not in w],
507+ msg="'WARN' found inside cloud-init.log")
508+ self.assertEqual(
509+ 1, len(warnings),
510+ msg="Did not find %s in cloud-init.log" % redirect_msg)
511+
512 # vi: ts=4 expandtab
513diff --git a/tests/cloud_tests/testcases/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml
514index fbef431..2530d72 100644
515--- a/tests/cloud_tests/testcases/modules/ntp.yaml
516+++ b/tests/cloud_tests/testcases/modules/ntp.yaml
517@@ -4,8 +4,8 @@
518 cloud_config: |
519 #cloud-config
520 ntp:
521- pools: {}
522- servers: {}
523+ pools: []
524+ servers: []
525 collect_scripts:
526 ntp_installed: |
527 #!/bin/bash
528diff --git a/tests/cloud_tests/testcases/modules/user_groups.py b/tests/cloud_tests/testcases/modules/user_groups.py
529index 67af527..93b7a82 100644
530--- a/tests/cloud_tests/testcases/modules/user_groups.py
531+++ b/tests/cloud_tests/testcases/modules/user_groups.py
532@@ -40,4 +40,10 @@ class TestUserGroups(base.CloudTestCase):
533 out = self.get_data_file('user_cloudy')
534 self.assertRegex(out, r'cloudy:x:[0-9]{3,4}:')
535
536+ def test_user_root_in_secret(self):
537+ """Test root user is in 'secret' group."""
538+ user, _, groups = self.get_data_file('root_groups').partition(":")
539+ self.assertIn("secret", groups.split(),
540+ msg="User root is not in group 'secret'")
541+
542 # vi: ts=4 expandtab
543diff --git a/tests/cloud_tests/testcases/modules/user_groups.yaml b/tests/cloud_tests/testcases/modules/user_groups.yaml
544index 71cc9da..22b5d70 100644
545--- a/tests/cloud_tests/testcases/modules/user_groups.yaml
546+++ b/tests/cloud_tests/testcases/modules/user_groups.yaml
547@@ -7,7 +7,7 @@ cloud_config: |
548 #cloud-config
549 # Add groups to the system
550 groups:
551- - secret: [foobar,barfoo]
552+ - secret: [root]
553 - cloud-users
554
555 # Add users to the system. Users are added after groups are added.
556@@ -23,7 +23,7 @@ cloud_config: |
557 - name: barfoo
558 gecos: Bar B. Foo
559 sudo: ALL=(ALL) NOPASSWD:ALL
560- groups: cloud-users
561+ groups: [cloud-users, secret]
562 lock_passwd: true
563 - name: cloudy
564 gecos: Magic Cloud App Daemon User
565@@ -48,5 +48,8 @@ collect_scripts:
566 user_cloudy: |
567 #!/bin/bash
568 getent passwd cloudy
569+ root_groups: |
570+ #!/bin/bash
571+ groups root
572
573 # vi: ts=4 expandtab
574diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py
575index f132a77..e0d9ab6 100644
576--- a/tests/unittests/test_handler/test_handler_lxd.py
577+++ b/tests/unittests/test_handler/test_handler_lxd.py
578@@ -5,17 +5,16 @@ from cloudinit.sources import DataSourceNoCloud
579 from cloudinit import (distros, helpers, cloud)
580 from cloudinit.tests import helpers as t_help
581
582-import logging
583-
584 try:
585 from unittest import mock
586 except ImportError:
587 import mock
588
589-LOG = logging.getLogger(__name__)
590
591+class TestLxd(t_help.CiTestCase):
592+
593+ with_logs = True
594
595-class TestLxd(t_help.TestCase):
596 lxd_cfg = {
597 'lxd': {
598 'init': {
599@@ -41,7 +40,7 @@ class TestLxd(t_help.TestCase):
600 def test_lxd_init(self, mock_util):
601 cc = self._get_cloud('ubuntu')
602 mock_util.which.return_value = True
603- cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, [])
604+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
605 self.assertTrue(mock_util.which.called)
606 init_call = mock_util.subp.call_args_list[0][0][0]
607 self.assertEqual(init_call,
608@@ -55,7 +54,8 @@ class TestLxd(t_help.TestCase):
609 cc = self._get_cloud('ubuntu')
610 cc.distro = mock.MagicMock()
611 mock_util.which.return_value = None
612- cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, LOG, [])
613+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
614+ self.assertNotIn('WARN', self.logs.getvalue())
615 self.assertTrue(cc.distro.install_packages.called)
616 install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
617 self.assertEqual(sorted(install_pkg), ['lxd', 'zfs'])
618@@ -64,7 +64,7 @@ class TestLxd(t_help.TestCase):
619 def test_no_init_does_nothing(self, mock_util):
620 cc = self._get_cloud('ubuntu')
621 cc.distro = mock.MagicMock()
622- cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, LOG, [])
623+ cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, self.logger, [])
624 self.assertFalse(cc.distro.install_packages.called)
625 self.assertFalse(mock_util.subp.called)
626
627@@ -72,7 +72,7 @@ class TestLxd(t_help.TestCase):
628 def test_no_lxd_does_nothing(self, mock_util):
629 cc = self._get_cloud('ubuntu')
630 cc.distro = mock.MagicMock()
631- cc_lxd.handle('cc_lxd', {'package_update': True}, cc, LOG, [])
632+ cc_lxd.handle('cc_lxd', {'package_update': True}, cc, self.logger, [])
633 self.assertFalse(cc.distro.install_packages.called)
634 self.assertFalse(mock_util.subp.called)
635
636diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py
637index 4f29124..3abe578 100644
638--- a/tests/unittests/test_handler/test_handler_ntp.py
639+++ b/tests/unittests/test_handler/test_handler_ntp.py
640@@ -293,23 +293,24 @@ class TestNtp(FilesystemMockingTestCase):
641
642 def test_ntp_handler_schema_validation_allows_empty_ntp_config(self):
643 """Ntp schema validation allows for an empty ntp: configuration."""
644- invalid_config = {'ntp': {}}
645+ valid_empty_configs = [{'ntp': {}}, {'ntp': None}]
646 distro = 'ubuntu'
647 cc = self._get_cloud(distro)
648 ntp_conf = os.path.join(self.new_root, 'ntp.conf')
649 with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream:
650 stream.write(NTP_TEMPLATE)
651- with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
652- cc_ntp.handle('cc_ntp', invalid_config, cc, None, [])
653+ for valid_empty_config in valid_empty_configs:
654+ with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf):
655+ cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, [])
656+ with open(ntp_conf) as stream:
657+ content = stream.read()
658+ default_pools = [
659+ "{0}.{1}.pool.ntp.org".format(x, distro)
660+ for x in range(0, cc_ntp.NR_POOL_SERVERS)]
661+ self.assertEqual(
662+ "servers []\npools {0}\n".format(default_pools),
663+ content)
664 self.assertNotIn('Invalid config:', self.logs.getvalue())
665- with open(ntp_conf) as stream:
666- content = stream.read()
667- default_pools = [
668- "{0}.{1}.pool.ntp.org".format(x, distro)
669- for x in range(0, cc_ntp.NR_POOL_SERVERS)]
670- self.assertEqual(
671- "servers []\npools {0}\n".format(default_pools),
672- content)
673
674 @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")
675 def test_ntp_handler_schema_validation_warns_non_string_item_type(self):
676diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py
677index 3e5d436..29d5574 100644
678--- a/tests/unittests/test_handler/test_handler_resizefs.py
679+++ b/tests/unittests/test_handler/test_handler_resizefs.py
680@@ -1,9 +1,9 @@
681 # This file is part of cloud-init. See LICENSE file for license information.
682
683 from cloudinit.config.cc_resizefs import (
684- can_skip_resize, handle, is_device_path_writable_block,
685- rootdev_from_cmdline)
686+ can_skip_resize, handle, maybe_get_writable_device_path)
687
688+from collections import namedtuple
689 import logging
690 import textwrap
691
692@@ -138,47 +138,48 @@ class TestRootDevFromCmdline(CiTestCase):
693 invalid_cases = [
694 'BOOT_IMAGE=/adsf asdfa werasef root adf', 'BOOT_IMAGE=/adsf', '']
695 for case in invalid_cases:
696- self.assertIsNone(rootdev_from_cmdline(case))
697+ self.assertIsNone(util.rootdev_from_cmdline(case))
698
699 def test_rootdev_from_cmdline_with_root_startswith_dev(self):
700 """Return the cmdline root when the path starts with /dev."""
701 self.assertEqual(
702- '/dev/this', rootdev_from_cmdline('asdf root=/dev/this'))
703+ '/dev/this', util.rootdev_from_cmdline('asdf root=/dev/this'))
704
705 def test_rootdev_from_cmdline_with_root_without_dev_prefix(self):
706 """Add /dev prefix to cmdline root when the path lacks the prefix."""
707- self.assertEqual('/dev/this', rootdev_from_cmdline('asdf root=this'))
708+ self.assertEqual(
709+ '/dev/this', util.rootdev_from_cmdline('asdf root=this'))
710
711 def test_rootdev_from_cmdline_with_root_with_label(self):
712 """When cmdline root contains a LABEL, our root is disk/by-label."""
713 self.assertEqual(
714 '/dev/disk/by-label/unique',
715- rootdev_from_cmdline('asdf root=LABEL=unique'))
716+ util.rootdev_from_cmdline('asdf root=LABEL=unique'))
717
718 def test_rootdev_from_cmdline_with_root_with_uuid(self):
719 """When cmdline root contains a UUID, our root is disk/by-uuid."""
720 self.assertEqual(
721 '/dev/disk/by-uuid/adsfdsaf-adsf',
722- rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf'))
723+ util.rootdev_from_cmdline('asdf root=UUID=adsfdsaf-adsf'))
724
725
726-class TestIsDevicePathWritableBlock(CiTestCase):
727+class TestMaybeGetDevicePathAsWritableBlock(CiTestCase):
728
729 with_logs = True
730
731- def test_is_device_path_writable_block_false_on_overlayroot(self):
732+ def test_maybe_get_writable_device_path_none_on_overlayroot(self):
733 """When devpath is overlayroot (on MAAS), is_dev_writable is False."""
734 info = 'does not matter'
735- is_writable = wrap_and_call(
736+ devpath = wrap_and_call(
737 'cloudinit.config.cc_resizefs.util',
738 {'is_container': {'return_value': False}},
739- is_device_path_writable_block, 'overlayroot', info, LOG)
740- self.assertFalse(is_writable)
741+ maybe_get_writable_device_path, 'overlayroot', info, LOG)
742+ self.assertIsNone(devpath)
743 self.assertIn(
744 "Not attempting to resize devpath 'overlayroot'",
745 self.logs.getvalue())
746
747- def test_is_device_path_writable_block_warns_missing_cmdline_root(self):
748+ def test_maybe_get_writable_device_path_warns_missing_cmdline_root(self):
749 """When root does not exist isn't in the cmdline, log warning."""
750 info = 'does not matter'
751
752@@ -190,43 +191,43 @@ class TestIsDevicePathWritableBlock(CiTestCase):
753 exists_mock_path = 'cloudinit.config.cc_resizefs.os.path.exists'
754 with mock.patch(exists_mock_path) as m_exists:
755 m_exists.return_value = False
756- is_writable = wrap_and_call(
757+ devpath = wrap_and_call(
758 'cloudinit.config.cc_resizefs.util',
759 {'is_container': {'return_value': False},
760 'get_mount_info': {'side_effect': fake_mount_info},
761 'get_cmdline': {'return_value': 'BOOT_IMAGE=/vmlinuz.efi'}},
762- is_device_path_writable_block, '/dev/root', info, LOG)
763- self.assertFalse(is_writable)
764+ maybe_get_writable_device_path, '/dev/root', info, LOG)
765+ self.assertIsNone(devpath)
766 logs = self.logs.getvalue()
767 self.assertIn("WARNING: Unable to find device '/dev/root'", logs)
768
769- def test_is_device_path_writable_block_does_not_exist(self):
770+ def test_maybe_get_writable_device_path_does_not_exist(self):
771 """When devpath does not exist, a warning is logged."""
772 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
773- is_writable = wrap_and_call(
774+ devpath = wrap_and_call(
775 'cloudinit.config.cc_resizefs.util',
776 {'is_container': {'return_value': False}},
777- is_device_path_writable_block, '/I/dont/exist', info, LOG)
778- self.assertFalse(is_writable)
779+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
780+ self.assertIsNone(devpath)
781 self.assertIn(
782 "WARNING: Device '/I/dont/exist' did not exist."
783 ' cannot resize: %s' % info,
784 self.logs.getvalue())
785
786- def test_is_device_path_writable_block_does_not_exist_in_container(self):
787+ def test_maybe_get_writable_device_path_does_not_exist_in_container(self):
788 """When devpath does not exist in a container, log a debug message."""
789 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
790- is_writable = wrap_and_call(
791+ devpath = wrap_and_call(
792 'cloudinit.config.cc_resizefs.util',
793 {'is_container': {'return_value': True}},
794- is_device_path_writable_block, '/I/dont/exist', info, LOG)
795- self.assertFalse(is_writable)
796+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
797+ self.assertIsNone(devpath)
798 self.assertIn(
799 "DEBUG: Device '/I/dont/exist' did not exist in container."
800 ' cannot resize: %s' % info,
801 self.logs.getvalue())
802
803- def test_is_device_path_writable_block_raises_oserror(self):
804+ def test_maybe_get_writable_device_path_raises_oserror(self):
805 """When unexpected OSError is raises by os.stat it is reraised."""
806 info = 'dev=/I/dont/exist mnt_point=/ path=/dev/none'
807 with self.assertRaises(OSError) as context_manager:
808@@ -234,41 +235,63 @@ class TestIsDevicePathWritableBlock(CiTestCase):
809 'cloudinit.config.cc_resizefs',
810 {'util.is_container': {'return_value': True},
811 'os.stat': {'side_effect': OSError('Something unexpected')}},
812- is_device_path_writable_block, '/I/dont/exist', info, LOG)
813+ maybe_get_writable_device_path, '/I/dont/exist', info, LOG)
814 self.assertEqual(
815 'Something unexpected', str(context_manager.exception))
816
817- def test_is_device_path_writable_block_non_block(self):
818+ def test_maybe_get_writable_device_path_non_block(self):
819 """When device is not a block device, emit warning return False."""
820 fake_devpath = self.tmp_path('dev/readwrite')
821 util.write_file(fake_devpath, '', mode=0o600) # read-write
822 info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
823
824- is_writable = wrap_and_call(
825+ devpath = wrap_and_call(
826 'cloudinit.config.cc_resizefs.util',
827 {'is_container': {'return_value': False}},
828- is_device_path_writable_block, fake_devpath, info, LOG)
829- self.assertFalse(is_writable)
830+ maybe_get_writable_device_path, fake_devpath, info, LOG)
831+ self.assertIsNone(devpath)
832 self.assertIn(
833 "WARNING: device '{0}' not a block device. cannot resize".format(
834 fake_devpath),
835 self.logs.getvalue())
836
837- def test_is_device_path_writable_block_non_block_on_container(self):
838+ def test_maybe_get_writable_device_path_non_block_on_container(self):
839 """When device is non-block device in container, emit debug log."""
840 fake_devpath = self.tmp_path('dev/readwrite')
841 util.write_file(fake_devpath, '', mode=0o600) # read-write
842 info = 'dev=/dev/root mnt_point=/ path={0}'.format(fake_devpath)
843
844- is_writable = wrap_and_call(
845+ devpath = wrap_and_call(
846 'cloudinit.config.cc_resizefs.util',
847 {'is_container': {'return_value': True}},
848- is_device_path_writable_block, fake_devpath, info, LOG)
849- self.assertFalse(is_writable)
850+ maybe_get_writable_device_path, fake_devpath, info, LOG)
851+ self.assertIsNone(devpath)
852 self.assertIn(
853 "DEBUG: device '{0}' not a block device in container."
854 ' cannot resize'.format(fake_devpath),
855 self.logs.getvalue())
856
857+ def test_maybe_get_writable_device_path_returns_cmdline_root(self):
858+ """When root device is UUID in kernel commandline, update devpath."""
859+ # XXX Long-term we want to use FilesystemMocking test to avoid
860+ # touching os.stat.
861+ FakeStat = namedtuple(
862+ 'FakeStat', ['st_mode', 'st_size', 'st_mtime']) # minimal def.
863+ info = 'dev=/dev/root mnt_point=/ path=/does/not/matter'
864+ devpath = wrap_and_call(
865+ 'cloudinit.config.cc_resizefs',
866+ {'util.get_cmdline': {'return_value': 'asdf root=UUID=my-uuid'},
867+ 'util.is_container': False,
868+ 'os.path.exists': False, # /dev/root doesn't exist
869+ 'os.stat': {
870+ 'return_value': FakeStat(25008, 0, 1)} # char block device
871+ },
872+ maybe_get_writable_device_path, '/dev/root', info, LOG)
873+ self.assertEqual('/dev/disk/by-uuid/my-uuid', devpath)
874+ self.assertIn(
875+ "DEBUG: Converted /dev/root to '/dev/disk/by-uuid/my-uuid'"
876+ " per kernel cmdline",
877+ self.logs.getvalue())
878+
879
880 # vi: ts=4 expandtab
881diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py
882index b8fc893..648573f 100644
883--- a/tests/unittests/test_handler/test_schema.py
884+++ b/tests/unittests/test_handler/test_schema.py
885@@ -4,11 +4,12 @@ from cloudinit.config.schema import (
886 CLOUD_CONFIG_HEADER, SchemaValidationError, annotated_cloudconfig_file,
887 get_schema_doc, get_schema, validate_cloudconfig_file,
888 validate_cloudconfig_schema, main)
889-from cloudinit.util import write_file
890+from cloudinit.util import subp, write_file
891
892 from cloudinit.tests.helpers import CiTestCase, mock, skipIf
893
894 from copy import copy
895+import os
896 from six import StringIO
897 from textwrap import dedent
898 from yaml import safe_load
899@@ -364,4 +365,38 @@ class MainTest(CiTestCase):
900 self.assertIn(
901 'Valid cloud-config file {0}'.format(myyaml), m_stdout.getvalue())
902
903+
904+class CloudTestsIntegrationTest(CiTestCase):
905+ """Validate all cloud-config yaml schema provided in integration tests.
906+
907+ It is less expensive to have unittests validate schema of all cloud-config
908+ yaml provided to integration tests, than to run an integration test which
909+ raises Warnings or errors on invalid cloud-config schema.
910+ """
911+
912+ @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency")
913+ def test_all_integration_test_cloud_config_schema(self):
914+ """Validate schema of cloud_tests yaml files looking for warnings."""
915+ schema = get_schema()
916+ testsdir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
917+ integration_testdir = os.path.sep.join(
918+ [testsdir, 'cloud_tests', 'testcases'])
919+ errors = []
920+ out, _ = subp(['find', integration_testdir, '-name', '*yaml'])
921+ for filename in out.splitlines():
922+ test_cfg = safe_load(open(filename))
923+ cloud_config = test_cfg.get('cloud_config')
924+ if cloud_config:
925+ cloud_config = safe_load(
926+ cloud_config.replace("#cloud-config\n", ""))
927+ try:
928+ validate_cloudconfig_schema(
929+ cloud_config, schema, strict=True)
930+ except SchemaValidationError as e:
931+ errors.append(
932+ '{0}: {1}'.format(
933+ filename, e))
934+ if errors:
935+ raise AssertionError(', '.join(errors))
936+
937 # vi: ts=4 expandtab syntax=python
938diff --git a/tools/read-dependencies b/tools/read-dependencies
939index 2a64868..421f470 100755
940--- a/tools/read-dependencies
941+++ b/tools/read-dependencies
942@@ -30,9 +30,35 @@ DISTRO_PKG_TYPE_MAP = {
943 'suse': 'suse'
944 }
945
946-DISTRO_INSTALL_PKG_CMD = {
947+MAYBE_RELIABLE_YUM_INSTALL = [
948+ 'sh', '-c',
949+ """
950+ error() { echo "$@" 1>&2; }
951+ n=0; max=10;
952+ bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
953+ while n=$(($n+1)); do
954+ error ":: running $bcmd $* [$n/$max]"
955+ $bcmd "$@"
956+ r=$?
957+ [ $r -eq 0 ] && break
958+ [ $n -ge $max ] && { error "gave up on $bcmd"; exit $r; }
959+ nap=$(($n*5))
960+ error ":: failed [$r] ($n/$max). sleeping $nap."
961+ sleep $nap
962+ done
963+ error ":: running yum install --cacheonly --assumeyes $*"
964+ yum install --cacheonly --assumeyes "$@"
965+ """,
966+ 'reliable-yum-install']
967+
968+DRY_DISTRO_INSTALL_PKG_CMD = {
969 'centos': ['yum', 'install', '--assumeyes'],
970 'redhat': ['yum', 'install', '--assumeyes'],
971+}
972+
973+DISTRO_INSTALL_PKG_CMD = {
974+ 'centos': MAYBE_RELIABLE_YUM_INSTALL,
975+ 'redhat': MAYBE_RELIABLE_YUM_INSTALL,
976 'debian': ['apt', 'install', '-y'],
977 'ubuntu': ['apt', 'install', '-y'],
978 'opensuse': ['zypper', 'install'],
979@@ -80,8 +106,8 @@ def get_parser():
980 help='Additionally install continuous integration system packages '
981 'required for build and test automation.')
982 parser.add_argument(
983- '-v', '--python-version', type=str, dest='python_version', default=None,
984- choices=["2", "3"],
985+ '-v', '--python-version', type=str, dest='python_version',
986+ default=None, choices=["2", "3"],
987 help='Override the version of python we want to generate system '
988 'package dependencies for. Defaults to the version of python '
989 'this script is called with')
990@@ -219,10 +245,15 @@ def pkg_install(pkg_list, distro, test_distro=False, dry_run=False):
991 '(dryrun)' if dry_run else '', ' '.join(pkg_list)))
992 install_cmd = []
993 if dry_run:
994- install_cmd.append('echo')
995+ install_cmd.append('echo')
996 if os.geteuid() != 0:
997 install_cmd.append('sudo')
998- install_cmd.extend(DISTRO_INSTALL_PKG_CMD[distro])
999+
1000+ cmd = DISTRO_INSTALL_PKG_CMD[distro]
1001+ if dry_run and distro in DRY_DISTRO_INSTALL_PKG_CMD:
1002+ cmd = DRY_DISTRO_INSTALL_PKG_CMD[distro]
1003+ install_cmd.extend(cmd)
1004+
1005 if distro in ['centos', 'redhat']:
1006 # CentOS and Redhat need epel-release to access oauthlib and jsonschema
1007 subprocess.check_call(install_cmd + ['epel-release'])
1008diff --git a/tools/run-centos b/tools/run-centos
1009index d44d514..d58ef3e 100755
1010--- a/tools/run-centos
1011+++ b/tools/run-centos
1012@@ -123,7 +123,22 @@ prep() {
1013 return 0
1014 fi
1015 error "Installing prep packages: ${needed}"
1016- yum install --assumeyes ${needed}
1017+ set -- $needed
1018+ local n max r
1019+ n=0; max=10;
1020+ bcmd="yum install --downloadonly --assumeyes --setopt=keepcache=1"
1021+ while n=$(($n+1)); do
1022+ error ":: running $bcmd $* [$n/$max]"
1023+ $bcmd "$@"
1024+ r=$?
1025+ [ $r -eq 0 ] && break
1026+ [ $n -ge $max ] && { error "gave up on $bcmd"; exit $r; }
1027+ nap=$(($n*5))
1028+ error ":: failed [$r] ($n/$max). sleeping $nap."
1029+ sleep $nap
1030+ done
1031+ error ":: running yum install --cacheonly --assumeyes $*"
1032+ yum install --cacheonly --assumeyes "$@"
1033 }
1034
1035 start_container() {
1036@@ -153,6 +168,7 @@ start_container() {
1037 if [ ! -z "${http_proxy-}" ]; then
1038 debug 1 "configuring proxy ${http_proxy}"
1039 inside "$name" sh -c "echo proxy=$http_proxy >> /etc/yum.conf"
1040+ inside "$name" sed -i s/enabled=1/enabled=0/ /etc/yum/pluginconf.d/fastestmirror.conf
1041 fi
1042 }
1043

Subscribers

People subscribed via source and target branches