Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel
- Git
- lp:~chad.smith/cloud-init
- ubuntu/devel
- Merge into 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) |
||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Scott Moser | Pending | ||
Review via email:
|
Commit message
Perform upstream snapshot from tip of master for release into Cosmic
Description of the change
To post a comment you must log in.
Revision history for this message

Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py |
2 | index 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 |
41 | diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py |
42 | index 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 |
178 | diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py |
179 | index 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): |
192 | diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py |
193 | index 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 |
205 | diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py |
206 | index 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): |
247 | diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py |
248 | index 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(): |
262 | diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py |
263 | index 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() |
365 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
366 | index 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: |
425 | diff --git a/cloudinit/warnings.py b/cloudinit/warnings.py |
426 | index 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) |
438 | diff --git a/debian/changelog b/debian/changelog |
439 | index 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. |
462 | diff --git a/integration-requirements.txt b/integration-requirements.txt |
463 | index 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 |
475 | diff --git a/tests/cloud_tests/platforms/instances.py b/tests/cloud_tests/platforms/instances.py |
476 | index 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()) |
489 | diff --git a/tests/cloud_tests/platforms/lxd/instance.py b/tests/cloud_tests/platforms/lxd/instance.py |
490 | index 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) |
579 | diff --git a/tests/cloud_tests/setup_image.py b/tests/cloud_tests/setup_image.py |
580 | index 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 |
607 | diff --git a/tests/cloud_tests/testcases.yaml b/tests/cloud_tests/testcases.yaml |
608 | index 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; } |
622 | diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/test_datasource/test_openstack.py |
623 | index 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, |
651 | diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py |
652 | index 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 |
931 | diff --git a/tools/net-convert.py b/tools/net-convert.py |
932 | index 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 |
PASSED: Continuous integration, rev:c4c6301f2e5 f6fe9960ceb7f67 0224f87b27e091 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 178/
https:/
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: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 178/rebuild
https:/