Merge ~chad.smith/cloud-init:ubuntu/zesty into cloud-init:ubuntu/zesty
- Git
- lp:~chad.smith/cloud-init
- ubuntu/zesty
- Merge into ubuntu/zesty
Proposed by
Chad Smith
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Approved by: | Scott Moser | ||||||||||||||||
Approved revision: | e6747555e0dc787fa99bd6bfff43846b02ef1bd1 | ||||||||||||||||
Merged at revision: | 2f3aae742b0a8456013f441b65b118872c60c001 | ||||||||||||||||
Proposed branch: | ~chad.smith/cloud-init:ubuntu/zesty | ||||||||||||||||
Merge into: | cloud-init:ubuntu/zesty | ||||||||||||||||
Diff against target: |
2207 lines (+787/-454) 51 files modified
cloudinit/cloud.py (+2/-2) cloudinit/config/cc_ntp.py (+7/-2) cloudinit/config/cc_rh_subscription.py (+28/-18) cloudinit/config/cc_update_etc_hosts.py (+2/-2) cloudinit/net/dhcp.py (+9/-3) cloudinit/net/tests/test_dhcp.py (+8/-1) cloudinit/sources/DataSourceAzure.py (+2/-23) cloudinit/sources/DataSourceEc2.py (+33/-11) cloudinit/user_data.py (+23/-10) debian/changelog (+26/-0) debian/cloud-init.templates (+3/-3) dev/null (+0/-26) sysvinit/gentoo/cloud-config (+0/-0) sysvinit/gentoo/cloud-final (+0/-0) sysvinit/gentoo/cloud-init (+0/-0) sysvinit/gentoo/cloud-init-local (+0/-0) templates/hosts.suse.tmpl (+8/-2) templates/ntp.conf.opensuse.tmpl (+88/-0) templates/ntp.conf.sles.tmpl (+0/-12) tests/cloud_tests/collect.py (+16/-2) tests/cloud_tests/images/base.py (+3/-16) tests/cloud_tests/images/lxd.py (+18/-14) tests/cloud_tests/images/nocloudkvm.py (+18/-24) tests/cloud_tests/instances/base.py (+4/-77) tests/cloud_tests/instances/lxd.py (+56/-48) tests/cloud_tests/instances/nocloudkvm.py (+51/-91) tests/cloud_tests/testcases/examples/run_commands.yaml (+2/-2) tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py (+3/-3) tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml (+3/-3) tests/cloud_tests/testcases/modules/keys_to_console.py (+4/-4) tests/cloud_tests/testcases/modules/runcmd.yaml (+2/-2) tests/cloud_tests/testcases/modules/set_hostname.py (+3/-1) tests/cloud_tests/testcases/modules/set_hostname.yaml (+2/-1) tests/cloud_tests/testcases/modules/set_hostname_fqdn.py (+8/-3) tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml (+3/-2) tests/cloud_tests/testcases/modules/set_password_expire.py (+1/-1) tests/cloud_tests/testcases/modules/set_password_expire.yaml (+2/-0) tests/cloud_tests/testcases/modules/set_password_list.yaml (+1/-0) tests/cloud_tests/testcases/modules/set_password_list_string.yaml (+1/-0) tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py (+0/-8) tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml (+0/-1) tests/cloud_tests/testcases/modules/ssh_keys_generate.py (+0/-5) tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml (+0/-6) tests/cloud_tests/testcases/modules/ssh_keys_provided.py (+0/-11) tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml (+0/-6) tests/cloud_tests/util.py (+154/-8) tests/unittests/test_data.py (+50/-0) tests/unittests/test_datasource/test_ec2.py (+33/-0) tests/unittests/test_handler/test_handler_etc_hosts.py (+69/-0) tests/unittests/test_handler/test_handler_ntp.py (+26/-0) tests/unittests/test_rh_subscription.py (+15/-0) |
||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email: mp+334071@code.launchpad.net |
Commit message
Description of the change
Upstream master snapshot for SRU into zesty.
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)
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py | |||
2 | index d8a9fc8..ba61678 100644 | |||
3 | --- a/cloudinit/cloud.py | |||
4 | +++ b/cloudinit/cloud.py | |||
5 | @@ -56,8 +56,8 @@ class Cloud(object): | |||
6 | 56 | def get_template_filename(self, name): | 56 | def get_template_filename(self, name): |
7 | 57 | fn = self.paths.template_tpl % (name) | 57 | fn = self.paths.template_tpl % (name) |
8 | 58 | if not os.path.isfile(fn): | 58 | if not os.path.isfile(fn): |
11 | 59 | LOG.warning("No template found at %s for template named %s", | 59 | LOG.warning("No template found in %s for template named %s", |
12 | 60 | fn, name) | 60 | os.path.dirname(fn), name) |
13 | 61 | return None | 61 | return None |
14 | 62 | return fn | 62 | return fn |
15 | 63 | 63 | ||
16 | diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py | |||
17 | index d43d060..f50bcb3 100644 | |||
18 | --- a/cloudinit/config/cc_ntp.py | |||
19 | +++ b/cloudinit/config/cc_ntp.py | |||
20 | @@ -23,7 +23,7 @@ frequency = PER_INSTANCE | |||
21 | 23 | NTP_CONF = '/etc/ntp.conf' | 23 | NTP_CONF = '/etc/ntp.conf' |
22 | 24 | TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' | 24 | TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' |
23 | 25 | NR_POOL_SERVERS = 4 | 25 | NR_POOL_SERVERS = 4 |
25 | 26 | distros = ['centos', 'debian', 'fedora', 'opensuse', 'ubuntu'] | 26 | distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu'] |
26 | 27 | 27 | ||
27 | 28 | 28 | ||
28 | 29 | # The schema definition for each cloud-config module is a strict contract for | 29 | # The schema definition for each cloud-config module is a strict contract for |
29 | @@ -174,8 +174,13 @@ def rename_ntp_conf(config=None): | |||
30 | 174 | 174 | ||
31 | 175 | def generate_server_names(distro): | 175 | def generate_server_names(distro): |
32 | 176 | names = [] | 176 | names = [] |
33 | 177 | pool_distro = distro | ||
34 | 178 | # For legal reasons x.pool.sles.ntp.org does not exist, | ||
35 | 179 | # use the opensuse pool | ||
36 | 180 | if distro == 'sles': | ||
37 | 181 | pool_distro = 'opensuse' | ||
38 | 177 | for x in range(0, NR_POOL_SERVERS): | 182 | for x in range(0, NR_POOL_SERVERS): |
40 | 178 | name = "%d.%s.pool.ntp.org" % (x, distro) | 183 | name = "%d.%s.pool.ntp.org" % (x, pool_distro) |
41 | 179 | names.append(name) | 184 | names.append(name) |
42 | 180 | return names | 185 | return names |
43 | 181 | 186 | ||
44 | diff --git a/cloudinit/config/cc_rh_subscription.py b/cloudinit/config/cc_rh_subscription.py | |||
45 | index 7f36cf8..a9d21e7 100644 | |||
46 | --- a/cloudinit/config/cc_rh_subscription.py | |||
47 | +++ b/cloudinit/config/cc_rh_subscription.py | |||
48 | @@ -38,14 +38,16 @@ Subscription`` example config. | |||
49 | 38 | server-hostname: <hostname> | 38 | server-hostname: <hostname> |
50 | 39 | """ | 39 | """ |
51 | 40 | 40 | ||
52 | 41 | from cloudinit import log as logging | ||
53 | 41 | from cloudinit import util | 42 | from cloudinit import util |
54 | 42 | 43 | ||
55 | 44 | LOG = logging.getLogger(__name__) | ||
56 | 45 | |||
57 | 43 | distros = ['fedora', 'rhel'] | 46 | distros = ['fedora', 'rhel'] |
58 | 44 | 47 | ||
59 | 45 | 48 | ||
60 | 46 | def handle(name, cfg, _cloud, log, _args): | 49 | def handle(name, cfg, _cloud, log, _args): |
63 | 47 | sm = SubscriptionManager(cfg) | 50 | sm = SubscriptionManager(cfg, log=log) |
62 | 48 | sm.log = log | ||
64 | 49 | if not sm.is_configured(): | 51 | if not sm.is_configured(): |
65 | 50 | log.debug("%s: module not configured.", name) | 52 | log.debug("%s: module not configured.", name) |
66 | 51 | return None | 53 | return None |
67 | @@ -86,10 +88,9 @@ def handle(name, cfg, _cloud, log, _args): | |||
68 | 86 | if not return_stat: | 88 | if not return_stat: |
69 | 87 | raise SubscriptionError("Unable to attach pools {0}" | 89 | raise SubscriptionError("Unable to attach pools {0}" |
70 | 88 | .format(sm.pools)) | 90 | .format(sm.pools)) |
75 | 89 | if (sm.enable_repo is not None) or (sm.disable_repo is not None): | 91 | return_stat = sm.update_repos() |
76 | 90 | return_stat = sm.update_repos(sm.enable_repo, sm.disable_repo) | 92 | if not return_stat: |
77 | 91 | if not return_stat: | 93 | raise SubscriptionError("Unable to add or remove repos") |
74 | 92 | raise SubscriptionError("Unable to add or remove repos") | ||
78 | 93 | sm.log_success("rh_subscription plugin completed successfully") | 94 | sm.log_success("rh_subscription plugin completed successfully") |
79 | 94 | except SubscriptionError as e: | 95 | except SubscriptionError as e: |
80 | 95 | sm.log_warn(str(e)) | 96 | sm.log_warn(str(e)) |
81 | @@ -108,7 +109,10 @@ class SubscriptionManager(object): | |||
82 | 108 | 'rhsm-baseurl', 'server-hostname', | 109 | 'rhsm-baseurl', 'server-hostname', |
83 | 109 | 'auto-attach', 'service-level'] | 110 | 'auto-attach', 'service-level'] |
84 | 110 | 111 | ||
86 | 111 | def __init__(self, cfg): | 112 | def __init__(self, cfg, log=None): |
87 | 113 | if log is None: | ||
88 | 114 | log = LOG | ||
89 | 115 | self.log = log | ||
90 | 112 | self.cfg = cfg | 116 | self.cfg = cfg |
91 | 113 | self.rhel_cfg = self.cfg.get('rh_subscription', {}) | 117 | self.rhel_cfg = self.cfg.get('rh_subscription', {}) |
92 | 114 | self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl') | 118 | self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl') |
93 | @@ -130,7 +134,7 @@ class SubscriptionManager(object): | |||
94 | 130 | 134 | ||
95 | 131 | def log_warn(self, msg): | 135 | def log_warn(self, msg): |
96 | 132 | '''Simple wrapper for logging warning messages. Useful for unittests''' | 136 | '''Simple wrapper for logging warning messages. Useful for unittests''' |
98 | 133 | self.log.warn(msg) | 137 | self.log.warning(msg) |
99 | 134 | 138 | ||
100 | 135 | def _verify_keys(self): | 139 | def _verify_keys(self): |
101 | 136 | ''' | 140 | ''' |
102 | @@ -245,7 +249,7 @@ class SubscriptionManager(object): | |||
103 | 245 | return False | 249 | return False |
104 | 246 | 250 | ||
105 | 247 | reg_id = return_out.split("ID: ")[1].rstrip() | 251 | reg_id = return_out.split("ID: ")[1].rstrip() |
107 | 248 | self.log.debug("Registered successfully with ID {0}".format(reg_id)) | 252 | self.log.debug("Registered successfully with ID %s", reg_id) |
108 | 249 | return True | 253 | return True |
109 | 250 | 254 | ||
110 | 251 | def _set_service_level(self): | 255 | def _set_service_level(self): |
111 | @@ -347,7 +351,7 @@ class SubscriptionManager(object): | |||
112 | 347 | try: | 351 | try: |
113 | 348 | self._sub_man_cli(cmd) | 352 | self._sub_man_cli(cmd) |
114 | 349 | self.log.debug("Attached the following pools to your " | 353 | self.log.debug("Attached the following pools to your " |
116 | 350 | "system: %s" % (", ".join(pool_list)) | 354 | "system: %s", (", ".join(pool_list)) |
117 | 351 | .replace('--pool=', '')) | 355 | .replace('--pool=', '')) |
118 | 352 | return True | 356 | return True |
119 | 353 | except util.ProcessExecutionError as e: | 357 | except util.ProcessExecutionError as e: |
120 | @@ -355,18 +359,24 @@ class SubscriptionManager(object): | |||
121 | 355 | "due to {1}".format(pool, e)) | 359 | "due to {1}".format(pool, e)) |
122 | 356 | return False | 360 | return False |
123 | 357 | 361 | ||
125 | 358 | def update_repos(self, erepos, drepos): | 362 | def update_repos(self): |
126 | 359 | ''' | 363 | ''' |
127 | 360 | Takes a list of yum repo ids that need to be disabled or enabled; then | 364 | Takes a list of yum repo ids that need to be disabled or enabled; then |
128 | 361 | it verifies if they are already enabled or disabled and finally | 365 | it verifies if they are already enabled or disabled and finally |
129 | 362 | executes the action to disable or enable | 366 | executes the action to disable or enable |
130 | 363 | ''' | 367 | ''' |
131 | 364 | 368 | ||
133 | 365 | if (erepos is not None) and (not isinstance(erepos, list)): | 369 | erepos = self.enable_repo |
134 | 370 | drepos = self.disable_repo | ||
135 | 371 | if erepos is None: | ||
136 | 372 | erepos = [] | ||
137 | 373 | if drepos is None: | ||
138 | 374 | drepos = [] | ||
139 | 375 | if not isinstance(erepos, list): | ||
140 | 366 | self.log_warn("Repo IDs must in the format of a list.") | 376 | self.log_warn("Repo IDs must in the format of a list.") |
141 | 367 | return False | 377 | return False |
142 | 368 | 378 | ||
144 | 369 | if (drepos is not None) and (not isinstance(drepos, list)): | 379 | if not isinstance(drepos, list): |
145 | 370 | self.log_warn("Repo IDs must in the format of a list.") | 380 | self.log_warn("Repo IDs must in the format of a list.") |
146 | 371 | return False | 381 | return False |
147 | 372 | 382 | ||
148 | @@ -399,14 +409,14 @@ class SubscriptionManager(object): | |||
149 | 399 | for fail in enable_list_fail: | 409 | for fail in enable_list_fail: |
150 | 400 | # Check if the repo exists or not | 410 | # Check if the repo exists or not |
151 | 401 | if fail in active_repos: | 411 | if fail in active_repos: |
153 | 402 | self.log.debug("Repo {0} is already enabled".format(fail)) | 412 | self.log.debug("Repo %s is already enabled", fail) |
154 | 403 | else: | 413 | else: |
155 | 404 | self.log_warn("Repo {0} does not appear to " | 414 | self.log_warn("Repo {0} does not appear to " |
156 | 405 | "exist".format(fail)) | 415 | "exist".format(fail)) |
157 | 406 | if len(disable_list_fail) > 0: | 416 | if len(disable_list_fail) > 0: |
158 | 407 | for fail in disable_list_fail: | 417 | for fail in disable_list_fail: |
161 | 408 | self.log.debug("Repo {0} not disabled " | 418 | self.log.debug("Repo %s not disabled " |
162 | 409 | "because it is not enabled".format(fail)) | 419 | "because it is not enabled", fail) |
163 | 410 | 420 | ||
164 | 411 | cmd = ['repos'] | 421 | cmd = ['repos'] |
165 | 412 | if len(disable_list) > 0: | 422 | if len(disable_list) > 0: |
166 | @@ -422,10 +432,10 @@ class SubscriptionManager(object): | |||
167 | 422 | return False | 432 | return False |
168 | 423 | 433 | ||
169 | 424 | if len(enable_list) > 0: | 434 | if len(enable_list) > 0: |
171 | 425 | self.log.debug("Enabled the following repos: %s" % | 435 | self.log.debug("Enabled the following repos: %s", |
172 | 426 | (", ".join(enable_list)).replace('--enable=', '')) | 436 | (", ".join(enable_list)).replace('--enable=', '')) |
173 | 427 | if len(disable_list) > 0: | 437 | if len(disable_list) > 0: |
175 | 428 | self.log.debug("Disabled the following repos: %s" % | 438 | self.log.debug("Disabled the following repos: %s", |
176 | 429 | (", ".join(disable_list)).replace('--disable=', '')) | 439 | (", ".join(disable_list)).replace('--disable=', '')) |
177 | 430 | return True | 440 | return True |
178 | 431 | 441 | ||
179 | diff --git a/cloudinit/config/cc_update_etc_hosts.py b/cloudinit/config/cc_update_etc_hosts.py | |||
180 | index b394784..c96eede 100644 | |||
181 | --- a/cloudinit/config/cc_update_etc_hosts.py | |||
182 | +++ b/cloudinit/config/cc_update_etc_hosts.py | |||
183 | @@ -23,8 +23,8 @@ using the template located in ``/etc/cloud/templates/hosts.tmpl``. In the | |||
184 | 23 | 23 | ||
185 | 24 | If ``manage_etc_hosts`` is set to ``localhost``, then cloud-init will not | 24 | If ``manage_etc_hosts`` is set to ``localhost``, then cloud-init will not |
186 | 25 | rewrite ``/etc/hosts`` entirely, but rather will ensure that a entry for the | 25 | rewrite ``/etc/hosts`` entirely, but rather will ensure that a entry for the |
189 | 26 | fqdn with ip ``127.0.1.1`` is present in ``/etc/hosts`` (i.e. | 26 | fqdn with a distribution dependent ip is present in ``/etc/hosts`` (i.e. |
190 | 27 | ``ping <hostname>`` will ping ``127.0.1.1``). | 27 | ``ping <hostname>`` will ping ``127.0.0.1`` or ``127.0.1.1`` or other ip). |
191 | 28 | 28 | ||
192 | 29 | .. note:: | 29 | .. note:: |
193 | 30 | if ``manage_etc_hosts`` is set ``true`` or ``template``, the contents | 30 | if ``manage_etc_hosts`` is set ``true`` or ``template``, the contents |
194 | diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py | |||
195 | index 0cba703..d8624d8 100644 | |||
196 | --- a/cloudinit/net/dhcp.py | |||
197 | +++ b/cloudinit/net/dhcp.py | |||
198 | @@ -8,6 +8,7 @@ import configobj | |||
199 | 8 | import logging | 8 | import logging |
200 | 9 | import os | 9 | import os |
201 | 10 | import re | 10 | import re |
202 | 11 | import signal | ||
203 | 11 | 12 | ||
204 | 12 | from cloudinit.net import find_fallback_nic, get_devicelist | 13 | from cloudinit.net import find_fallback_nic, get_devicelist |
205 | 13 | from cloudinit import temp_utils | 14 | from cloudinit import temp_utils |
206 | @@ -41,8 +42,7 @@ def maybe_perform_dhcp_discovery(nic=None): | |||
207 | 41 | if nic is None: | 42 | if nic is None: |
208 | 42 | nic = find_fallback_nic() | 43 | nic = find_fallback_nic() |
209 | 43 | if nic is None: | 44 | if nic is None: |
212 | 44 | LOG.debug( | 45 | LOG.debug('Skip dhcp_discovery: Unable to find fallback nic.') |
211 | 45 | 'Skip dhcp_discovery: Unable to find fallback nic.') | ||
213 | 46 | return {} | 46 | return {} |
214 | 47 | elif nic not in get_devicelist(): | 47 | elif nic not in get_devicelist(): |
215 | 48 | LOG.debug( | 48 | LOG.debug( |
216 | @@ -119,7 +119,13 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir): | |||
217 | 119 | cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, | 119 | cmd = [sandbox_dhclient_cmd, '-1', '-v', '-lf', lease_file, |
218 | 120 | '-pf', pid_file, interface, '-sf', '/bin/true'] | 120 | '-pf', pid_file, interface, '-sf', '/bin/true'] |
219 | 121 | util.subp(cmd, capture=True) | 121 | util.subp(cmd, capture=True) |
221 | 122 | return parse_dhcp_lease_file(lease_file) | 122 | pid = None |
222 | 123 | try: | ||
223 | 124 | pid = int(util.load_file(pid_file).strip()) | ||
224 | 125 | return parse_dhcp_lease_file(lease_file) | ||
225 | 126 | finally: | ||
226 | 127 | if pid: | ||
227 | 128 | os.kill(pid, signal.SIGKILL) | ||
228 | 123 | 129 | ||
229 | 124 | 130 | ||
230 | 125 | def networkd_parse_lease(content): | 131 | def networkd_parse_lease(content): |
231 | diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py | |||
232 | index 1c1f504..3d8e15c 100644 | |||
233 | --- a/cloudinit/net/tests/test_dhcp.py | |||
234 | +++ b/cloudinit/net/tests/test_dhcp.py | |||
235 | @@ -2,6 +2,7 @@ | |||
236 | 2 | 2 | ||
237 | 3 | import mock | 3 | import mock |
238 | 4 | import os | 4 | import os |
239 | 5 | import signal | ||
240 | 5 | from textwrap import dedent | 6 | from textwrap import dedent |
241 | 6 | 7 | ||
242 | 7 | from cloudinit.net.dhcp import ( | 8 | from cloudinit.net.dhcp import ( |
243 | @@ -114,8 +115,9 @@ class TestDHCPDiscoveryClean(CiTestCase): | |||
244 | 114 | self.assertEqual('eth9', call[0][1]) | 115 | self.assertEqual('eth9', call[0][1]) |
245 | 115 | self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2]) | 116 | self.assertIn('/var/tmp/cloud-init/cloud-init-dhcp-', call[0][2]) |
246 | 116 | 117 | ||
247 | 118 | @mock.patch('cloudinit.net.dhcp.os.kill') | ||
248 | 117 | @mock.patch('cloudinit.net.dhcp.util.subp') | 119 | @mock.patch('cloudinit.net.dhcp.util.subp') |
250 | 118 | def test_dhcp_discovery_run_in_sandbox(self, m_subp): | 120 | def test_dhcp_discovery_run_in_sandbox(self, m_subp, m_kill): |
251 | 119 | """dhcp_discovery brings up the interface and runs dhclient. | 121 | """dhcp_discovery brings up the interface and runs dhclient. |
252 | 120 | 122 | ||
253 | 121 | It also returns the parsed dhcp.leases file generated in the sandbox. | 123 | It also returns the parsed dhcp.leases file generated in the sandbox. |
254 | @@ -134,6 +136,10 @@ class TestDHCPDiscoveryClean(CiTestCase): | |||
255 | 134 | """) | 136 | """) |
256 | 135 | lease_file = os.path.join(tmpdir, 'dhcp.leases') | 137 | lease_file = os.path.join(tmpdir, 'dhcp.leases') |
257 | 136 | write_file(lease_file, lease_content) | 138 | write_file(lease_file, lease_content) |
258 | 139 | pid_file = os.path.join(tmpdir, 'dhclient.pid') | ||
259 | 140 | my_pid = 1 | ||
260 | 141 | write_file(pid_file, "%d\n" % my_pid) | ||
261 | 142 | |||
262 | 137 | self.assertItemsEqual( | 143 | self.assertItemsEqual( |
263 | 138 | [{'interface': 'eth9', 'fixed-address': '192.168.2.74', | 144 | [{'interface': 'eth9', 'fixed-address': '192.168.2.74', |
264 | 139 | 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], | 145 | 'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}], |
265 | @@ -149,6 +155,7 @@ class TestDHCPDiscoveryClean(CiTestCase): | |||
266 | 149 | [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf', | 155 | [os.path.join(tmpdir, 'dhclient'), '-1', '-v', '-lf', |
267 | 150 | lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), | 156 | lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'), |
268 | 151 | 'eth9', '-sf', '/bin/true'], capture=True)]) | 157 | 'eth9', '-sf', '/bin/true'], capture=True)]) |
269 | 158 | m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)]) | ||
270 | 152 | 159 | ||
271 | 153 | 160 | ||
272 | 154 | class TestSystemdParseLeases(CiTestCase): | 161 | class TestSystemdParseLeases(CiTestCase): |
273 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py | |||
274 | index 80c2bd1..8c3492d 100644 | |||
275 | --- a/cloudinit/sources/DataSourceAzure.py | |||
276 | +++ b/cloudinit/sources/DataSourceAzure.py | |||
277 | @@ -465,10 +465,8 @@ class DataSourceAzure(sources.DataSource): | |||
278 | 465 | 465 | ||
279 | 466 | 1. Probe the drivers of the net-devices present and inject them in | 466 | 1. Probe the drivers of the net-devices present and inject them in |
280 | 467 | the network configuration under params: driver: <driver> value | 467 | the network configuration under params: driver: <driver> value |
285 | 468 | 2. If the driver value is 'mlx4_core', the control mode should be | 468 | 2. Generate a fallback network config that does not include any of |
286 | 469 | set to manual. The device will be later used to build a bond, | 469 | the blacklisted devices. |
283 | 470 | for now we want to ensure the device gets named but does not | ||
284 | 471 | break any network configuration | ||
287 | 472 | """ | 470 | """ |
288 | 473 | blacklist = ['mlx4_core'] | 471 | blacklist = ['mlx4_core'] |
289 | 474 | if not self._network_config: | 472 | if not self._network_config: |
290 | @@ -477,25 +475,6 @@ class DataSourceAzure(sources.DataSource): | |||
291 | 477 | netconfig = net.generate_fallback_config( | 475 | netconfig = net.generate_fallback_config( |
292 | 478 | blacklist_drivers=blacklist, config_driver=True) | 476 | blacklist_drivers=blacklist, config_driver=True) |
293 | 479 | 477 | ||
294 | 480 | # if we have any blacklisted devices, update the network_config to | ||
295 | 481 | # include the device, mac, and driver values, but with no ip | ||
296 | 482 | # config; this ensures udev rules are generated but won't affect | ||
297 | 483 | # ip configuration | ||
298 | 484 | bl_found = 0 | ||
299 | 485 | for bl_dev in [dev for dev in net.get_devicelist() | ||
300 | 486 | if net.device_driver(dev) in blacklist]: | ||
301 | 487 | bl_found += 1 | ||
302 | 488 | cfg = { | ||
303 | 489 | 'type': 'physical', | ||
304 | 490 | 'name': 'vf%d' % bl_found, | ||
305 | 491 | 'mac_address': net.get_interface_mac(bl_dev), | ||
306 | 492 | 'params': { | ||
307 | 493 | 'driver': net.device_driver(bl_dev), | ||
308 | 494 | 'device_id': net.device_devid(bl_dev), | ||
309 | 495 | }, | ||
310 | 496 | } | ||
311 | 497 | netconfig['config'].append(cfg) | ||
312 | 498 | |||
313 | 499 | self._network_config = netconfig | 478 | self._network_config = netconfig |
314 | 500 | 479 | ||
315 | 501 | return self._network_config | 480 | return self._network_config |
316 | diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py | |||
317 | index 0ef2217..7bbbfb6 100644 | |||
318 | --- a/cloudinit/sources/DataSourceEc2.py | |||
319 | +++ b/cloudinit/sources/DataSourceEc2.py | |||
320 | @@ -65,7 +65,7 @@ class DataSourceEc2(sources.DataSource): | |||
321 | 65 | get_network_metadata = False | 65 | get_network_metadata = False |
322 | 66 | 66 | ||
323 | 67 | # Track the discovered fallback nic for use in configuration generation. | 67 | # Track the discovered fallback nic for use in configuration generation. |
325 | 68 | fallback_nic = None | 68 | _fallback_interface = None |
326 | 69 | 69 | ||
327 | 70 | def __init__(self, sys_cfg, distro, paths): | 70 | def __init__(self, sys_cfg, distro, paths): |
328 | 71 | sources.DataSource.__init__(self, sys_cfg, distro, paths) | 71 | sources.DataSource.__init__(self, sys_cfg, distro, paths) |
329 | @@ -92,18 +92,17 @@ class DataSourceEc2(sources.DataSource): | |||
330 | 92 | elif self.cloud_platform == Platforms.NO_EC2_METADATA: | 92 | elif self.cloud_platform == Platforms.NO_EC2_METADATA: |
331 | 93 | return False | 93 | return False |
332 | 94 | 94 | ||
333 | 95 | self.fallback_nic = net.find_fallback_nic() | ||
334 | 96 | if self.get_network_metadata: # Setup networking in init-local stage. | 95 | if self.get_network_metadata: # Setup networking in init-local stage. |
335 | 97 | if util.is_FreeBSD(): | 96 | if util.is_FreeBSD(): |
336 | 98 | LOG.debug("FreeBSD doesn't support running dhclient with -sf") | 97 | LOG.debug("FreeBSD doesn't support running dhclient with -sf") |
337 | 99 | return False | 98 | return False |
339 | 100 | dhcp_leases = dhcp.maybe_perform_dhcp_discovery(self.fallback_nic) | 99 | dhcp_leases = dhcp.maybe_perform_dhcp_discovery( |
340 | 100 | self.fallback_interface) | ||
341 | 101 | if not dhcp_leases: | 101 | if not dhcp_leases: |
342 | 102 | # DataSourceEc2Local failed in init-local stage. DataSourceEc2 | 102 | # DataSourceEc2Local failed in init-local stage. DataSourceEc2 |
343 | 103 | # will still run in init-network stage. | 103 | # will still run in init-network stage. |
344 | 104 | return False | 104 | return False |
345 | 105 | dhcp_opts = dhcp_leases[-1] | 105 | dhcp_opts = dhcp_leases[-1] |
346 | 106 | self.fallback_nic = dhcp_opts.get('interface') | ||
347 | 107 | net_params = {'interface': dhcp_opts.get('interface'), | 106 | net_params = {'interface': dhcp_opts.get('interface'), |
348 | 108 | 'ip': dhcp_opts.get('fixed-address'), | 107 | 'ip': dhcp_opts.get('fixed-address'), |
349 | 109 | 'prefix_or_mask': dhcp_opts.get('subnet-mask'), | 108 | 'prefix_or_mask': dhcp_opts.get('subnet-mask'), |
350 | @@ -301,21 +300,44 @@ class DataSourceEc2(sources.DataSource): | |||
351 | 301 | return None | 300 | return None |
352 | 302 | 301 | ||
353 | 303 | result = None | 302 | result = None |
355 | 304 | net_md = self.metadata.get('network') | 303 | no_network_metadata_on_aws = bool( |
356 | 304 | 'network' not in self.metadata and | ||
357 | 305 | self.cloud_platform == Platforms.AWS) | ||
358 | 306 | if no_network_metadata_on_aws: | ||
359 | 307 | LOG.debug("Metadata 'network' not present:" | ||
360 | 308 | " Refreshing stale metadata from prior to upgrade.") | ||
361 | 309 | util.log_time( | ||
362 | 310 | logfunc=LOG.debug, msg='Re-crawl of metadata service', | ||
363 | 311 | func=self._crawl_metadata) | ||
364 | 312 | |||
365 | 305 | # Limit network configuration to only the primary/fallback nic | 313 | # Limit network configuration to only the primary/fallback nic |
368 | 306 | macs_to_nics = { | 314 | iface = self.fallback_interface |
369 | 307 | net.get_interface_mac(self.fallback_nic): self.fallback_nic} | 315 | macs_to_nics = {net.get_interface_mac(iface): iface} |
370 | 316 | net_md = self.metadata.get('network') | ||
371 | 308 | if isinstance(net_md, dict): | 317 | if isinstance(net_md, dict): |
372 | 309 | result = convert_ec2_metadata_network_config( | 318 | result = convert_ec2_metadata_network_config( |
375 | 310 | net_md, macs_to_nics=macs_to_nics, | 319 | net_md, macs_to_nics=macs_to_nics, fallback_nic=iface) |
374 | 311 | fallback_nic=self.fallback_nic) | ||
376 | 312 | else: | 320 | else: |
379 | 313 | LOG.warning("unexpected metadata 'network' key not valid: %s", | 321 | LOG.warning("Metadata 'network' key not valid: %s.", net_md) |
378 | 314 | net_md) | ||
380 | 315 | self._network_config = result | 322 | self._network_config = result |
381 | 316 | 323 | ||
382 | 317 | return self._network_config | 324 | return self._network_config |
383 | 318 | 325 | ||
384 | 326 | @property | ||
385 | 327 | def fallback_interface(self): | ||
386 | 328 | if self._fallback_interface is None: | ||
387 | 329 | # fallback_nic was used at one point, so restored objects may | ||
388 | 330 | # have an attribute there. respect that if found. | ||
389 | 331 | _legacy_fbnic = getattr(self, 'fallback_nic', None) | ||
390 | 332 | if _legacy_fbnic: | ||
391 | 333 | self._fallback_interface = _legacy_fbnic | ||
392 | 334 | self.fallback_nic = None | ||
393 | 335 | else: | ||
394 | 336 | self._fallback_interface = net.find_fallback_nic() | ||
395 | 337 | if self._fallback_interface is None: | ||
396 | 338 | LOG.warning("Did not find a fallback interface on EC2.") | ||
397 | 339 | return self._fallback_interface | ||
398 | 340 | |||
399 | 319 | def _crawl_metadata(self): | 341 | def _crawl_metadata(self): |
400 | 320 | """Crawl metadata service when available. | 342 | """Crawl metadata service when available. |
401 | 321 | 343 | ||
402 | diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py | |||
403 | index 88cb7f8..cc55daf 100644 | |||
404 | --- a/cloudinit/user_data.py | |||
405 | +++ b/cloudinit/user_data.py | |||
406 | @@ -19,6 +19,7 @@ import six | |||
407 | 19 | 19 | ||
408 | 20 | from cloudinit import handlers | 20 | from cloudinit import handlers |
409 | 21 | from cloudinit import log as logging | 21 | from cloudinit import log as logging |
410 | 22 | from cloudinit.url_helper import UrlError | ||
411 | 22 | from cloudinit import util | 23 | from cloudinit import util |
412 | 23 | 24 | ||
413 | 24 | LOG = logging.getLogger(__name__) | 25 | LOG = logging.getLogger(__name__) |
414 | @@ -222,16 +223,28 @@ class UserDataProcessor(object): | |||
415 | 222 | if include_once_on and os.path.isfile(include_once_fn): | 223 | if include_once_on and os.path.isfile(include_once_fn): |
416 | 223 | content = util.load_file(include_once_fn) | 224 | content = util.load_file(include_once_fn) |
417 | 224 | else: | 225 | else: |
428 | 225 | resp = util.read_file_or_url(include_url, | 226 | try: |
429 | 226 | ssl_details=self.ssl_details) | 227 | resp = util.read_file_or_url(include_url, |
430 | 227 | if include_once_on and resp.ok(): | 228 | ssl_details=self.ssl_details) |
431 | 228 | util.write_file(include_once_fn, resp.contents, mode=0o600) | 229 | if include_once_on and resp.ok(): |
432 | 229 | if resp.ok(): | 230 | util.write_file(include_once_fn, resp.contents, |
433 | 230 | content = resp.contents | 231 | mode=0o600) |
434 | 231 | else: | 232 | if resp.ok(): |
435 | 232 | LOG.warning(("Fetching from %s resulted in" | 233 | content = resp.contents |
436 | 233 | " a invalid http code of %s"), | 234 | else: |
437 | 234 | include_url, resp.code) | 235 | LOG.warning(("Fetching from %s resulted in" |
438 | 236 | " a invalid http code of %s"), | ||
439 | 237 | include_url, resp.code) | ||
440 | 238 | except UrlError as urle: | ||
441 | 239 | message = str(urle) | ||
442 | 240 | # Older versions of requests.exceptions.HTTPError may not | ||
443 | 241 | # include the errant url. Append it for clarity in logs. | ||
444 | 242 | if include_url not in message: | ||
445 | 243 | message += ' for url: {0}'.format(include_url) | ||
446 | 244 | LOG.warning(message) | ||
447 | 245 | except IOError as ioe: | ||
448 | 246 | LOG.warning("Fetching from %s resulted in %s", | ||
449 | 247 | include_url, ioe) | ||
450 | 235 | 248 | ||
451 | 236 | if content is not None: | 249 | if content is not None: |
452 | 237 | new_msg = convert_string(content) | 250 | new_msg = convert_string(content) |
453 | diff --git a/debian/changelog b/debian/changelog | |||
454 | index bc6c449..6c01326 100644 | |||
455 | --- a/debian/changelog | |||
456 | +++ b/debian/changelog | |||
457 | @@ -1,3 +1,29 @@ | |||
458 | 1 | cloud-init (17.1-41-g76243487-0ubuntu1~17.04.1) zesty-proposed; urgency=medium | ||
459 | 2 | |||
460 | 3 | * cherry-pick 1110f30: debian/cloud-init.templates: Fix capitilazation | ||
461 | 4 | in 'AliYun' (LP: #1728186) | ||
462 | 5 | * New upstream snapshot (LP: #1733653) | ||
463 | 6 | - integration test: replace curtin test ppa with cloud-init test ppa. | ||
464 | 7 | - EC2: Fix bug using fallback_nic and metadata when restoring from cache. | ||
465 | 8 | - EC2: Kill dhclient process used in sandbox dhclient. | ||
466 | 9 | - ntp: fix configuration template rendering for openSUSE and SLES | ||
467 | 10 | - centos: Provide the failed #include url in error messages | ||
468 | 11 | - Catch UrlError when #include'ing URLs [Andrew Jorgensen] | ||
469 | 12 | - hosts: Fix openSUSE and SLES setup for /etc/hosts and clarify docs. | ||
470 | 13 | [Robert Schweikert] | ||
471 | 14 | - rh_subscription: Perform null checks for enabled and disabled repos. | ||
472 | 15 | [Dave Mulford] | ||
473 | 16 | - Improve warning message when a template is not found. | ||
474 | 17 | [Robert Schweikert] | ||
475 | 18 | - Replace the temporary i9n.brickies.net with i9n.cloud-init.io. | ||
476 | 19 | - Azure: don't generate network configuration for SRIOV devices | ||
477 | 20 | - tests: address some minor feedback missed in last merge. | ||
478 | 21 | - tests: integration test cleanup and full pass of nocloud-kvm. | ||
479 | 22 | - Gentoo: chmod +x on all files in sysvinit/gentoo/ | ||
480 | 23 | [Carlos Konstanski] | ||
481 | 24 | |||
482 | 25 | -- Chad Smith <chad.smith@canonical.com> Tue, 21 Nov 2017 11:48:32 -0700 | ||
483 | 26 | |||
484 | 1 | cloud-init (17.1-27-geb292c18-0ubuntu1~17.04.1) zesty-proposed; urgency=medium | 27 | cloud-init (17.1-27-geb292c18-0ubuntu1~17.04.1) zesty-proposed; urgency=medium |
485 | 2 | 28 | ||
486 | 3 | * New upstream snapshot. | 29 | * New upstream snapshot. |
487 | diff --git a/debian/cloud-init.templates b/debian/cloud-init.templates | |||
488 | index 0a251e3..5ed37f7 100644 | |||
489 | --- a/debian/cloud-init.templates | |||
490 | +++ b/debian/cloud-init.templates | |||
491 | @@ -1,8 +1,8 @@ | |||
492 | 1 | Template: cloud-init/datasources | 1 | Template: cloud-init/datasources |
493 | 2 | Type: multiselect | 2 | Type: multiselect |
497 | 3 | Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None | 3 | Default: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None |
498 | 4 | Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, Aliyun, Ec2, CloudStack, None | 4 | Choices-C: NoCloud, ConfigDrive, OpenNebula, DigitalOcean, Azure, AltCloud, OVF, MAAS, GCE, OpenStack, CloudSigma, SmartOS, Bigstep, Scaleway, AliYun, Ec2, CloudStack, None |
499 | 5 | Choices: NoCloud: Reads info from /var/lib/cloud/seed only, ConfigDrive: Reads data from Openstack Config Drive, OpenNebula: read from OpenNebula context disk, DigitalOcean: reads data from Droplet datasource, Azure: read from MS Azure cdrom. Requires walinux-agent, AltCloud: config disks for RHEVm and vSphere, OVF: Reads data from OVF Transports, MAAS: Reads data from Ubuntu MAAS, GCE: google compute metadata service, OpenStack: native openstack metadata service, CloudSigma: metadata over serial for cloudsigma.com, SmartOS: Read from SmartOS metadata service, Bigstep: Bigstep metadata service, Scaleway: Scaleway metadata service, Aliyun: Alibaba metadata service, Ec2: reads data from EC2 Metadata service, CloudStack: Read from CloudStack metadata service, None: Failsafe datasource | 5 | Choices: NoCloud: Reads info from /var/lib/cloud/seed only, ConfigDrive: Reads data from Openstack Config Drive, OpenNebula: read from OpenNebula context disk, DigitalOcean: reads data from Droplet datasource, Azure: read from MS Azure cdrom. Requires walinux-agent, AltCloud: config disks for RHEVm and vSphere, OVF: Reads data from OVF Transports, MAAS: Reads data from Ubuntu MAAS, GCE: google compute metadata service, OpenStack: native openstack metadata service, CloudSigma: metadata over serial for cloudsigma.com, SmartOS: Read from SmartOS metadata service, Bigstep: Bigstep metadata service, Scaleway: Scaleway metadata service, AliYun: Alibaba metadata service, Ec2: reads data from EC2 Metadata service, CloudStack: Read from CloudStack metadata service, None: Failsafe datasource |
500 | 6 | Description: Which data sources should be searched? | 6 | Description: Which data sources should be searched? |
501 | 7 | Cloud-init supports searching different "Data Sources" for information | 7 | Cloud-init supports searching different "Data Sources" for information |
502 | 8 | that it uses to configure a cloud instance. | 8 | that it uses to configure a cloud instance. |
503 | diff --git a/sysvinit/gentoo/cloud-config b/sysvinit/gentoo/cloud-config | |||
504 | 9 | old mode 100644 | 9 | old mode 100644 |
505 | 10 | new mode 100755 | 10 | new mode 100755 |
506 | index 5618472..5618472 | |||
507 | --- a/sysvinit/gentoo/cloud-config | |||
508 | +++ b/sysvinit/gentoo/cloud-config | |||
509 | diff --git a/sysvinit/gentoo/cloud-final b/sysvinit/gentoo/cloud-final | |||
510 | 11 | old mode 100644 | 11 | old mode 100644 |
511 | 12 | new mode 100755 | 12 | new mode 100755 |
512 | index a9bf01f..a9bf01f | |||
513 | --- a/sysvinit/gentoo/cloud-final | |||
514 | +++ b/sysvinit/gentoo/cloud-final | |||
515 | diff --git a/sysvinit/gentoo/cloud-init b/sysvinit/gentoo/cloud-init | |||
516 | 13 | old mode 100644 | 13 | old mode 100644 |
517 | 14 | new mode 100755 | 14 | new mode 100755 |
518 | index 531a715..531a715 | |||
519 | --- a/sysvinit/gentoo/cloud-init | |||
520 | +++ b/sysvinit/gentoo/cloud-init | |||
521 | diff --git a/sysvinit/gentoo/cloud-init-local b/sysvinit/gentoo/cloud-init-local | |||
522 | 15 | old mode 100644 | 15 | old mode 100644 |
523 | 16 | new mode 100755 | 16 | new mode 100755 |
524 | index 0f8cf65..0f8cf65 | |||
525 | --- a/sysvinit/gentoo/cloud-init-local | |||
526 | +++ b/sysvinit/gentoo/cloud-init-local | |||
527 | diff --git a/templates/hosts.opensuse.tmpl b/templates/hosts.opensuse.tmpl | |||
528 | 17 | deleted file mode 100644 | 17 | deleted file mode 100644 |
529 | index 655da3f..0000000 | |||
530 | --- a/templates/hosts.opensuse.tmpl | |||
531 | +++ /dev/null | |||
532 | @@ -1,26 +0,0 @@ | |||
533 | 1 | * | ||
534 | 2 | This file /etc/cloud/templates/hosts.opensuse.tmpl is only utilized | ||
535 | 3 | if enabled in cloud-config. Specifically, in order to enable it | ||
536 | 4 | you need to add the following to config: | ||
537 | 5 | manage_etc_hosts: True | ||
538 | 6 | *# | ||
539 | 7 | # Your system has configured 'manage_etc_hosts' as True. | ||
540 | 8 | # As a result, if you wish for changes to this file to persist | ||
541 | 9 | # then you will need to either | ||
542 | 10 | # a.) make changes to the master file in | ||
543 | 11 | # /etc/cloud/templates/hosts.opensuse.tmpl | ||
544 | 12 | # b.) change or remove the value of 'manage_etc_hosts' in | ||
545 | 13 | # /etc/cloud/cloud.cfg or cloud-config from user-data | ||
546 | 14 | # | ||
547 | 15 | # The following lines are desirable for IPv4 capable hosts | ||
548 | 16 | 127.0.0.1 localhost | ||
549 | 17 | |||
550 | 18 | # The following lines are desirable for IPv6 capable hosts | ||
551 | 19 | ::1 localhost ipv6-localhost ipv6-loopback | ||
552 | 20 | fe00::0 ipv6-localnet | ||
553 | 21 | |||
554 | 22 | ff00::0 ipv6-mcastprefix | ||
555 | 23 | ff02::1 ipv6-allnodes | ||
556 | 24 | ff02::2 ipv6-allrouters | ||
557 | 25 | ff02::3 ipv6-allhosts | ||
558 | 26 | |||
559 | diff --git a/templates/hosts.suse.tmpl b/templates/hosts.suse.tmpl | |||
560 | index b608269..8e664db 100644 | |||
561 | --- a/templates/hosts.suse.tmpl | |||
562 | +++ b/templates/hosts.suse.tmpl | |||
563 | @@ -13,12 +13,18 @@ you need to add the following to config: | |||
564 | 13 | # /etc/cloud/cloud.cfg or cloud-config from user-data | 13 | # /etc/cloud/cloud.cfg or cloud-config from user-data |
565 | 14 | # | 14 | # |
566 | 15 | # The following lines are desirable for IPv4 capable hosts | 15 | # The following lines are desirable for IPv4 capable hosts |
568 | 16 | 127.0.0.1 localhost | 16 | 127.0.0.1 {{fqdn}} {{hostname}} |
569 | 17 | 127.0.0.1 localhost.localdomain localhost | ||
570 | 18 | 127.0.0.1 localhost4.localdomain4 localhost4 | ||
571 | 17 | 19 | ||
572 | 18 | # The following lines are desirable for IPv6 capable hosts | 20 | # The following lines are desirable for IPv6 capable hosts |
573 | 21 | ::1 {{fqdn}} {{hostname}} | ||
574 | 22 | ::1 localhost.localdomain localhost | ||
575 | 23 | ::1 localhost6.localdomain6 localhost6 | ||
576 | 19 | ::1 localhost ipv6-localhost ipv6-loopback | 24 | ::1 localhost ipv6-localhost ipv6-loopback |
577 | 20 | fe00::0 ipv6-localnet | ||
578 | 21 | 25 | ||
579 | 26 | |||
580 | 27 | fe00::0 ipv6-localnet | ||
581 | 22 | ff00::0 ipv6-mcastprefix | 28 | ff00::0 ipv6-mcastprefix |
582 | 23 | ff02::1 ipv6-allnodes | 29 | ff02::1 ipv6-allnodes |
583 | 24 | ff02::2 ipv6-allrouters | 30 | ff02::2 ipv6-allrouters |
584 | diff --git a/templates/ntp.conf.opensuse.tmpl b/templates/ntp.conf.opensuse.tmpl | |||
585 | 25 | new file mode 100644 | 31 | new file mode 100644 |
586 | index 0000000..f3ab565 | |||
587 | --- /dev/null | |||
588 | +++ b/templates/ntp.conf.opensuse.tmpl | |||
589 | @@ -0,0 +1,88 @@ | |||
590 | 1 | ## template:jinja | ||
591 | 2 | |||
592 | 3 | ## | ||
593 | 4 | ## Radio and modem clocks by convention have addresses in the | ||
594 | 5 | ## form 127.127.t.u, where t is the clock type and u is a unit | ||
595 | 6 | ## number in the range 0-3. | ||
596 | 7 | ## | ||
597 | 8 | ## Most of these clocks require support in the form of a | ||
598 | 9 | ## serial port or special bus peripheral. The particular | ||
599 | 10 | ## device is normally specified by adding a soft link | ||
600 | 11 | ## /dev/device-u to the particular hardware device involved, | ||
601 | 12 | ## where u correspond to the unit number above. | ||
602 | 13 | ## | ||
603 | 14 | ## Generic DCF77 clock on serial port (Conrad DCF77) | ||
604 | 15 | ## Address: 127.127.8.u | ||
605 | 16 | ## Serial Port: /dev/refclock-u | ||
606 | 17 | ## | ||
607 | 18 | ## (create soft link /dev/refclock-0 to the particular ttyS?) | ||
608 | 19 | ## | ||
609 | 20 | # server 127.127.8.0 mode 5 prefer | ||
610 | 21 | |||
611 | 22 | ## | ||
612 | 23 | ## Undisciplined Local Clock. This is a fake driver intended for backup | ||
613 | 24 | ## and when no outside source of synchronized time is available. | ||
614 | 25 | ## | ||
615 | 26 | # server 127.127.1.0 # local clock (LCL) | ||
616 | 27 | # fudge 127.127.1.0 stratum 10 # LCL is unsynchronized | ||
617 | 28 | |||
618 | 29 | ## | ||
619 | 30 | ## Add external Servers using | ||
620 | 31 | ## # rcntpd addserver <yourserver> | ||
621 | 32 | ## The servers will only be added to the currently running instance, not | ||
622 | 33 | ## to /etc/ntp.conf. | ||
623 | 34 | ## | ||
624 | 35 | {% if pools %}# pools | ||
625 | 36 | {% endif %} | ||
626 | 37 | {% for pool in pools -%} | ||
627 | 38 | pool {{pool}} iburst | ||
628 | 39 | {% endfor %} | ||
629 | 40 | {%- if servers %}# servers | ||
630 | 41 | {% endif %} | ||
631 | 42 | {% for server in servers -%} | ||
632 | 43 | server {{server}} iburst | ||
633 | 44 | {% endfor %} | ||
634 | 45 | |||
635 | 46 | # Access control configuration; see /usr/share/doc/packages/ntp/html/accopt.html for | ||
636 | 47 | # details. The web page <http://support.ntp.org/bin/view/Support/AccessRestrictions> | ||
637 | 48 | # might also be helpful. | ||
638 | 49 | # | ||
639 | 50 | # Note that "restrict" applies to both servers and clients, so a configuration | ||
640 | 51 | # that might be intended to block requests from certain clients could also end | ||
641 | 52 | # up blocking replies from your own upstream servers. | ||
642 | 53 | |||
643 | 54 | # By default, exchange time with everybody, but don't allow configuration. | ||
644 | 55 | restrict -4 default notrap nomodify nopeer noquery | ||
645 | 56 | restrict -6 default notrap nomodify nopeer noquery | ||
646 | 57 | |||
647 | 58 | # Local users may interrogate the ntp server more closely. | ||
648 | 59 | restrict 127.0.0.1 | ||
649 | 60 | restrict ::1 | ||
650 | 61 | |||
651 | 62 | # Clients from this (example!) subnet have unlimited access, but only if | ||
652 | 63 | # cryptographically authenticated. | ||
653 | 64 | #restrict 192.168.123.0 mask 255.255.255.0 notrust | ||
654 | 65 | |||
655 | 66 | ## | ||
656 | 67 | ## Miscellaneous stuff | ||
657 | 68 | ## | ||
658 | 69 | |||
659 | 70 | driftfile /var/lib/ntp/drift/ntp.drift # path for drift file | ||
660 | 71 | |||
661 | 72 | logfile /var/log/ntp # alternate log file | ||
662 | 73 | # logconfig =syncstatus + sysevents | ||
663 | 74 | # logconfig =all | ||
664 | 75 | |||
665 | 76 | # statsdir /tmp/ # directory for statistics files | ||
666 | 77 | # filegen peerstats file peerstats type day enable | ||
667 | 78 | # filegen loopstats file loopstats type day enable | ||
668 | 79 | # filegen clockstats file clockstats type day enable | ||
669 | 80 | |||
670 | 81 | # | ||
671 | 82 | # Authentication stuff | ||
672 | 83 | # | ||
673 | 84 | keys /etc/ntp.keys # path for keys file | ||
674 | 85 | trustedkey 1 # define trusted keys | ||
675 | 86 | requestkey 1 # key (7) for accessing server variables | ||
676 | 87 | controlkey 1 # key (6) for accessing server variables | ||
677 | 88 | |||
678 | diff --git a/templates/ntp.conf.sles.tmpl b/templates/ntp.conf.sles.tmpl | |||
679 | index 5c5fc4d..f3ab565 100644 | |||
680 | --- a/templates/ntp.conf.sles.tmpl | |||
681 | +++ b/templates/ntp.conf.sles.tmpl | |||
682 | @@ -1,17 +1,5 @@ | |||
683 | 1 | ## template:jinja | 1 | ## template:jinja |
684 | 2 | 2 | ||
685 | 3 | ################################################################################ | ||
686 | 4 | ## /etc/ntp.conf | ||
687 | 5 | ## | ||
688 | 6 | ## Sample NTP configuration file. | ||
689 | 7 | ## See package 'ntp-doc' for documentation, Mini-HOWTO and FAQ. | ||
690 | 8 | ## Copyright (c) 1998 S.u.S.E. GmbH Fuerth, Germany. | ||
691 | 9 | ## | ||
692 | 10 | ## Author: Michael Andres, <ma@suse.de> | ||
693 | 11 | ## Michael Skibbe, <mskibbe@suse.de> | ||
694 | 12 | ## | ||
695 | 13 | ################################################################################ | ||
696 | 14 | |||
697 | 15 | ## | 3 | ## |
698 | 16 | ## Radio and modem clocks by convention have addresses in the | 4 | ## Radio and modem clocks by convention have addresses in the |
699 | 17 | ## form 127.127.t.u, where t is the clock type and u is a unit | 5 | ## form 127.127.t.u, where t is the clock type and u is a unit |
700 | diff --git a/tests/cloud_tests/collect.py b/tests/cloud_tests/collect.py | |||
701 | index 4a2422e..71ee764 100644 | |||
702 | --- a/tests/cloud_tests/collect.py | |||
703 | +++ b/tests/cloud_tests/collect.py | |||
704 | @@ -22,11 +22,21 @@ def collect_script(instance, base_dir, script, script_name): | |||
705 | 22 | """ | 22 | """ |
706 | 23 | LOG.debug('running collect script: %s', script_name) | 23 | LOG.debug('running collect script: %s', script_name) |
707 | 24 | (out, err, exit) = instance.run_script( | 24 | (out, err, exit) = instance.run_script( |
709 | 25 | script, rcs=range(0, 256), | 25 | script.encode(), rcs=False, |
710 | 26 | description='collect: {}'.format(script_name)) | 26 | description='collect: {}'.format(script_name)) |
711 | 27 | c_util.write_file(os.path.join(base_dir, script_name), out) | 27 | c_util.write_file(os.path.join(base_dir, script_name), out) |
712 | 28 | 28 | ||
713 | 29 | 29 | ||
714 | 30 | def collect_console(instance, base_dir): | ||
715 | 31 | LOG.debug('getting console log') | ||
716 | 32 | try: | ||
717 | 33 | data = instance.console_log() | ||
718 | 34 | except NotImplementedError as e: | ||
719 | 35 | data = 'Not Implemented: %s' % e | ||
720 | 36 | with open(os.path.join(base_dir, 'console.log'), "wb") as fp: | ||
721 | 37 | fp.write(data) | ||
722 | 38 | |||
723 | 39 | |||
724 | 30 | def collect_test_data(args, snapshot, os_name, test_name): | 40 | def collect_test_data(args, snapshot, os_name, test_name): |
725 | 31 | """Collect data for test case. | 41 | """Collect data for test case. |
726 | 32 | 42 | ||
727 | @@ -79,8 +89,12 @@ def collect_test_data(args, snapshot, os_name, test_name): | |||
728 | 79 | test_output_dir, script, script_name)) | 89 | test_output_dir, script, script_name)) |
729 | 80 | for script_name, script in test_scripts.items()] | 90 | for script_name, script in test_scripts.items()] |
730 | 81 | 91 | ||
731 | 92 | console_log = partial( | ||
732 | 93 | run_single, 'collect console', | ||
733 | 94 | partial(collect_console, instance, test_output_dir)) | ||
734 | 95 | |||
735 | 82 | res = run_stage('collect for test: {}'.format(test_name), | 96 | res = run_stage('collect for test: {}'.format(test_name), |
737 | 83 | [start_call] + collect_calls) | 97 | [start_call] + collect_calls + [console_log]) |
738 | 84 | 98 | ||
739 | 85 | return res | 99 | return res |
740 | 86 | 100 | ||
741 | diff --git a/tests/cloud_tests/images/base.py b/tests/cloud_tests/images/base.py | |||
742 | index 0a1e056..d503108 100644 | |||
743 | --- a/tests/cloud_tests/images/base.py | |||
744 | +++ b/tests/cloud_tests/images/base.py | |||
745 | @@ -2,8 +2,10 @@ | |||
746 | 2 | 2 | ||
747 | 3 | """Base class for images.""" | 3 | """Base class for images.""" |
748 | 4 | 4 | ||
749 | 5 | from ..util import TargetBase | ||
750 | 5 | 6 | ||
752 | 6 | class Image(object): | 7 | |
753 | 8 | class Image(TargetBase): | ||
754 | 7 | """Base class for images.""" | 9 | """Base class for images.""" |
755 | 8 | 10 | ||
756 | 9 | platform_name = None | 11 | platform_name = None |
757 | @@ -43,21 +45,6 @@ class Image(object): | |||
758 | 43 | # NOTE: more sophisticated options may be requied at some point | 45 | # NOTE: more sophisticated options may be requied at some point |
759 | 44 | return self.config.get('setup_overrides', {}) | 46 | return self.config.get('setup_overrides', {}) |
760 | 45 | 47 | ||
761 | 46 | def execute(self, *args, **kwargs): | ||
762 | 47 | """Execute command in image, modifying image.""" | ||
763 | 48 | raise NotImplementedError | ||
764 | 49 | |||
765 | 50 | def push_file(self, local_path, remote_path): | ||
766 | 51 | """Copy file at 'local_path' to instance at 'remote_path'.""" | ||
767 | 52 | raise NotImplementedError | ||
768 | 53 | |||
769 | 54 | def run_script(self, *args, **kwargs): | ||
770 | 55 | """Run script in image, modifying image. | ||
771 | 56 | |||
772 | 57 | @return_value: script output | ||
773 | 58 | """ | ||
774 | 59 | raise NotImplementedError | ||
775 | 60 | |||
776 | 61 | def snapshot(self): | 48 | def snapshot(self): |
777 | 62 | """Create snapshot of image, block until done.""" | 49 | """Create snapshot of image, block until done.""" |
778 | 63 | raise NotImplementedError | 50 | raise NotImplementedError |
779 | diff --git a/tests/cloud_tests/images/lxd.py b/tests/cloud_tests/images/lxd.py | |||
780 | index fd4e93c..5caeba4 100644 | |||
781 | --- a/tests/cloud_tests/images/lxd.py | |||
782 | +++ b/tests/cloud_tests/images/lxd.py | |||
783 | @@ -24,7 +24,7 @@ class LXDImage(base.Image): | |||
784 | 24 | @param config: image configuration | 24 | @param config: image configuration |
785 | 25 | """ | 25 | """ |
786 | 26 | self.modified = False | 26 | self.modified = False |
788 | 27 | self._instance = None | 27 | self._img_instance = None |
789 | 28 | self._pylxd_image = None | 28 | self._pylxd_image = None |
790 | 29 | self.pylxd_image = pylxd_image | 29 | self.pylxd_image = pylxd_image |
791 | 30 | super(LXDImage, self).__init__(platform, config) | 30 | super(LXDImage, self).__init__(platform, config) |
792 | @@ -38,9 +38,9 @@ class LXDImage(base.Image): | |||
793 | 38 | 38 | ||
794 | 39 | @pylxd_image.setter | 39 | @pylxd_image.setter |
795 | 40 | def pylxd_image(self, pylxd_image): | 40 | def pylxd_image(self, pylxd_image): |
797 | 41 | if self._instance: | 41 | if self._img_instance: |
798 | 42 | self._instance.destroy() | 42 | self._instance.destroy() |
800 | 43 | self._instance = None | 43 | self._img_instance = None |
801 | 44 | if (self._pylxd_image and | 44 | if (self._pylxd_image and |
802 | 45 | (self._pylxd_image is not pylxd_image) and | 45 | (self._pylxd_image is not pylxd_image) and |
803 | 46 | (not self.config.get('cache_base_image') or self.modified)): | 46 | (not self.config.get('cache_base_image') or self.modified)): |
804 | @@ -49,15 +49,19 @@ class LXDImage(base.Image): | |||
805 | 49 | self._pylxd_image = pylxd_image | 49 | self._pylxd_image = pylxd_image |
806 | 50 | 50 | ||
807 | 51 | @property | 51 | @property |
812 | 52 | def instance(self): | 52 | def _instance(self): |
813 | 53 | """Property function.""" | 53 | """Internal use only, returns a instance |
814 | 54 | if not self._instance: | 54 | |
815 | 55 | self._instance = self.platform.launch_container( | 55 | This starts an lxc instance from the image, so it is "dirty". |
816 | 56 | Better would be some way to modify this "at rest". | ||
817 | 57 | lxc-pstart would be an option.""" | ||
818 | 58 | if not self._img_instance: | ||
819 | 59 | self._img_instance = self.platform.launch_container( | ||
820 | 56 | self.properties, self.config, self.features, | 60 | self.properties, self.config, self.features, |
821 | 57 | use_desc='image-modification', image_desc=str(self), | 61 | use_desc='image-modification', image_desc=str(self), |
822 | 58 | image=self.pylxd_image.fingerprint) | 62 | image=self.pylxd_image.fingerprint) |
825 | 59 | self._instance.start() | 63 | self._img_instance.start() |
826 | 60 | return self._instance | 64 | return self._img_instance |
827 | 61 | 65 | ||
828 | 62 | @property | 66 | @property |
829 | 63 | def properties(self): | 67 | def properties(self): |
830 | @@ -144,20 +148,20 @@ class LXDImage(base.Image): | |||
831 | 144 | shutil.rmtree(export_dir) | 148 | shutil.rmtree(export_dir) |
832 | 145 | shutil.rmtree(extract_dir) | 149 | shutil.rmtree(extract_dir) |
833 | 146 | 150 | ||
835 | 147 | def execute(self, *args, **kwargs): | 151 | def _execute(self, *args, **kwargs): |
836 | 148 | """Execute command in image, modifying image.""" | 152 | """Execute command in image, modifying image.""" |
838 | 149 | return self.instance.execute(*args, **kwargs) | 153 | return self._instance._execute(*args, **kwargs) |
839 | 150 | 154 | ||
840 | 151 | def push_file(self, local_path, remote_path): | 155 | def push_file(self, local_path, remote_path): |
841 | 152 | """Copy file at 'local_path' to instance at 'remote_path'.""" | 156 | """Copy file at 'local_path' to instance at 'remote_path'.""" |
843 | 153 | return self.instance.push_file(local_path, remote_path) | 157 | return self._instance.push_file(local_path, remote_path) |
844 | 154 | 158 | ||
845 | 155 | def run_script(self, *args, **kwargs): | 159 | def run_script(self, *args, **kwargs): |
846 | 156 | """Run script in image, modifying image. | 160 | """Run script in image, modifying image. |
847 | 157 | 161 | ||
848 | 158 | @return_value: script output | 162 | @return_value: script output |
849 | 159 | """ | 163 | """ |
851 | 160 | return self.instance.run_script(*args, **kwargs) | 164 | return self._instance.run_script(*args, **kwargs) |
852 | 161 | 165 | ||
853 | 162 | def snapshot(self): | 166 | def snapshot(self): |
854 | 163 | """Create snapshot of image, block until done.""" | 167 | """Create snapshot of image, block until done.""" |
855 | @@ -169,7 +173,7 @@ class LXDImage(base.Image): | |||
856 | 169 | # clone current instance | 173 | # clone current instance |
857 | 170 | instance = self.platform.launch_container( | 174 | instance = self.platform.launch_container( |
858 | 171 | self.properties, self.config, self.features, | 175 | self.properties, self.config, self.features, |
860 | 172 | container=self.instance.name, image_desc=str(self), | 176 | container=self._instance.name, image_desc=str(self), |
861 | 173 | use_desc='snapshot', container_config=conf) | 177 | use_desc='snapshot', container_config=conf) |
862 | 174 | # wait for cloud-init before boot_clean_script is run to ensure | 178 | # wait for cloud-init before boot_clean_script is run to ensure |
863 | 175 | # /var/lib/cloud is removed cleanly | 179 | # /var/lib/cloud is removed cleanly |
864 | diff --git a/tests/cloud_tests/images/nocloudkvm.py b/tests/cloud_tests/images/nocloudkvm.py | |||
865 | index a7af0e5..1e7962c 100644 | |||
866 | --- a/tests/cloud_tests/images/nocloudkvm.py | |||
867 | +++ b/tests/cloud_tests/images/nocloudkvm.py | |||
868 | @@ -2,6 +2,8 @@ | |||
869 | 2 | 2 | ||
870 | 3 | """NoCloud KVM Image Base Class.""" | 3 | """NoCloud KVM Image Base Class.""" |
871 | 4 | 4 | ||
872 | 5 | from cloudinit import util as c_util | ||
873 | 6 | |||
874 | 5 | from tests.cloud_tests.images import base | 7 | from tests.cloud_tests.images import base |
875 | 6 | from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot | 8 | from tests.cloud_tests.snapshots import nocloudkvm as nocloud_kvm_snapshot |
876 | 7 | 9 | ||
877 | @@ -19,24 +21,11 @@ class NoCloudKVMImage(base.Image): | |||
878 | 19 | @param img_path: path to the image | 21 | @param img_path: path to the image |
879 | 20 | """ | 22 | """ |
880 | 21 | self.modified = False | 23 | self.modified = False |
881 | 22 | self._instance = None | ||
882 | 23 | self._img_path = img_path | 24 | self._img_path = img_path |
883 | 24 | 25 | ||
884 | 25 | super(NoCloudKVMImage, self).__init__(platform, config) | 26 | super(NoCloudKVMImage, self).__init__(platform, config) |
885 | 26 | 27 | ||
886 | 27 | @property | 28 | @property |
887 | 28 | def instance(self): | ||
888 | 29 | """Returns an instance of an image.""" | ||
889 | 30 | if not self._instance: | ||
890 | 31 | if not self._img_path: | ||
891 | 32 | raise RuntimeError() | ||
892 | 33 | |||
893 | 34 | self._instance = self.platform.create_image( | ||
894 | 35 | self.properties, self.config, self.features, self._img_path, | ||
895 | 36 | image_desc=str(self), use_desc='image-modification') | ||
896 | 37 | return self._instance | ||
897 | 38 | |||
898 | 39 | @property | ||
899 | 40 | def properties(self): | 29 | def properties(self): |
900 | 41 | """Dictionary containing: 'arch', 'os', 'version', 'release'.""" | 30 | """Dictionary containing: 'arch', 'os', 'version', 'release'.""" |
901 | 42 | return { | 31 | return { |
902 | @@ -46,20 +35,26 @@ class NoCloudKVMImage(base.Image): | |||
903 | 46 | 'version': self.config['version'], | 35 | 'version': self.config['version'], |
904 | 47 | } | 36 | } |
905 | 48 | 37 | ||
907 | 49 | def execute(self, *args, **kwargs): | 38 | def _execute(self, command, stdin=None, env=None): |
908 | 50 | """Execute command in image, modifying image.""" | 39 | """Execute command in image, modifying image.""" |
910 | 51 | return self.instance.execute(*args, **kwargs) | 40 | return self.mount_image_callback(command, stdin=stdin, env=env) |
911 | 52 | 41 | ||
915 | 53 | def push_file(self, local_path, remote_path): | 42 | def mount_image_callback(self, command, stdin=None, env=None): |
916 | 54 | """Copy file at 'local_path' to instance at 'remote_path'.""" | 43 | """Run mount-image-callback.""" |
914 | 55 | return self.instance.push_file(local_path, remote_path) | ||
917 | 56 | 44 | ||
920 | 57 | def run_script(self, *args, **kwargs): | 45 | env_args = [] |
921 | 58 | """Run script in image, modifying image. | 46 | if env: |
922 | 47 | env_args = ['env'] + ["%s=%s" for k, v in env.items()] | ||
923 | 59 | 48 | ||
927 | 60 | @return_value: script output | 49 | mic_chroot = ['sudo', 'mount-image-callback', '--system-mounts', |
928 | 61 | """ | 50 | '--system-resolvconf', self._img_path, |
929 | 62 | return self.instance.run_script(*args, **kwargs) | 51 | '--', 'chroot', '_MOUNTPOINT_'] |
930 | 52 | try: | ||
931 | 53 | out, err = c_util.subp(mic_chroot + env_args + list(command), | ||
932 | 54 | data=stdin, decode=False) | ||
933 | 55 | return (out, err, 0) | ||
934 | 56 | except c_util.ProcessExecutionError as e: | ||
935 | 57 | return (e.stdout, e.stderr, e.exit_code) | ||
936 | 63 | 58 | ||
937 | 64 | def snapshot(self): | 59 | def snapshot(self): |
938 | 65 | """Create snapshot of image, block until done.""" | 60 | """Create snapshot of image, block until done.""" |
939 | @@ -82,7 +77,6 @@ class NoCloudKVMImage(base.Image): | |||
940 | 82 | framework decide whether to keep or destroy everything. | 77 | framework decide whether to keep or destroy everything. |
941 | 83 | """ | 78 | """ |
942 | 84 | self._img_path = None | 79 | self._img_path = None |
943 | 85 | self._instance.destroy() | ||
944 | 86 | super(NoCloudKVMImage, self).destroy() | 80 | super(NoCloudKVMImage, self).destroy() |
945 | 87 | 81 | ||
946 | 88 | # vi: ts=4 expandtab | 82 | # vi: ts=4 expandtab |
947 | diff --git a/tests/cloud_tests/instances/base.py b/tests/cloud_tests/instances/base.py | |||
948 | index 9bdda60..8c59d62 100644 | |||
949 | --- a/tests/cloud_tests/instances/base.py | |||
950 | +++ b/tests/cloud_tests/instances/base.py | |||
951 | @@ -2,8 +2,10 @@ | |||
952 | 2 | 2 | ||
953 | 3 | """Base instance.""" | 3 | """Base instance.""" |
954 | 4 | 4 | ||
955 | 5 | from ..util import TargetBase | ||
956 | 5 | 6 | ||
958 | 6 | class Instance(object): | 7 | |
959 | 8 | class Instance(TargetBase): | ||
960 | 7 | """Base instance object.""" | 9 | """Base instance object.""" |
961 | 8 | 10 | ||
962 | 9 | platform_name = None | 11 | platform_name = None |
963 | @@ -22,82 +24,7 @@ class Instance(object): | |||
964 | 22 | self.properties = properties | 24 | self.properties = properties |
965 | 23 | self.config = config | 25 | self.config = config |
966 | 24 | self.features = features | 26 | self.features = features |
1043 | 25 | 27 | self._tmp_count = 0 | |
968 | 26 | def execute(self, command, stdout=None, stderr=None, env=None, | ||
969 | 27 | rcs=None, description=None): | ||
970 | 28 | """Execute command in instance, recording output, error and exit code. | ||
971 | 29 | |||
972 | 30 | Assumes functional networking and execution as root with the | ||
973 | 31 | target filesystem being available at /. | ||
974 | 32 | |||
975 | 33 | @param command: the command to execute as root inside the image | ||
976 | 34 | if command is a string, then it will be executed as: | ||
977 | 35 | ['sh', '-c', command] | ||
978 | 36 | @param stdout, stderr: file handles to write output and error to | ||
979 | 37 | @param env: environment variables | ||
980 | 38 | @param rcs: allowed return codes from command | ||
981 | 39 | @param description: purpose of command | ||
982 | 40 | @return_value: tuple containing stdout data, stderr data, exit code | ||
983 | 41 | """ | ||
984 | 42 | raise NotImplementedError | ||
985 | 43 | |||
986 | 44 | def read_data(self, remote_path, decode=False): | ||
987 | 45 | """Read data from instance filesystem. | ||
988 | 46 | |||
989 | 47 | @param remote_path: path in instance | ||
990 | 48 | @param decode: return as string | ||
991 | 49 | @return_value: data as str or bytes | ||
992 | 50 | """ | ||
993 | 51 | raise NotImplementedError | ||
994 | 52 | |||
995 | 53 | def write_data(self, remote_path, data): | ||
996 | 54 | """Write data to instance filesystem. | ||
997 | 55 | |||
998 | 56 | @param remote_path: path in instance | ||
999 | 57 | @param data: data to write, either str or bytes | ||
1000 | 58 | """ | ||
1001 | 59 | raise NotImplementedError | ||
1002 | 60 | |||
1003 | 61 | def pull_file(self, remote_path, local_path): | ||
1004 | 62 | """Copy file at 'remote_path', from instance to 'local_path'. | ||
1005 | 63 | |||
1006 | 64 | @param remote_path: path on remote instance | ||
1007 | 65 | @param local_path: path on local instance | ||
1008 | 66 | """ | ||
1009 | 67 | with open(local_path, 'wb') as fp: | ||
1010 | 68 | fp.write(self.read_data(remote_path)) | ||
1011 | 69 | |||
1012 | 70 | def push_file(self, local_path, remote_path): | ||
1013 | 71 | """Copy file at 'local_path' to instance at 'remote_path'. | ||
1014 | 72 | |||
1015 | 73 | @param local_path: path on local instance | ||
1016 | 74 | @param remote_path: path on remote instance | ||
1017 | 75 | """ | ||
1018 | 76 | with open(local_path, 'rb') as fp: | ||
1019 | 77 | self.write_data(remote_path, fp.read()) | ||
1020 | 78 | |||
1021 | 79 | def run_script(self, script, rcs=None, description=None): | ||
1022 | 80 | """Run script in target and return stdout. | ||
1023 | 81 | |||
1024 | 82 | @param script: script contents | ||
1025 | 83 | @param rcs: allowed return codes from script | ||
1026 | 84 | @param description: purpose of script | ||
1027 | 85 | @return_value: stdout from script | ||
1028 | 86 | """ | ||
1029 | 87 | script_path = self.tmpfile() | ||
1030 | 88 | try: | ||
1031 | 89 | self.write_data(script_path, script) | ||
1032 | 90 | return self.execute( | ||
1033 | 91 | ['/bin/bash', script_path], rcs=rcs, description=description) | ||
1034 | 92 | finally: | ||
1035 | 93 | self.execute(['rm', '-f', script_path], rcs=rcs) | ||
1036 | 94 | |||
1037 | 95 | def tmpfile(self): | ||
1038 | 96 | """Get a tmp file in the target. | ||
1039 | 97 | |||
1040 | 98 | @return_value: path to new file in target | ||
1041 | 99 | """ | ||
1042 | 100 | return self.execute(['mktemp'])[0].strip() | ||
1044 | 101 | 28 | ||
1045 | 102 | def console_log(self): | 29 | def console_log(self): |
1046 | 103 | """Instance console. | 30 | """Instance console. |
1047 | diff --git a/tests/cloud_tests/instances/lxd.py b/tests/cloud_tests/instances/lxd.py | |||
1048 | index a43918c..3b035d8 100644 | |||
1049 | --- a/tests/cloud_tests/instances/lxd.py | |||
1050 | +++ b/tests/cloud_tests/instances/lxd.py | |||
1051 | @@ -2,8 +2,11 @@ | |||
1052 | 2 | 2 | ||
1053 | 3 | """Base LXD instance.""" | 3 | """Base LXD instance.""" |
1054 | 4 | 4 | ||
1057 | 5 | from tests.cloud_tests.instances import base | 5 | from . import base |
1058 | 6 | from tests.cloud_tests import util | 6 | |
1059 | 7 | import os | ||
1060 | 8 | import shutil | ||
1061 | 9 | from tempfile import mkdtemp | ||
1062 | 7 | 10 | ||
1063 | 8 | 11 | ||
1064 | 9 | class LXDInstance(base.Instance): | 12 | class LXDInstance(base.Instance): |
1065 | @@ -24,6 +27,8 @@ class LXDInstance(base.Instance): | |||
1066 | 24 | self._pylxd_container = pylxd_container | 27 | self._pylxd_container = pylxd_container |
1067 | 25 | super(LXDInstance, self).__init__( | 28 | super(LXDInstance, self).__init__( |
1068 | 26 | platform, name, properties, config, features) | 29 | platform, name, properties, config, features) |
1069 | 30 | self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name)) | ||
1070 | 31 | self._setup_console_log() | ||
1071 | 27 | 32 | ||
1072 | 28 | @property | 33 | @property |
1073 | 29 | def pylxd_container(self): | 34 | def pylxd_container(self): |
1074 | @@ -31,74 +36,69 @@ class LXDInstance(base.Instance): | |||
1075 | 31 | self._pylxd_container.sync() | 36 | self._pylxd_container.sync() |
1076 | 32 | return self._pylxd_container | 37 | return self._pylxd_container |
1077 | 33 | 38 | ||
1095 | 34 | def execute(self, command, stdout=None, stderr=None, env=None, | 39 | def _setup_console_log(self): |
1096 | 35 | rcs=None, description=None): | 40 | logf = os.path.join(self.tmpd, "console.log") |
1097 | 36 | """Execute command in instance, recording output, error and exit code. | 41 | |
1098 | 37 | 42 | # doing this ensures we can read it. Otherwise it ends up root:root. | |
1099 | 38 | Assumes functional networking and execution as root with the | 43 | with open(logf, "w") as fp: |
1100 | 39 | target filesystem being available at /. | 44 | fp.write("# %s\n" % self.name) |
1101 | 40 | 45 | ||
1102 | 41 | @param command: the command to execute as root inside the image | 46 | cfg = "lxc.console.logfile=%s" % logf |
1103 | 42 | if command is a string, then it will be executed as: | 47 | orig = self._pylxd_container.config.get('raw.lxc', "") |
1104 | 43 | ['sh', '-c', command] | 48 | if orig: |
1105 | 44 | @param stdout: file handler to write output | 49 | orig += "\n" |
1106 | 45 | @param stderr: file handler to write error | 50 | self._pylxd_container.config['raw.lxc'] = orig + cfg |
1107 | 46 | @param env: environment variables | 51 | self._pylxd_container.save() |
1108 | 47 | @param rcs: allowed return codes from command | 52 | self._console_log_file = logf |
1109 | 48 | @param description: purpose of command | 53 | |
1110 | 49 | @return_value: tuple containing stdout data, stderr data, exit code | 54 | def _execute(self, command, stdin=None, env=None): |
1094 | 50 | """ | ||
1111 | 51 | if env is None: | 55 | if env is None: |
1112 | 52 | env = {} | 56 | env = {} |
1113 | 53 | 57 | ||
1116 | 54 | if isinstance(command, str): | 58 | if stdin is not None: |
1117 | 55 | command = ['sh', '-c', command] | 59 | # pylxd does not support input to execute. |
1118 | 60 | # https://github.com/lxc/pylxd/issues/244 | ||
1119 | 61 | # | ||
1120 | 62 | # The solution here is write a tmp file in the container | ||
1121 | 63 | # and then execute a shell that sets it standard in to | ||
1122 | 64 | # be from that file, removes it, and calls the comand. | ||
1123 | 65 | tmpf = self.tmpfile() | ||
1124 | 66 | self.write_data(tmpf, stdin) | ||
1125 | 67 | ncmd = 'exec <"{tmpf}"; rm -f "{tmpf}"; exec "$@"' | ||
1126 | 68 | command = (['sh', '-c', ncmd.format(tmpf=tmpf), 'stdinhack'] + | ||
1127 | 69 | list(command)) | ||
1128 | 56 | 70 | ||
1129 | 57 | # ensure instance is running and execute the command | 71 | # ensure instance is running and execute the command |
1130 | 58 | self.start() | 72 | self.start() |
1131 | 73 | # execute returns a ContainerExecuteResult, named tuple | ||
1132 | 74 | # (exit_code, stdout, stderr) | ||
1133 | 59 | res = self.pylxd_container.execute(command, environment=env) | 75 | res = self.pylxd_container.execute(command, environment=env) |
1134 | 60 | 76 | ||
1135 | 61 | # get out, exit and err from pylxd return | 77 | # get out, exit and err from pylxd return |
1141 | 62 | if hasattr(res, 'exit_code'): | 78 | if not hasattr(res, 'exit_code'): |
1137 | 63 | # pylxd 2.2 returns ContainerExecuteResult, named tuple of | ||
1138 | 64 | # (exit_code, out, err) | ||
1139 | 65 | (exit, out, err) = res | ||
1140 | 66 | else: | ||
1142 | 67 | # pylxd 2.1.3 and earlier only return out and err, no exit | 79 | # pylxd 2.1.3 and earlier only return out and err, no exit |
1159 | 68 | # LOG.warning('using pylxd version < 2.2') | 80 | raise RuntimeError( |
1160 | 69 | (out, err) = res | 81 | "No 'exit_code' in pylxd.container.execute return.\n" |
1161 | 70 | exit = 0 | 82 | "pylxd > 2.2 is required.") |
1146 | 71 | |||
1147 | 72 | # write data to file descriptors if needed | ||
1148 | 73 | if stdout: | ||
1149 | 74 | stdout.write(out) | ||
1150 | 75 | if stderr: | ||
1151 | 76 | stderr.write(err) | ||
1152 | 77 | |||
1153 | 78 | # if the command exited with a code not allowed in rcs, then fail | ||
1154 | 79 | if exit not in (rcs if rcs else (0,)): | ||
1155 | 80 | error_desc = ('Failed command to: {}'.format(description) | ||
1156 | 81 | if description else None) | ||
1157 | 82 | raise util.InTargetExecuteError( | ||
1158 | 83 | out, err, exit, command, self.name, error_desc) | ||
1162 | 84 | 83 | ||
1164 | 85 | return (out, err, exit) | 84 | return res.stdout, res.stderr, res.exit_code |
1165 | 86 | 85 | ||
1166 | 87 | def read_data(self, remote_path, decode=False): | 86 | def read_data(self, remote_path, decode=False): |
1167 | 88 | """Read data from instance filesystem. | 87 | """Read data from instance filesystem. |
1168 | 89 | 88 | ||
1169 | 90 | @param remote_path: path in instance | 89 | @param remote_path: path in instance |
1172 | 91 | @param decode: return as string | 90 | @param decode: decode data before returning. |
1173 | 92 | @return_value: data as str or bytes | 91 | @return_value: content of remote_path as bytes if 'decode' is False, |
1174 | 92 | and as string if 'decode' is True. | ||
1175 | 93 | """ | 93 | """ |
1176 | 94 | data = self.pylxd_container.files.get(remote_path) | 94 | data = self.pylxd_container.files.get(remote_path) |
1178 | 95 | return data.decode() if decode and isinstance(data, bytes) else data | 95 | return data.decode() if decode else data |
1179 | 96 | 96 | ||
1180 | 97 | def write_data(self, remote_path, data): | 97 | def write_data(self, remote_path, data): |
1181 | 98 | """Write data to instance filesystem. | 98 | """Write data to instance filesystem. |
1182 | 99 | 99 | ||
1183 | 100 | @param remote_path: path in instance | 100 | @param remote_path: path in instance |
1185 | 101 | @param data: data to write, either str or bytes | 101 | @param data: data to write in bytes |
1186 | 102 | """ | 102 | """ |
1187 | 103 | self.pylxd_container.files.put(remote_path, data) | 103 | self.pylxd_container.files.put(remote_path, data) |
1188 | 104 | 104 | ||
1189 | @@ -107,7 +107,14 @@ class LXDInstance(base.Instance): | |||
1190 | 107 | 107 | ||
1191 | 108 | @return_value: bytes of this instance’s console | 108 | @return_value: bytes of this instance’s console |
1192 | 109 | """ | 109 | """ |
1194 | 110 | raise NotImplementedError | 110 | if not os.path.exists(self._console_log_file): |
1195 | 111 | raise NotImplementedError( | ||
1196 | 112 | "Console log '%s' does not exist. If this is a remote " | ||
1197 | 113 | "lxc, then this is really NotImplementedError. If it is " | ||
1198 | 114 | "A local lxc, then this is a RuntimeError." | ||
1199 | 115 | "https://github.com/lxc/lxd/issues/1129") | ||
1200 | 116 | with open(self._console_log_file, "rb") as fp: | ||
1201 | 117 | return fp.read() | ||
1202 | 111 | 118 | ||
1203 | 112 | def reboot(self, wait=True): | 119 | def reboot(self, wait=True): |
1204 | 113 | """Reboot instance.""" | 120 | """Reboot instance.""" |
1205 | @@ -144,6 +151,7 @@ class LXDInstance(base.Instance): | |||
1206 | 144 | if self.platform.container_exists(self.name): | 151 | if self.platform.container_exists(self.name): |
1207 | 145 | raise OSError('container {} was not properly removed' | 152 | raise OSError('container {} was not properly removed' |
1208 | 146 | .format(self.name)) | 153 | .format(self.name)) |
1209 | 154 | shutil.rmtree(self.tmpd) | ||
1210 | 147 | super(LXDInstance, self).destroy() | 155 | super(LXDInstance, self).destroy() |
1211 | 148 | 156 | ||
1212 | 149 | # vi: ts=4 expandtab | 157 | # vi: ts=4 expandtab |
1213 | diff --git a/tests/cloud_tests/instances/nocloudkvm.py b/tests/cloud_tests/instances/nocloudkvm.py | |||
1214 | index 8a0e531..cc82580 100644 | |||
1215 | --- a/tests/cloud_tests/instances/nocloudkvm.py | |||
1216 | +++ b/tests/cloud_tests/instances/nocloudkvm.py | |||
1217 | @@ -12,11 +12,18 @@ from cloudinit import util as c_util | |||
1218 | 12 | from tests.cloud_tests.instances import base | 12 | from tests.cloud_tests.instances import base |
1219 | 13 | from tests.cloud_tests import util | 13 | from tests.cloud_tests import util |
1220 | 14 | 14 | ||
1221 | 15 | # This domain contains reverse lookups for hostnames that are used. | ||
1222 | 16 | # The primary reason is so sudo will return quickly when it attempts | ||
1223 | 17 | # to look up the hostname. i9n is just short for 'integration'. | ||
1224 | 18 | # see also bug 1730744 for why we had to do this. | ||
1225 | 19 | CI_DOMAIN = "i9n.cloud-init.io" | ||
1226 | 20 | |||
1227 | 15 | 21 | ||
1228 | 16 | class NoCloudKVMInstance(base.Instance): | 22 | class NoCloudKVMInstance(base.Instance): |
1229 | 17 | """NoCloud KVM backed instance.""" | 23 | """NoCloud KVM backed instance.""" |
1230 | 18 | 24 | ||
1231 | 19 | platform_name = "nocloud-kvm" | 25 | platform_name = "nocloud-kvm" |
1232 | 26 | _ssh_client = None | ||
1233 | 20 | 27 | ||
1234 | 21 | def __init__(self, platform, name, properties, config, features, | 28 | def __init__(self, platform, name, properties, config, features, |
1235 | 22 | user_data, meta_data): | 29 | user_data, meta_data): |
1236 | @@ -35,6 +42,7 @@ class NoCloudKVMInstance(base.Instance): | |||
1237 | 35 | self.ssh_port = None | 42 | self.ssh_port = None |
1238 | 36 | self.pid = None | 43 | self.pid = None |
1239 | 37 | self.pid_file = None | 44 | self.pid_file = None |
1240 | 45 | self.console_file = None | ||
1241 | 38 | 46 | ||
1242 | 39 | super(NoCloudKVMInstance, self).__init__( | 47 | super(NoCloudKVMInstance, self).__init__( |
1243 | 40 | platform, name, properties, config, features) | 48 | platform, name, properties, config, features) |
1244 | @@ -51,43 +59,18 @@ class NoCloudKVMInstance(base.Instance): | |||
1245 | 51 | os.remove(self.pid_file) | 59 | os.remove(self.pid_file) |
1246 | 52 | 60 | ||
1247 | 53 | self.pid = None | 61 | self.pid = None |
1271 | 54 | super(NoCloudKVMInstance, self).destroy() | 62 | if self._ssh_client: |
1272 | 55 | 63 | self._ssh_client.close() | |
1273 | 56 | def execute(self, command, stdout=None, stderr=None, env=None, | 64 | self._ssh_client = None |
1251 | 57 | rcs=None, description=None): | ||
1252 | 58 | """Execute command in instance. | ||
1253 | 59 | |||
1254 | 60 | Assumes functional networking and execution as root with the | ||
1255 | 61 | target filesystem being available at /. | ||
1256 | 62 | |||
1257 | 63 | @param command: the command to execute as root inside the image | ||
1258 | 64 | if command is a string, then it will be executed as: | ||
1259 | 65 | ['sh', '-c', command] | ||
1260 | 66 | @param stdout, stderr: file handles to write output and error to | ||
1261 | 67 | @param env: environment variables | ||
1262 | 68 | @param rcs: allowed return codes from command | ||
1263 | 69 | @param description: purpose of command | ||
1264 | 70 | @return_value: tuple containing stdout data, stderr data, exit code | ||
1265 | 71 | """ | ||
1266 | 72 | if env is None: | ||
1267 | 73 | env = {} | ||
1268 | 74 | |||
1269 | 75 | if isinstance(command, str): | ||
1270 | 76 | command = ['sh', '-c', command] | ||
1274 | 77 | 65 | ||
1279 | 78 | if self.pid: | 66 | super(NoCloudKVMInstance, self).destroy() |
1276 | 79 | return self.ssh(command) | ||
1277 | 80 | else: | ||
1278 | 81 | return self.mount_image_callback(command) + (0,) | ||
1280 | 82 | 67 | ||
1287 | 83 | def mount_image_callback(self, cmd): | 68 | def _execute(self, command, stdin=None, env=None): |
1288 | 84 | """Run mount-image-callback.""" | 69 | env_args = [] |
1289 | 85 | out, err = c_util.subp(['sudo', 'mount-image-callback', | 70 | if env: |
1290 | 86 | '--system-mounts', '--system-resolvconf', | 71 | env_args = ['env'] + ["%s=%s" for k, v in env.items()] |
1285 | 87 | self.name, '--', 'chroot', | ||
1286 | 88 | '_MOUNTPOINT_'] + cmd) | ||
1291 | 89 | 72 | ||
1293 | 90 | return out, err | 73 | return self.ssh(['sudo'] + env_args + list(command), stdin=stdin) |
1294 | 91 | 74 | ||
1295 | 92 | def generate_seed(self, tmpdir): | 75 | def generate_seed(self, tmpdir): |
1296 | 93 | """Generate nocloud seed from user-data""" | 76 | """Generate nocloud seed from user-data""" |
1297 | @@ -109,57 +92,31 @@ class NoCloudKVMInstance(base.Instance): | |||
1298 | 109 | s.close() | 92 | s.close() |
1299 | 110 | return num | 93 | return num |
1300 | 111 | 94 | ||
1334 | 112 | def push_file(self, local_path, remote_path): | 95 | def ssh(self, command, stdin=None): |
1302 | 113 | """Copy file at 'local_path' to instance at 'remote_path'. | ||
1303 | 114 | |||
1304 | 115 | If we have a pid then SSH is up, otherwise, use | ||
1305 | 116 | mount-image-callback. | ||
1306 | 117 | |||
1307 | 118 | @param local_path: path on local instance | ||
1308 | 119 | @param remote_path: path on remote instance | ||
1309 | 120 | """ | ||
1310 | 121 | if self.pid: | ||
1311 | 122 | super(NoCloudKVMInstance, self).push_file() | ||
1312 | 123 | else: | ||
1313 | 124 | local_file = open(local_path) | ||
1314 | 125 | p = subprocess.Popen(['sudo', 'mount-image-callback', | ||
1315 | 126 | '--system-mounts', '--system-resolvconf', | ||
1316 | 127 | self.name, '--', 'chroot', '_MOUNTPOINT_', | ||
1317 | 128 | '/bin/sh', '-c', 'cat - > %s' % remote_path], | ||
1318 | 129 | stdin=local_file, | ||
1319 | 130 | stdout=subprocess.PIPE, | ||
1320 | 131 | stderr=subprocess.PIPE) | ||
1321 | 132 | p.wait() | ||
1322 | 133 | |||
1323 | 134 | def sftp_put(self, path, data): | ||
1324 | 135 | """SFTP put a file.""" | ||
1325 | 136 | client = self._ssh_connect() | ||
1326 | 137 | sftp = client.open_sftp() | ||
1327 | 138 | |||
1328 | 139 | with sftp.open(path, 'w') as f: | ||
1329 | 140 | f.write(data) | ||
1330 | 141 | |||
1331 | 142 | client.close() | ||
1332 | 143 | |||
1333 | 144 | def ssh(self, command): | ||
1335 | 145 | """Run a command via SSH.""" | 96 | """Run a command via SSH.""" |
1336 | 146 | client = self._ssh_connect() | 97 | client = self._ssh_connect() |
1337 | 147 | 98 | ||
1338 | 99 | cmd = util.shell_pack(command) | ||
1339 | 148 | try: | 100 | try: |
1350 | 149 | _, out, err = client.exec_command(util.shell_pack(command)) | 101 | fp_in, fp_out, fp_err = client.exec_command(cmd) |
1351 | 150 | except paramiko.SSHException: | 102 | channel = fp_in.channel |
1352 | 151 | raise util.InTargetExecuteError('', '', -1, command, self.name) | 103 | if stdin is not None: |
1353 | 152 | 104 | fp_in.write(stdin) | |
1354 | 153 | exit = out.channel.recv_exit_status() | 105 | fp_in.close() |
1355 | 154 | out = ''.join(out.readlines()) | 106 | |
1356 | 155 | err = ''.join(err.readlines()) | 107 | channel.shutdown_write() |
1357 | 156 | client.close() | 108 | rc = channel.recv_exit_status() |
1358 | 157 | 109 | return (fp_out.read(), fp_err.read(), rc) | |
1359 | 158 | return out, err, exit | 110 | except paramiko.SSHException as e: |
1360 | 111 | raise util.InTargetExecuteError( | ||
1361 | 112 | b'', b'', -1, command, self.name, reason=e) | ||
1362 | 159 | 113 | ||
1363 | 160 | def _ssh_connect(self, hostname='localhost', username='ubuntu', | 114 | def _ssh_connect(self, hostname='localhost', username='ubuntu', |
1364 | 161 | banner_timeout=120, retry_attempts=30): | 115 | banner_timeout=120, retry_attempts=30): |
1365 | 162 | """Connect via SSH.""" | 116 | """Connect via SSH.""" |
1366 | 117 | if self._ssh_client: | ||
1367 | 118 | return self._ssh_client | ||
1368 | 119 | |||
1369 | 163 | private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file) | 120 | private_key = paramiko.RSAKey.from_private_key_file(self.ssh_key_file) |
1370 | 164 | client = paramiko.SSHClient() | 121 | client = paramiko.SSHClient() |
1371 | 165 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | 122 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) |
1372 | @@ -168,6 +125,7 @@ class NoCloudKVMInstance(base.Instance): | |||
1373 | 168 | client.connect(hostname=hostname, username=username, | 125 | client.connect(hostname=hostname, username=username, |
1374 | 169 | port=self.ssh_port, pkey=private_key, | 126 | port=self.ssh_port, pkey=private_key, |
1375 | 170 | banner_timeout=banner_timeout) | 127 | banner_timeout=banner_timeout) |
1376 | 128 | self._ssh_client = client | ||
1377 | 171 | return client | 129 | return client |
1378 | 172 | except (paramiko.SSHException, TypeError): | 130 | except (paramiko.SSHException, TypeError): |
1379 | 173 | time.sleep(1) | 131 | time.sleep(1) |
1380 | @@ -183,15 +141,19 @@ class NoCloudKVMInstance(base.Instance): | |||
1381 | 183 | tmpdir = self.platform.config['data_dir'] | 141 | tmpdir = self.platform.config['data_dir'] |
1382 | 184 | seed = self.generate_seed(tmpdir) | 142 | seed = self.generate_seed(tmpdir) |
1383 | 185 | self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name) | 143 | self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name) |
1384 | 144 | self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name) | ||
1385 | 186 | self.ssh_port = self.get_free_port() | 145 | self.ssh_port = self.get_free_port() |
1386 | 187 | 146 | ||
1394 | 188 | subprocess.Popen(['./tools/xkvm', | 147 | cmd = ['./tools/xkvm', |
1395 | 189 | '--disk', '%s,cache=unsafe' % self.name, | 148 | '--disk', '%s,cache=unsafe' % self.name, |
1396 | 190 | '--disk', '%s,cache=unsafe' % seed, | 149 | '--disk', '%s,cache=unsafe' % seed, |
1397 | 191 | '--netdev', | 150 | '--netdev', ','.join(['user', |
1398 | 192 | 'user,hostfwd=tcp::%s-:22' % self.ssh_port, | 151 | 'hostfwd=tcp::%s-:22' % self.ssh_port, |
1399 | 193 | '--', '-pidfile', self.pid_file, '-vnc', 'none', | 152 | 'dnssearch=%s' % CI_DOMAIN]), |
1400 | 194 | '-m', '2G', '-smp', '2'], | 153 | '--', '-pidfile', self.pid_file, '-vnc', 'none', |
1401 | 154 | '-m', '2G', '-smp', '2', '-nographic', | ||
1402 | 155 | '-serial', 'file:' + self.console_file] | ||
1403 | 156 | subprocess.Popen(cmd, | ||
1404 | 195 | close_fds=True, | 157 | close_fds=True, |
1405 | 196 | stdin=subprocess.PIPE, | 158 | stdin=subprocess.PIPE, |
1406 | 197 | stdout=subprocess.PIPE, | 159 | stdout=subprocess.PIPE, |
1407 | @@ -206,12 +168,10 @@ class NoCloudKVMInstance(base.Instance): | |||
1408 | 206 | if wait: | 168 | if wait: |
1409 | 207 | self._wait_for_system(wait_for_cloud_init) | 169 | self._wait_for_system(wait_for_cloud_init) |
1410 | 208 | 170 | ||
1418 | 209 | def write_data(self, remote_path, data): | 171 | def console_log(self): |
1419 | 210 | """Write data to instance filesystem. | 172 | if not self.console_file: |
1420 | 211 | 173 | return b'' | |
1421 | 212 | @param remote_path: path in instance | 174 | with open(self.console_file, "rb") as fp: |
1422 | 213 | @param data: data to write, either str or bytes | 175 | return fp.read() |
1416 | 214 | """ | ||
1417 | 215 | self.sftp_put(remote_path, data) | ||
1423 | 216 | 176 | ||
1424 | 217 | # vi: ts=4 expandtab | 177 | # vi: ts=4 expandtab |
1425 | diff --git a/tests/cloud_tests/testcases/examples/run_commands.yaml b/tests/cloud_tests/testcases/examples/run_commands.yaml | |||
1426 | index b0e311b..f80eb8c 100644 | |||
1427 | --- a/tests/cloud_tests/testcases/examples/run_commands.yaml | |||
1428 | +++ b/tests/cloud_tests/testcases/examples/run_commands.yaml | |||
1429 | @@ -7,10 +7,10 @@ enabled: False | |||
1430 | 7 | cloud_config: | | 7 | cloud_config: | |
1431 | 8 | #cloud-config | 8 | #cloud-config |
1432 | 9 | runcmd: | 9 | runcmd: |
1434 | 10 | - echo cloud-init run cmd test > /tmp/run_cmd | 10 | - echo cloud-init run cmd test > /var/tmp/run_cmd |
1435 | 11 | collect_scripts: | 11 | collect_scripts: |
1436 | 12 | run_cmd: | | 12 | run_cmd: | |
1437 | 13 | #!/bin/bash | 13 | #!/bin/bash |
1439 | 14 | cat /tmp/run_cmd | 14 | cat /var/tmp/run_cmd |
1440 | 15 | 15 | ||
1441 | 16 | # vi: ts=4 expandtab | 16 | # vi: ts=4 expandtab |
1442 | diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py | |||
1443 | index d299e9a..dfbdead 100644 | |||
1444 | --- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py | |||
1445 | +++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.py | |||
1446 | @@ -11,13 +11,13 @@ class TestAptconfigureSourcesPPA(base.CloudTestCase): | |||
1447 | 11 | """Test specific ppa added.""" | 11 | """Test specific ppa added.""" |
1448 | 12 | out = self.get_data_file('sources.list') | 12 | out = self.get_data_file('sources.list') |
1449 | 13 | self.assertIn( | 13 | self.assertIn( |
1451 | 14 | 'http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu', out) | 14 | 'http://ppa.launchpad.net/cloud-init-dev/test-archive/ubuntu', out) |
1452 | 15 | 15 | ||
1453 | 16 | def test_ppa_key(self): | 16 | def test_ppa_key(self): |
1454 | 17 | """Test ppa key added.""" | 17 | """Test ppa key added.""" |
1455 | 18 | out = self.get_data_file('apt-key') | 18 | out = self.get_data_file('apt-key') |
1456 | 19 | self.assertIn( | 19 | self.assertIn( |
1459 | 20 | '1BC3 0F71 5A3B 8612 47A8 1A5E 55FE 7C8C 0165 013E', out) | 20 | '1FF0 D853 5EF7 E719 E5C8 1B9C 083D 06FB E4D3 04DF', out) |
1460 | 21 | self.assertIn('Launchpad PPA for curtin developers', out) | 21 | self.assertIn('Launchpad PPA for cloud init development team', out) |
1461 | 22 | 22 | ||
1462 | 23 | # vi: ts=4 expandtab | 23 | # vi: ts=4 expandtab |
1463 | diff --git a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml | |||
1464 | index 9efdae5..b997bcf 100644 | |||
1465 | --- a/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml | |||
1466 | +++ b/tests/cloud_tests/testcases/modules/apt_configure_sources_ppa.yaml | |||
1467 | @@ -2,7 +2,7 @@ | |||
1468 | 2 | # Add a PPA to source.list | 2 | # Add a PPA to source.list |
1469 | 3 | # | 3 | # |
1470 | 4 | # NOTE: on older ubuntu releases the sources file added is named | 4 | # NOTE: on older ubuntu releases the sources file added is named |
1472 | 5 | # 'curtin-dev-test-archive-trusty', without 'ubuntu' in the middle | 5 | # 'cloud-init-dev-test-archive-trusty', without 'ubuntu' in the middle |
1473 | 6 | required_features: | 6 | required_features: |
1474 | 7 | - apt | 7 | - apt |
1475 | 8 | - ppa | 8 | - ppa |
1476 | @@ -14,11 +14,11 @@ cloud_config: | | |||
1477 | 14 | source1: | 14 | source1: |
1478 | 15 | keyid: 0165013E | 15 | keyid: 0165013E |
1479 | 16 | keyserver: keyserver.ubuntu.com | 16 | keyserver: keyserver.ubuntu.com |
1481 | 17 | source: "ppa:curtin-dev/test-archive" | 17 | source: "ppa:cloud-init-dev/test-archive" |
1482 | 18 | collect_scripts: | 18 | collect_scripts: |
1483 | 19 | sources.list: | | 19 | sources.list: | |
1484 | 20 | #!/bin/bash | 20 | #!/bin/bash |
1486 | 21 | cat /etc/apt/sources.list.d/curtin-dev-ubuntu-test-archive-*.list | 21 | cat /etc/apt/sources.list.d/cloud-init-dev-ubuntu-test-archive-*.list |
1487 | 22 | apt-key: | | 22 | apt-key: | |
1488 | 23 | #!/bin/bash | 23 | #!/bin/bash |
1489 | 24 | apt-key finger | 24 | apt-key finger |
1490 | diff --git a/tests/cloud_tests/testcases/modules/keys_to_console.py b/tests/cloud_tests/testcases/modules/keys_to_console.py | |||
1491 | index 88b6812..07f3811 100644 | |||
1492 | --- a/tests/cloud_tests/testcases/modules/keys_to_console.py | |||
1493 | +++ b/tests/cloud_tests/testcases/modules/keys_to_console.py | |||
1494 | @@ -10,13 +10,13 @@ class TestKeysToConsole(base.CloudTestCase): | |||
1495 | 10 | def test_excluded_keys(self): | 10 | def test_excluded_keys(self): |
1496 | 11 | """Test excluded keys missing.""" | 11 | """Test excluded keys missing.""" |
1497 | 12 | out = self.get_data_file('syslog') | 12 | out = self.get_data_file('syslog') |
1500 | 13 | self.assertNotIn('DSA', out) | 13 | self.assertNotIn('(DSA)', out) |
1501 | 14 | self.assertNotIn('ECDSA', out) | 14 | self.assertNotIn('(ECDSA)', out) |
1502 | 15 | 15 | ||
1503 | 16 | def test_expected_keys(self): | 16 | def test_expected_keys(self): |
1504 | 17 | """Test expected keys exist.""" | 17 | """Test expected keys exist.""" |
1505 | 18 | out = self.get_data_file('syslog') | 18 | out = self.get_data_file('syslog') |
1508 | 19 | self.assertIn('ED25519', out) | 19 | self.assertIn('(ED25519)', out) |
1509 | 20 | self.assertIn('RSA', out) | 20 | self.assertIn('(RSA)', out) |
1510 | 21 | 21 | ||
1511 | 22 | # vi: ts=4 expandtab | 22 | # vi: ts=4 expandtab |
1512 | diff --git a/tests/cloud_tests/testcases/modules/runcmd.yaml b/tests/cloud_tests/testcases/modules/runcmd.yaml | |||
1513 | index 04e5a05..8309a88 100644 | |||
1514 | --- a/tests/cloud_tests/testcases/modules/runcmd.yaml | |||
1515 | +++ b/tests/cloud_tests/testcases/modules/runcmd.yaml | |||
1516 | @@ -4,10 +4,10 @@ | |||
1517 | 4 | cloud_config: | | 4 | cloud_config: | |
1518 | 5 | #cloud-config | 5 | #cloud-config |
1519 | 6 | runcmd: | 6 | runcmd: |
1521 | 7 | - echo cloud-init run cmd test > /tmp/run_cmd | 7 | - echo cloud-init run cmd test > /var/tmp/run_cmd |
1522 | 8 | collect_scripts: | 8 | collect_scripts: |
1523 | 9 | run_cmd: | | 9 | run_cmd: | |
1524 | 10 | #!/bin/bash | 10 | #!/bin/bash |
1526 | 11 | cat /tmp/run_cmd | 11 | cat /var/tmp/run_cmd |
1527 | 12 | 12 | ||
1528 | 13 | # vi: ts=4 expandtab | 13 | # vi: ts=4 expandtab |
1529 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname.py b/tests/cloud_tests/testcases/modules/set_hostname.py | |||
1530 | index 6e96a75..1dbe64c 100644 | |||
1531 | --- a/tests/cloud_tests/testcases/modules/set_hostname.py | |||
1532 | +++ b/tests/cloud_tests/testcases/modules/set_hostname.py | |||
1533 | @@ -7,9 +7,11 @@ from tests.cloud_tests.testcases import base | |||
1534 | 7 | class TestHostname(base.CloudTestCase): | 7 | class TestHostname(base.CloudTestCase): |
1535 | 8 | """Test hostname module.""" | 8 | """Test hostname module.""" |
1536 | 9 | 9 | ||
1537 | 10 | ex_hostname = "cloudinit2" | ||
1538 | 11 | |||
1539 | 10 | def test_hostname(self): | 12 | def test_hostname(self): |
1540 | 11 | """Test hostname command shows correct output.""" | 13 | """Test hostname command shows correct output.""" |
1541 | 12 | out = self.get_data_file('hostname') | 14 | out = self.get_data_file('hostname') |
1543 | 13 | self.assertIn('myhostname', out) | 15 | self.assertIn(self.ex_hostname, out) |
1544 | 14 | 16 | ||
1545 | 15 | # vi: ts=4 expandtab | 17 | # vi: ts=4 expandtab |
1546 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname.yaml b/tests/cloud_tests/testcases/modules/set_hostname.yaml | |||
1547 | index c96344c..071fb22 100644 | |||
1548 | --- a/tests/cloud_tests/testcases/modules/set_hostname.yaml | |||
1549 | +++ b/tests/cloud_tests/testcases/modules/set_hostname.yaml | |||
1550 | @@ -5,7 +5,8 @@ required_features: | |||
1551 | 5 | - hostname | 5 | - hostname |
1552 | 6 | cloud_config: | | 6 | cloud_config: | |
1553 | 7 | #cloud-config | 7 | #cloud-config |
1555 | 8 | hostname: myhostname | 8 | hostname: cloudinit2 |
1556 | 9 | |||
1557 | 9 | collect_scripts: | 10 | collect_scripts: |
1558 | 10 | hosts: | | 11 | hosts: | |
1559 | 11 | #!/bin/bash | 12 | #!/bin/bash |
1560 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py | |||
1561 | index 398f3d4..eb6f065 100644 | |||
1562 | --- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py | |||
1563 | +++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.py | |||
1564 | @@ -1,26 +1,31 @@ | |||
1565 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | 1 | # This file is part of cloud-init. See LICENSE file for license information. |
1566 | 2 | 2 | ||
1567 | 3 | """cloud-init Integration Test Verify Script.""" | 3 | """cloud-init Integration Test Verify Script.""" |
1568 | 4 | from tests.cloud_tests.instances.nocloudkvm import CI_DOMAIN | ||
1569 | 4 | from tests.cloud_tests.testcases import base | 5 | from tests.cloud_tests.testcases import base |
1570 | 5 | 6 | ||
1571 | 6 | 7 | ||
1572 | 7 | class TestHostnameFqdn(base.CloudTestCase): | 8 | class TestHostnameFqdn(base.CloudTestCase): |
1573 | 8 | """Test Hostname module.""" | 9 | """Test Hostname module.""" |
1574 | 9 | 10 | ||
1575 | 11 | ex_hostname = "cloudinit1" | ||
1576 | 12 | ex_fqdn = "cloudinit2." + CI_DOMAIN | ||
1577 | 13 | |||
1578 | 10 | def test_hostname(self): | 14 | def test_hostname(self): |
1579 | 11 | """Test hostname output.""" | 15 | """Test hostname output.""" |
1580 | 12 | out = self.get_data_file('hostname') | 16 | out = self.get_data_file('hostname') |
1582 | 13 | self.assertIn('myhostname', out) | 17 | self.assertIn(self.ex_hostname, out) |
1583 | 14 | 18 | ||
1584 | 15 | def test_hostname_fqdn(self): | 19 | def test_hostname_fqdn(self): |
1585 | 16 | """Test hostname fqdn output.""" | 20 | """Test hostname fqdn output.""" |
1586 | 17 | out = self.get_data_file('fqdn') | 21 | out = self.get_data_file('fqdn') |
1588 | 18 | self.assertIn('host.myorg.com', out) | 22 | self.assertIn(self.ex_fqdn, out) |
1589 | 19 | 23 | ||
1590 | 20 | def test_hosts(self): | 24 | def test_hosts(self): |
1591 | 21 | """Test /etc/hosts file.""" | 25 | """Test /etc/hosts file.""" |
1592 | 22 | out = self.get_data_file('hosts') | 26 | out = self.get_data_file('hosts') |
1594 | 23 | self.assertIn('127.0.1.1 host.myorg.com myhostname', out) | 27 | self.assertIn('127.0.1.1 %s %s' % (self.ex_fqdn, self.ex_hostname), |
1595 | 28 | out) | ||
1596 | 24 | self.assertIn('127.0.0.1 localhost', out) | 29 | self.assertIn('127.0.0.1 localhost', out) |
1597 | 25 | 30 | ||
1598 | 26 | # vi: ts=4 expandtab | 31 | # vi: ts=4 expandtab |
1599 | diff --git a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml | |||
1600 | index daf7593..a85ee79 100644 | |||
1601 | --- a/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml | |||
1602 | +++ b/tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml | |||
1603 | @@ -6,8 +6,9 @@ required_features: | |||
1604 | 6 | cloud_config: | | 6 | cloud_config: | |
1605 | 7 | #cloud-config | 7 | #cloud-config |
1606 | 8 | manage_etc_hosts: true | 8 | manage_etc_hosts: true |
1609 | 9 | hostname: myhostname | 9 | hostname: cloudinit1 |
1610 | 10 | fqdn: host.myorg.com | 10 | # this needs changing if CI_DOMAIN were updated. |
1611 | 11 | fqdn: cloudinit2.i9n.cloud-init.io | ||
1612 | 11 | collect_scripts: | 12 | collect_scripts: |
1613 | 12 | hosts: | | 13 | hosts: | |
1614 | 13 | #!/bin/bash | 14 | #!/bin/bash |
1615 | diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.py b/tests/cloud_tests/testcases/modules/set_password_expire.py | |||
1616 | index a1c3aa0..967aca7 100644 | |||
1617 | --- a/tests/cloud_tests/testcases/modules/set_password_expire.py | |||
1618 | +++ b/tests/cloud_tests/testcases/modules/set_password_expire.py | |||
1619 | @@ -18,6 +18,6 @@ class TestPasswordExpire(base.CloudTestCase): | |||
1620 | 18 | def test_sshd_config(self): | 18 | def test_sshd_config(self): |
1621 | 19 | """Test sshd config allows passwords.""" | 19 | """Test sshd config allows passwords.""" |
1622 | 20 | out = self.get_data_file('sshd_config') | 20 | out = self.get_data_file('sshd_config') |
1624 | 21 | self.assertIn('PasswordAuthentication no', out) | 21 | self.assertIn('PasswordAuthentication yes', out) |
1625 | 22 | 22 | ||
1626 | 23 | # vi: ts=4 expandtab | 23 | # vi: ts=4 expandtab |
1627 | diff --git a/tests/cloud_tests/testcases/modules/set_password_expire.yaml b/tests/cloud_tests/testcases/modules/set_password_expire.yaml | |||
1628 | index 789604b..ba6344b 100644 | |||
1629 | --- a/tests/cloud_tests/testcases/modules/set_password_expire.yaml | |||
1630 | +++ b/tests/cloud_tests/testcases/modules/set_password_expire.yaml | |||
1631 | @@ -6,7 +6,9 @@ required_features: | |||
1632 | 6 | cloud_config: | | 6 | cloud_config: | |
1633 | 7 | #cloud-config | 7 | #cloud-config |
1634 | 8 | chpasswd: { expire: True } | 8 | chpasswd: { expire: True } |
1635 | 9 | ssh_pwauth: yes | ||
1636 | 9 | users: | 10 | users: |
1637 | 11 | - default | ||
1638 | 10 | - name: tom | 12 | - name: tom |
1639 | 11 | password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE. | 13 | password: $1$xyz$sPMsLNmf66Ohl.ol6JvzE. |
1640 | 12 | lock_passwd: false | 14 | lock_passwd: false |
1641 | diff --git a/tests/cloud_tests/testcases/modules/set_password_list.yaml b/tests/cloud_tests/testcases/modules/set_password_list.yaml | |||
1642 | index a2a89c9..fd3e1e4 100644 | |||
1643 | --- a/tests/cloud_tests/testcases/modules/set_password_list.yaml | |||
1644 | +++ b/tests/cloud_tests/testcases/modules/set_password_list.yaml | |||
1645 | @@ -5,6 +5,7 @@ cloud_config: | | |||
1646 | 5 | #cloud-config | 5 | #cloud-config |
1647 | 6 | ssh_pwauth: yes | 6 | ssh_pwauth: yes |
1648 | 7 | users: | 7 | users: |
1649 | 8 | - default | ||
1650 | 8 | - name: tom | 9 | - name: tom |
1651 | 9 | # md5 gotomgo | 10 | # md5 gotomgo |
1652 | 10 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" | 11 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" |
1653 | diff --git a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml | |||
1654 | index c2a0f63..e9fe54b 100644 | |||
1655 | --- a/tests/cloud_tests/testcases/modules/set_password_list_string.yaml | |||
1656 | +++ b/tests/cloud_tests/testcases/modules/set_password_list_string.yaml | |||
1657 | @@ -5,6 +5,7 @@ cloud_config: | | |||
1658 | 5 | #cloud-config | 5 | #cloud-config |
1659 | 6 | ssh_pwauth: yes | 6 | ssh_pwauth: yes |
1660 | 7 | users: | 7 | users: |
1661 | 8 | - default | ||
1662 | 8 | - name: tom | 9 | - name: tom |
1663 | 9 | # md5 gotomgo | 10 | # md5 gotomgo |
1664 | 10 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" | 11 | passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" |
1665 | diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py | |||
1666 | index 8222321..e7329d4 100644 | |||
1667 | --- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py | |||
1668 | +++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.py | |||
1669 | @@ -13,12 +13,4 @@ class TestSshKeyFingerprintsDisable(base.CloudTestCase): | |||
1670 | 13 | self.assertIn('Skipping module named ssh-authkey-fingerprints, ' | 13 | self.assertIn('Skipping module named ssh-authkey-fingerprints, ' |
1671 | 14 | 'logging of ssh fingerprints disabled', out) | 14 | 'logging of ssh fingerprints disabled', out) |
1672 | 15 | 15 | ||
1673 | 16 | def test_syslog(self): | ||
1674 | 17 | """Verify output of syslog.""" | ||
1675 | 18 | out = self.get_data_file('syslog') | ||
1676 | 19 | self.assertNotRegex(out, r'256 SHA256:.*(ECDSA)') | ||
1677 | 20 | self.assertNotRegex(out, r'256 SHA256:.*(ED25519)') | ||
1678 | 21 | self.assertNotRegex(out, r'1024 SHA256:.*(DSA)') | ||
1679 | 22 | self.assertNotRegex(out, r'2048 SHA256:.*(RSA)') | ||
1680 | 23 | |||
1681 | 24 | # vi: ts=4 expandtab | 16 | # vi: ts=4 expandtab |
1682 | diff --git a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml | |||
1683 | index 746653e..d93893e 100644 | |||
1684 | --- a/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml | |||
1685 | +++ b/tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml | |||
1686 | @@ -5,7 +5,6 @@ required_features: | |||
1687 | 5 | - syslog | 5 | - syslog |
1688 | 6 | cloud_config: | | 6 | cloud_config: | |
1689 | 7 | #cloud-config | 7 | #cloud-config |
1690 | 8 | ssh_genkeytypes: [] | ||
1691 | 9 | no_ssh_fingerprints: true | 8 | no_ssh_fingerprints: true |
1692 | 10 | collect_scripts: | 9 | collect_scripts: |
1693 | 11 | syslog: | | 10 | syslog: | |
1694 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py | |||
1695 | index fd6d9ba..b68f556 100644 | |||
1696 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.py | |||
1697 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.py | |||
1698 | @@ -9,11 +9,6 @@ class TestSshKeysGenerate(base.CloudTestCase): | |||
1699 | 9 | 9 | ||
1700 | 10 | # TODO: Check cloud-init-output for the correct keys being generated | 10 | # TODO: Check cloud-init-output for the correct keys being generated |
1701 | 11 | 11 | ||
1702 | 12 | def test_ubuntu_authorized_keys(self): | ||
1703 | 13 | """Test passed in key is not in list for ubuntu.""" | ||
1704 | 14 | out = self.get_data_file('auth_keys_ubuntu') | ||
1705 | 15 | self.assertEqual('', out) | ||
1706 | 16 | |||
1707 | 17 | def test_dsa_public(self): | 12 | def test_dsa_public(self): |
1708 | 18 | """Test dsa public key not generated.""" | 13 | """Test dsa public key not generated.""" |
1709 | 19 | out = self.get_data_file('dsa_public') | 14 | out = self.get_data_file('dsa_public') |
1710 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml | |||
1711 | index 659fd93..0a7adf6 100644 | |||
1712 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml | |||
1713 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml | |||
1714 | @@ -10,12 +10,6 @@ cloud_config: | | |||
1715 | 10 | - ed25519 | 10 | - ed25519 |
1716 | 11 | authkey_hash: sha512 | 11 | authkey_hash: sha512 |
1717 | 12 | collect_scripts: | 12 | collect_scripts: |
1718 | 13 | auth_keys_root: | | ||
1719 | 14 | #!/bin/bash | ||
1720 | 15 | cat /root/.ssh/authorized_keys | ||
1721 | 16 | auth_keys_ubuntu: | | ||
1722 | 17 | #!/bin/bash | ||
1723 | 18 | cat /home/ubuntu/ssh/authorized_keys | ||
1724 | 19 | dsa_public: | | 13 | dsa_public: | |
1725 | 20 | #!/bin/bash | 14 | #!/bin/bash |
1726 | 21 | cat /etc/ssh/ssh_host_dsa_key.pub | 15 | cat /etc/ssh/ssh_host_dsa_key.pub |
1727 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py | |||
1728 | index 544649d..add3f46 100644 | |||
1729 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.py | |||
1730 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.py | |||
1731 | @@ -7,17 +7,6 @@ from tests.cloud_tests.testcases import base | |||
1732 | 7 | class TestSshKeysProvided(base.CloudTestCase): | 7 | class TestSshKeysProvided(base.CloudTestCase): |
1733 | 8 | """Test ssh keys module.""" | 8 | """Test ssh keys module.""" |
1734 | 9 | 9 | ||
1735 | 10 | def test_ubuntu_authorized_keys(self): | ||
1736 | 11 | """Test passed in key is not in list for ubuntu.""" | ||
1737 | 12 | out = self.get_data_file('auth_keys_ubuntu') | ||
1738 | 13 | self.assertEqual('', out) | ||
1739 | 14 | |||
1740 | 15 | def test_root_authorized_keys(self): | ||
1741 | 16 | """Test passed in key is in authorized list for root.""" | ||
1742 | 17 | out = self.get_data_file('auth_keys_root') | ||
1743 | 18 | self.assertIn('lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs50' | ||
1744 | 19 | '6oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w==', out) | ||
1745 | 20 | |||
1746 | 21 | def test_dsa_public(self): | 10 | def test_dsa_public(self): |
1747 | 22 | """Test dsa public key passed in.""" | 11 | """Test dsa public key passed in.""" |
1748 | 23 | out = self.get_data_file('dsa_public') | 12 | out = self.get_data_file('dsa_public') |
1749 | diff --git a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml | |||
1750 | index 5ceb362..41f6355 100644 | |||
1751 | --- a/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml | |||
1752 | +++ b/tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml | |||
1753 | @@ -71,12 +71,6 @@ cloud_config: | | |||
1754 | 71 | -----END EC PRIVATE KEY----- | 71 | -----END EC PRIVATE KEY----- |
1755 | 72 | ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd | 72 | ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd |
1756 | 73 | collect_scripts: | 73 | collect_scripts: |
1757 | 74 | auth_keys_root: | | ||
1758 | 75 | #!/bin/bash | ||
1759 | 76 | cat /root/.ssh/authorized_keys | ||
1760 | 77 | auth_keys_ubuntu: | | ||
1761 | 78 | #!/bin/bash | ||
1762 | 79 | cat /home/ubuntu/ssh/authorized_keys | ||
1763 | 80 | dsa_public: | | 74 | dsa_public: | |
1764 | 81 | #!/bin/bash | 75 | #!/bin/bash |
1765 | 82 | cat /etc/ssh/ssh_host_dsa_key.pub | 76 | cat /etc/ssh/ssh_host_dsa_key.pub |
1766 | diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py | |||
1767 | index 4357fbb..c5cd697 100644 | |||
1768 | --- a/tests/cloud_tests/util.py | |||
1769 | +++ b/tests/cloud_tests/util.py | |||
1770 | @@ -7,6 +7,7 @@ import copy | |||
1771 | 7 | import glob | 7 | import glob |
1772 | 8 | import os | 8 | import os |
1773 | 9 | import random | 9 | import random |
1774 | 10 | import shlex | ||
1775 | 10 | import shutil | 11 | import shutil |
1776 | 11 | import string | 12 | import string |
1777 | 12 | import subprocess | 13 | import subprocess |
1778 | @@ -285,20 +286,165 @@ def shell_pack(cmd): | |||
1779 | 285 | return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64 | 286 | return 'eval set -- "$(echo %s | base64 --decode)" && exec "$@"' % b64 |
1780 | 286 | 287 | ||
1781 | 287 | 288 | ||
1782 | 289 | def shell_quote(cmd): | ||
1783 | 290 | if isinstance(cmd, (tuple, list)): | ||
1784 | 291 | return ' '.join([shlex.quote(x) for x in cmd]) | ||
1785 | 292 | return shlex.quote(cmd) | ||
1786 | 293 | |||
1787 | 294 | |||
1788 | 295 | class TargetBase(object): | ||
1789 | 296 | _tmp_count = 0 | ||
1790 | 297 | |||
1791 | 298 | def execute(self, command, stdin=None, env=None, | ||
1792 | 299 | rcs=None, description=None): | ||
1793 | 300 | """Execute command in instance, recording output, error and exit code. | ||
1794 | 301 | |||
1795 | 302 | Assumes functional networking and execution as root with the | ||
1796 | 303 | target filesystem being available at /. | ||
1797 | 304 | |||
1798 | 305 | @param command: the command to execute as root inside the image | ||
1799 | 306 | if command is a string, then it will be executed as: | ||
1800 | 307 | ['sh', '-c', command] | ||
1801 | 308 | @param stdin: bytes content for standard in | ||
1802 | 309 | @param env: environment variables | ||
1803 | 310 | @param rcs: return codes. | ||
1804 | 311 | None (default): non-zero exit code will raise exception. | ||
1805 | 312 | False: any is allowed (No execption raised). | ||
1806 | 313 | list of int: any rc not in the list will raise exception. | ||
1807 | 314 | @param description: purpose of command | ||
1808 | 315 | @return_value: tuple containing stdout data, stderr data, exit code | ||
1809 | 316 | """ | ||
1810 | 317 | if isinstance(command, str): | ||
1811 | 318 | command = ['sh', '-c', command] | ||
1812 | 319 | |||
1813 | 320 | if rcs is None: | ||
1814 | 321 | rcs = (0,) | ||
1815 | 322 | |||
1816 | 323 | if description: | ||
1817 | 324 | LOG.debug('Executing "%s"', description) | ||
1818 | 325 | else: | ||
1819 | 326 | LOG.debug("Executing command: %s", shell_quote(command)) | ||
1820 | 327 | |||
1821 | 328 | out, err, rc = self._execute(command=command, stdin=stdin, env=env) | ||
1822 | 329 | |||
1823 | 330 | # False means accept anything. | ||
1824 | 331 | if (rcs is False or rc in rcs): | ||
1825 | 332 | return out, err, rc | ||
1826 | 333 | |||
1827 | 334 | raise InTargetExecuteError(out, err, rc, command, description) | ||
1828 | 335 | |||
1829 | 336 | def _execute(self, command, stdin=None, env=None): | ||
1830 | 337 | """Execute command in inside, return stdout, stderr and exit code. | ||
1831 | 338 | |||
1832 | 339 | Assumes functional networking and execution as root with the | ||
1833 | 340 | target filesystem being available at /. | ||
1834 | 341 | |||
1835 | 342 | @param stdin: bytes content for standard in | ||
1836 | 343 | @param env: environment variables | ||
1837 | 344 | @return_value: tuple containing stdout data, stderr data, exit code | ||
1838 | 345 | |||
1839 | 346 | This is intended to be implemented by the Image or Instance. | ||
1840 | 347 | Many callers will use the higher level 'execute'.""" | ||
1841 | 348 | raise NotImplementedError("_execute must be implemented by subclass.") | ||
1842 | 349 | |||
1843 | 350 | def read_data(self, remote_path, decode=False): | ||
1844 | 351 | """Read data from instance filesystem. | ||
1845 | 352 | |||
1846 | 353 | @param remote_path: path in instance | ||
1847 | 354 | @param decode: decode data before returning. | ||
1848 | 355 | @return_value: content of remote_path as bytes if 'decode' is False, | ||
1849 | 356 | and as string if 'decode' is True. | ||
1850 | 357 | """ | ||
1851 | 358 | # when sh is invoked with '-c', then the first argument is "$0" | ||
1852 | 359 | # which is commonly understood as the "program name". | ||
1853 | 360 | # 'read_data' is the program name, and 'remote_path' is '$1' | ||
1854 | 361 | stdout, stderr, rc = self._execute( | ||
1855 | 362 | ["sh", "-c", 'exec cat "$1"', 'read_data', remote_path]) | ||
1856 | 363 | if rc != 0: | ||
1857 | 364 | raise RuntimeError("Failed to read file '%s'" % remote_path) | ||
1858 | 365 | |||
1859 | 366 | if decode: | ||
1860 | 367 | return stdout.decode() | ||
1861 | 368 | return stdout | ||
1862 | 369 | |||
1863 | 370 | def write_data(self, remote_path, data): | ||
1864 | 371 | """Write data to instance filesystem. | ||
1865 | 372 | |||
1866 | 373 | @param remote_path: path in instance | ||
1867 | 374 | @param data: data to write in bytes | ||
1868 | 375 | """ | ||
1869 | 376 | # when sh is invoked with '-c', then the first argument is "$0" | ||
1870 | 377 | # which is commonly understood as the "program name". | ||
1871 | 378 | # 'write_data' is the program name, and 'remote_path' is '$1' | ||
1872 | 379 | _, _, rc = self._execute( | ||
1873 | 380 | ["sh", "-c", 'exec cat >"$1"', 'write_data', remote_path], | ||
1874 | 381 | stdin=data) | ||
1875 | 382 | |||
1876 | 383 | if rc != 0: | ||
1877 | 384 | raise RuntimeError("Failed to write to '%s'" % remote_path) | ||
1878 | 385 | return | ||
1879 | 386 | |||
1880 | 387 | def pull_file(self, remote_path, local_path): | ||
1881 | 388 | """Copy file at 'remote_path', from instance to 'local_path'. | ||
1882 | 389 | |||
1883 | 390 | @param remote_path: path on remote instance | ||
1884 | 391 | @param local_path: path on local instance | ||
1885 | 392 | """ | ||
1886 | 393 | with open(local_path, 'wb') as fp: | ||
1887 | 394 | fp.write(self.read_data(remote_path)) | ||
1888 | 395 | |||
1889 | 396 | def push_file(self, local_path, remote_path): | ||
1890 | 397 | """Copy file at 'local_path' to instance at 'remote_path'. | ||
1891 | 398 | |||
1892 | 399 | @param local_path: path on local instance | ||
1893 | 400 | @param remote_path: path on remote instance""" | ||
1894 | 401 | with open(local_path, "rb") as fp: | ||
1895 | 402 | self.write_data(remote_path, data=fp.read()) | ||
1896 | 403 | |||
1897 | 404 | def run_script(self, script, rcs=None, description=None): | ||
1898 | 405 | """Run script in target and return stdout. | ||
1899 | 406 | |||
1900 | 407 | @param script: script contents | ||
1901 | 408 | @param rcs: allowed return codes from script | ||
1902 | 409 | @param description: purpose of script | ||
1903 | 410 | @return_value: stdout from script | ||
1904 | 411 | """ | ||
1905 | 412 | # Just write to a file, add execute, run it, then remove it. | ||
1906 | 413 | shblob = '; '.join(( | ||
1907 | 414 | 'set -e', | ||
1908 | 415 | 's="$1"', | ||
1909 | 416 | 'shift', | ||
1910 | 417 | 'cat > "$s"', | ||
1911 | 418 | 'trap "rm -f $s" EXIT', | ||
1912 | 419 | 'chmod +x "$s"', | ||
1913 | 420 | '"$s" "$@"')) | ||
1914 | 421 | return self.execute( | ||
1915 | 422 | ['sh', '-c', shblob, 'runscript', self.tmpfile()], | ||
1916 | 423 | stdin=script, description=description, rcs=rcs) | ||
1917 | 424 | |||
1918 | 425 | def tmpfile(self): | ||
1919 | 426 | """Get a tmp file in the target. | ||
1920 | 427 | |||
1921 | 428 | @return_value: path to new file in target | ||
1922 | 429 | """ | ||
1923 | 430 | path = "/tmp/%s-%04d" % (type(self).__name__, self._tmp_count) | ||
1924 | 431 | self._tmp_count += 1 | ||
1925 | 432 | return path | ||
1926 | 433 | |||
1927 | 434 | |||
1928 | 288 | class InTargetExecuteError(c_util.ProcessExecutionError): | 435 | class InTargetExecuteError(c_util.ProcessExecutionError): |
1929 | 289 | """Error type for in target commands that fail.""" | 436 | """Error type for in target commands that fail.""" |
1930 | 290 | 437 | ||
1932 | 291 | default_desc = 'Unexpected error while running command in target instance' | 438 | default_desc = 'Unexpected error while running command.' |
1933 | 292 | 439 | ||
1936 | 293 | def __init__(self, stdout, stderr, exit_code, cmd, instance, | 440 | def __init__(self, stdout, stderr, exit_code, cmd, description=None, |
1937 | 294 | description=None): | 441 | reason=None): |
1938 | 295 | """Init error and parent error class.""" | 442 | """Init error and parent error class.""" |
1939 | 296 | if isinstance(cmd, (tuple, list)): | ||
1940 | 297 | cmd = ' '.join(cmd) | ||
1941 | 298 | super(InTargetExecuteError, self).__init__( | 443 | super(InTargetExecuteError, self).__init__( |
1945 | 299 | stdout=stdout, stderr=stderr, exit_code=exit_code, cmd=cmd, | 444 | stdout=stdout, stderr=stderr, exit_code=exit_code, |
1946 | 300 | reason="Instance: {}".format(instance), | 445 | cmd=shell_quote(cmd), |
1947 | 301 | description=description if description else self.default_desc) | 446 | description=description if description else self.default_desc, |
1948 | 447 | reason=reason) | ||
1949 | 302 | 448 | ||
1950 | 303 | 449 | ||
1951 | 304 | class TempDir(object): | 450 | class TempDir(object): |
1952 | diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py | |||
1953 | index 6d621d2..275b16d 100644 | |||
1954 | --- a/tests/unittests/test_data.py | |||
1955 | +++ b/tests/unittests/test_data.py | |||
1956 | @@ -18,6 +18,8 @@ from email.mime.application import MIMEApplication | |||
1957 | 18 | from email.mime.base import MIMEBase | 18 | from email.mime.base import MIMEBase |
1958 | 19 | from email.mime.multipart import MIMEMultipart | 19 | from email.mime.multipart import MIMEMultipart |
1959 | 20 | 20 | ||
1960 | 21 | import httpretty | ||
1961 | 22 | |||
1962 | 21 | from cloudinit import handlers | 23 | from cloudinit import handlers |
1963 | 22 | from cloudinit import helpers as c_helpers | 24 | from cloudinit import helpers as c_helpers |
1964 | 23 | from cloudinit import log | 25 | from cloudinit import log |
1965 | @@ -522,6 +524,54 @@ c: 4 | |||
1966 | 522 | self.assertEqual(cfg.get('password'), 'gocubs') | 524 | self.assertEqual(cfg.get('password'), 'gocubs') |
1967 | 523 | self.assertEqual(cfg.get('locale'), 'chicago') | 525 | self.assertEqual(cfg.get('locale'), 'chicago') |
1968 | 524 | 526 | ||
1969 | 527 | @httpretty.activate | ||
1970 | 528 | @mock.patch('cloudinit.url_helper.time.sleep') | ||
1971 | 529 | def test_include(self, mock_sleep): | ||
1972 | 530 | """Test #include.""" | ||
1973 | 531 | included_url = 'http://hostname/path' | ||
1974 | 532 | included_data = '#cloud-config\nincluded: true\n' | ||
1975 | 533 | httpretty.register_uri(httpretty.GET, included_url, included_data) | ||
1976 | 534 | |||
1977 | 535 | blob = '#include\n%s\n' % included_url | ||
1978 | 536 | |||
1979 | 537 | self.reRoot() | ||
1980 | 538 | ci = stages.Init() | ||
1981 | 539 | ci.datasource = FakeDataSource(blob) | ||
1982 | 540 | ci.fetch() | ||
1983 | 541 | ci.consume_data() | ||
1984 | 542 | cc_contents = util.load_file(ci.paths.get_ipath("cloud_config")) | ||
1985 | 543 | cc = util.load_yaml(cc_contents) | ||
1986 | 544 | self.assertTrue(cc.get('included')) | ||
1987 | 545 | |||
1988 | 546 | @httpretty.activate | ||
1989 | 547 | @mock.patch('cloudinit.url_helper.time.sleep') | ||
1990 | 548 | def test_include_bad_url(self, mock_sleep): | ||
1991 | 549 | """Test #include with a bad URL.""" | ||
1992 | 550 | bad_url = 'http://bad/forbidden' | ||
1993 | 551 | bad_data = '#cloud-config\nbad: true\n' | ||
1994 | 552 | httpretty.register_uri(httpretty.GET, bad_url, bad_data, status=403) | ||
1995 | 553 | |||
1996 | 554 | included_url = 'http://hostname/path' | ||
1997 | 555 | included_data = '#cloud-config\nincluded: true\n' | ||
1998 | 556 | httpretty.register_uri(httpretty.GET, included_url, included_data) | ||
1999 | 557 | |||
2000 | 558 | blob = '#include\n%s\n%s' % (bad_url, included_url) | ||
2001 | 559 | |||
2002 | 560 | self.reRoot() | ||
2003 | 561 | ci = stages.Init() | ||
2004 | 562 | ci.datasource = FakeDataSource(blob) | ||
2005 | 563 | log_file = self.capture_log(logging.WARNING) | ||
2006 | 564 | ci.fetch() | ||
2007 | 565 | ci.consume_data() | ||
2008 | 566 | |||
2009 | 567 | self.assertIn("403 Client Error: Forbidden for url: %s" % bad_url, | ||
2010 | 568 | log_file.getvalue()) | ||
2011 | 569 | |||
2012 | 570 | cc_contents = util.load_file(ci.paths.get_ipath("cloud_config")) | ||
2013 | 571 | cc = util.load_yaml(cc_contents) | ||
2014 | 572 | self.assertIsNone(cc.get('bad')) | ||
2015 | 573 | self.assertTrue(cc.get('included')) | ||
2016 | 574 | |||
2017 | 525 | 575 | ||
2018 | 526 | class TestUDProcess(helpers.ResourceUsingTestCase): | 576 | class TestUDProcess(helpers.ResourceUsingTestCase): |
2019 | 527 | 577 | ||
2020 | diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/test_datasource/test_ec2.py | |||
2021 | index 6af699a..ba328ee 100644 | |||
2022 | --- a/tests/unittests/test_datasource/test_ec2.py | |||
2023 | +++ b/tests/unittests/test_datasource/test_ec2.py | |||
2024 | @@ -307,6 +307,39 @@ class TestEc2(test_helpers.HttprettyTestCase): | |||
2025 | 307 | 307 | ||
2026 | 308 | @httpretty.activate | 308 | @httpretty.activate |
2027 | 309 | @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') | 309 | @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') |
2028 | 310 | def test_network_config_cached_property_refreshed_on_upgrade(self, m_dhcp): | ||
2029 | 311 | """Refresh the network_config Ec2 cache if network key is absent. | ||
2030 | 312 | |||
2031 | 313 | This catches an upgrade issue where obj.pkl contained stale metadata | ||
2032 | 314 | which lacked newly required network key. | ||
2033 | 315 | """ | ||
2034 | 316 | old_metadata = copy.deepcopy(DEFAULT_METADATA) | ||
2035 | 317 | old_metadata.pop('network') | ||
2036 | 318 | ds = self._setup_ds( | ||
2037 | 319 | platform_data=self.valid_platform_data, | ||
2038 | 320 | sys_cfg={'datasource': {'Ec2': {'strict_id': True}}}, | ||
2039 | 321 | md=old_metadata) | ||
2040 | 322 | self.assertTrue(ds.get_data()) | ||
2041 | 323 | # Provide new revision of metadata that contains network data | ||
2042 | 324 | register_mock_metaserver( | ||
2043 | 325 | 'http://169.254.169.254/2009-04-04/meta-data/', DEFAULT_METADATA) | ||
2044 | 326 | mac1 = '06:17:04:d7:26:09' # Defined in DEFAULT_METADATA | ||
2045 | 327 | get_interface_mac_path = ( | ||
2046 | 328 | 'cloudinit.sources.DataSourceEc2.net.get_interface_mac') | ||
2047 | 329 | ds.fallback_nic = 'eth9' | ||
2048 | 330 | with mock.patch(get_interface_mac_path) as m_get_interface_mac: | ||
2049 | 331 | m_get_interface_mac.return_value = mac1 | ||
2050 | 332 | ds.network_config # Will re-crawl network metadata | ||
2051 | 333 | self.assertIn('Re-crawl of metadata service', self.logs.getvalue()) | ||
2052 | 334 | expected = {'version': 1, 'config': [ | ||
2053 | 335 | {'mac_address': '06:17:04:d7:26:09', | ||
2054 | 336 | 'name': 'eth9', | ||
2055 | 337 | 'subnets': [{'type': 'dhcp4'}, {'type': 'dhcp6'}], | ||
2056 | 338 | 'type': 'physical'}]} | ||
2057 | 339 | self.assertEqual(expected, ds.network_config) | ||
2058 | 340 | |||
2059 | 341 | @httpretty.activate | ||
2060 | 342 | @mock.patch('cloudinit.net.dhcp.maybe_perform_dhcp_discovery') | ||
2061 | 310 | def test_valid_platform_with_strict_true(self, m_dhcp): | 343 | def test_valid_platform_with_strict_true(self, m_dhcp): |
2062 | 311 | """Valid platform data should return true with strict_id true.""" | 344 | """Valid platform data should return true with strict_id true.""" |
2063 | 312 | ds = self._setup_ds( | 345 | ds = self._setup_ds( |
2064 | diff --git a/tests/unittests/test_handler/test_handler_etc_hosts.py b/tests/unittests/test_handler/test_handler_etc_hosts.py | |||
2065 | 313 | new file mode 100644 | 346 | new file mode 100644 |
2066 | index 0000000..ced05a8 | |||
2067 | --- /dev/null | |||
2068 | +++ b/tests/unittests/test_handler/test_handler_etc_hosts.py | |||
2069 | @@ -0,0 +1,69 @@ | |||
2070 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
2071 | 2 | |||
2072 | 3 | from cloudinit.config import cc_update_etc_hosts | ||
2073 | 4 | |||
2074 | 5 | from cloudinit import cloud | ||
2075 | 6 | from cloudinit import distros | ||
2076 | 7 | from cloudinit import helpers | ||
2077 | 8 | from cloudinit import util | ||
2078 | 9 | |||
2079 | 10 | from cloudinit.tests import helpers as t_help | ||
2080 | 11 | |||
2081 | 12 | import logging | ||
2082 | 13 | import os | ||
2083 | 14 | import shutil | ||
2084 | 15 | |||
2085 | 16 | LOG = logging.getLogger(__name__) | ||
2086 | 17 | |||
2087 | 18 | |||
2088 | 19 | class TestHostsFile(t_help.FilesystemMockingTestCase): | ||
2089 | 20 | def setUp(self): | ||
2090 | 21 | super(TestHostsFile, self).setUp() | ||
2091 | 22 | self.tmp = self.tmp_dir() | ||
2092 | 23 | |||
2093 | 24 | def _fetch_distro(self, kind): | ||
2094 | 25 | cls = distros.fetch(kind) | ||
2095 | 26 | paths = helpers.Paths({}) | ||
2096 | 27 | return cls(kind, {}, paths) | ||
2097 | 28 | |||
2098 | 29 | def test_write_etc_hosts_suse_localhost(self): | ||
2099 | 30 | cfg = { | ||
2100 | 31 | 'manage_etc_hosts': 'localhost', | ||
2101 | 32 | 'hostname': 'cloud-init.test.us' | ||
2102 | 33 | } | ||
2103 | 34 | os.makedirs('%s/etc/' % self.tmp) | ||
2104 | 35 | hosts_content = '192.168.1.1 blah.blah.us blah\n' | ||
2105 | 36 | fout = open('%s/etc/hosts' % self.tmp, 'w') | ||
2106 | 37 | fout.write(hosts_content) | ||
2107 | 38 | fout.close() | ||
2108 | 39 | distro = self._fetch_distro('sles') | ||
2109 | 40 | distro.hosts_fn = '%s/etc/hosts' % self.tmp | ||
2110 | 41 | paths = helpers.Paths({}) | ||
2111 | 42 | ds = None | ||
2112 | 43 | cc = cloud.Cloud(ds, paths, {}, distro, None) | ||
2113 | 44 | self.patchUtils(self.tmp) | ||
2114 | 45 | cc_update_etc_hosts.handle('test', cfg, cc, LOG, []) | ||
2115 | 46 | contents = util.load_file('%s/etc/hosts' % self.tmp) | ||
2116 | 47 | if '127.0.0.1\tcloud-init.test.us\tcloud-init' not in contents: | ||
2117 | 48 | self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') | ||
2118 | 49 | if '192.168.1.1\tblah.blah.us\tblah' not in contents: | ||
2119 | 50 | self.assertIsNone('Default etc/hosts content modified') | ||
2120 | 51 | |||
2121 | 52 | def test_write_etc_hosts_suse_template(self): | ||
2122 | 53 | cfg = { | ||
2123 | 54 | 'manage_etc_hosts': 'template', | ||
2124 | 55 | 'hostname': 'cloud-init.test.us' | ||
2125 | 56 | } | ||
2126 | 57 | shutil.copytree('templates', '%s/etc/cloud/templates' % self.tmp) | ||
2127 | 58 | distro = self._fetch_distro('sles') | ||
2128 | 59 | paths = helpers.Paths({}) | ||
2129 | 60 | paths.template_tpl = '%s' % self.tmp + '/etc/cloud/templates/%s.tmpl' | ||
2130 | 61 | ds = None | ||
2131 | 62 | cc = cloud.Cloud(ds, paths, {}, distro, None) | ||
2132 | 63 | self.patchUtils(self.tmp) | ||
2133 | 64 | cc_update_etc_hosts.handle('test', cfg, cc, LOG, []) | ||
2134 | 65 | contents = util.load_file('%s/etc/hosts' % self.tmp) | ||
2135 | 66 | if '127.0.0.1 cloud-init.test.us cloud-init' not in contents: | ||
2136 | 67 | self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') | ||
2137 | 68 | if '::1 cloud-init.test.us cloud-init' not in contents: | ||
2138 | 69 | self.assertIsNone('No entry for 127.0.0.1 in etc/hosts') | ||
2139 | diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py | |||
2140 | index 3abe578..28a8455 100644 | |||
2141 | --- a/tests/unittests/test_handler/test_handler_ntp.py | |||
2142 | +++ b/tests/unittests/test_handler/test_handler_ntp.py | |||
2143 | @@ -430,5 +430,31 @@ class TestNtp(FilesystemMockingTestCase): | |||
2144 | 430 | "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n", | 430 | "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n", |
2145 | 431 | content.decode()) | 431 | content.decode()) |
2146 | 432 | 432 | ||
2147 | 433 | def test_write_ntp_config_template_defaults_pools_empty_lists_sles(self): | ||
2148 | 434 | """write_ntp_config_template defaults pools servers upon empty config. | ||
2149 | 435 | |||
2150 | 436 | When both pools and servers are empty, default NR_POOL_SERVERS get | ||
2151 | 437 | configured. | ||
2152 | 438 | """ | ||
2153 | 439 | distro = 'sles' | ||
2154 | 440 | mycloud = self._get_cloud(distro) | ||
2155 | 441 | ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist | ||
2156 | 442 | # Create ntp.conf.tmpl | ||
2157 | 443 | with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: | ||
2158 | 444 | stream.write(NTP_TEMPLATE) | ||
2159 | 445 | with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): | ||
2160 | 446 | cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) | ||
2161 | 447 | content = util.read_file_or_url('file://' + ntp_conf).contents | ||
2162 | 448 | default_pools = [ | ||
2163 | 449 | "{0}.opensuse.pool.ntp.org".format(x) | ||
2164 | 450 | for x in range(0, cc_ntp.NR_POOL_SERVERS)] | ||
2165 | 451 | self.assertEqual( | ||
2166 | 452 | "servers []\npools {0}\n".format(default_pools), | ||
2167 | 453 | content.decode()) | ||
2168 | 454 | self.assertIn( | ||
2169 | 455 | "Adding distro default ntp pool servers: {0}".format( | ||
2170 | 456 | ",".join(default_pools)), | ||
2171 | 457 | self.logs.getvalue()) | ||
2172 | 458 | |||
2173 | 433 | 459 | ||
2174 | 434 | # vi: ts=4 expandtab | 460 | # vi: ts=4 expandtab |
2175 | diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/test_rh_subscription.py | |||
2176 | index e9d5702..2271810 100644 | |||
2177 | --- a/tests/unittests/test_rh_subscription.py | |||
2178 | +++ b/tests/unittests/test_rh_subscription.py | |||
2179 | @@ -2,6 +2,7 @@ | |||
2180 | 2 | 2 | ||
2181 | 3 | """Tests for registering RHEL subscription via rh_subscription.""" | 3 | """Tests for registering RHEL subscription via rh_subscription.""" |
2182 | 4 | 4 | ||
2183 | 5 | import copy | ||
2184 | 5 | import logging | 6 | import logging |
2185 | 6 | 7 | ||
2186 | 7 | from cloudinit.config import cc_rh_subscription | 8 | from cloudinit.config import cc_rh_subscription |
2187 | @@ -68,6 +69,20 @@ class GoodTests(TestCase): | |||
2188 | 68 | self.assertEqual(self.SM.log_success.call_count, 1) | 69 | self.assertEqual(self.SM.log_success.call_count, 1) |
2189 | 69 | self.assertEqual(self.SM._sub_man_cli.call_count, 2) | 70 | self.assertEqual(self.SM._sub_man_cli.call_count, 2) |
2190 | 70 | 71 | ||
2191 | 72 | @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_getRepos") | ||
2192 | 73 | @mock.patch.object(cc_rh_subscription.SubscriptionManager, "_sub_man_cli") | ||
2193 | 74 | def test_update_repos_disable_with_none(self, m_sub_man_cli, m_get_repos): | ||
2194 | 75 | cfg = copy.deepcopy(self.config) | ||
2195 | 76 | m_get_repos.return_value = ([], ['repo1']) | ||
2196 | 77 | m_sub_man_cli.return_value = (b'', b'') | ||
2197 | 78 | cfg['rh_subscription'].update( | ||
2198 | 79 | {'enable-repo': ['repo1'], 'disable-repo': None}) | ||
2199 | 80 | mysm = cc_rh_subscription.SubscriptionManager(cfg) | ||
2200 | 81 | self.assertEqual(True, mysm.update_repos()) | ||
2201 | 82 | m_get_repos.assert_called_with() | ||
2202 | 83 | self.assertEqual(m_sub_man_cli.call_args_list, | ||
2203 | 84 | [mock.call(['repos', '--enable=repo1'])]) | ||
2204 | 85 | |||
2205 | 71 | def test_full_registration(self): | 86 | def test_full_registration(self): |
2206 | 72 | ''' | 87 | ''' |
2207 | 73 | Registration with auto-attach, service-level, adding pools, | 88 | Registration with auto-attach, service-level, adding pools, |
PASSED: Continuous integration, rev:e6747555e0d c787fa99bd6bfff 43846b02ef1bd1 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 532/
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/ 532/rebuild
https:/