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

Proposed by Chad Smith
Status: Merged
Merged at revision: c4c6301f2e5f6fe9960ceb7f670224f87b27e091
Proposed branch: ~chad.smith/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 1008 lines (+356/-145)
18 files modified
cloudinit/config/cc_lxd.py (+8/-8)
cloudinit/config/cc_rh_subscription.py (+22/-21)
cloudinit/sources/DataSourceOpenStack.py (+2/-1)
cloudinit/sources/DataSourceSmartOS.py (+1/-1)
cloudinit/sources/__init__.py (+7/-5)
cloudinit/sources/tests/test_init.py (+2/-1)
cloudinit/tests/test_util.py (+76/-2)
cloudinit/util.py (+33/-1)
cloudinit/warnings.py (+1/-1)
debian/changelog (+16/-0)
integration-requirements.txt (+1/-1)
tests/cloud_tests/platforms/instances.py (+2/-1)
tests/cloud_tests/platforms/lxd/instance.py (+38/-4)
tests/cloud_tests/setup_image.py (+9/-1)
tests/cloud_tests/testcases.yaml (+4/-0)
tests/unittests/test_datasource/test_openstack.py (+18/-0)
tests/unittests/test_rh_subscription.py (+92/-93)
tools/net-convert.py (+24/-4)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Pending
Review via email: mp+351925@code.launchpad.net

Commit message

Perform upstream snapshot from tip of master for release into Cosmic

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:c4c6301f2e5f6fe9960ceb7f670224f87b27e091
https://jenkins.ubuntu.com/server/job/cloud-init-ci/178/
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/178/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 ac72ac4..a604825 100644
3--- a/cloudinit/config/cc_lxd.py
4+++ b/cloudinit/config/cc_lxd.py
5@@ -276,27 +276,27 @@ def maybe_cleanup_default(net_name, did_init, create, attach,
6 if net_name != _DEFAULT_NETWORK_NAME or not did_init:
7 return
8
9- fail_assume_enoent = " failed. Assuming it did not exist."
10- succeeded = " succeeded."
11+ fail_assume_enoent = "failed. Assuming it did not exist."
12+ succeeded = "succeeded."
13 if create:
14- msg = "Deletion of lxd network '%s'" % net_name
15+ msg = "Deletion of lxd network '%s' %s"
16 try:
17 _lxc(["network", "delete", net_name])
18- LOG.debug(msg + succeeded)
19+ LOG.debug(msg, net_name, succeeded)
20 except util.ProcessExecutionError as e:
21 if e.exit_code != 1:
22 raise e
23- LOG.debug(msg + fail_assume_enoent)
24+ LOG.debug(msg, net_name, fail_assume_enoent)
25
26 if attach:
27- msg = "Removal of device '%s' from profile '%s'" % (nic_name, profile)
28+ msg = "Removal of device '%s' from profile '%s' %s"
29 try:
30 _lxc(["profile", "device", "remove", profile, nic_name])
31- LOG.debug(msg + succeeded)
32+ LOG.debug(msg, nic_name, profile, succeeded)
33 except util.ProcessExecutionError as e:
34 if e.exit_code != 1:
35 raise e
36- LOG.debug(msg + fail_assume_enoent)
37+ LOG.debug(msg, nic_name, profile, fail_assume_enoent)
38
39
40 # vi: ts=4 expandtab
41diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py
42index 1c67943..edee01e 100644
43--- a/cloudinit/config/cc_rh_subscription.py
44+++ b/cloudinit/config/cc_rh_subscription.py
45@@ -126,7 +126,6 @@ class SubscriptionManager(object):
46 self.enable_repo = self.rhel_cfg.get('enable-repo')
47 self.disable_repo = self.rhel_cfg.get('disable-repo')
48 self.servicelevel = self.rhel_cfg.get('service-level')
49- self.subman = ['subscription-manager']
50
51 def log_success(self, msg):
52 '''Simple wrapper for logging info messages. Useful for unittests'''
53@@ -173,21 +172,12 @@ class SubscriptionManager(object):
54 cmd = ['identity']
55
56 try:
57- self._sub_man_cli(cmd)
58+ _sub_man_cli(cmd)
59 except util.ProcessExecutionError:
60 return False
61
62 return True
63
64- def _sub_man_cli(self, cmd, logstring_val=False):
65- '''
66- Uses the prefered cloud-init subprocess def of util.subp
67- and runs subscription-manager. Breaking this to a
68- separate function for later use in mocking and unittests
69- '''
70- cmd = self.subman + cmd
71- return util.subp(cmd, logstring=logstring_val)
72-
73 def rhn_register(self):
74 '''
75 Registers the system by userid and password or activation key
76@@ -209,7 +199,7 @@ class SubscriptionManager(object):
77 cmd.append("--serverurl={0}".format(self.server_hostname))
78
79 try:
80- return_out = self._sub_man_cli(cmd, logstring_val=True)[0]
81+ return_out = _sub_man_cli(cmd, logstring_val=True)[0]
82 except util.ProcessExecutionError as e:
83 if e.stdout == "":
84 self.log_warn("Registration failed due "
85@@ -232,7 +222,7 @@ class SubscriptionManager(object):
86
87 # Attempting to register the system only
88 try:
89- return_out = self._sub_man_cli(cmd, logstring_val=True)[0]
90+ return_out = _sub_man_cli(cmd, logstring_val=True)[0]
91 except util.ProcessExecutionError as e:
92 if e.stdout == "":
93 self.log_warn("Registration failed due "
94@@ -255,7 +245,7 @@ class SubscriptionManager(object):
95 .format(self.servicelevel)]
96
97 try:
98- return_out = self._sub_man_cli(cmd)[0]
99+ return_out = _sub_man_cli(cmd)[0]
100 except util.ProcessExecutionError as e:
101 if e.stdout.rstrip() != '':
102 for line in e.stdout.split("\n"):
103@@ -273,7 +263,7 @@ class SubscriptionManager(object):
104 def _set_auto_attach(self):
105 cmd = ['attach', '--auto']
106 try:
107- return_out = self._sub_man_cli(cmd)[0]
108+ return_out = _sub_man_cli(cmd)[0]
109 except util.ProcessExecutionError as e:
110 self.log_warn("Auto-attach failed with: {0}".format(e))
111 return False
112@@ -292,12 +282,12 @@ class SubscriptionManager(object):
113
114 # Get all available pools
115 cmd = ['list', '--available', '--pool-only']
116- results = self._sub_man_cli(cmd)[0]
117+ results = _sub_man_cli(cmd)[0]
118 available = (results.rstrip()).split("\n")
119
120 # Get all consumed pools
121 cmd = ['list', '--consumed', '--pool-only']
122- results = self._sub_man_cli(cmd)[0]
123+ results = _sub_man_cli(cmd)[0]
124 consumed = (results.rstrip()).split("\n")
125
126 return available, consumed
127@@ -309,14 +299,14 @@ class SubscriptionManager(object):
128 '''
129
130 cmd = ['repos', '--list-enabled']
131- return_out = self._sub_man_cli(cmd)[0]
132+ return_out = _sub_man_cli(cmd)[0]
133 active_repos = []
134 for repo in return_out.split("\n"):
135 if "Repo ID:" in repo:
136 active_repos.append((repo.split(':')[1]).strip())
137
138 cmd = ['repos', '--list-disabled']
139- return_out = self._sub_man_cli(cmd)[0]
140+ return_out = _sub_man_cli(cmd)[0]
141
142 inactive_repos = []
143 for repo in return_out.split("\n"):
144@@ -346,7 +336,7 @@ class SubscriptionManager(object):
145 if len(pool_list) > 0:
146 cmd.extend(pool_list)
147 try:
148- self._sub_man_cli(cmd)
149+ _sub_man_cli(cmd)
150 self.log.debug("Attached the following pools to your "
151 "system: %s", (", ".join(pool_list))
152 .replace('--pool=', ''))
153@@ -423,7 +413,7 @@ class SubscriptionManager(object):
154 cmd.extend(enable_list)
155
156 try:
157- self._sub_man_cli(cmd)
158+ _sub_man_cli(cmd)
159 except util.ProcessExecutionError as e:
160 self.log_warn("Unable to alter repos due to {0}".format(e))
161 return False
162@@ -439,4 +429,15 @@ class SubscriptionManager(object):
163 def is_configured(self):
164 return bool((self.userid and self.password) or self.activation_key)
165
166+
167+def _sub_man_cli(cmd, logstring_val=False):
168+ '''
169+ Uses the prefered cloud-init subprocess def of util.subp
170+ and runs subscription-manager. Breaking this to a
171+ separate function for later use in mocking and unittests
172+ '''
173+ return util.subp(['subscription-manager'] + cmd,
174+ logstring=logstring_val)
175+
176+
177 # vi: ts=4 expandtab
178diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py
179index 365af96..b9ade90 100644
180--- a/cloudinit/sources/DataSourceOpenStack.py
181+++ b/cloudinit/sources/DataSourceOpenStack.py
182@@ -28,7 +28,8 @@ DMI_PRODUCT_NOVA = 'OpenStack Nova'
183 DMI_PRODUCT_COMPUTE = 'OpenStack Compute'
184 VALID_DMI_PRODUCT_NAMES = [DMI_PRODUCT_NOVA, DMI_PRODUCT_COMPUTE]
185 DMI_ASSET_TAG_OPENTELEKOM = 'OpenTelekomCloud'
186-VALID_DMI_ASSET_TAGS = [DMI_ASSET_TAG_OPENTELEKOM]
187+DMI_ASSET_TAG_ORACLE_CLOUD = 'OracleCloud.com'
188+VALID_DMI_ASSET_TAGS = [DMI_ASSET_TAG_OPENTELEKOM, DMI_ASSET_TAG_ORACLE_CLOUD]
189
190
191 class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource):
192diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
193index f92e8b5..ad8cfb9 100644
194--- a/cloudinit/sources/DataSourceSmartOS.py
195+++ b/cloudinit/sources/DataSourceSmartOS.py
196@@ -564,7 +564,7 @@ class JoyentMetadataSerialClient(JoyentMetadataClient):
197 continue
198 LOG.warning('Unexpected response "%s" during flush', response)
199 except JoyentMetadataTimeoutException:
200- LOG.warning('Timeout while initializing metadata client. ' +
201+ LOG.warning('Timeout while initializing metadata client. '
202 'Is the host metadata service running?')
203 LOG.debug('Got "invalid command". Flush complete.')
204 self.fp.timeout = timeout
205diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py
206index f424316..06e613f 100644
207--- a/cloudinit/sources/__init__.py
208+++ b/cloudinit/sources/__init__.py
209@@ -103,14 +103,14 @@ class DataSource(object):
210 url_timeout = 10 # timeout for each metadata url read attempt
211 url_retries = 5 # number of times to retry url upon 404
212
213- # The datasource defines a list of supported EventTypes during which
214+ # The datasource defines a set of supported EventTypes during which
215 # the datasource can react to changes in metadata and regenerate
216 # network configuration on metadata changes.
217 # A datasource which supports writing network config on each system boot
218- # would set update_events = {'network': [EventType.BOOT]}
219+ # would call update_events['network'].add(EventType.BOOT).
220
221 # Default: generate network config on new instance id (first boot).
222- update_events = {'network': [EventType.BOOT_NEW_INSTANCE]}
223+ update_events = {'network': set([EventType.BOOT_NEW_INSTANCE])}
224
225 # N-tuple listing default values for any metadata-related class
226 # attributes cached on an instance by a process_data runs. These attribute
227@@ -475,8 +475,8 @@ class DataSource(object):
228 for update_scope, update_events in self.update_events.items():
229 if event in update_events:
230 if not supported_events.get(update_scope):
231- supported_events[update_scope] = []
232- supported_events[update_scope].append(event)
233+ supported_events[update_scope] = set()
234+ supported_events[update_scope].add(event)
235 for scope, matched_events in supported_events.items():
236 LOG.debug(
237 "Update datasource metadata and %s config due to events: %s",
238@@ -490,6 +490,8 @@ class DataSource(object):
239 result = self.get_data()
240 if result:
241 return True
242+ LOG.debug("Datasource %s not updated for events: %s", self,
243+ ', '.join(source_event_types))
244 return False
245
246 def check_instance_id(self, sys_cfg):
247diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py
248index dcd221b..9e939c1 100644
249--- a/cloudinit/sources/tests/test_init.py
250+++ b/cloudinit/sources/tests/test_init.py
251@@ -429,8 +429,9 @@ class TestDataSource(CiTestCase):
252
253 def test_update_metadata_only_acts_on_supported_update_events(self):
254 """update_metadata won't get_data on unsupported update events."""
255+ self.datasource.update_events['network'].discard(EventType.BOOT)
256 self.assertEqual(
257- {'network': [EventType.BOOT_NEW_INSTANCE]},
258+ {'network': set([EventType.BOOT_NEW_INSTANCE])},
259 self.datasource.update_events)
260
261 def fake_get_data():
262diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py
263index 6a31e50..edb0c18 100644
264--- a/cloudinit/tests/test_util.py
265+++ b/cloudinit/tests/test_util.py
266@@ -57,6 +57,34 @@ OS_RELEASE_CENTOS = dedent("""\
267 REDHAT_SUPPORT_PRODUCT_VERSION="7"
268 """)
269
270+OS_RELEASE_REDHAT_7 = dedent("""\
271+ NAME="Red Hat Enterprise Linux Server"
272+ VERSION="7.5 (Maipo)"
273+ ID="rhel"
274+ ID_LIKE="fedora"
275+ VARIANT="Server"
276+ VARIANT_ID="server"
277+ VERSION_ID="7.5"
278+ PRETTY_NAME="Red Hat"
279+ ANSI_COLOR="0;31"
280+ CPE_NAME="cpe:/o:redhat:enterprise_linux:7.5:GA:server"
281+ HOME_URL="https://www.redhat.com/"
282+ BUG_REPORT_URL="https://bugzilla.redhat.com/"
283+
284+ REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 7"
285+ REDHAT_BUGZILLA_PRODUCT_VERSION=7.5
286+ REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
287+ REDHAT_SUPPORT_PRODUCT_VERSION="7.5"
288+""")
289+
290+REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)"
291+REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)"
292+REDHAT_RELEASE_REDHAT_6 = (
293+ "Red Hat Enterprise Linux Server release 6.10 (Santiago)")
294+REDHAT_RELEASE_REDHAT_7 = (
295+ "Red Hat Enterprise Linux Server release 7.5 (Maipo)")
296+
297+
298 OS_RELEASE_DEBIAN = dedent("""\
299 PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
300 NAME="Debian GNU/Linux"
301@@ -337,6 +365,12 @@ class TestGetLinuxDistro(CiTestCase):
302 if path == '/etc/os-release':
303 return 1
304
305+ @classmethod
306+ def redhat_release_exists(self, path):
307+ """Side effect function """
308+ if path == '/etc/redhat-release':
309+ return 1
310+
311 @mock.patch('cloudinit.util.load_file')
312 def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists):
313 """Verify we get the correct name if the os-release file has
314@@ -356,8 +390,48 @@ class TestGetLinuxDistro(CiTestCase):
315 self.assertEqual(('ubuntu', '16.04', 'xenial'), dist)
316
317 @mock.patch('cloudinit.util.load_file')
318- def test_get_linux_centos(self, m_os_release, m_path_exists):
319- """Verify we get the correct name and release name on CentOS."""
320+ def test_get_linux_centos6(self, m_os_release, m_path_exists):
321+ """Verify we get the correct name and release name on CentOS 6."""
322+ m_os_release.return_value = REDHAT_RELEASE_CENTOS_6
323+ m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
324+ dist = util.get_linux_distro()
325+ self.assertEqual(('centos', '6.10', 'Final'), dist)
326+
327+ @mock.patch('cloudinit.util.load_file')
328+ def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists):
329+ """Verify the correct release info on CentOS 7 without os-release."""
330+ m_os_release.return_value = REDHAT_RELEASE_CENTOS_7
331+ m_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
332+ dist = util.get_linux_distro()
333+ self.assertEqual(('centos', '7.5.1804', 'Core'), dist)
334+
335+ @mock.patch('cloudinit.util.load_file')
336+ def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists):
337+ """Verify redhat 7 read from os-release."""
338+ m_os_release.return_value = OS_RELEASE_REDHAT_7
339+ m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
340+ dist = util.get_linux_distro()
341+ self.assertEqual(('redhat', '7.5', 'Maipo'), dist)
342+
343+ @mock.patch('cloudinit.util.load_file')
344+ def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists):
345+ """Verify redhat 7 read from redhat-release."""
346+ m_os_release.return_value = REDHAT_RELEASE_REDHAT_7
347+ m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
348+ dist = util.get_linux_distro()
349+ self.assertEqual(('redhat', '7.5', 'Maipo'), dist)
350+
351+ @mock.patch('cloudinit.util.load_file')
352+ def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists):
353+ """Verify redhat 6 read from redhat-release."""
354+ m_os_release.return_value = REDHAT_RELEASE_REDHAT_6
355+ m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists
356+ dist = util.get_linux_distro()
357+ self.assertEqual(('redhat', '6.10', 'Santiago'), dist)
358+
359+ @mock.patch('cloudinit.util.load_file')
360+ def test_get_linux_copr_centos(self, m_os_release, m_path_exists):
361+ """Verify we get the correct name and release name on COPR CentOS."""
362 m_os_release.return_value = OS_RELEASE_CENTOS
363 m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists
364 dist = util.get_linux_distro()
365diff --git a/cloudinit/util.py b/cloudinit/util.py
366index d0b0e90..5068096 100644
367--- a/cloudinit/util.py
368+++ b/cloudinit/util.py
369@@ -576,12 +576,42 @@ def get_cfg_option_int(yobj, key, default=0):
370 return int(get_cfg_option_str(yobj, key, default=default))
371
372
373+def _parse_redhat_release(release_file=None):
374+ """Return a dictionary of distro info fields from /etc/redhat-release.
375+
376+ Dict keys will align with /etc/os-release keys:
377+ ID, VERSION_ID, VERSION_CODENAME
378+ """
379+
380+ if not release_file:
381+ release_file = '/etc/redhat-release'
382+ if not os.path.exists(release_file):
383+ return {}
384+ redhat_release = load_file(release_file)
385+ redhat_regex = (
386+ r'(?P<name>.+) release (?P<version>[\d\.]+) '
387+ r'\((?P<codename>[^)]+)\)')
388+ match = re.match(redhat_regex, redhat_release)
389+ if match:
390+ group = match.groupdict()
391+ group['name'] = group['name'].lower().partition(' linux')[0]
392+ if group['name'] == 'red hat enterprise':
393+ group['name'] = 'redhat'
394+ return {'ID': group['name'], 'VERSION_ID': group['version'],
395+ 'VERSION_CODENAME': group['codename']}
396+ return {}
397+
398+
399 def get_linux_distro():
400 distro_name = ''
401 distro_version = ''
402 flavor = ''
403+ os_release = {}
404 if os.path.exists('/etc/os-release'):
405 os_release = load_shell_content(load_file('/etc/os-release'))
406+ if not os_release:
407+ os_release = _parse_redhat_release()
408+ if os_release:
409 distro_name = os_release.get('ID', '')
410 distro_version = os_release.get('VERSION_ID', '')
411 if 'sles' in distro_name or 'suse' in distro_name:
412@@ -594,9 +624,11 @@ def get_linux_distro():
413 flavor = os_release.get('VERSION_CODENAME', '')
414 if not flavor:
415 match = re.match(r'[^ ]+ \((?P<codename>[^)]+)\)',
416- os_release.get('VERSION'))
417+ os_release.get('VERSION', ''))
418 if match:
419 flavor = match.groupdict()['codename']
420+ if distro_name == 'rhel':
421+ distro_name = 'redhat'
422 else:
423 dist = ('', '', '')
424 try:
425diff --git a/cloudinit/warnings.py b/cloudinit/warnings.py
426index f9f7a63..1da90c4 100644
427--- a/cloudinit/warnings.py
428+++ b/cloudinit/warnings.py
429@@ -130,7 +130,7 @@ def show_warning(name, cfg=None, sleep=None, mode=True, **kwargs):
430 os.path.join(_get_warn_dir(cfg), name),
431 topline + "\n".join(fmtlines) + "\n" + topline)
432
433- LOG.warning(topline + "\n".join(fmtlines) + "\n" + closeline)
434+ LOG.warning("%s%s\n%s", topline, "\n".join(fmtlines), closeline)
435
436 if sleep:
437 LOG.debug("sleeping %d seconds for warning '%s'", sleep, name)
438diff --git a/debian/changelog b/debian/changelog
439index d6a89e4..05932be 100644
440--- a/debian/changelog
441+++ b/debian/changelog
442@@ -1,3 +1,19 @@
443+cloud-init (18.3-18-g3cee0bf8-0ubuntu1) cosmic; urgency=medium
444+
445+ * New upstream snapshot.
446+ - oracle: fix detect_openstack to report True on OracleCloud.com DMI data
447+ - tests: improve LXDInstance trying to workaround or catch bug.
448+ - update_metadata re-config on every boot comments and tests not quite
449+ right [Mike Gerdts]
450+ - tests: Collect build_info from system if available.
451+ - pylint: Fix pylint warnings reported in pylint 2.0.0.
452+ - get_linux_distro: add support for rhel via redhat-release.
453+ - get_linux_distro: add support for centos6 and rawhide flavors of redhat
454+ - tools: add '--debug' to tools/net-convert.py
455+ - tests: bump the version of paramiko to 2.4.1.
456+
457+ -- Chad Smith <chad.smith@canonical.com> Tue, 31 Jul 2018 12:50:28 -0600
458+
459 cloud-init (18.3-9-g2e62cb8a-0ubuntu1) cosmic; urgency=medium
460
461 * New upstream snapshot.
462diff --git a/integration-requirements.txt b/integration-requirements.txt
463index 01baebd..f80cb94 100644
464--- a/integration-requirements.txt
465+++ b/integration-requirements.txt
466@@ -9,7 +9,7 @@
467 boto3==1.5.9
468
469 # ssh communication
470-paramiko==2.4.0
471+paramiko==2.4.1
472
473 # lxd backend
474 # 04/03/2018: enables use of lxd 3.0
475diff --git a/tests/cloud_tests/platforms/instances.py b/tests/cloud_tests/platforms/instances.py
476index 95bc3b1..529e79c 100644
477--- a/tests/cloud_tests/platforms/instances.py
478+++ b/tests/cloud_tests/platforms/instances.py
479@@ -97,7 +97,8 @@ class Instance(TargetBase):
480 return self._ssh_client
481
482 if not self.ssh_ip or not self.ssh_port:
483- raise ValueError
484+ raise ValueError("Cannot ssh_connect, ssh_ip=%s ssh_port=%s" %
485+ (self.ssh_ip, self.ssh_port))
486
487 client = paramiko.SSHClient()
488 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
489diff --git a/tests/cloud_tests/platforms/lxd/instance.py b/tests/cloud_tests/platforms/lxd/instance.py
490index d396519..83c97ab 100644
491--- a/tests/cloud_tests/platforms/lxd/instance.py
492+++ b/tests/cloud_tests/platforms/lxd/instance.py
493@@ -12,6 +12,8 @@ from tests.cloud_tests.util import PlatformError
494
495 from ..instances import Instance
496
497+from pylxd import exceptions as pylxd_exc
498+
499
500 class LXDInstance(Instance):
501 """LXD container backed instance."""
502@@ -30,6 +32,9 @@ class LXDInstance(Instance):
503 @param config: image config
504 @param features: supported feature flags
505 """
506+ if not pylxd_container:
507+ raise ValueError("Invalid value pylxd_container: %s" %
508+ pylxd_container)
509 self._pylxd_container = pylxd_container
510 super(LXDInstance, self).__init__(
511 platform, name, properties, config, features)
512@@ -40,9 +45,19 @@ class LXDInstance(Instance):
513 @property
514 def pylxd_container(self):
515 """Property function."""
516+ if self._pylxd_container is None:
517+ raise RuntimeError(
518+ "%s: Attempted use of pylxd_container after deletion." % self)
519 self._pylxd_container.sync()
520 return self._pylxd_container
521
522+ def __str__(self):
523+ return (
524+ '%s(name=%s) status=%s' %
525+ (self.__class__.__name__, self.name,
526+ ("deleted" if self._pylxd_container is None else
527+ self.pylxd_container.status)))
528+
529 def _execute(self, command, stdin=None, env=None):
530 if env is None:
531 env = {}
532@@ -165,10 +180,27 @@ class LXDInstance(Instance):
533 self.shutdown(wait=wait)
534 self.start(wait=wait)
535
536- def shutdown(self, wait=True):
537+ def shutdown(self, wait=True, retry=1):
538 """Shutdown instance."""
539- if self.pylxd_container.status != 'Stopped':
540+ if self.pylxd_container.status == 'Stopped':
541+ return
542+
543+ try:
544+ LOG.debug("%s: shutting down (wait=%s)", self, wait)
545 self.pylxd_container.stop(wait=wait)
546+ except (pylxd_exc.LXDAPIException, pylxd_exc.NotFound) as e:
547+ # An exception happens here sometimes (LP: #1783198)
548+ # LOG it, and try again.
549+ LOG.warning(
550+ ("%s: shutdown(retry=%d) caught %s in shutdown "
551+ "(response=%s): %s"),
552+ self, retry, e.__class__.__name__, e.response, e)
553+ if isinstance(e, pylxd_exc.NotFound):
554+ LOG.debug("container_exists(%s) == %s",
555+ self.name, self.platform.container_exists(self.name))
556+ if retry == 0:
557+ raise e
558+ return self.shutdown(wait=wait, retry=retry - 1)
559
560 def start(self, wait=True, wait_for_cloud_init=False):
561 """Start instance."""
562@@ -189,12 +221,14 @@ class LXDInstance(Instance):
563
564 def destroy(self):
565 """Clean up instance."""
566+ LOG.debug("%s: deleting container.", self)
567 self.unfreeze()
568 self.shutdown()
569 self.pylxd_container.delete(wait=True)
570+ self._pylxd_container = None
571+
572 if self.platform.container_exists(self.name):
573- raise OSError('container {} was not properly removed'
574- .format(self.name))
575+ raise OSError('%s: container was not properly removed' % self)
576 if self._console_log_file and os.path.exists(self._console_log_file):
577 os.unlink(self._console_log_file)
578 shutil.rmtree(self.tmpd)
579diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py
580index 4e19570..39f4517 100644
581--- a/tests/cloud_tests/setup_image.py
582+++ b/tests/cloud_tests/setup_image.py
583@@ -4,6 +4,7 @@
584
585 from functools import partial
586 import os
587+import yaml
588
589 from tests.cloud_tests import LOG
590 from tests.cloud_tests import stage, util
591@@ -220,7 +221,14 @@ def setup_image(args, image):
592 calls = [partial(stage.run_single, desc, partial(func, args, image))
593 for name, func, desc in handlers if getattr(args, name, None)]
594
595- LOG.info('setting up %s', image)
596+ try:
597+ data = yaml.load(image.read_data("/etc/cloud/build.info", decode=True))
598+ info = ' '.join(["%s=%s" % (k, data.get(k))
599+ for k in ("build_name", "serial") if k in data])
600+ except Exception as e:
601+ info = "N/A (%s)" % e
602+
603+ LOG.info('setting up %s (%s)', image, info)
604 res = stage.run_stage(
605 'set up for {}'.format(image), calls, continue_after_error=False)
606 return res
607diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml
608index a16d1dd..fb9a5d2 100644
609--- a/tests/cloud_tests/testcases.yaml
610+++ b/tests/cloud_tests/testcases.yaml
611@@ -27,6 +27,10 @@ base_test_data:
612 package-versions: |
613 #!/bin/sh
614 dpkg-query --show
615+ build.info: |
616+ #!/bin/sh
617+ binfo=/etc/cloud/build.info
618+ [ -f "$binfo" ] && cat "$binfo" || echo "N/A"
619 system.journal.gz: |
620 #!/bin/sh
621 [ -d /run/systemd ] || { echo "not systemd."; exit 0; }
622diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py
623index 585acc3..d862f4b 100644
624--- a/tests/unittests/test_datasource/test_openstack.py
625+++ b/tests/unittests/test_datasource/test_openstack.py
626@@ -510,6 +510,24 @@ class TestDetectOpenStack(test_helpers.CiTestCase):
627 ds.detect_openstack(),
628 'Expected detect_openstack == True on OpenTelekomCloud')
629
630+ @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
631+ def test_detect_openstack_oraclecloud_chassis_asset_tag(self, m_dmi,
632+ m_is_x86):
633+ """Return True on OpenStack reporting Oracle cloud asset-tag."""
634+ m_is_x86.return_value = True
635+
636+ def fake_dmi_read(dmi_key):
637+ if dmi_key == 'system-product-name':
638+ return 'Standard PC (i440FX + PIIX, 1996)' # No match
639+ if dmi_key == 'chassis-asset-tag':
640+ return 'OracleCloud.com'
641+ assert False, 'Unexpected dmi read of %s' % dmi_key
642+
643+ m_dmi.side_effect = fake_dmi_read
644+ self.assertTrue(
645+ ds.detect_openstack(),
646+ 'Expected detect_openstack == True on OracleCloud.com')
647+
648 @test_helpers.mock.patch(MOCK_PATH + 'util.get_proc_env')
649 @test_helpers.mock.patch(MOCK_PATH + 'util.read_dmi_data')
650 def test_detect_openstack_by_proc_1_environ(self, m_dmi, m_proc_env,
651diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py
652index 2271810..4cd27ee 100644
653--- a/tests/unittests/test_rh_subscription.py
654+++ b/tests/unittests/test_rh_subscription.py
655@@ -8,10 +8,16 @@ import logging
656 from cloudinit.config import cc_rh_subscription
657 from cloudinit import util
658
659-from cloudinit.tests.helpers import TestCase, mock
660+from cloudinit.tests.helpers import CiTestCase, mock
661
662+SUBMGR = cc_rh_subscription.SubscriptionManager
663+SUB_MAN_CLI = 'cloudinit.config.cc_rh_subscription._sub_man_cli'
664+
665+
666+@mock.patch(SUB_MAN_CLI)
667+class GoodTests(CiTestCase):
668+ with_logs = True
669
670-class GoodTests(TestCase):
671 def setUp(self):
672 super(GoodTests, self).setUp()
673 self.name = "cc_rh_subscription"
674@@ -19,7 +25,6 @@ class GoodTests(TestCase):
675 self.log = logging.getLogger("good_tests")
676 self.args = []
677 self.handle = cc_rh_subscription.handle
678- self.SM = cc_rh_subscription.SubscriptionManager
679
680 self.config = {'rh_subscription':
681 {'username': 'scooby@do.com',
682@@ -35,55 +40,47 @@ class GoodTests(TestCase):
683 'disable-repo': ['repo4', 'repo5']
684 }}
685
686- def test_already_registered(self):
687+ def test_already_registered(self, m_sman_cli):
688 '''
689 Emulates a system that is already registered. Ensure it gets
690 a non-ProcessExecution error from is_registered()
691 '''
692- with mock.patch.object(cc_rh_subscription.SubscriptionManager,
693- '_sub_man_cli') as mockobj:
694- self.SM.log_success = mock.MagicMock()
695- self.handle(self.name, self.config, self.cloud_init,
696- self.log, self.args)
697- self.assertEqual(self.SM.log_success.call_count, 1)
698- self.assertEqual(mockobj.call_count, 1)
699-
700- def test_simple_registration(self):
701+ self.handle(self.name, self.config, self.cloud_init,
702+ self.log, self.args)
703+ self.assertEqual(m_sman_cli.call_count, 1)
704+ self.assertIn('System is already registered', self.logs.getvalue())
705+
706+ def test_simple_registration(self, m_sman_cli):
707 '''
708 Simple registration with username and password
709 '''
710- self.SM.log_success = mock.MagicMock()
711 reg = "The system has been registered with ID:" \
712 " 12345678-abde-abcde-1234-1234567890abc"
713- self.SM._sub_man_cli = mock.MagicMock(
714- side_effect=[util.ProcessExecutionError, (reg, 'bar')])
715+ m_sman_cli.side_effect = [util.ProcessExecutionError, (reg, 'bar')]
716 self.handle(self.name, self.config, self.cloud_init,
717 self.log, self.args)
718- self.assertIn(mock.call(['identity']),
719- self.SM._sub_man_cli.call_args_list)
720+ self.assertIn(mock.call(['identity']), m_sman_cli.call_args_list)
721 self.assertIn(mock.call(['register', '--username=scooby@do.com',
722 '--password=scooby-snacks'],
723 logstring_val=True),
724- self.SM._sub_man_cli.call_args_list)
725-
726- self.assertEqual(self.SM.log_success.call_count, 1)
727- self.assertEqual(self.SM._sub_man_cli.call_count, 2)
728+ m_sman_cli.call_args_list)
729+ self.assertIn('rh_subscription plugin completed successfully',
730+ self.logs.getvalue())
731+ self.assertEqual(m_sman_cli.call_count, 2)
732
733 @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos")
734- @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_sub_man_cli")
735- def test_update_repos_disable_with_none(self, m_sub_man_cli, m_get_repos):
736+ def test_update_repos_disable_with_none(self, m_get_repos, m_sman_cli):
737 cfg = copy.deepcopy(self.config)
738 m_get_repos.return_value = ([], ['repo1'])
739- m_sub_man_cli.return_value = (b'', b'')
740 cfg['rh_subscription'].update(
741 {'enable-repo': ['repo1'], 'disable-repo': None})
742 mysm = cc_rh_subscription.SubscriptionManager(cfg)
743 self.assertEqual(True, mysm.update_repos())
744 m_get_repos.assert_called_with()
745- self.assertEqual(m_sub_man_cli.call_args_list,
746+ self.assertEqual(m_sman_cli.call_args_list,
747 [mock.call(['repos', '--enable=repo1'])])
748
749- def test_full_registration(self):
750+ def test_full_registration(self, m_sman_cli):
751 '''
752 Registration with auto-attach, service-level, adding pools,
753 and enabling and disabling yum repos
754@@ -93,26 +90,28 @@ class GoodTests(TestCase):
755 call_lists.append(['repos', '--disable=repo5', '--enable=repo2',
756 '--enable=repo3'])
757 call_lists.append(['attach', '--auto', '--servicelevel=self-support'])
758- self.SM.log_success = mock.MagicMock()
759 reg = "The system has been registered with ID:" \
760 " 12345678-abde-abcde-1234-1234567890abc"
761- self.SM._sub_man_cli = mock.MagicMock(
762- side_effect=[util.ProcessExecutionError, (reg, 'bar'),
763- ('Service level set to: self-support', ''),
764- ('pool1\npool3\n', ''), ('pool2\n', ''), ('', ''),
765- ('Repo ID: repo1\nRepo ID: repo5\n', ''),
766- ('Repo ID: repo2\nRepo ID: repo3\nRepo ID: '
767- 'repo4', ''),
768- ('', '')])
769+ m_sman_cli.side_effect = [
770+ util.ProcessExecutionError,
771+ (reg, 'bar'),
772+ ('Service level set to: self-support', ''),
773+ ('pool1\npool3\n', ''), ('pool2\n', ''), ('', ''),
774+ ('Repo ID: repo1\nRepo ID: repo5\n', ''),
775+ ('Repo ID: repo2\nRepo ID: repo3\nRepo ID: repo4', ''),
776+ ('', '')]
777 self.handle(self.name, self.config_full, self.cloud_init,
778 self.log, self.args)
779+ self.assertEqual(m_sman_cli.call_count, 9)
780 for call in call_lists:
781- self.assertIn(mock.call(call), self.SM._sub_man_cli.call_args_list)
782- self.assertEqual(self.SM.log_success.call_count, 1)
783- self.assertEqual(self.SM._sub_man_cli.call_count, 9)
784+ self.assertIn(mock.call(call), m_sman_cli.call_args_list)
785+ self.assertIn("rh_subscription plugin completed successfully",
786+ self.logs.getvalue())
787
788
789-class TestBadInput(TestCase):
790+@mock.patch(SUB_MAN_CLI)
791+class TestBadInput(CiTestCase):
792+ with_logs = True
793 name = "cc_rh_subscription"
794 cloud_init = None
795 log = logging.getLogger("bad_tests")
796@@ -155,81 +154,81 @@ class TestBadInput(TestCase):
797 super(TestBadInput, self).setUp()
798 self.handle = cc_rh_subscription.handle
799
800- def test_no_password(self):
801- '''
802- Attempt to register without the password key/value
803- '''
804- self.SM._sub_man_cli = mock.MagicMock(
805- side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
806+ def assert_logged_warnings(self, warnings):
807+ logs = self.logs.getvalue()
808+ missing = [w for w in warnings if "WARNING: " + w not in logs]
809+ self.assertEqual([], missing, "Missing expected warnings.")
810+
811+ def test_no_password(self, m_sman_cli):
812+ '''Attempt to register without the password key/value.'''
813+ m_sman_cli.side_effect = [util.ProcessExecutionError,
814+ (self.reg, 'bar')]
815 self.handle(self.name, self.config_no_password, self.cloud_init,
816 self.log, self.args)
817- self.assertEqual(self.SM._sub_man_cli.call_count, 0)
818+ self.assertEqual(m_sman_cli.call_count, 0)
819
820- def test_no_org(self):
821- '''
822- Attempt to register without the org key/value
823- '''
824- self.input_is_missing_data(self.config_no_key)
825-
826- def test_service_level_without_auto(self):
827- '''
828- Attempt to register using service-level without the auto-attach key
829- '''
830- self.SM.log_warn = mock.MagicMock()
831- self.SM._sub_man_cli = mock.MagicMock(
832- side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
833+ def test_no_org(self, m_sman_cli):
834+ '''Attempt to register without the org key/value.'''
835+ m_sman_cli.side_effect = [util.ProcessExecutionError]
836+ self.handle(self.name, self.config_no_key, self.cloud_init,
837+ self.log, self.args)
838+ m_sman_cli.assert_called_with(['identity'])
839+ self.assertEqual(m_sman_cli.call_count, 1)
840+ self.assert_logged_warnings((
841+ 'Unable to register system due to incomplete information.',
842+ 'Use either activationkey and org *or* userid and password',
843+ 'Registration failed or did not run completely',
844+ 'rh_subscription plugin did not complete successfully'))
845+
846+ def test_service_level_without_auto(self, m_sman_cli):
847+ '''Attempt to register using service-level without auto-attach key.'''
848+ m_sman_cli.side_effect = [util.ProcessExecutionError,
849+ (self.reg, 'bar')]
850 self.handle(self.name, self.config_service, self.cloud_init,
851 self.log, self.args)
852- self.assertEqual(self.SM._sub_man_cli.call_count, 1)
853- self.assertEqual(self.SM.log_warn.call_count, 2)
854+ self.assertEqual(m_sman_cli.call_count, 1)
855+ self.assert_logged_warnings((
856+ 'The service-level key must be used in conjunction with ',
857+ 'rh_subscription plugin did not complete successfully'))
858
859- def test_pool_not_a_list(self):
860+ def test_pool_not_a_list(self, m_sman_cli):
861 '''
862 Register with pools that are not in the format of a list
863 '''
864- self.SM.log_warn = mock.MagicMock()
865- self.SM._sub_man_cli = mock.MagicMock(
866- side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
867+ m_sman_cli.side_effect = [util.ProcessExecutionError,
868+ (self.reg, 'bar')]
869 self.handle(self.name, self.config_badpool, self.cloud_init,
870 self.log, self.args)
871- self.assertEqual(self.SM._sub_man_cli.call_count, 2)
872- self.assertEqual(self.SM.log_warn.call_count, 2)
873+ self.assertEqual(m_sman_cli.call_count, 2)
874+ self.assert_logged_warnings((
875+ 'Pools must in the format of a list',
876+ 'rh_subscription plugin did not complete successfully'))
877
878- def test_repo_not_a_list(self):
879+ def test_repo_not_a_list(self, m_sman_cli):
880 '''
881 Register with repos that are not in the format of a list
882 '''
883- self.SM.log_warn = mock.MagicMock()
884- self.SM._sub_man_cli = mock.MagicMock(
885- side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
886+ m_sman_cli.side_effect = [util.ProcessExecutionError,
887+ (self.reg, 'bar')]
888 self.handle(self.name, self.config_badrepo, self.cloud_init,
889 self.log, self.args)
890- self.assertEqual(self.SM.log_warn.call_count, 3)
891- self.assertEqual(self.SM._sub_man_cli.call_count, 2)
892+ self.assertEqual(m_sman_cli.call_count, 2)
893+ self.assert_logged_warnings((
894+ 'Repo IDs must in the format of a list.',
895+ 'Unable to add or remove repos',
896+ 'rh_subscription plugin did not complete successfully'))
897
898- def test_bad_key_value(self):
899+ def test_bad_key_value(self, m_sman_cli):
900 '''
901 Attempt to register with a key that we don't know
902 '''
903- self.SM.log_warn = mock.MagicMock()
904- self.SM._sub_man_cli = mock.MagicMock(
905- side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
906+ m_sman_cli.side_effect = [util.ProcessExecutionError,
907+ (self.reg, 'bar')]
908 self.handle(self.name, self.config_badkey, self.cloud_init,
909 self.log, self.args)
910- self.assertEqual(self.SM.log_warn.call_count, 2)
911- self.assertEqual(self.SM._sub_man_cli.call_count, 1)
912-
913- def input_is_missing_data(self, config):
914- '''
915- Helper def for tests that having missing information
916- '''
917- self.SM.log_warn = mock.MagicMock()
918- self.SM._sub_man_cli = mock.MagicMock(
919- side_effect=[util.ProcessExecutionError])
920- self.handle(self.name, config, self.cloud_init,
921- self.log, self.args)
922- self.SM._sub_man_cli.assert_called_with(['identity'])
923- self.assertEqual(self.SM.log_warn.call_count, 4)
924- self.assertEqual(self.SM._sub_man_cli.call_count, 1)
925+ self.assertEqual(m_sman_cli.call_count, 1)
926+ self.assert_logged_warnings((
927+ 'fookey is not a valid key for rh_subscription. Valid keys are:',
928+ 'rh_subscription plugin did not complete successfully'))
929
930 # vi: ts=4 expandtab
931diff --git a/tools/net-convert.py b/tools/net-convert.py
932index 68559cb..d1a4a64 100755
933--- a/tools/net-convert.py
934+++ b/tools/net-convert.py
935@@ -4,11 +4,13 @@
936 import argparse
937 import json
938 import os
939+import sys
940 import yaml
941
942 from cloudinit.sources.helpers import openstack
943
944 from cloudinit.net import eni
945+from cloudinit import log
946 from cloudinit.net import netplan
947 from cloudinit.net import network_state
948 from cloudinit.net import sysconfig
949@@ -29,14 +31,23 @@ def main():
950 metavar="name,mac",
951 action='append',
952 help="interface name to mac mapping")
953+ parser.add_argument("--debug", action='store_true',
954+ help='enable debug logging to stderr.')
955 parser.add_argument("--output-kind", "-ok",
956 choices=['eni', 'netplan', 'sysconfig'],
957 required=True)
958 args = parser.parse_args()
959
960+ if not args.directory.endswith("/"):
961+ args.directory += "/"
962+
963 if not os.path.isdir(args.directory):
964 os.makedirs(args.directory)
965
966+ if args.debug:
967+ log.setupBasicLogging(level=log.DEBUG)
968+ else:
969+ log.setupBasicLogging(level=log.WARN)
970 if args.mac:
971 known_macs = {}
972 for item in args.mac:
973@@ -53,8 +64,10 @@ def main():
974 pre_ns = yaml.load(net_data)
975 if 'network' in pre_ns:
976 pre_ns = pre_ns.get('network')
977- print("Input YAML")
978- print(yaml.dump(pre_ns, default_flow_style=False, indent=4))
979+ if args.debug:
980+ sys.stderr.write('\n'.join(
981+ ["Input YAML",
982+ yaml.dump(pre_ns, default_flow_style=False, indent=4), ""]))
983 ns = network_state.parse_net_config_data(pre_ns)
984 else:
985 pre_ns = openstack.convert_net_json(
986@@ -65,8 +78,10 @@ def main():
987 raise RuntimeError("No valid network_state object created from"
988 "input data")
989
990- print("\nInternal State")
991- print(yaml.dump(ns, default_flow_style=False, indent=4))
992+ if args.debug:
993+ sys.stderr.write('\n'.join([
994+ "", "Internal State",
995+ yaml.dump(ns, default_flow_style=False, indent=4), ""]))
996 if args.output_kind == "eni":
997 r_cls = eni.Renderer
998 elif args.output_kind == "netplan":
999@@ -75,6 +90,11 @@ def main():
1000 r_cls = sysconfig.Renderer
1001
1002 r = r_cls()
1003+ sys.stderr.write(''.join([
1004+ "Read input format '%s' from '%s'.\n" % (
1005+ args.kind, args.network_data.name),
1006+ "Wrote output format '%s' to '%s'\n" % (
1007+ args.output_kind, args.directory)]) + "\n")
1008 r.render_network_state(network_state=ns, target=args.directory)
1009
1010

Subscribers

People subscribed via source and target branches