Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel
- Git
- lp:~chad.smith/cloud-init
- ubuntu/devel
- Merge into ubuntu/devel
Proposed by
Chad Smith
Status: | Merged | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Merged at revision: | a6262577f56d32fb6005a55f9022309c5dc7dce5 | ||||||||||||||||
Proposed branch: | ~chad.smith/cloud-init:ubuntu/devel | ||||||||||||||||
Merge into: | cloud-init:ubuntu/devel | ||||||||||||||||
Diff against target: |
1999 lines (+765/-204) 46 files modified
.pylintrc (+11/-1) cloudinit/cloud.py (+3/-2) cloudinit/cmd/main.py (+29/-6) cloudinit/cmd/tests/test_main.py (+161/-0) cloudinit/config/cc_keys_to_console.py (+1/-3) cloudinit/config/cc_runcmd.py (+4/-2) cloudinit/config/cc_salt_minion.py (+59/-23) cloudinit/config/cc_set_hostname.py (+35/-6) cloudinit/config/cc_ssh_authkey_fingerprints.py (+4/-5) cloudinit/distros/arch.py (+1/-4) cloudinit/distros/freebsd.py (+6/-0) cloudinit/distros/opensuse.py (+2/-3) cloudinit/sources/DataSourceAzure.py (+2/-0) cloudinit/sources/DataSourceOpenNebula.py (+1/-4) cloudinit/sources/__init__.py (+17/-4) cloudinit/sources/tests/test_init.py (+69/-1) cloudinit/stages.py (+1/-2) cloudinit/tests/helpers.py (+13/-0) cloudinit/tests/test_util.py (+97/-0) cloudinit/url_helper.py (+2/-2) cloudinit/util.py (+32/-14) config/cloud.cfg.tmpl (+1/-1) debian/changelog (+22/-0) doc/rtd/topics/capabilities.rst (+8/-6) doc/rtd/topics/debugging.rst (+31/-26) doc/rtd/topics/network-config.rst (+2/-2) doc/rtd/topics/tests.rst (+10/-10) tests/cloud_tests/bddeb.py (+1/-1) tests/cloud_tests/platforms/ec2/__init__.py (+0/-0) tests/cloud_tests/platforms/lxd/__init__.py (+0/-0) tests/cloud_tests/platforms/lxd/platform.py (+0/-4) tests/cloud_tests/platforms/nocloudkvm/__init__.py (+0/-0) tests/cloud_tests/platforms/nocloudkvm/instance.py (+1/-1) tests/cloud_tests/platforms/nocloudkvm/platform.py (+0/-4) tests/cloud_tests/platforms/platforms.py (+12/-2) tests/cloud_tests/testcases/modules/salt_minion.py (+5/-0) tests/cloud_tests/testcases/modules/salt_minion.yaml (+4/-1) tests/cloud_tests/util.py (+5/-1) tests/unittests/test_datasource/test_azure.py (+15/-0) tests/unittests/test_handler/test_handler_bootcmd.py (+7/-12) tests/unittests/test_handler/test_handler_ntp.py (+6/-12) tests/unittests/test_handler/test_handler_resizefs.py (+3/-11) tests/unittests/test_handler/test_handler_runcmd.py (+4/-10) tests/unittests/test_handler/test_handler_set_hostname.py (+53/-4) tests/unittests/test_handler/test_schema.py (+7/-14) tests/unittests/test_util.py (+18/-0) |
||||||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Scott Moser | Pending | ||
Review via email:
|
Commit message
Description of the change
Sync tip of master for publish in Bionic.
To post a comment you must log in.
Revision history for this message

Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/.pylintrc b/.pylintrc |
2 | index 05a086d..0bdfa59 100644 |
3 | --- a/.pylintrc |
4 | +++ b/.pylintrc |
5 | @@ -46,7 +46,17 @@ reports=no |
6 | # (useful for modules/projects where namespaces are manipulated during runtime |
7 | # and thus existing member attributes cannot be deduced by static analysis. It |
8 | # supports qualified module names, as well as Unix pattern matching. |
9 | -ignored-modules=six.moves,pkg_resources,httplib,http.client,paramiko,simplestreams |
10 | +ignored-modules= |
11 | + http.client, |
12 | + httplib, |
13 | + pkg_resources, |
14 | + six.moves, |
15 | + # cloud_tests requirements. |
16 | + boto3, |
17 | + botocore, |
18 | + paramiko, |
19 | + pylxd, |
20 | + simplestreams |
21 | |
22 | # List of class names for which member attributes should not be checked (useful |
23 | # for classes with dynamically set attributes). This supports the use of |
24 | diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py |
25 | index ba61678..6d12c43 100644 |
26 | --- a/cloudinit/cloud.py |
27 | +++ b/cloudinit/cloud.py |
28 | @@ -78,8 +78,9 @@ class Cloud(object): |
29 | def get_locale(self): |
30 | return self.datasource.get_locale() |
31 | |
32 | - def get_hostname(self, fqdn=False): |
33 | - return self.datasource.get_hostname(fqdn=fqdn) |
34 | + def get_hostname(self, fqdn=False, metadata_only=False): |
35 | + return self.datasource.get_hostname( |
36 | + fqdn=fqdn, metadata_only=metadata_only) |
37 | |
38 | def device_name_to_device(self, name): |
39 | return self.datasource.device_name_to_device(name) |
40 | diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py |
41 | index d2f1b77..3f2dbb9 100644 |
42 | --- a/cloudinit/cmd/main.py |
43 | +++ b/cloudinit/cmd/main.py |
44 | @@ -40,6 +40,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE, |
45 | |
46 | from cloudinit import atomic_helper |
47 | |
48 | +from cloudinit.config import cc_set_hostname |
49 | from cloudinit.dhclient_hook import LogDhclient |
50 | |
51 | |
52 | @@ -215,12 +216,10 @@ def main_init(name, args): |
53 | if args.local: |
54 | deps = [sources.DEP_FILESYSTEM] |
55 | |
56 | - early_logs = [] |
57 | - early_logs.append( |
58 | - attempt_cmdline_url( |
59 | - path=os.path.join("%s.d" % CLOUD_CONFIG, |
60 | - "91_kernel_cmdline_url.cfg"), |
61 | - network=not args.local)) |
62 | + early_logs = [attempt_cmdline_url( |
63 | + path=os.path.join("%s.d" % CLOUD_CONFIG, |
64 | + "91_kernel_cmdline_url.cfg"), |
65 | + network=not args.local)] |
66 | |
67 | # Cloud-init 'init' stage is broken up into the following sub-stages |
68 | # 1. Ensure that the init object fetches its config without errors |
69 | @@ -354,6 +353,11 @@ def main_init(name, args): |
70 | LOG.debug("[%s] %s will now be targeting instance id: %s. new=%s", |
71 | mode, name, iid, init.is_new_instance()) |
72 | |
73 | + if mode == sources.DSMODE_LOCAL: |
74 | + # Before network comes up, set any configured hostname to allow |
75 | + # dhcp clients to advertize this hostname to any DDNS services |
76 | + # LP: #1746455. |
77 | + _maybe_set_hostname(init, stage='local', retry_stage='network') |
78 | init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL)) |
79 | |
80 | if mode == sources.DSMODE_LOCAL: |
81 | @@ -370,6 +374,7 @@ def main_init(name, args): |
82 | init.setup_datasource() |
83 | # update fully realizes user-data (pulling in #include if necessary) |
84 | init.update() |
85 | + _maybe_set_hostname(init, stage='init-net', retry_stage='modules:config') |
86 | # Stage 7 |
87 | try: |
88 | # Attempt to consume the data per instance. |
89 | @@ -683,6 +688,24 @@ def status_wrapper(name, args, data_d=None, link_d=None): |
90 | return len(v1[mode]['errors']) |
91 | |
92 | |
93 | +def _maybe_set_hostname(init, stage, retry_stage): |
94 | + """Call set-hostname if metadata, vendordata or userdata provides it. |
95 | + |
96 | + @param stage: String representing current stage in which we are running. |
97 | + @param retry_stage: String represented logs upon error setting hostname. |
98 | + """ |
99 | + cloud = init.cloudify() |
100 | + (hostname, _fqdn) = util.get_hostname_fqdn( |
101 | + init.cfg, cloud, metadata_only=True) |
102 | + if hostname: # meta-data or user-data hostname content |
103 | + try: |
104 | + cc_set_hostname.handle('set-hostname', init.cfg, cloud, LOG, None) |
105 | + except cc_set_hostname.SetHostnameError as e: |
106 | + LOG.debug( |
107 | + 'Failed setting hostname in %s stage. Will' |
108 | + ' retry in %s stage. Error: %s.', stage, retry_stage, str(e)) |
109 | + |
110 | + |
111 | def main_features(name, args): |
112 | sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n') |
113 | |
114 | diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py |
115 | new file mode 100644 |
116 | index 0000000..dbe421c |
117 | --- /dev/null |
118 | +++ b/cloudinit/cmd/tests/test_main.py |
119 | @@ -0,0 +1,161 @@ |
120 | +# This file is part of cloud-init. See LICENSE file for license information. |
121 | + |
122 | +from collections import namedtuple |
123 | +import copy |
124 | +import os |
125 | +from six import StringIO |
126 | + |
127 | +from cloudinit.cmd import main |
128 | +from cloudinit.util import ( |
129 | + ensure_dir, load_file, write_file, yaml_dumps) |
130 | +from cloudinit.tests.helpers import ( |
131 | + FilesystemMockingTestCase, wrap_and_call) |
132 | + |
133 | +mypaths = namedtuple('MyPaths', 'run_dir') |
134 | +myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand') |
135 | + |
136 | + |
137 | +class TestMain(FilesystemMockingTestCase): |
138 | + |
139 | + with_logs = True |
140 | + |
141 | + def setUp(self): |
142 | + super(TestMain, self).setUp() |
143 | + self.new_root = self.tmp_dir() |
144 | + self.cloud_dir = self.tmp_path('var/lib/cloud/', dir=self.new_root) |
145 | + os.makedirs(self.cloud_dir) |
146 | + self.replicateTestRoot('simple_ubuntu', self.new_root) |
147 | + self.cfg = { |
148 | + 'datasource_list': ['None'], |
149 | + 'runcmd': ['ls /etc'], # test ALL_DISTROS |
150 | + 'system_info': {'paths': {'cloud_dir': self.cloud_dir, |
151 | + 'run_dir': self.new_root}}, |
152 | + 'write_files': [ |
153 | + { |
154 | + 'path': '/etc/blah.ini', |
155 | + 'content': 'blah', |
156 | + 'permissions': 0o755, |
157 | + }, |
158 | + ], |
159 | + 'cloud_init_modules': ['write-files', 'runcmd'], |
160 | + } |
161 | + cloud_cfg = yaml_dumps(self.cfg) |
162 | + ensure_dir(os.path.join(self.new_root, 'etc', 'cloud')) |
163 | + self.cloud_cfg_file = os.path.join( |
164 | + self.new_root, 'etc', 'cloud', 'cloud.cfg') |
165 | + write_file(self.cloud_cfg_file, cloud_cfg) |
166 | + self.patchOS(self.new_root) |
167 | + self.patchUtils(self.new_root) |
168 | + self.stderr = StringIO() |
169 | + self.patchStdoutAndStderr(stderr=self.stderr) |
170 | + |
171 | + def test_main_init_run_net_stops_on_file_no_net(self): |
172 | + """When no-net file is present, main_init does not process modules.""" |
173 | + stop_file = os.path.join(self.cloud_dir, 'data', 'no-net') # stop file |
174 | + write_file(stop_file, '') |
175 | + cmdargs = myargs( |
176 | + debug=False, files=None, force=False, local=False, reporter=None, |
177 | + subcommand='init') |
178 | + (item1, item2) = wrap_and_call( |
179 | + 'cloudinit.cmd.main', |
180 | + {'util.close_stdin': True, |
181 | + 'netinfo.debug_info': 'my net debug info', |
182 | + 'util.fixup_output': ('outfmt', 'errfmt')}, |
183 | + main.main_init, 'init', cmdargs) |
184 | + # We should not run write_files module |
185 | + self.assertFalse( |
186 | + os.path.exists(os.path.join(self.new_root, 'etc/blah.ini')), |
187 | + 'Unexpected run of write_files module produced blah.ini') |
188 | + self.assertEqual([], item2) |
189 | + # Instancify is called |
190 | + instance_id_path = 'var/lib/cloud/data/instance-id' |
191 | + self.assertFalse( |
192 | + os.path.exists(os.path.join(self.new_root, instance_id_path)), |
193 | + 'Unexpected call to datasource.instancify produced instance-id') |
194 | + expected_logs = [ |
195 | + "Exiting. stop file ['{stop_file}'] existed\n".format( |
196 | + stop_file=stop_file), |
197 | + 'my net debug info' # netinfo.debug_info |
198 | + ] |
199 | + for log in expected_logs: |
200 | + self.assertIn(log, self.stderr.getvalue()) |
201 | + |
202 | + def test_main_init_run_net_runs_modules(self): |
203 | + """Modules like write_files are run in 'net' mode.""" |
204 | + cmdargs = myargs( |
205 | + debug=False, files=None, force=False, local=False, reporter=None, |
206 | + subcommand='init') |
207 | + (item1, item2) = wrap_and_call( |
208 | + 'cloudinit.cmd.main', |
209 | + {'util.close_stdin': True, |
210 | + 'netinfo.debug_info': 'my net debug info', |
211 | + 'util.fixup_output': ('outfmt', 'errfmt')}, |
212 | + main.main_init, 'init', cmdargs) |
213 | + self.assertEqual([], item2) |
214 | + # Instancify is called |
215 | + instance_id_path = 'var/lib/cloud/data/instance-id' |
216 | + self.assertEqual( |
217 | + 'iid-datasource-none\n', |
218 | + os.path.join(load_file( |
219 | + os.path.join(self.new_root, instance_id_path)))) |
220 | + # modules are run (including write_files) |
221 | + self.assertEqual( |
222 | + 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini'))) |
223 | + expected_logs = [ |
224 | + 'network config is disabled by fallback', # apply_network_config |
225 | + 'my net debug info', # netinfo.debug_info |
226 | + 'no previous run detected' |
227 | + ] |
228 | + for log in expected_logs: |
229 | + self.assertIn(log, self.stderr.getvalue()) |
230 | + |
231 | + def test_main_init_run_net_calls_set_hostname_when_metadata_present(self): |
232 | + """When local-hostname metadata is present, call cc_set_hostname.""" |
233 | + self.cfg['datasource'] = { |
234 | + 'None': {'metadata': {'local-hostname': 'md-hostname'}}} |
235 | + cloud_cfg = yaml_dumps(self.cfg) |
236 | + write_file(self.cloud_cfg_file, cloud_cfg) |
237 | + cmdargs = myargs( |
238 | + debug=False, files=None, force=False, local=False, reporter=None, |
239 | + subcommand='init') |
240 | + |
241 | + def set_hostname(name, cfg, cloud, log, args): |
242 | + self.assertEqual('set-hostname', name) |
243 | + updated_cfg = copy.deepcopy(self.cfg) |
244 | + updated_cfg.update( |
245 | + {'def_log_file': '/var/log/cloud-init.log', |
246 | + 'log_cfgs': [], |
247 | + 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel'], |
248 | + 'vendor_data': {'enabled': True, 'prefix': []}}) |
249 | + updated_cfg.pop('system_info') |
250 | + |
251 | + self.assertEqual(updated_cfg, cfg) |
252 | + self.assertEqual(main.LOG, log) |
253 | + self.assertIsNone(args) |
254 | + |
255 | + (item1, item2) = wrap_and_call( |
256 | + 'cloudinit.cmd.main', |
257 | + {'util.close_stdin': True, |
258 | + 'netinfo.debug_info': 'my net debug info', |
259 | + 'cc_set_hostname.handle': {'side_effect': set_hostname}, |
260 | + 'util.fixup_output': ('outfmt', 'errfmt')}, |
261 | + main.main_init, 'init', cmdargs) |
262 | + self.assertEqual([], item2) |
263 | + # Instancify is called |
264 | + instance_id_path = 'var/lib/cloud/data/instance-id' |
265 | + self.assertEqual( |
266 | + 'iid-datasource-none\n', |
267 | + os.path.join(load_file( |
268 | + os.path.join(self.new_root, instance_id_path)))) |
269 | + # modules are run (including write_files) |
270 | + self.assertEqual( |
271 | + 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini'))) |
272 | + expected_logs = [ |
273 | + 'network config is disabled by fallback', # apply_network_config |
274 | + 'my net debug info', # netinfo.debug_info |
275 | + 'no previous run detected' |
276 | + ] |
277 | + for log in expected_logs: |
278 | + self.assertIn(log, self.stderr.getvalue()) |
279 | + |
280 | +# vi: ts=4 expandtab |
281 | diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py |
282 | index efedd4a..aff4010 100644 |
283 | --- a/cloudinit/config/cc_keys_to_console.py |
284 | +++ b/cloudinit/config/cc_keys_to_console.py |
285 | @@ -63,9 +63,7 @@ def handle(name, cfg, cloud, log, _args): |
286 | ["ssh-dss"]) |
287 | |
288 | try: |
289 | - cmd = [helper_path] |
290 | - cmd.append(','.join(fp_blacklist)) |
291 | - cmd.append(','.join(key_blacklist)) |
292 | + cmd = [helper_path, ','.join(fp_blacklist), ','.join(key_blacklist)] |
293 | (stdout, _stderr) = util.subp(cmd) |
294 | util.multi_log("%s\n" % (stdout.strip()), |
295 | stderr=False, console=True) |
296 | diff --git a/cloudinit/config/cc_runcmd.py b/cloudinit/config/cc_runcmd.py |
297 | index 449872f..539cbd5 100644 |
298 | --- a/cloudinit/config/cc_runcmd.py |
299 | +++ b/cloudinit/config/cc_runcmd.py |
300 | @@ -39,8 +39,10 @@ schema = { |
301 | using ``sh``. |
302 | |
303 | .. note:: |
304 | - all commands must be proper yaml, so you have to quote any characters |
305 | - yaml would eat (':' can be problematic)"""), |
306 | + |
307 | + all commands must be proper yaml, so you have to quote any characters |
308 | + yaml would eat (':' can be problematic) |
309 | + """), |
310 | 'distros': distros, |
311 | 'examples': [dedent("""\ |
312 | runcmd: |
313 | diff --git a/cloudinit/config/cc_salt_minion.py b/cloudinit/config/cc_salt_minion.py |
314 | index 5112a34..d6a21d7 100644 |
315 | --- a/cloudinit/config/cc_salt_minion.py |
316 | +++ b/cloudinit/config/cc_salt_minion.py |
317 | @@ -12,7 +12,9 @@ key is present in the config parts, then salt minion will be installed and |
318 | started. Configuration for salt minion can be specified in the ``conf`` key |
319 | under ``salt_minion``. Any conf values present there will be assigned in |
320 | ``/etc/salt/minion``. The public and private keys to use for salt minion can be |
321 | -specified with ``public_key`` and ``private_key`` respectively. |
322 | +specified with ``public_key`` and ``private_key`` respectively. Optionally if |
323 | +you have a custom package name, service name or config directory you can |
324 | +specify them with ``pkg_name``, ``service_name`` and ``config_dir``. |
325 | |
326 | **Internal name:** ``cc_salt_minion`` |
327 | |
328 | @@ -23,6 +25,9 @@ specified with ``public_key`` and ``private_key`` respectively. |
329 | **Config keys**:: |
330 | |
331 | salt_minion: |
332 | + pkg_name: 'salt-minion' |
333 | + service_name: 'salt-minion' |
334 | + config_dir: '/etc/salt' |
335 | conf: |
336 | master: salt.example.com |
337 | grains: |
338 | @@ -42,7 +47,34 @@ import os |
339 | |
340 | from cloudinit import util |
341 | |
342 | -# Note: see http://saltstack.org/topics/installation/ |
343 | +# Note: see https://docs.saltstack.com/en/latest/topics/installation/ |
344 | +# Note: see https://docs.saltstack.com/en/latest/ref/configuration/ |
345 | + |
346 | + |
347 | +class SaltConstants(object): |
348 | + """ |
349 | + defines default distribution specific salt variables |
350 | + """ |
351 | + def __init__(self, cfg): |
352 | + |
353 | + # constants tailored for FreeBSD |
354 | + if util.is_FreeBSD(): |
355 | + self.pkg_name = 'py27-salt' |
356 | + self.srv_name = 'salt_minion' |
357 | + self.conf_dir = '/usr/local/etc/salt' |
358 | + # constants for any other OS |
359 | + else: |
360 | + self.pkg_name = 'salt-minion' |
361 | + self.srv_name = 'salt-minion' |
362 | + self.conf_dir = '/etc/salt' |
363 | + |
364 | + # if there are constants given in cloud config use those |
365 | + self.pkg_name = util.get_cfg_option_str(cfg, 'pkg_name', |
366 | + self.pkg_name) |
367 | + self.conf_dir = util.get_cfg_option_str(cfg, 'config_dir', |
368 | + self.conf_dir) |
369 | + self.srv_name = util.get_cfg_option_str(cfg, 'service_name', |
370 | + self.srv_name) |
371 | |
372 | |
373 | def handle(name, cfg, cloud, log, _args): |
374 | @@ -52,45 +84,49 @@ def handle(name, cfg, cloud, log, _args): |
375 | " no 'salt_minion' key in configuration"), name) |
376 | return |
377 | |
378 | - salt_cfg = cfg['salt_minion'] |
379 | + s_cfg = cfg['salt_minion'] |
380 | + const = SaltConstants(cfg=s_cfg) |
381 | |
382 | # Start by installing the salt package ... |
383 | - cloud.distro.install_packages(('salt-minion',)) |
384 | + cloud.distro.install_packages(const.pkg_name) |
385 | |
386 | # Ensure we can configure files at the right dir |
387 | - config_dir = salt_cfg.get("config_dir", '/etc/salt') |
388 | - util.ensure_dir(config_dir) |
389 | + util.ensure_dir(const.conf_dir) |
390 | |
391 | # ... and then update the salt configuration |
392 | - if 'conf' in salt_cfg: |
393 | - # Add all sections from the conf object to /etc/salt/minion |
394 | - minion_config = os.path.join(config_dir, 'minion') |
395 | - minion_data = util.yaml_dumps(salt_cfg.get('conf')) |
396 | + if 'conf' in s_cfg: |
397 | + # Add all sections from the conf object to minion config file |
398 | + minion_config = os.path.join(const.conf_dir, 'minion') |
399 | + minion_data = util.yaml_dumps(s_cfg.get('conf')) |
400 | util.write_file(minion_config, minion_data) |
401 | |
402 | - if 'grains' in salt_cfg: |
403 | + if 'grains' in s_cfg: |
404 | # add grains to /etc/salt/grains |
405 | - grains_config = os.path.join(config_dir, 'grains') |
406 | - grains_data = util.yaml_dumps(salt_cfg.get('grains')) |
407 | + grains_config = os.path.join(const.conf_dir, 'grains') |
408 | + grains_data = util.yaml_dumps(s_cfg.get('grains')) |
409 | util.write_file(grains_config, grains_data) |
410 | |
411 | # ... copy the key pair if specified |
412 | - if 'public_key' in salt_cfg and 'private_key' in salt_cfg: |
413 | - if os.path.isdir("/etc/salt/pki/minion"): |
414 | - pki_dir_default = "/etc/salt/pki/minion" |
415 | - else: |
416 | - pki_dir_default = "/etc/salt/pki" |
417 | + if 'public_key' in s_cfg and 'private_key' in s_cfg: |
418 | + pki_dir_default = os.path.join(const.conf_dir, "pki/minion") |
419 | + if not os.path.isdir(pki_dir_default): |
420 | + pki_dir_default = os.path.join(const.conf_dir, "pki") |
421 | |
422 | - pki_dir = salt_cfg.get('pki_dir', pki_dir_default) |
423 | + pki_dir = s_cfg.get('pki_dir', pki_dir_default) |
424 | with util.umask(0o77): |
425 | util.ensure_dir(pki_dir) |
426 | pub_name = os.path.join(pki_dir, 'minion.pub') |
427 | pem_name = os.path.join(pki_dir, 'minion.pem') |
428 | - util.write_file(pub_name, salt_cfg['public_key']) |
429 | - util.write_file(pem_name, salt_cfg['private_key']) |
430 | + util.write_file(pub_name, s_cfg['public_key']) |
431 | + util.write_file(pem_name, s_cfg['private_key']) |
432 | + |
433 | + # we need to have the salt minion service enabled in rc in order to be |
434 | + # able to start the service. this does only apply on FreeBSD servers. |
435 | + if cloud.distro.osfamily == 'freebsd': |
436 | + cloud.distro.updatercconf('salt_minion_enable', 'YES') |
437 | |
438 | - # restart salt-minion. 'service' will start even if not started. if it |
439 | + # restart salt-minion. 'service' will start even if not started. if it |
440 | # was started, it needs to be restarted for config change. |
441 | - util.subp(['service', 'salt-minion', 'restart'], capture=False) |
442 | + util.subp(['service', const.srv_name, 'restart'], capture=False) |
443 | |
444 | # vi: ts=4 expandtab |
445 | diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py |
446 | index aa3dfe5..3d2b2da 100644 |
447 | --- a/cloudinit/config/cc_set_hostname.py |
448 | +++ b/cloudinit/config/cc_set_hostname.py |
449 | @@ -32,22 +32,51 @@ will be used. |
450 | hostname: <fqdn/hostname> |
451 | """ |
452 | |
453 | +import os |
454 | + |
455 | + |
456 | +from cloudinit.atomic_helper import write_json |
457 | from cloudinit import util |
458 | |
459 | |
460 | +class SetHostnameError(Exception): |
461 | + """Raised when the distro runs into an exception when setting hostname. |
462 | + |
463 | + This may happen if we attempt to set the hostname early in cloud-init's |
464 | + init-local timeframe as certain services may not be running yet. |
465 | + """ |
466 | + pass |
467 | + |
468 | + |
469 | def handle(name, cfg, cloud, log, _args): |
470 | if util.get_cfg_option_bool(cfg, "preserve_hostname", False): |
471 | log.debug(("Configuration option 'preserve_hostname' is set," |
472 | " not setting the hostname in module %s"), name) |
473 | return |
474 | - |
475 | (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) |
476 | + # Check for previous successful invocation of set-hostname |
477 | + |
478 | + # set-hostname artifact file accounts for both hostname and fqdn |
479 | + # deltas. As such, it's format is different than cc_update_hostname's |
480 | + # previous-hostname file which only contains the base hostname. |
481 | + # TODO consolidate previous-hostname and set-hostname artifact files and |
482 | + # distro._read_hostname implementation so we only validate one artifact. |
483 | + prev_fn = os.path.join(cloud.get_cpath('data'), "set-hostname") |
484 | + prev_hostname = {} |
485 | + if os.path.exists(prev_fn): |
486 | + prev_hostname = util.load_json(util.load_file(prev_fn)) |
487 | + hostname_changed = (hostname != prev_hostname.get('hostname') or |
488 | + fqdn != prev_hostname.get('fqdn')) |
489 | + if not hostname_changed: |
490 | + log.debug('No hostname changes. Skipping set-hostname') |
491 | + return |
492 | + log.debug("Setting the hostname to %s (%s)", fqdn, hostname) |
493 | try: |
494 | - log.debug("Setting the hostname to %s (%s)", fqdn, hostname) |
495 | cloud.distro.set_hostname(hostname, fqdn) |
496 | - except Exception: |
497 | - util.logexc(log, "Failed to set the hostname to %s (%s)", fqdn, |
498 | - hostname) |
499 | - raise |
500 | + except Exception as e: |
501 | + msg = "Failed to set the hostname to %s (%s)" % (fqdn, hostname) |
502 | + util.logexc(log, msg) |
503 | + raise SetHostnameError("%s: %s" % (msg, e)) |
504 | + write_json(prev_fn, {'hostname': hostname, 'fqdn': fqdn}) |
505 | |
506 | # vi: ts=4 expandtab |
507 | diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py |
508 | index 35d8c57..98b0e66 100755 |
509 | --- a/cloudinit/config/cc_ssh_authkey_fingerprints.py |
510 | +++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py |
511 | @@ -77,11 +77,10 @@ def _pprint_key_entries(user, key_fn, key_entries, hash_meth='md5', |
512 | tbl = SimpleTable(tbl_fields) |
513 | for entry in key_entries: |
514 | if _is_printable_key(entry): |
515 | - row = [] |
516 | - row.append(entry.keytype or '-') |
517 | - row.append(_gen_fingerprint(entry.base64, hash_meth) or '-') |
518 | - row.append(entry.options or '-') |
519 | - row.append(entry.comment or '-') |
520 | + row = [entry.keytype or '-', |
521 | + _gen_fingerprint(entry.base64, hash_meth) or '-', |
522 | + entry.options or '-', |
523 | + entry.comment or '-'] |
524 | tbl.add_row(row) |
525 | authtbl_s = tbl.get_string() |
526 | authtbl_lines = authtbl_s.splitlines() |
527 | diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py |
528 | index f87a343..b814c8b 100644 |
529 | --- a/cloudinit/distros/arch.py |
530 | +++ b/cloudinit/distros/arch.py |
531 | @@ -129,11 +129,8 @@ class Distro(distros.Distro): |
532 | if pkgs is None: |
533 | pkgs = [] |
534 | |
535 | - cmd = ['pacman'] |
536 | + cmd = ['pacman', "-Sy", "--quiet", "--noconfirm"] |
537 | # Redirect output |
538 | - cmd.append("-Sy") |
539 | - cmd.append("--quiet") |
540 | - cmd.append("--noconfirm") |
541 | |
542 | if args and isinstance(args, str): |
543 | cmd.append(args) |
544 | diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py |
545 | index aa468bc..754d3df 100644 |
546 | --- a/cloudinit/distros/freebsd.py |
547 | +++ b/cloudinit/distros/freebsd.py |
548 | @@ -132,6 +132,12 @@ class Distro(distros.Distro): |
549 | LOG.debug("Using network interface %s", bsddev) |
550 | return bsddev |
551 | |
552 | + def _select_hostname(self, hostname, fqdn): |
553 | + # Should be FQDN if available. See rc.conf(5) in FreeBSD |
554 | + if fqdn: |
555 | + return fqdn |
556 | + return hostname |
557 | + |
558 | def _read_system_hostname(self): |
559 | sys_hostname = self._read_hostname(filename=None) |
560 | return ('rc.conf', sys_hostname) |
561 | diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py |
562 | index a219e9f..162dfa0 100644 |
563 | --- a/cloudinit/distros/opensuse.py |
564 | +++ b/cloudinit/distros/opensuse.py |
565 | @@ -67,11 +67,10 @@ class Distro(distros.Distro): |
566 | if pkgs is None: |
567 | pkgs = [] |
568 | |
569 | - cmd = ['zypper'] |
570 | # No user interaction possible, enable non-interactive mode |
571 | - cmd.append('--non-interactive') |
572 | + cmd = ['zypper', '--non-interactive'] |
573 | |
574 | - # Comand is the operation, such as install |
575 | + # Command is the operation, such as install |
576 | if command == 'upgrade': |
577 | command = 'update' |
578 | cmd.append(command) |
579 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py |
580 | index 4bcbf3a..0bb7fad 100644 |
581 | --- a/cloudinit/sources/DataSourceAzure.py |
582 | +++ b/cloudinit/sources/DataSourceAzure.py |
583 | @@ -223,6 +223,8 @@ DEF_PASSWD_REDACTION = 'REDACTED' |
584 | |
585 | |
586 | def get_hostname(hostname_command='hostname'): |
587 | + if not isinstance(hostname_command, (list, tuple)): |
588 | + hostname_command = (hostname_command,) |
589 | return util.subp(hostname_command, capture=True)[0].strip() |
590 | |
591 | |
592 | diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py |
593 | index ce47b6b..9450835 100644 |
594 | --- a/cloudinit/sources/DataSourceOpenNebula.py |
595 | +++ b/cloudinit/sources/DataSourceOpenNebula.py |
596 | @@ -173,10 +173,7 @@ class OpenNebulaNetwork(object): |
597 | def gen_conf(self): |
598 | global_dns = self.context.get('DNS', "").split() |
599 | |
600 | - conf = [] |
601 | - conf.append('auto lo') |
602 | - conf.append('iface lo inet loopback') |
603 | - conf.append('') |
604 | + conf = ['auto lo', 'iface lo inet loopback', ''] |
605 | |
606 | for mac, dev in self.ifaces.items(): |
607 | mac = mac.lower() |
608 | diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py |
609 | index a05ca2f..df0b374 100644 |
610 | --- a/cloudinit/sources/__init__.py |
611 | +++ b/cloudinit/sources/__init__.py |
612 | @@ -276,21 +276,34 @@ class DataSource(object): |
613 | return "iid-datasource" |
614 | return str(self.metadata['instance-id']) |
615 | |
616 | - def get_hostname(self, fqdn=False, resolve_ip=False): |
617 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
618 | + """Get hostname or fqdn from the datasource. Look it up if desired. |
619 | + |
620 | + @param fqdn: Boolean, set True to return hostname with domain. |
621 | + @param resolve_ip: Boolean, set True to attempt to resolve an ipv4 |
622 | + address provided in local-hostname meta-data. |
623 | + @param metadata_only: Boolean, set True to avoid looking up hostname |
624 | + if meta-data doesn't have local-hostname present. |
625 | + |
626 | + @return: hostname or qualified hostname. Optionally return None when |
627 | + metadata_only is True and local-hostname data is not available. |
628 | + """ |
629 | defdomain = "localdomain" |
630 | defhost = "localhost" |
631 | domain = defdomain |
632 | |
633 | if not self.metadata or 'local-hostname' not in self.metadata: |
634 | + if metadata_only: |
635 | + return None |
636 | # this is somewhat questionable really. |
637 | # the cloud datasource was asked for a hostname |
638 | # and didn't have one. raising error might be more appropriate |
639 | # but instead, basically look up the existing hostname |
640 | toks = [] |
641 | hostname = util.get_hostname() |
642 | - fqdn = util.get_fqdn_from_hosts(hostname) |
643 | - if fqdn and fqdn.find(".") > 0: |
644 | - toks = str(fqdn).split(".") |
645 | + hosts_fqdn = util.get_fqdn_from_hosts(hostname) |
646 | + if hosts_fqdn and hosts_fqdn.find(".") > 0: |
647 | + toks = str(hosts_fqdn).split(".") |
648 | elif hostname and hostname.find(".") > 0: |
649 | toks = str(hostname).split(".") |
650 | elif hostname: |
651 | diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py |
652 | index af15115..5065083 100644 |
653 | --- a/cloudinit/sources/tests/test_init.py |
654 | +++ b/cloudinit/sources/tests/test_init.py |
655 | @@ -7,7 +7,7 @@ import stat |
656 | from cloudinit.helpers import Paths |
657 | from cloudinit.sources import ( |
658 | INSTANCE_JSON_FILE, DataSource) |
659 | -from cloudinit.tests.helpers import CiTestCase, skipIf |
660 | +from cloudinit.tests.helpers import CiTestCase, skipIf, mock |
661 | from cloudinit.user_data import UserDataProcessor |
662 | from cloudinit import util |
663 | |
664 | @@ -108,6 +108,74 @@ class TestDataSource(CiTestCase): |
665 | self.assertEqual('userdata_raw', datasource.userdata_raw) |
666 | self.assertEqual('vendordata_raw', datasource.vendordata_raw) |
667 | |
668 | + def test_get_hostname_strips_local_hostname_without_domain(self): |
669 | + """Datasource.get_hostname strips metadata local-hostname of domain.""" |
670 | + tmp = self.tmp_dir() |
671 | + datasource = DataSourceTestSubclassNet( |
672 | + self.sys_cfg, self.distro, Paths({'run_dir': tmp})) |
673 | + self.assertTrue(datasource.get_data()) |
674 | + self.assertEqual( |
675 | + 'test-subclass-hostname', datasource.metadata['local-hostname']) |
676 | + self.assertEqual('test-subclass-hostname', datasource.get_hostname()) |
677 | + datasource.metadata['local-hostname'] = 'hostname.my.domain.com' |
678 | + self.assertEqual('hostname', datasource.get_hostname()) |
679 | + |
680 | + def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self): |
681 | + """Datasource.get_hostname with fqdn set gets qualified hostname.""" |
682 | + tmp = self.tmp_dir() |
683 | + datasource = DataSourceTestSubclassNet( |
684 | + self.sys_cfg, self.distro, Paths({'run_dir': tmp})) |
685 | + self.assertTrue(datasource.get_data()) |
686 | + datasource.metadata['local-hostname'] = 'hostname.my.domain.com' |
687 | + self.assertEqual( |
688 | + 'hostname.my.domain.com', datasource.get_hostname(fqdn=True)) |
689 | + |
690 | + def test_get_hostname_without_metadata_uses_system_hostname(self): |
691 | + """Datasource.gethostname runs util.get_hostname when no metadata.""" |
692 | + tmp = self.tmp_dir() |
693 | + datasource = DataSourceTestSubclassNet( |
694 | + self.sys_cfg, self.distro, Paths({'run_dir': tmp})) |
695 | + self.assertEqual({}, datasource.metadata) |
696 | + mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' |
697 | + with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: |
698 | + with mock.patch(mock_fqdn) as m_fqdn: |
699 | + m_gethost.return_value = 'systemhostname.domain.com' |
700 | + m_fqdn.return_value = None # No maching fqdn in /etc/hosts |
701 | + self.assertEqual('systemhostname', datasource.get_hostname()) |
702 | + self.assertEqual( |
703 | + 'systemhostname.domain.com', |
704 | + datasource.get_hostname(fqdn=True)) |
705 | + |
706 | + def test_get_hostname_without_metadata_returns_none(self): |
707 | + """Datasource.gethostname returns None when metadata_only and no MD.""" |
708 | + tmp = self.tmp_dir() |
709 | + datasource = DataSourceTestSubclassNet( |
710 | + self.sys_cfg, self.distro, Paths({'run_dir': tmp})) |
711 | + self.assertEqual({}, datasource.metadata) |
712 | + mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' |
713 | + with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: |
714 | + with mock.patch(mock_fqdn) as m_fqdn: |
715 | + self.assertIsNone(datasource.get_hostname(metadata_only=True)) |
716 | + self.assertIsNone( |
717 | + datasource.get_hostname(fqdn=True, metadata_only=True)) |
718 | + self.assertEqual([], m_gethost.call_args_list) |
719 | + self.assertEqual([], m_fqdn.call_args_list) |
720 | + |
721 | + def test_get_hostname_without_metadata_prefers_etc_hosts(self): |
722 | + """Datasource.gethostname prefers /etc/hosts to util.get_hostname.""" |
723 | + tmp = self.tmp_dir() |
724 | + datasource = DataSourceTestSubclassNet( |
725 | + self.sys_cfg, self.distro, Paths({'run_dir': tmp})) |
726 | + self.assertEqual({}, datasource.metadata) |
727 | + mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' |
728 | + with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: |
729 | + with mock.patch(mock_fqdn) as m_fqdn: |
730 | + m_gethost.return_value = 'systemhostname.domain.com' |
731 | + m_fqdn.return_value = 'fqdnhostname.domain.com' |
732 | + self.assertEqual('fqdnhostname', datasource.get_hostname()) |
733 | + self.assertEqual('fqdnhostname.domain.com', |
734 | + datasource.get_hostname(fqdn=True)) |
735 | + |
736 | def test_get_data_write_json_instance_data(self): |
737 | """get_data writes INSTANCE_JSON_FILE to run_dir as readonly root.""" |
738 | tmp = self.tmp_dir() |
739 | diff --git a/cloudinit/stages.py b/cloudinit/stages.py |
740 | index d045268..bc4ebc8 100644 |
741 | --- a/cloudinit/stages.py |
742 | +++ b/cloudinit/stages.py |
743 | @@ -132,8 +132,7 @@ class Init(object): |
744 | return initial_dirs |
745 | |
746 | def purge_cache(self, rm_instance_lnk=False): |
747 | - rm_list = [] |
748 | - rm_list.append(self.paths.boot_finished) |
749 | + rm_list = [self.paths.boot_finished] |
750 | if rm_instance_lnk: |
751 | rm_list.append(self.paths.instance_link) |
752 | for f in rm_list: |
753 | diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py |
754 | index 41d9a8e..14c0b0b 100644 |
755 | --- a/cloudinit/tests/helpers.py |
756 | +++ b/cloudinit/tests/helpers.py |
757 | @@ -409,6 +409,19 @@ except AttributeError: |
758 | return decorator |
759 | |
760 | |
761 | +try: |
762 | + import jsonschema |
763 | + assert jsonschema # avoid pyflakes error F401: import unused |
764 | + _missing_jsonschema_dep = False |
765 | +except ImportError: |
766 | + _missing_jsonschema_dep = True |
767 | + |
768 | + |
769 | +def skipUnlessJsonSchema(): |
770 | + return skipIf( |
771 | + _missing_jsonschema_dep, "No python-jsonschema dependency present.") |
772 | + |
773 | + |
774 | # older versions of mock do not have the useful 'assert_not_called' |
775 | if not hasattr(mock.Mock, 'assert_not_called'): |
776 | def __mock_assert_not_called(mmock): |
777 | diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py |
778 | index ba6bf69..d30643d 100644 |
779 | --- a/cloudinit/tests/test_util.py |
780 | +++ b/cloudinit/tests/test_util.py |
781 | @@ -16,6 +16,25 @@ MOUNT_INFO = [ |
782 | ] |
783 | |
784 | |
785 | +class FakeCloud(object): |
786 | + |
787 | + def __init__(self, hostname, fqdn): |
788 | + self.hostname = hostname |
789 | + self.fqdn = fqdn |
790 | + self.calls = [] |
791 | + |
792 | + def get_hostname(self, fqdn=None, metadata_only=None): |
793 | + myargs = {} |
794 | + if fqdn is not None: |
795 | + myargs['fqdn'] = fqdn |
796 | + if metadata_only is not None: |
797 | + myargs['metadata_only'] = metadata_only |
798 | + self.calls.append(myargs) |
799 | + if fqdn: |
800 | + return self.fqdn |
801 | + return self.hostname |
802 | + |
803 | + |
804 | class TestUtil(CiTestCase): |
805 | |
806 | def test_parse_mount_info_no_opts_no_arg(self): |
807 | @@ -44,3 +63,81 @@ class TestUtil(CiTestCase): |
808 | m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime') |
809 | is_rw = util.mount_is_read_write('/') |
810 | self.assertEqual(is_rw, False) |
811 | + |
812 | + |
813 | +class TestShellify(CiTestCase): |
814 | + |
815 | + def test_input_dict_raises_type_error(self): |
816 | + self.assertRaisesRegex( |
817 | + TypeError, 'Input.*was.*dict.*xpected', |
818 | + util.shellify, {'mykey': 'myval'}) |
819 | + |
820 | + def test_input_str_raises_type_error(self): |
821 | + self.assertRaisesRegex( |
822 | + TypeError, 'Input.*was.*str.*xpected', util.shellify, "foobar") |
823 | + |
824 | + def test_value_with_int_raises_type_error(self): |
825 | + self.assertRaisesRegex( |
826 | + TypeError, 'shellify.*int', util.shellify, ["foo", 1]) |
827 | + |
828 | + def test_supports_strings_and_lists(self): |
829 | + self.assertEqual( |
830 | + '\n'.join(["#!/bin/sh", "echo hi mom", "'echo' 'hi dad'", |
831 | + "'echo' 'hi' 'sis'", ""]), |
832 | + util.shellify(["echo hi mom", ["echo", "hi dad"], |
833 | + ('echo', 'hi', 'sis')])) |
834 | + |
835 | + |
836 | +class TestGetHostnameFqdn(CiTestCase): |
837 | + |
838 | + def test_get_hostname_fqdn_from_only_cfg_fqdn(self): |
839 | + """When cfg only has the fqdn key, derive hostname and fqdn from it.""" |
840 | + hostname, fqdn = util.get_hostname_fqdn( |
841 | + cfg={'fqdn': 'myhost.domain.com'}, cloud=None) |
842 | + self.assertEqual('myhost', hostname) |
843 | + self.assertEqual('myhost.domain.com', fqdn) |
844 | + |
845 | + def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): |
846 | + """When cfg has both fqdn and hostname keys, return them.""" |
847 | + hostname, fqdn = util.get_hostname_fqdn( |
848 | + cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None) |
849 | + self.assertEqual('other', hostname) |
850 | + self.assertEqual('myhost.domain.com', fqdn) |
851 | + |
852 | + def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): |
853 | + """When cfg has only hostname key which represents a fqdn, use that.""" |
854 | + hostname, fqdn = util.get_hostname_fqdn( |
855 | + cfg={'hostname': 'myhost.domain.com'}, cloud=None) |
856 | + self.assertEqual('myhost', hostname) |
857 | + self.assertEqual('myhost.domain.com', fqdn) |
858 | + |
859 | + def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): |
860 | + """When cfg has a hostname without a '.' query cloud.get_hostname.""" |
861 | + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') |
862 | + hostname, fqdn = util.get_hostname_fqdn( |
863 | + cfg={'hostname': 'myhost'}, cloud=mycloud) |
864 | + self.assertEqual('myhost', hostname) |
865 | + self.assertEqual('cloudhost.mycloud.com', fqdn) |
866 | + self.assertEqual( |
867 | + [{'fqdn': True, 'metadata_only': False}], mycloud.calls) |
868 | + |
869 | + def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): |
870 | + """When cfg has neither hostname nor fqdn cloud.get_hostname.""" |
871 | + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') |
872 | + hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud) |
873 | + self.assertEqual('cloudhost', hostname) |
874 | + self.assertEqual('cloudhost.mycloud.com', fqdn) |
875 | + self.assertEqual( |
876 | + [{'fqdn': True, 'metadata_only': False}, |
877 | + {'metadata_only': False}], mycloud.calls) |
878 | + |
879 | + def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): |
880 | + """Calls to cloud.get_hostname pass the metadata_only parameter.""" |
881 | + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') |
882 | + hostname, fqdn = util.get_hostname_fqdn( |
883 | + cfg={}, cloud=mycloud, metadata_only=True) |
884 | + self.assertEqual( |
885 | + [{'fqdn': True, 'metadata_only': True}, |
886 | + {'metadata_only': True}], mycloud.calls) |
887 | + |
888 | +# vi: ts=4 expandtab |
889 | diff --git a/cloudinit/url_helper.py b/cloudinit/url_helper.py |
890 | index 0a5be0b..4e814a5 100644 |
891 | --- a/cloudinit/url_helper.py |
892 | +++ b/cloudinit/url_helper.py |
893 | @@ -47,7 +47,7 @@ try: |
894 | _REQ_VER = LooseVersion(_REQ.version) # pylint: disable=no-member |
895 | if _REQ_VER >= LooseVersion('0.8.8'): |
896 | SSL_ENABLED = True |
897 | - if _REQ_VER >= LooseVersion('0.7.0') and _REQ_VER < LooseVersion('1.0.0'): |
898 | + if LooseVersion('0.7.0') <= _REQ_VER < LooseVersion('1.0.0'): |
899 | CONFIG_ENABLED = True |
900 | except ImportError: |
901 | pass |
902 | @@ -121,7 +121,7 @@ class UrlResponse(object): |
903 | upper = 300 |
904 | if redirects_ok: |
905 | upper = 400 |
906 | - if self.code >= 200 and self.code < upper: |
907 | + if 200 <= self.code < upper: |
908 | return True |
909 | else: |
910 | return False |
911 | diff --git a/cloudinit/util.py b/cloudinit/util.py |
912 | index 02dc2ce..823d80b 100644 |
913 | --- a/cloudinit/util.py |
914 | +++ b/cloudinit/util.py |
915 | @@ -546,7 +546,7 @@ def is_ipv4(instr): |
916 | return False |
917 | |
918 | try: |
919 | - toks = [x for x in toks if int(x) < 256 and int(x) >= 0] |
920 | + toks = [x for x in toks if 0 <= int(x) < 256] |
921 | except Exception: |
922 | return False |
923 | |
924 | @@ -716,8 +716,7 @@ def redirect_output(outfmt, errfmt, o_out=None, o_err=None): |
925 | def make_url(scheme, host, port=None, |
926 | path='', params='', query='', fragment=''): |
927 | |
928 | - pieces = [] |
929 | - pieces.append(scheme or '') |
930 | + pieces = [scheme or ''] |
931 | |
932 | netloc = '' |
933 | if host: |
934 | @@ -1026,9 +1025,16 @@ def dos2unix(contents): |
935 | return contents.replace('\r\n', '\n') |
936 | |
937 | |
938 | -def get_hostname_fqdn(cfg, cloud): |
939 | - # return the hostname and fqdn from 'cfg'. If not found in cfg, |
940 | - # then fall back to data from cloud |
941 | +def get_hostname_fqdn(cfg, cloud, metadata_only=False): |
942 | + """Get hostname and fqdn from config if present and fallback to cloud. |
943 | + |
944 | + @param cfg: Dictionary of merged user-data configuration (from init.cfg). |
945 | + @param cloud: Cloud instance from init.cloudify(). |
946 | + @param metadata_only: Boolean, set True to only query cloud meta-data, |
947 | + returning None if not present in meta-data. |
948 | + @return: a Tuple of strings <hostname>, <fqdn>. Values can be none when |
949 | + metadata_only is True and no cfg or metadata provides hostname info. |
950 | + """ |
951 | if "fqdn" in cfg: |
952 | # user specified a fqdn. Default hostname then is based off that |
953 | fqdn = cfg['fqdn'] |
954 | @@ -1042,11 +1048,11 @@ def get_hostname_fqdn(cfg, cloud): |
955 | else: |
956 | # no fqdn set, get fqdn from cloud. |
957 | # get hostname from cfg if available otherwise cloud |
958 | - fqdn = cloud.get_hostname(fqdn=True) |
959 | + fqdn = cloud.get_hostname(fqdn=True, metadata_only=metadata_only) |
960 | if "hostname" in cfg: |
961 | hostname = cfg['hostname'] |
962 | else: |
963 | - hostname = cloud.get_hostname() |
964 | + hostname = cloud.get_hostname(metadata_only=metadata_only) |
965 | return (hostname, fqdn) |
966 | |
967 | |
968 | @@ -1868,8 +1874,14 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False, |
969 | # Popen converts entries in the arguments array from non-bytes to bytes. |
970 | # When locale is unset it may use ascii for that encoding which can |
971 | # cause UnicodeDecodeErrors. (LP: #1751051) |
972 | - bytes_args = [x if isinstance(x, six.binary_type) else x.encode("utf-8") |
973 | - for x in args] |
974 | + if isinstance(args, six.binary_type): |
975 | + bytes_args = args |
976 | + elif isinstance(args, six.string_types): |
977 | + bytes_args = args.encode("utf-8") |
978 | + else: |
979 | + bytes_args = [ |
980 | + x if isinstance(x, six.binary_type) else x.encode("utf-8") |
981 | + for x in args] |
982 | try: |
983 | sp = subprocess.Popen(bytes_args, stdout=stdout, |
984 | stderr=stderr, stdin=stdin, |
985 | @@ -1923,6 +1935,11 @@ def abs_join(*paths): |
986 | # if it is an array, shell protect it (with single ticks) |
987 | # if it is a string, do nothing |
988 | def shellify(cmdlist, add_header=True): |
989 | + if not isinstance(cmdlist, (tuple, list)): |
990 | + raise TypeError( |
991 | + "Input to shellify was type '%s'. Expected list or tuple." % |
992 | + (type_utils.obj_name(cmdlist))) |
993 | + |
994 | content = '' |
995 | if add_header: |
996 | content += "#!/bin/sh\n" |
997 | @@ -1931,7 +1948,7 @@ def shellify(cmdlist, add_header=True): |
998 | for args in cmdlist: |
999 | # If the item is a list, wrap all items in single tick. |
1000 | # If its not, then just write it directly. |
1001 | - if isinstance(args, list): |
1002 | + if isinstance(args, (list, tuple)): |
1003 | fixed = [] |
1004 | for f in args: |
1005 | fixed.append("'%s'" % (six.text_type(f).replace("'", escaped))) |
1006 | @@ -1941,9 +1958,10 @@ def shellify(cmdlist, add_header=True): |
1007 | content = "%s%s\n" % (content, args) |
1008 | cmds_made += 1 |
1009 | else: |
1010 | - raise RuntimeError(("Unable to shellify type %s" |
1011 | - " which is not a list or string") |
1012 | - % (type_utils.obj_name(args))) |
1013 | + raise TypeError( |
1014 | + "Unable to shellify type '%s'. Expected list, string, tuple. " |
1015 | + "Got: %s" % (type_utils.obj_name(args), args)) |
1016 | + |
1017 | LOG.debug("Shellified %s commands.", cmds_made) |
1018 | return content |
1019 | |
1020 | diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl |
1021 | index fad1184..cf2e240 100644 |
1022 | --- a/config/cloud.cfg.tmpl |
1023 | +++ b/config/cloud.cfg.tmpl |
1024 | @@ -113,9 +113,9 @@ cloud_final_modules: |
1025 | {% if variant not in ["freebsd"] %} |
1026 | - puppet |
1027 | - chef |
1028 | - - salt-minion |
1029 | - mcollective |
1030 | {% endif %} |
1031 | + - salt-minion |
1032 | - rightscale_userdata |
1033 | - scripts-vendor |
1034 | - scripts-per-once |
1035 | diff --git a/debian/changelog b/debian/changelog |
1036 | index 27dba2c..f1ba6ef 100644 |
1037 | --- a/debian/changelog |
1038 | +++ b/debian/changelog |
1039 | @@ -1,3 +1,25 @@ |
1040 | +cloud-init (18.1-17-g97012fbb-0ubuntu1) bionic; urgency=medium |
1041 | + |
1042 | + * New upstream snapshot. |
1043 | + - util: Fix subp regression. Allow specifying subp command as a string. |
1044 | + (LP: #1755965) |
1045 | + - doc: fix all warnings issued by 'tox -e doc' |
1046 | + - FreeBSD: Set hostname to FQDN. [Dominic Schlegel] (LP: #1753499) |
1047 | + - tests: fix run_tree and bddeb |
1048 | + - tests: Fix some warnings in tests that popped up with newer python. |
1049 | + - set_hostname: When present in metadata, set it before network bringup. |
1050 | + (LP: #1746455) |
1051 | + - tests: Centralize and re-use skipTest based on json schema presense. |
1052 | + - This commit fixes get_hostname on the AzureDataSource. |
1053 | + [Douglas Jordan] (LP: #1754495) |
1054 | + - shellify: raise TypeError on bad input. |
1055 | + - Make salt minion module work on FreeBSD. |
1056 | + [Dominic Schlegel] (LP: #1721503) |
1057 | + - Simplify some comparisions. [Rémy Léone] |
1058 | + - Change some list creation and population to literal. [Rémy Léone] |
1059 | + |
1060 | + -- Chad Smith <chad.smith@canonical.com> Thu, 15 Mar 2018 14:48:29 -0600 |
1061 | + |
1062 | cloud-init (18.1-5-g40e77380-0ubuntu1) bionic; urgency=medium |
1063 | |
1064 | * New upstream snapshot. |
1065 | diff --git a/doc/rtd/topics/capabilities.rst b/doc/rtd/topics/capabilities.rst |
1066 | index ae3a0c7..3e2c9e3 100644 |
1067 | --- a/doc/rtd/topics/capabilities.rst |
1068 | +++ b/doc/rtd/topics/capabilities.rst |
1069 | @@ -44,13 +44,14 @@ Currently defined feature names include: |
1070 | CLI Interface |
1071 | ============= |
1072 | |
1073 | - The command line documentation is accessible on any cloud-init |
1074 | -installed system: |
1075 | +The command line documentation is accessible on any cloud-init installed |
1076 | +system: |
1077 | |
1078 | -.. code-block:: bash |
1079 | +.. code-block:: shell-session |
1080 | |
1081 | % cloud-init --help |
1082 | usage: cloud-init [-h] [--version] [--file FILES] |
1083 | + |
1084 | [--debug] [--force] |
1085 | {init,modules,single,dhclient-hook,features,analyze,devel,collect-logs,clean,status} |
1086 | ... |
1087 | @@ -88,7 +89,7 @@ Print out each feature supported. If cloud-init does not have the |
1088 | features subcommand, it also does not support any features described in |
1089 | this document. |
1090 | |
1091 | -.. code-block:: bash |
1092 | +.. code-block:: shell-session |
1093 | |
1094 | % cloud-init features |
1095 | NETWORK_CONFIG_V1 |
1096 | @@ -100,10 +101,11 @@ cloud-init status |
1097 | ----------------- |
1098 | Report whether cloud-init is running, done, disabled or errored. Exits |
1099 | non-zero if an error is detected in cloud-init. |
1100 | + |
1101 | * **--long**: Detailed status information. |
1102 | * **--wait**: Block until cloud-init completes. |
1103 | |
1104 | -.. code-block:: bash |
1105 | +.. code-block:: shell-session |
1106 | |
1107 | % cloud-init status --long |
1108 | status: done |
1109 | @@ -214,7 +216,7 @@ of once-per-instance: |
1110 | * **--frequency**: Optionally override the declared module frequency |
1111 | with one of (always|once-per-instance|once) |
1112 | |
1113 | -.. code-block:: bash |
1114 | +.. code-block:: shell-session |
1115 | |
1116 | % cloud-init single --name set_hostname --frequency always |
1117 | |
1118 | diff --git a/doc/rtd/topics/debugging.rst b/doc/rtd/topics/debugging.rst |
1119 | index c2b47ed..cacc8a2 100644 |
1120 | --- a/doc/rtd/topics/debugging.rst |
1121 | +++ b/doc/rtd/topics/debugging.rst |
1122 | @@ -1,6 +1,6 @@ |
1123 | -********************** |
1124 | +******************************** |
1125 | Testing and debugging cloud-init |
1126 | -********************** |
1127 | +******************************** |
1128 | |
1129 | Overview |
1130 | ======== |
1131 | @@ -10,7 +10,7 @@ deployed instances. |
1132 | .. _boot_time_analysis: |
1133 | |
1134 | Boot Time Analysis - cloud-init analyze |
1135 | -====================================== |
1136 | +======================================= |
1137 | Occasionally instances don't appear as performant as we would like and |
1138 | cloud-init packages a simple facility to inspect what operations took |
1139 | cloud-init the longest during boot and setup. |
1140 | @@ -22,9 +22,9 @@ determine the long-pole in cloud-init configuration and setup. These |
1141 | subcommands default to reading /var/log/cloud-init.log. |
1142 | |
1143 | * ``analyze show`` Parse and organize cloud-init.log events by stage and |
1144 | -include each sub-stage granularity with time delta reports. |
1145 | + include each sub-stage granularity with time delta reports. |
1146 | |
1147 | -.. code-block:: bash |
1148 | +.. code-block:: shell-session |
1149 | |
1150 | $ cloud-init analyze show -i my-cloud-init.log |
1151 | -- Boot Record 01 -- |
1152 | @@ -41,9 +41,9 @@ include each sub-stage granularity with time delta reports. |
1153 | |
1154 | |
1155 | * ``analyze dump`` Parse cloud-init.log into event records and return a list of |
1156 | -dictionaries that can be consumed for other reporting needs. |
1157 | + dictionaries that can be consumed for other reporting needs. |
1158 | |
1159 | -.. code-block:: bash |
1160 | +.. code-block:: shell-session |
1161 | |
1162 | $ cloud-init analyze blame -i my-cloud-init.log |
1163 | [ |
1164 | @@ -56,10 +56,10 @@ dictionaries that can be consumed for other reporting needs. |
1165 | },... |
1166 | |
1167 | * ``analyze blame`` Parse cloud-init.log into event records and sort them based |
1168 | -on highest time cost for quick assessment of areas of cloud-init that may need |
1169 | -improvement. |
1170 | + on highest time cost for quick assessment of areas of cloud-init that may |
1171 | + need improvement. |
1172 | |
1173 | -.. code-block:: bash |
1174 | +.. code-block:: shell-session |
1175 | |
1176 | $ cloud-init analyze blame -i my-cloud-init.log |
1177 | -- Boot Record 11 -- |
1178 | @@ -73,31 +73,36 @@ Analyze quickstart - LXC |
1179 | --------------------------- |
1180 | To quickly obtain a cloud-init log try using lxc on any ubuntu system: |
1181 | |
1182 | -.. code-block:: bash |
1183 | +.. code-block:: shell-session |
1184 | + |
1185 | + $ lxc init ubuntu-daily:xenial x1 |
1186 | + $ lxc start x1 |
1187 | + $ # Take lxc's cloud-init.log and pipe it to the analyzer |
1188 | + $ lxc file pull x1/var/log/cloud-init.log - | cloud-init analyze dump -i - |
1189 | + $ lxc file pull x1/var/log/cloud-init.log - | \ |
1190 | + python3 -m cloudinit.analyze dump -i - |
1191 | |
1192 | - $ lxc init ubuntu-daily:xenial x1 |
1193 | - $ lxc start x1 |
1194 | - # Take lxc's cloud-init.log and pipe it to the analyzer |
1195 | - $ lxc file pull x1/var/log/cloud-init.log - | cloud-init analyze dump -i - |
1196 | - $ lxc file pull x1/var/log/cloud-init.log - | \ |
1197 | - python3 -m cloudinit.analyze dump -i - |
1198 | |
1199 | Analyze quickstart - KVM |
1200 | --------------------------- |
1201 | To quickly analyze a KVM a cloud-init log: |
1202 | |
1203 | 1. Download the current cloud image |
1204 | - wget https://cloud-images.ubuntu.com/daily/server/xenial/current/xenial-server-cloudimg-amd64.img |
1205 | + |
1206 | +.. code-block:: shell-session |
1207 | + |
1208 | + $ wget https://cloud-images.ubuntu.com/daily/server/xenial/current/xenial-server-cloudimg-amd64.img |
1209 | + |
1210 | 2. Create a snapshot image to preserve the original cloud-image |
1211 | |
1212 | -.. code-block:: bash |
1213 | +.. code-block:: shell-session |
1214 | |
1215 | $ qemu-img create -b xenial-server-cloudimg-amd64.img -f qcow2 \ |
1216 | test-cloudinit.qcow2 |
1217 | |
1218 | 3. Create a seed image with metadata using `cloud-localds` |
1219 | |
1220 | -.. code-block:: bash |
1221 | +.. code-block:: shell-session |
1222 | |
1223 | $ cat > user-data <<EOF |
1224 | #cloud-config |
1225 | @@ -108,18 +113,18 @@ To quickly analyze a KVM a cloud-init log: |
1226 | |
1227 | 4. Launch your modified VM |
1228 | |
1229 | -.. code-block:: bash |
1230 | +.. code-block:: shell-session |
1231 | |
1232 | $ kvm -m 512 -net nic -net user -redir tcp:2222::22 \ |
1233 | - -drive file=test-cloudinit.qcow2,if=virtio,format=qcow2 \ |
1234 | - -drive file=my-seed.img,if=virtio,format=raw |
1235 | + -drive file=test-cloudinit.qcow2,if=virtio,format=qcow2 \ |
1236 | + -drive file=my-seed.img,if=virtio,format=raw |
1237 | |
1238 | 5. Analyze the boot (blame, dump, show) |
1239 | |
1240 | -.. code-block:: bash |
1241 | +.. code-block:: shell-session |
1242 | |
1243 | $ ssh -p 2222 ubuntu@localhost 'cat /var/log/cloud-init.log' | \ |
1244 | - cloud-init analyze blame -i - |
1245 | + cloud-init analyze blame -i - |
1246 | |
1247 | |
1248 | Running single cloud config modules |
1249 | @@ -136,7 +141,7 @@ prevents a module from running again if it has already been run. To ensure that |
1250 | a module is run again, the desired frequency can be overridden on the |
1251 | commandline: |
1252 | |
1253 | -.. code-block:: bash |
1254 | +.. code-block:: shell-session |
1255 | |
1256 | $ sudo cloud-init single --name cc_ssh --frequency always |
1257 | ... |
1258 | diff --git a/doc/rtd/topics/network-config.rst b/doc/rtd/topics/network-config.rst |
1259 | index 96c1cf5..1e99455 100644 |
1260 | --- a/doc/rtd/topics/network-config.rst |
1261 | +++ b/doc/rtd/topics/network-config.rst |
1262 | @@ -202,7 +202,7 @@ is helpful for examining expected output for a given input format. |
1263 | |
1264 | CLI Interface : |
1265 | |
1266 | -.. code-block:: bash |
1267 | +.. code-block:: shell-session |
1268 | |
1269 | % tools/net-convert.py --help |
1270 | usage: net-convert.py [-h] --network-data PATH --kind |
1271 | @@ -222,7 +222,7 @@ CLI Interface : |
1272 | |
1273 | Example output converting V2 to sysconfig: |
1274 | |
1275 | -.. code-block:: bash |
1276 | +.. code-block:: shell-session |
1277 | |
1278 | % tools/net-convert.py --network-data v2.yaml --kind yaml \ |
1279 | --output-kind sysconfig -d target |
1280 | diff --git a/doc/rtd/topics/tests.rst b/doc/rtd/topics/tests.rst |
1281 | index bf04bb3..cac4a6e 100644 |
1282 | --- a/doc/rtd/topics/tests.rst |
1283 | +++ b/doc/rtd/topics/tests.rst |
1284 | @@ -21,7 +21,7 @@ Overview |
1285 | In order to avoid the need for dependencies and ease the setup and |
1286 | configuration users can run the integration tests via tox: |
1287 | |
1288 | -.. code-block:: bash |
1289 | +.. code-block:: shell-session |
1290 | |
1291 | $ git clone https://git.launchpad.net/cloud-init |
1292 | $ cd cloud-init |
1293 | @@ -51,7 +51,7 @@ The first example will provide a complete end-to-end run of data |
1294 | collection and verification. There are additional examples below |
1295 | explaining how to run one or the other independently. |
1296 | |
1297 | -.. code-block:: bash |
1298 | +.. code-block:: shell-session |
1299 | |
1300 | $ git clone https://git.launchpad.net/cloud-init |
1301 | $ cd cloud-init |
1302 | @@ -93,7 +93,7 @@ If developing tests it may be necessary to see if cloud-config works as |
1303 | expected and the correct files are pulled down. In this case only a |
1304 | collect can be ran by running: |
1305 | |
1306 | -.. code-block:: bash |
1307 | +.. code-block:: shell-session |
1308 | |
1309 | $ tox -e citest -- collect -n xenial --data-dir /tmp/collection |
1310 | |
1311 | @@ -106,7 +106,7 @@ Verify |
1312 | When developing tests it is much easier to simply rerun the verify scripts |
1313 | without the more lengthy collect process. This can be done by running: |
1314 | |
1315 | -.. code-block:: bash |
1316 | +.. code-block:: shell-session |
1317 | |
1318 | $ tox -e citest -- verify --data-dir /tmp/collection |
1319 | |
1320 | @@ -133,7 +133,7 @@ cloud-init deb from or use the ``tree_run`` command using a copy of |
1321 | cloud-init located in a different directory, use the option ``--cloud-init |
1322 | /path/to/cloud-init``. |
1323 | |
1324 | -.. code-block:: bash |
1325 | +.. code-block:: shell-session |
1326 | |
1327 | $ tox -e citest -- tree_run --verbose \ |
1328 | --os-name xenial --os-name stretch \ |
1329 | @@ -331,7 +331,7 @@ Integration tests are located under the `tests/cloud_tests` directory. |
1330 | Test configurations are placed under `configs` and the test verification |
1331 | scripts under `testcases`: |
1332 | |
1333 | -.. code-block:: bash |
1334 | +.. code-block:: shell-session |
1335 | |
1336 | cloud-init$ tree -d tests/cloud_tests/ |
1337 | tests/cloud_tests/ |
1338 | @@ -362,7 +362,7 @@ The following would create a test case named ``example`` under the |
1339 | ``modules`` category with the given description, and cloud config data read |
1340 | in from ``/tmp/user_data``. |
1341 | |
1342 | -.. code-block:: bash |
1343 | +.. code-block:: shell-session |
1344 | |
1345 | $ tox -e citest -- create modules/example \ |
1346 | -d "a simple example test case" -c "$(< /tmp/user_data)" |
1347 | @@ -385,7 +385,7 @@ Development Checklist |
1348 | * Placed in the appropriate sub-folder in the test cases directory |
1349 | * Tested by running the test: |
1350 | |
1351 | - .. code-block:: bash |
1352 | + .. code-block:: shell-session |
1353 | |
1354 | $ tox -e citest -- run -verbose \ |
1355 | --os-name <release target> \ |
1356 | @@ -404,14 +404,14 @@ These configuration files are the standard that the AWS cli and other AWS |
1357 | tools utilize for interacting directly with AWS itself and are normally |
1358 | generated when running ``aws configure``: |
1359 | |
1360 | -.. code-block:: bash |
1361 | +.. code-block:: shell-session |
1362 | |
1363 | $ cat $HOME/.aws/credentials |
1364 | [default] |
1365 | aws_access_key_id = <KEY HERE> |
1366 | aws_secret_access_key = <KEY HERE> |
1367 | |
1368 | -.. code-block:: bash |
1369 | +.. code-block:: shell-session |
1370 | |
1371 | $ cat $HOME/.aws/config |
1372 | [default] |
1373 | diff --git a/tests/cloud_tests/bddeb.py b/tests/cloud_tests/bddeb.py |
1374 | index a6d5069..b9cfcfa 100644 |
1375 | --- a/tests/cloud_tests/bddeb.py |
1376 | +++ b/tests/cloud_tests/bddeb.py |
1377 | @@ -16,7 +16,7 @@ pre_reqs = ['devscripts', 'equivs', 'git', 'tar'] |
1378 | |
1379 | def _out(cmd_res): |
1380 | """Get clean output from cmd result.""" |
1381 | - return cmd_res[0].strip() |
1382 | + return cmd_res[0].decode("utf-8").strip() |
1383 | |
1384 | |
1385 | def build_deb(args, instance): |
1386 | diff --git a/tests/cloud_tests/platforms/ec2/__init__.py b/tests/cloud_tests/platforms/ec2/__init__.py |
1387 | new file mode 100644 |
1388 | index 0000000..e69de29 |
1389 | --- /dev/null |
1390 | +++ b/tests/cloud_tests/platforms/ec2/__init__.py |
1391 | diff --git a/tests/cloud_tests/platforms/lxd/__init__.py b/tests/cloud_tests/platforms/lxd/__init__.py |
1392 | new file mode 100644 |
1393 | index 0000000..e69de29 |
1394 | --- /dev/null |
1395 | +++ b/tests/cloud_tests/platforms/lxd/__init__.py |
1396 | diff --git a/tests/cloud_tests/platforms/lxd/platform.py b/tests/cloud_tests/platforms/lxd/platform.py |
1397 | index 6a01692..f7251a0 100644 |
1398 | --- a/tests/cloud_tests/platforms/lxd/platform.py |
1399 | +++ b/tests/cloud_tests/platforms/lxd/platform.py |
1400 | @@ -101,8 +101,4 @@ class LXDPlatform(Platform): |
1401 | """ |
1402 | return self.client.images.get_by_alias(alias) |
1403 | |
1404 | - def destroy(self): |
1405 | - """Clean up platform data.""" |
1406 | - super(LXDPlatform, self).destroy() |
1407 | - |
1408 | # vi: ts=4 expandtab |
1409 | diff --git a/tests/cloud_tests/platforms/nocloudkvm/__init__.py b/tests/cloud_tests/platforms/nocloudkvm/__init__.py |
1410 | new file mode 100644 |
1411 | index 0000000..e69de29 |
1412 | --- /dev/null |
1413 | +++ b/tests/cloud_tests/platforms/nocloudkvm/__init__.py |
1414 | diff --git a/tests/cloud_tests/platforms/nocloudkvm/instance.py b/tests/cloud_tests/platforms/nocloudkvm/instance.py |
1415 | index 932dc0f..33ff3f2 100644 |
1416 | --- a/tests/cloud_tests/platforms/nocloudkvm/instance.py |
1417 | +++ b/tests/cloud_tests/platforms/nocloudkvm/instance.py |
1418 | @@ -109,7 +109,7 @@ class NoCloudKVMInstance(Instance): |
1419 | if self.pid: |
1420 | try: |
1421 | c_util.subp(['kill', '-9', self.pid]) |
1422 | - except util.ProcessExectuionError: |
1423 | + except c_util.ProcessExecutionError: |
1424 | pass |
1425 | |
1426 | if self.pid_file: |
1427 | diff --git a/tests/cloud_tests/platforms/nocloudkvm/platform.py b/tests/cloud_tests/platforms/nocloudkvm/platform.py |
1428 | index a7e6f5d..8593346 100644 |
1429 | --- a/tests/cloud_tests/platforms/nocloudkvm/platform.py |
1430 | +++ b/tests/cloud_tests/platforms/nocloudkvm/platform.py |
1431 | @@ -21,10 +21,6 @@ class NoCloudKVMPlatform(Platform): |
1432 | |
1433 | platform_name = 'nocloud-kvm' |
1434 | |
1435 | - def __init__(self, config): |
1436 | - """Set up platform.""" |
1437 | - super(NoCloudKVMPlatform, self).__init__(config) |
1438 | - |
1439 | def get_image(self, img_conf): |
1440 | """Get image using specified image configuration. |
1441 | |
1442 | diff --git a/tests/cloud_tests/platforms/platforms.py b/tests/cloud_tests/platforms/platforms.py |
1443 | index 1542b3b..abbfebb 100644 |
1444 | --- a/tests/cloud_tests/platforms/platforms.py |
1445 | +++ b/tests/cloud_tests/platforms/platforms.py |
1446 | @@ -2,12 +2,15 @@ |
1447 | |
1448 | """Base platform class.""" |
1449 | import os |
1450 | +import shutil |
1451 | |
1452 | from simplestreams import filters, mirrors |
1453 | from simplestreams import util as s_util |
1454 | |
1455 | from cloudinit import util as c_util |
1456 | |
1457 | +from tests.cloud_tests import util |
1458 | + |
1459 | |
1460 | class Platform(object): |
1461 | """Base class for platforms.""" |
1462 | @@ -17,7 +20,14 @@ class Platform(object): |
1463 | def __init__(self, config): |
1464 | """Set up platform.""" |
1465 | self.config = config |
1466 | - self._generate_ssh_keys(config['data_dir']) |
1467 | + self.tmpdir = util.mkdtemp() |
1468 | + if 'data_dir' in config: |
1469 | + self.data_dir = config['data_dir'] |
1470 | + else: |
1471 | + self.data_dir = os.path.join(self.tmpdir, "data_dir") |
1472 | + os.mkdir(self.data_dir) |
1473 | + |
1474 | + self._generate_ssh_keys(self.data_dir) |
1475 | |
1476 | def get_image(self, img_conf): |
1477 | """Get image using specified image configuration. |
1478 | @@ -29,7 +39,7 @@ class Platform(object): |
1479 | |
1480 | def destroy(self): |
1481 | """Clean up platform data.""" |
1482 | - pass |
1483 | + shutil.rmtree(self.tmpdir) |
1484 | |
1485 | def _generate_ssh_keys(self, data_dir): |
1486 | """Generate SSH keys to be used with image.""" |
1487 | diff --git a/tests/cloud_tests/testcases/modules/salt_minion.py b/tests/cloud_tests/testcases/modules/salt_minion.py |
1488 | index f13b48a..70917a4 100644 |
1489 | --- a/tests/cloud_tests/testcases/modules/salt_minion.py |
1490 | +++ b/tests/cloud_tests/testcases/modules/salt_minion.py |
1491 | @@ -31,4 +31,9 @@ class Test(base.CloudTestCase): |
1492 | out = self.get_data_file('grains') |
1493 | self.assertIn('role: web', out) |
1494 | |
1495 | + def test_minion_installed(self): |
1496 | + """Test if the salt-minion package is installed""" |
1497 | + out = self.get_data_file('minion_installed') |
1498 | + self.assertEqual(1, int(out)) |
1499 | + |
1500 | # vi: ts=4 expandtab |
1501 | diff --git a/tests/cloud_tests/testcases/modules/salt_minion.yaml b/tests/cloud_tests/testcases/modules/salt_minion.yaml |
1502 | index ab0e05b..f20b976 100644 |
1503 | --- a/tests/cloud_tests/testcases/modules/salt_minion.yaml |
1504 | +++ b/tests/cloud_tests/testcases/modules/salt_minion.yaml |
1505 | @@ -3,7 +3,7 @@ |
1506 | # |
1507 | # 2016-11-17: Currently takes >60 seconds results in test failure |
1508 | # |
1509 | -enabled: False |
1510 | +enabled: True |
1511 | cloud_config: | |
1512 | #cloud-config |
1513 | salt_minion: |
1514 | @@ -35,5 +35,8 @@ collect_scripts: |
1515 | grains: | |
1516 | #!/bin/bash |
1517 | cat /etc/salt/grains |
1518 | + minion_installed: | |
1519 | + #!/bin/bash |
1520 | + dpkg -l | grep salt-minion | grep ii | wc -l |
1521 | |
1522 | # vi: ts=4 expandtab |
1523 | diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py |
1524 | index 6ff285e..3dd4996 100644 |
1525 | --- a/tests/cloud_tests/util.py |
1526 | +++ b/tests/cloud_tests/util.py |
1527 | @@ -460,6 +460,10 @@ class PlatformError(IOError): |
1528 | IOError.__init__(self, message) |
1529 | |
1530 | |
1531 | +def mkdtemp(prefix='cloud_test_data'): |
1532 | + return tempfile.mkdtemp(prefix=prefix) |
1533 | + |
1534 | + |
1535 | class TempDir(object): |
1536 | """Configurable temporary directory like tempfile.TemporaryDirectory.""" |
1537 | |
1538 | @@ -480,7 +484,7 @@ class TempDir(object): |
1539 | @return_value: tempdir path |
1540 | """ |
1541 | if not self.tmpdir: |
1542 | - self.tmpdir = tempfile.mkdtemp(prefix=self.prefix) |
1543 | + self.tmpdir = mkdtemp(prefix=self.prefix) |
1544 | LOG.debug('using tmpdir: %s', self.tmpdir) |
1545 | return self.tmpdir |
1546 | |
1547 | diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py |
1548 | index 254e987..da7da0c 100644 |
1549 | --- a/tests/unittests/test_datasource/test_azure.py |
1550 | +++ b/tests/unittests/test_datasource/test_azure.py |
1551 | @@ -643,6 +643,21 @@ fdescfs /dev/fd fdescfs rw 0 0 |
1552 | expected_config['config'].append(blacklist_config) |
1553 | self.assertEqual(netconfig, expected_config) |
1554 | |
1555 | + @mock.patch("cloudinit.sources.DataSourceAzure.util.subp") |
1556 | + def test_get_hostname_with_no_args(self, subp): |
1557 | + dsaz.get_hostname() |
1558 | + subp.assert_called_once_with(("hostname",), capture=True) |
1559 | + |
1560 | + @mock.patch("cloudinit.sources.DataSourceAzure.util.subp") |
1561 | + def test_get_hostname_with_string_arg(self, subp): |
1562 | + dsaz.get_hostname(hostname_command="hostname") |
1563 | + subp.assert_called_once_with(("hostname",), capture=True) |
1564 | + |
1565 | + @mock.patch("cloudinit.sources.DataSourceAzure.util.subp") |
1566 | + def test_get_hostname_with_iterable_arg(self, subp): |
1567 | + dsaz.get_hostname(hostname_command=("hostname",)) |
1568 | + subp.assert_called_once_with(("hostname",), capture=True) |
1569 | + |
1570 | |
1571 | class TestAzureBounce(CiTestCase): |
1572 | |
1573 | diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/test_handler/test_handler_bootcmd.py |
1574 | index dbf43e0..29fc25e 100644 |
1575 | --- a/tests/unittests/test_handler/test_handler_bootcmd.py |
1576 | +++ b/tests/unittests/test_handler/test_handler_bootcmd.py |
1577 | @@ -3,17 +3,11 @@ |
1578 | from cloudinit.config import cc_bootcmd |
1579 | from cloudinit.sources import DataSourceNone |
1580 | from cloudinit import (distros, helpers, cloud, util) |
1581 | -from cloudinit.tests.helpers import CiTestCase, mock, skipIf |
1582 | +from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema |
1583 | |
1584 | import logging |
1585 | import tempfile |
1586 | |
1587 | -try: |
1588 | - import jsonschema |
1589 | - assert jsonschema # avoid pyflakes error F401: import unused |
1590 | - _missing_jsonschema_dep = False |
1591 | -except ImportError: |
1592 | - _missing_jsonschema_dep = True |
1593 | |
1594 | LOG = logging.getLogger(__name__) |
1595 | |
1596 | @@ -69,10 +63,10 @@ class TestBootcmd(CiTestCase): |
1597 | cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, []) |
1598 | self.assertIn('Failed to shellify bootcmd', self.logs.getvalue()) |
1599 | self.assertEqual( |
1600 | - "'int' object is not iterable", |
1601 | + "Input to shellify was type 'int'. Expected list or tuple.", |
1602 | str(context_manager.exception)) |
1603 | |
1604 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1605 | + @skipUnlessJsonSchema() |
1606 | def test_handler_schema_validation_warns_non_array_type(self): |
1607 | """Schema validation warns of non-array type for bootcmd key. |
1608 | |
1609 | @@ -88,7 +82,7 @@ class TestBootcmd(CiTestCase): |
1610 | self.logs.getvalue()) |
1611 | self.assertIn('Failed to shellify', self.logs.getvalue()) |
1612 | |
1613 | - @skipIf(_missing_jsonschema_dep, 'No python-jsonschema dependency') |
1614 | + @skipUnlessJsonSchema() |
1615 | def test_handler_schema_validation_warns_non_array_item_type(self): |
1616 | """Schema validation warns of non-array or string bootcmd items. |
1617 | |
1618 | @@ -98,7 +92,7 @@ class TestBootcmd(CiTestCase): |
1619 | invalid_config = { |
1620 | 'bootcmd': ['ls /', 20, ['wget', 'http://stuff/blah'], {'a': 'n'}]} |
1621 | cc = self._get_cloud('ubuntu') |
1622 | - with self.assertRaises(RuntimeError) as context_manager: |
1623 | + with self.assertRaises(TypeError) as context_manager: |
1624 | cc_bootcmd.handle('cc_bootcmd', invalid_config, cc, LOG, []) |
1625 | expected_warnings = [ |
1626 | 'bootcmd.1: 20 is not valid under any of the given schemas', |
1627 | @@ -110,7 +104,8 @@ class TestBootcmd(CiTestCase): |
1628 | self.assertIn(warning, logs) |
1629 | self.assertIn('Failed to shellify', logs) |
1630 | self.assertEqual( |
1631 | - 'Unable to shellify type int which is not a list or string', |
1632 | + ("Unable to shellify type 'int'. Expected list, string, tuple. " |
1633 | + "Got: 20"), |
1634 | str(context_manager.exception)) |
1635 | |
1636 | def test_handler_creates_and_runs_bootcmd_script_with_instance_id(self): |
1637 | diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py |
1638 | index 28a8455..695897c 100644 |
1639 | --- a/tests/unittests/test_handler/test_handler_ntp.py |
1640 | +++ b/tests/unittests/test_handler/test_handler_ntp.py |
1641 | @@ -3,7 +3,8 @@ |
1642 | from cloudinit.config import cc_ntp |
1643 | from cloudinit.sources import DataSourceNone |
1644 | from cloudinit import (distros, helpers, cloud, util) |
1645 | -from cloudinit.tests.helpers import FilesystemMockingTestCase, mock, skipIf |
1646 | +from cloudinit.tests.helpers import ( |
1647 | + FilesystemMockingTestCase, mock, skipUnlessJsonSchema) |
1648 | |
1649 | |
1650 | import os |
1651 | @@ -24,13 +25,6 @@ NTP={% for host in servers|list + pools|list %}{{ host }} {% endfor -%} |
1652 | {% endif -%} |
1653 | """ |
1654 | |
1655 | -try: |
1656 | - import jsonschema |
1657 | - assert jsonschema # avoid pyflakes error F401: import unused |
1658 | - _missing_jsonschema_dep = False |
1659 | -except ImportError: |
1660 | - _missing_jsonschema_dep = True |
1661 | - |
1662 | |
1663 | class TestNtp(FilesystemMockingTestCase): |
1664 | |
1665 | @@ -312,7 +306,7 @@ class TestNtp(FilesystemMockingTestCase): |
1666 | content) |
1667 | self.assertNotIn('Invalid config:', self.logs.getvalue()) |
1668 | |
1669 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1670 | + @skipUnlessJsonSchema() |
1671 | def test_ntp_handler_schema_validation_warns_non_string_item_type(self): |
1672 | """Ntp schema validation warns of non-strings in pools or servers. |
1673 | |
1674 | @@ -333,7 +327,7 @@ class TestNtp(FilesystemMockingTestCase): |
1675 | content = stream.read() |
1676 | self.assertEqual("servers ['valid', None]\npools [123]\n", content) |
1677 | |
1678 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1679 | + @skipUnlessJsonSchema() |
1680 | def test_ntp_handler_schema_validation_warns_of_non_array_type(self): |
1681 | """Ntp schema validation warns of non-array pools or servers types. |
1682 | |
1683 | @@ -354,7 +348,7 @@ class TestNtp(FilesystemMockingTestCase): |
1684 | content = stream.read() |
1685 | self.assertEqual("servers non-array\npools 123\n", content) |
1686 | |
1687 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1688 | + @skipUnlessJsonSchema() |
1689 | def test_ntp_handler_schema_validation_warns_invalid_key_present(self): |
1690 | """Ntp schema validation warns of invalid keys present in ntp config. |
1691 | |
1692 | @@ -378,7 +372,7 @@ class TestNtp(FilesystemMockingTestCase): |
1693 | "servers []\npools ['0.mycompany.pool.ntp.org']\n", |
1694 | content) |
1695 | |
1696 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1697 | + @skipUnlessJsonSchema() |
1698 | def test_ntp_handler_schema_validation_warns_of_duplicates(self): |
1699 | """Ntp schema validation warns of duplicates in servers or pools. |
1700 | |
1701 | diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/test_handler/test_handler_resizefs.py |
1702 | index 5aa3c49..c2a7f9f 100644 |
1703 | --- a/tests/unittests/test_handler/test_handler_resizefs.py |
1704 | +++ b/tests/unittests/test_handler/test_handler_resizefs.py |
1705 | @@ -7,21 +7,13 @@ from collections import namedtuple |
1706 | import logging |
1707 | import textwrap |
1708 | |
1709 | -from cloudinit.tests.helpers import (CiTestCase, mock, skipIf, util, |
1710 | - wrap_and_call) |
1711 | +from cloudinit.tests.helpers import ( |
1712 | + CiTestCase, mock, skipUnlessJsonSchema, util, wrap_and_call) |
1713 | |
1714 | |
1715 | LOG = logging.getLogger(__name__) |
1716 | |
1717 | |
1718 | -try: |
1719 | - import jsonschema |
1720 | - assert jsonschema # avoid pyflakes error F401: import unused |
1721 | - _missing_jsonschema_dep = False |
1722 | -except ImportError: |
1723 | - _missing_jsonschema_dep = True |
1724 | - |
1725 | - |
1726 | class TestResizefs(CiTestCase): |
1727 | with_logs = True |
1728 | |
1729 | @@ -76,7 +68,7 @@ class TestResizefs(CiTestCase): |
1730 | 'DEBUG: Skipping module named cc_resizefs, resizing disabled\n', |
1731 | self.logs.getvalue()) |
1732 | |
1733 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1734 | + @skipUnlessJsonSchema() |
1735 | def test_handle_schema_validation_logs_invalid_resize_rootfs_value(self): |
1736 | """The handle reports json schema violations as a warning. |
1737 | |
1738 | diff --git a/tests/unittests/test_handler/test_handler_runcmd.py b/tests/unittests/test_handler/test_handler_runcmd.py |
1739 | index 374c1d3..dbbb271 100644 |
1740 | --- a/tests/unittests/test_handler/test_handler_runcmd.py |
1741 | +++ b/tests/unittests/test_handler/test_handler_runcmd.py |
1742 | @@ -3,19 +3,13 @@ |
1743 | from cloudinit.config import cc_runcmd |
1744 | from cloudinit.sources import DataSourceNone |
1745 | from cloudinit import (distros, helpers, cloud, util) |
1746 | -from cloudinit.tests.helpers import FilesystemMockingTestCase, skipIf |
1747 | +from cloudinit.tests.helpers import ( |
1748 | + FilesystemMockingTestCase, skipUnlessJsonSchema) |
1749 | |
1750 | import logging |
1751 | import os |
1752 | import stat |
1753 | |
1754 | -try: |
1755 | - import jsonschema |
1756 | - assert jsonschema # avoid pyflakes error F401: import unused |
1757 | - _missing_jsonschema_dep = False |
1758 | -except ImportError: |
1759 | - _missing_jsonschema_dep = True |
1760 | - |
1761 | LOG = logging.getLogger(__name__) |
1762 | |
1763 | |
1764 | @@ -56,7 +50,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
1765 | ' /var/lib/cloud/instances/iid-datasource-none/scripts/runcmd', |
1766 | self.logs.getvalue()) |
1767 | |
1768 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1769 | + @skipUnlessJsonSchema() |
1770 | def test_handler_schema_validation_warns_non_array_type(self): |
1771 | """Schema validation warns of non-array type for runcmd key. |
1772 | |
1773 | @@ -71,7 +65,7 @@ class TestRuncmd(FilesystemMockingTestCase): |
1774 | self.logs.getvalue()) |
1775 | self.assertIn('Failed to shellify', self.logs.getvalue()) |
1776 | |
1777 | - @skipIf(_missing_jsonschema_dep, 'No python-jsonschema dependency') |
1778 | + @skipUnlessJsonSchema() |
1779 | def test_handler_schema_validation_warns_non_array_item_type(self): |
1780 | """Schema validation warns of non-array or string runcmd items. |
1781 | |
1782 | diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/test_handler/test_handler_set_hostname.py |
1783 | index abdc17e..d09ec23 100644 |
1784 | --- a/tests/unittests/test_handler/test_handler_set_hostname.py |
1785 | +++ b/tests/unittests/test_handler/test_handler_set_hostname.py |
1786 | @@ -11,6 +11,7 @@ from cloudinit.tests import helpers as t_help |
1787 | |
1788 | from configobj import ConfigObj |
1789 | import logging |
1790 | +import os |
1791 | import shutil |
1792 | from six import BytesIO |
1793 | import tempfile |
1794 | @@ -19,14 +20,18 @@ LOG = logging.getLogger(__name__) |
1795 | |
1796 | |
1797 | class TestHostname(t_help.FilesystemMockingTestCase): |
1798 | + |
1799 | + with_logs = True |
1800 | + |
1801 | def setUp(self): |
1802 | super(TestHostname, self).setUp() |
1803 | self.tmp = tempfile.mkdtemp() |
1804 | + util.ensure_dir(os.path.join(self.tmp, 'data')) |
1805 | self.addCleanup(shutil.rmtree, self.tmp) |
1806 | |
1807 | def _fetch_distro(self, kind): |
1808 | cls = distros.fetch(kind) |
1809 | - paths = helpers.Paths({}) |
1810 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1811 | return cls(kind, {}, paths) |
1812 | |
1813 | def test_write_hostname_rhel(self): |
1814 | @@ -34,7 +39,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): |
1815 | 'hostname': 'blah.blah.blah.yahoo.com', |
1816 | } |
1817 | distro = self._fetch_distro('rhel') |
1818 | - paths = helpers.Paths({}) |
1819 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1820 | ds = None |
1821 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
1822 | self.patchUtils(self.tmp) |
1823 | @@ -51,7 +56,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): |
1824 | 'hostname': 'blah.blah.blah.yahoo.com', |
1825 | } |
1826 | distro = self._fetch_distro('debian') |
1827 | - paths = helpers.Paths({}) |
1828 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1829 | ds = None |
1830 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
1831 | self.patchUtils(self.tmp) |
1832 | @@ -65,7 +70,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): |
1833 | 'hostname': 'blah.blah.blah.suse.com', |
1834 | } |
1835 | distro = self._fetch_distro('sles') |
1836 | - paths = helpers.Paths({}) |
1837 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1838 | ds = None |
1839 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
1840 | self.patchUtils(self.tmp) |
1841 | @@ -74,4 +79,48 @@ class TestHostname(t_help.FilesystemMockingTestCase): |
1842 | contents = util.load_file(distro.hostname_conf_fn) |
1843 | self.assertEqual('blah', contents.strip()) |
1844 | |
1845 | + def test_multiple_calls_skips_unchanged_hostname(self): |
1846 | + """Only new hostname or fqdn values will generate a hostname call.""" |
1847 | + distro = self._fetch_distro('debian') |
1848 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1849 | + ds = None |
1850 | + cc = cloud.Cloud(ds, paths, {}, distro, None) |
1851 | + self.patchUtils(self.tmp) |
1852 | + cc_set_hostname.handle( |
1853 | + 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, []) |
1854 | + contents = util.load_file("/etc/hostname") |
1855 | + self.assertEqual('hostname1', contents.strip()) |
1856 | + cc_set_hostname.handle( |
1857 | + 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, []) |
1858 | + self.assertIn( |
1859 | + 'DEBUG: No hostname changes. Skipping set-hostname\n', |
1860 | + self.logs.getvalue()) |
1861 | + cc_set_hostname.handle( |
1862 | + 'cc_set_hostname', {'hostname': 'hostname2.me.com'}, cc, LOG, []) |
1863 | + contents = util.load_file("/etc/hostname") |
1864 | + self.assertEqual('hostname2', contents.strip()) |
1865 | + self.assertIn( |
1866 | + 'Non-persistently setting the system hostname to hostname2', |
1867 | + self.logs.getvalue()) |
1868 | + |
1869 | + def test_error_on_distro_set_hostname_errors(self): |
1870 | + """Raise SetHostnameError on exceptions from distro.set_hostname.""" |
1871 | + distro = self._fetch_distro('debian') |
1872 | + |
1873 | + def set_hostname_error(hostname, fqdn): |
1874 | + raise Exception("OOPS on: %s" % fqdn) |
1875 | + |
1876 | + distro.set_hostname = set_hostname_error |
1877 | + paths = helpers.Paths({'cloud_dir': self.tmp}) |
1878 | + ds = None |
1879 | + cc = cloud.Cloud(ds, paths, {}, distro, None) |
1880 | + self.patchUtils(self.tmp) |
1881 | + with self.assertRaises(cc_set_hostname.SetHostnameError) as ctx_mgr: |
1882 | + cc_set_hostname.handle( |
1883 | + 'somename', {'hostname': 'hostname1.me.com'}, cc, LOG, []) |
1884 | + self.assertEqual( |
1885 | + 'Failed to set the hostname to hostname1.me.com (hostname1):' |
1886 | + ' OOPS on: hostname1.me.com', |
1887 | + str(ctx_mgr.exception)) |
1888 | + |
1889 | # vi: ts=4 expandtab |
1890 | diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py |
1891 | index df67a0e..1ecb6c6 100644 |
1892 | --- a/tests/unittests/test_handler/test_schema.py |
1893 | +++ b/tests/unittests/test_handler/test_schema.py |
1894 | @@ -6,7 +6,7 @@ from cloudinit.config.schema import ( |
1895 | validate_cloudconfig_schema, main) |
1896 | from cloudinit.util import subp, write_file |
1897 | |
1898 | -from cloudinit.tests.helpers import CiTestCase, mock, skipIf |
1899 | +from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema |
1900 | |
1901 | from copy import copy |
1902 | import os |
1903 | @@ -14,13 +14,6 @@ from six import StringIO |
1904 | from textwrap import dedent |
1905 | from yaml import safe_load |
1906 | |
1907 | -try: |
1908 | - import jsonschema |
1909 | - assert jsonschema # avoid pyflakes error F401: import unused |
1910 | - _missing_jsonschema_dep = False |
1911 | -except ImportError: |
1912 | - _missing_jsonschema_dep = True |
1913 | - |
1914 | |
1915 | class GetSchemaTest(CiTestCase): |
1916 | |
1917 | @@ -73,7 +66,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase): |
1918 | |
1919 | with_logs = True |
1920 | |
1921 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1922 | + @skipUnlessJsonSchema() |
1923 | def test_validateconfig_schema_non_strict_emits_warnings(self): |
1924 | """When strict is False validate_cloudconfig_schema emits warnings.""" |
1925 | schema = {'properties': {'p1': {'type': 'string'}}} |
1926 | @@ -82,7 +75,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase): |
1927 | "Invalid config:\np1: -1 is not of type 'string'\n", |
1928 | self.logs.getvalue()) |
1929 | |
1930 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1931 | + @skipUnlessJsonSchema() |
1932 | def test_validateconfig_schema_emits_warning_on_missing_jsonschema(self): |
1933 | """Warning from validate_cloudconfig_schema when missing jsonschema.""" |
1934 | schema = {'properties': {'p1': {'type': 'string'}}} |
1935 | @@ -92,7 +85,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase): |
1936 | 'Ignoring schema validation. python-jsonschema is not present', |
1937 | self.logs.getvalue()) |
1938 | |
1939 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1940 | + @skipUnlessJsonSchema() |
1941 | def test_validateconfig_schema_strict_raises_errors(self): |
1942 | """When strict is True validate_cloudconfig_schema raises errors.""" |
1943 | schema = {'properties': {'p1': {'type': 'string'}}} |
1944 | @@ -102,7 +95,7 @@ class ValidateCloudConfigSchemaTest(CiTestCase): |
1945 | "Cloud config schema errors: p1: -1 is not of type 'string'", |
1946 | str(context_mgr.exception)) |
1947 | |
1948 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1949 | + @skipUnlessJsonSchema() |
1950 | def test_validateconfig_schema_honors_formats(self): |
1951 | """With strict True, validate_cloudconfig_schema errors on format.""" |
1952 | schema = { |
1953 | @@ -153,7 +146,7 @@ class ValidateCloudConfigFileTest(CiTestCase): |
1954 | self.config_file), |
1955 | str(context_mgr.exception)) |
1956 | |
1957 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1958 | + @skipUnlessJsonSchema() |
1959 | def test_validateconfig_file_sctricty_validates_schema(self): |
1960 | """validate_cloudconfig_file raises errors on invalid schema.""" |
1961 | schema = { |
1962 | @@ -376,7 +369,7 @@ class CloudTestsIntegrationTest(CiTestCase): |
1963 | raises Warnings or errors on invalid cloud-config schema. |
1964 | """ |
1965 | |
1966 | - @skipIf(_missing_jsonschema_dep, "No python-jsonschema dependency") |
1967 | + @skipUnlessJsonSchema() |
1968 | def test_all_integration_test_cloud_config_schema(self): |
1969 | """Validate schema of cloud_tests yaml files looking for warnings.""" |
1970 | schema = get_schema() |
1971 | diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py |
1972 | index 89ae40f..499e7c9 100644 |
1973 | --- a/tests/unittests/test_util.py |
1974 | +++ b/tests/unittests/test_util.py |
1975 | @@ -632,6 +632,24 @@ class TestSubp(helpers.CiTestCase): |
1976 | # but by using bash, we remove dependency on another program. |
1977 | return([BASH, '-c', 'printf "$@"', 'printf'] + list(args)) |
1978 | |
1979 | + def test_subp_handles_bytestrings(self): |
1980 | + """subp can run a bytestring command if shell is True.""" |
1981 | + tmp_file = self.tmp_path('test.out') |
1982 | + cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file) |
1983 | + (out, _err) = util.subp(cmd.encode('utf-8'), shell=True) |
1984 | + self.assertEqual(u'', out) |
1985 | + self.assertEqual(u'', _err) |
1986 | + self.assertEqual('HI MOM\n', util.load_file(tmp_file)) |
1987 | + |
1988 | + def test_subp_handles_strings(self): |
1989 | + """subp can run a string command if shell is True.""" |
1990 | + tmp_file = self.tmp_path('test.out') |
1991 | + cmd = 'echo HI MOM >> {tmp_file}'.format(tmp_file=tmp_file) |
1992 | + (out, _err) = util.subp(cmd, shell=True) |
1993 | + self.assertEqual(u'', out) |
1994 | + self.assertEqual(u'', _err) |
1995 | + self.assertEqual('HI MOM\n', util.load_file(tmp_file)) |
1996 | + |
1997 | def test_subp_handles_utf8(self): |
1998 | # The given bytes contain utf-8 accented characters as seen in e.g. |
1999 | # the "deja dup" package in Ubuntu. |
PASSED: Continuous integration, rev:a6262577f56 d32fb6005a55f90 22309c5dc7dce5 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 859/
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/ 859/rebuild
https:/