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