Merge lp:~oddbloke/cloud-init/lp1375252 into lp:~cloud-init-dev/cloud-init/trunk
- lp1375252
- Merge into trunk
Proposed by
Dan Watkins
Status: | Merged |
---|---|
Merged at revision: | 1098 |
Proposed branch: | lp:~oddbloke/cloud-init/lp1375252 |
Merge into: | lp:~cloud-init-dev/cloud-init/trunk |
Diff against target: |
425 lines (+247/-106) 2 files modified
cloudinit/sources/DataSourceAzure.py (+75/-61) tests/unittests/test_datasource/test_azure.py (+172/-45) |
To merge this branch: | bzr merge lp:~oddbloke/cloud-init/lp1375252 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
cloud-init Commiters | Pending | ||
Review via email: mp+256291@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Dan Watkins (oddbloke) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'cloudinit/sources/DataSourceAzure.py' |
2 | --- cloudinit/sources/DataSourceAzure.py 2015-02-18 13:30:51 +0000 |
3 | +++ cloudinit/sources/DataSourceAzure.py 2015-04-15 11:26:23 +0000 |
4 | @@ -17,6 +17,7 @@ |
5 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
6 | |
7 | import base64 |
8 | +import contextlib |
9 | import crypt |
10 | import fnmatch |
11 | import os |
12 | @@ -66,6 +67,36 @@ |
13 | DEF_EPHEMERAL_LABEL = 'Temporary Storage' |
14 | |
15 | |
16 | +def get_hostname(hostname_command='hostname'): |
17 | + return util.subp(hostname_command, capture=True)[0].strip() |
18 | + |
19 | + |
20 | +def set_hostname(hostname, hostname_command='hostname'): |
21 | + util.subp([hostname_command, hostname]) |
22 | + |
23 | + |
24 | +@contextlib.contextmanager |
25 | +def temporary_hostname(temp_hostname, cfg, hostname_command='hostname'): |
26 | + """ |
27 | + Set a temporary hostname, restoring the previous hostname on exit. |
28 | + |
29 | + Will have the value of the previous hostname when used as a context |
30 | + manager, or None if the hostname was not changed. |
31 | + """ |
32 | + policy = cfg['hostname_bounce']['policy'] |
33 | + previous_hostname = get_hostname(hostname_command) |
34 | + if (not util.is_true(cfg.get('set_hostname')) |
35 | + or util.is_false(policy) |
36 | + or (previous_hostname == temp_hostname and policy != 'force')): |
37 | + yield None |
38 | + return |
39 | + set_hostname(temp_hostname, hostname_command) |
40 | + try: |
41 | + yield previous_hostname |
42 | + finally: |
43 | + set_hostname(previous_hostname, hostname_command) |
44 | + |
45 | + |
46 | class DataSourceAzureNet(sources.DataSource): |
47 | def __init__(self, sys_cfg, distro, paths): |
48 | sources.DataSource.__init__(self, sys_cfg, distro, paths) |
49 | @@ -154,33 +185,40 @@ |
50 | # the directory to be protected. |
51 | write_files(ddir, files, dirmode=0o700) |
52 | |
53 | - # handle the hostname 'publishing' |
54 | - try: |
55 | - handle_set_hostname(mycfg.get('set_hostname'), |
56 | - self.metadata.get('local-hostname'), |
57 | - mycfg['hostname_bounce']) |
58 | - except Exception as e: |
59 | - LOG.warn("Failed publishing hostname: %s", e) |
60 | - util.logexc(LOG, "handling set_hostname failed") |
61 | - |
62 | - try: |
63 | - invoke_agent(mycfg['agent_command']) |
64 | - except util.ProcessExecutionError: |
65 | - # claim the datasource even if the command failed |
66 | - util.logexc(LOG, "agent command '%s' failed.", |
67 | - mycfg['agent_command']) |
68 | - |
69 | - shcfgxml = os.path.join(ddir, "SharedConfig.xml") |
70 | - wait_for = [shcfgxml] |
71 | - |
72 | - fp_files = [] |
73 | - for pk in self.cfg.get('_pubkeys', []): |
74 | - bname = str(pk['fingerprint'] + ".crt") |
75 | - fp_files += [os.path.join(ddir, bname)] |
76 | - |
77 | - missing = util.log_time(logfunc=LOG.debug, msg="waiting for files", |
78 | - func=wait_for_files, |
79 | - args=(wait_for + fp_files,)) |
80 | + temp_hostname = self.metadata.get('local-hostname') |
81 | + hostname_command = mycfg['hostname_bounce']['hostname_command'] |
82 | + with temporary_hostname(temp_hostname, mycfg, |
83 | + hostname_command=hostname_command) \ |
84 | + as previous_hostname: |
85 | + if (previous_hostname is not None |
86 | + and util.is_true(mycfg.get('set_hostname'))): |
87 | + cfg = mycfg['hostname_bounce'] |
88 | + try: |
89 | + perform_hostname_bounce(hostname=temp_hostname, |
90 | + cfg=cfg, |
91 | + prev_hostname=previous_hostname) |
92 | + except Exception as e: |
93 | + LOG.warn("Failed publishing hostname: %s", e) |
94 | + util.logexc(LOG, "handling set_hostname failed") |
95 | + |
96 | + try: |
97 | + invoke_agent(mycfg['agent_command']) |
98 | + except util.ProcessExecutionError: |
99 | + # claim the datasource even if the command failed |
100 | + util.logexc(LOG, "agent command '%s' failed.", |
101 | + mycfg['agent_command']) |
102 | + |
103 | + shcfgxml = os.path.join(ddir, "SharedConfig.xml") |
104 | + wait_for = [shcfgxml] |
105 | + |
106 | + fp_files = [] |
107 | + for pk in self.cfg.get('_pubkeys', []): |
108 | + bname = str(pk['fingerprint'] + ".crt") |
109 | + fp_files += [os.path.join(ddir, bname)] |
110 | + |
111 | + missing = util.log_time(logfunc=LOG.debug, msg="waiting for files", |
112 | + func=wait_for_files, |
113 | + args=(wait_for + fp_files,)) |
114 | if len(missing): |
115 | LOG.warn("Did not find files, but going on: %s", missing) |
116 | |
117 | @@ -299,39 +337,15 @@ |
118 | return mod_list |
119 | |
120 | |
121 | -def handle_set_hostname(enabled, hostname, cfg): |
122 | - if not util.is_true(enabled): |
123 | - return |
124 | - |
125 | - if not hostname: |
126 | - LOG.warn("set_hostname was true but no local-hostname") |
127 | - return |
128 | - |
129 | - apply_hostname_bounce(hostname=hostname, policy=cfg['policy'], |
130 | - interface=cfg['interface'], |
131 | - command=cfg['command'], |
132 | - hostname_command=cfg['hostname_command']) |
133 | - |
134 | - |
135 | -def apply_hostname_bounce(hostname, policy, interface, command, |
136 | - hostname_command="hostname"): |
137 | +def perform_hostname_bounce(hostname, cfg, prev_hostname): |
138 | # set the hostname to 'hostname' if it is not already set to that. |
139 | # then, if policy is not off, bounce the interface using command |
140 | - prev_hostname = util.subp(hostname_command, capture=True)[0].strip() |
141 | - |
142 | - util.subp([hostname_command, hostname]) |
143 | - |
144 | - msg = ("phostname=%s hostname=%s policy=%s interface=%s" % |
145 | - (prev_hostname, hostname, policy, interface)) |
146 | - |
147 | - if util.is_false(policy): |
148 | - LOG.debug("pubhname: policy false, skipping [%s]", msg) |
149 | - return |
150 | - |
151 | - if prev_hostname == hostname and policy != "force": |
152 | - LOG.debug("pubhname: no change, policy != force. skipping. [%s]", msg) |
153 | - return |
154 | - |
155 | + command = cfg['command'] |
156 | + interface = cfg['interface'] |
157 | + policy = cfg['policy'] |
158 | + |
159 | + msg = ("hostname=%s policy=%s interface=%s" % |
160 | + (hostname, policy, interface)) |
161 | env = os.environ.copy() |
162 | env['interface'] = interface |
163 | env['hostname'] = hostname |
164 | @@ -344,9 +358,9 @@ |
165 | shell = not isinstance(command, (list, tuple)) |
166 | # capture=False, see comments in bug 1202758 and bug 1206164. |
167 | util.log_time(logfunc=LOG.debug, msg="publishing hostname", |
168 | - get_uptime=True, func=util.subp, |
169 | - kwargs={'args': command, 'shell': shell, 'capture': False, |
170 | - 'env': env}) |
171 | + get_uptime=True, func=util.subp, |
172 | + kwargs={'args': command, 'shell': shell, 'capture': False, |
173 | + 'env': env}) |
174 | |
175 | |
176 | def crtfile_to_pubkey(fname): |
177 | |
178 | === modified file 'tests/unittests/test_datasource/test_azure.py' |
179 | --- tests/unittests/test_datasource/test_azure.py 2015-02-24 16:58:22 +0000 |
180 | +++ tests/unittests/test_datasource/test_azure.py 2015-04-15 11:26:23 +0000 |
181 | @@ -116,9 +116,6 @@ |
182 | data['iid_from_shared_cfg'] = path |
183 | return 'i-my-azure-id' |
184 | |
185 | - def _apply_hostname_bounce(**kwargs): |
186 | - data['apply_hostname_bounce'] = kwargs |
187 | - |
188 | if data.get('ovfcontent') is not None: |
189 | populate_dir(os.path.join(self.paths.seed_dir, "azure"), |
190 | {'ovf-env.xml': data['ovfcontent']}) |
191 | @@ -132,7 +129,9 @@ |
192 | (mod, 'wait_for_files', _wait_for_files), |
193 | (mod, 'pubkeys_from_crt_files', _pubkeys_from_crt_files), |
194 | (mod, 'iid_from_shared_config', _iid_from_shared_config), |
195 | - (mod, 'apply_hostname_bounce', _apply_hostname_bounce), |
196 | + (mod, 'perform_hostname_bounce', mock.MagicMock()), |
197 | + (mod, 'get_hostname', mock.MagicMock()), |
198 | + (mod, 'set_hostname', mock.MagicMock()), |
199 | ]) |
200 | |
201 | dsrc = mod.DataSourceAzureNet( |
202 | @@ -272,47 +271,6 @@ |
203 | for mypk in mypklist: |
204 | self.assertIn(mypk, dsrc.cfg['_pubkeys']) |
205 | |
206 | - def test_disabled_bounce(self): |
207 | - pass |
208 | - |
209 | - def test_apply_bounce_call_1(self): |
210 | - # hostname needs to get through to apply_hostname_bounce |
211 | - odata = {'HostName': 'my-random-hostname'} |
212 | - data = {'ovfcontent': construct_valid_ovf_env(data=odata)} |
213 | - |
214 | - self._get_ds(data).get_data() |
215 | - self.assertIn('hostname', data['apply_hostname_bounce']) |
216 | - self.assertEqual(data['apply_hostname_bounce']['hostname'], |
217 | - odata['HostName']) |
218 | - |
219 | - def test_apply_bounce_call_configurable(self): |
220 | - # hostname_bounce should be configurable in datasource cfg |
221 | - cfg = {'hostname_bounce': {'interface': 'eth1', 'policy': 'off', |
222 | - 'command': 'my-bounce-command', |
223 | - 'hostname_command': 'my-hostname-command'}} |
224 | - odata = {'HostName': "xhost", |
225 | - 'dscfg': {'text': b64e(yaml.dump(cfg)), |
226 | - 'encoding': 'base64'}} |
227 | - data = {'ovfcontent': construct_valid_ovf_env(data=odata)} |
228 | - self._get_ds(data).get_data() |
229 | - |
230 | - for k in cfg['hostname_bounce']: |
231 | - self.assertIn(k, data['apply_hostname_bounce']) |
232 | - |
233 | - for k, v in cfg['hostname_bounce'].items(): |
234 | - self.assertEqual(data['apply_hostname_bounce'][k], v) |
235 | - |
236 | - def test_set_hostname_disabled(self): |
237 | - # config specifying set_hostname off should not bounce |
238 | - cfg = {'set_hostname': False} |
239 | - odata = {'HostName': "xhost", |
240 | - 'dscfg': {'text': b64e(yaml.dump(cfg)), |
241 | - 'encoding': 'base64'}} |
242 | - data = {'ovfcontent': construct_valid_ovf_env(data=odata)} |
243 | - self._get_ds(data).get_data() |
244 | - |
245 | - self.assertEqual(data.get('apply_hostname_bounce', "N/A"), "N/A") |
246 | - |
247 | def test_default_ephemeral(self): |
248 | # make sure the ephemeral device works |
249 | odata = {} |
250 | @@ -425,6 +383,175 @@ |
251 | load_file(os.path.join(self.waagent_d, 'ovf-env.xml'))) |
252 | |
253 | |
254 | +class TestAzureBounce(TestCase): |
255 | + |
256 | + def mock_out_azure_moving_parts(self): |
257 | + self.patches.enter_context( |
258 | + mock.patch.object(DataSourceAzure, 'invoke_agent')) |
259 | + self.patches.enter_context( |
260 | + mock.patch.object(DataSourceAzure, 'wait_for_files')) |
261 | + self.patches.enter_context( |
262 | + mock.patch.object(DataSourceAzure, 'iid_from_shared_config', |
263 | + mock.MagicMock(return_value='i-my-azure-id'))) |
264 | + self.patches.enter_context( |
265 | + mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs', |
266 | + mock.MagicMock(return_value=[]))) |
267 | + self.patches.enter_context( |
268 | + mock.patch.object(DataSourceAzure, 'find_ephemeral_disk', |
269 | + mock.MagicMock(return_value=None))) |
270 | + self.patches.enter_context( |
271 | + mock.patch.object(DataSourceAzure, 'find_ephemeral_part', |
272 | + mock.MagicMock(return_value=None))) |
273 | + |
274 | + def setUp(self): |
275 | + super(TestAzureBounce, self).setUp() |
276 | + self.tmp = tempfile.mkdtemp() |
277 | + self.waagent_d = os.path.join(self.tmp, 'var', 'lib', 'waagent') |
278 | + self.paths = helpers.Paths({'cloud_dir': self.tmp}) |
279 | + self.addCleanup(shutil.rmtree, self.tmp) |
280 | + DataSourceAzure.BUILTIN_DS_CONFIG['data_dir'] = self.waagent_d |
281 | + self.patches = ExitStack() |
282 | + self.mock_out_azure_moving_parts() |
283 | + self.get_hostname = self.patches.enter_context( |
284 | + mock.patch.object(DataSourceAzure, 'get_hostname')) |
285 | + self.set_hostname = self.patches.enter_context( |
286 | + mock.patch.object(DataSourceAzure, 'set_hostname')) |
287 | + self.subp = self.patches.enter_context( |
288 | + mock.patch('cloudinit.sources.DataSourceAzure.util.subp')) |
289 | + |
290 | + def tearDown(self): |
291 | + self.patches.close() |
292 | + |
293 | + def _get_ds(self, ovfcontent=None): |
294 | + if ovfcontent is not None: |
295 | + populate_dir(os.path.join(self.paths.seed_dir, "azure"), |
296 | + {'ovf-env.xml': ovfcontent}) |
297 | + return DataSourceAzure.DataSourceAzureNet( |
298 | + {}, distro=None, paths=self.paths) |
299 | + |
300 | + def get_ovf_env_with_dscfg(self, hostname, cfg): |
301 | + odata = { |
302 | + 'HostName': hostname, |
303 | + 'dscfg': { |
304 | + 'text': b64e(yaml.dump(cfg)), |
305 | + 'encoding': 'base64' |
306 | + } |
307 | + } |
308 | + return construct_valid_ovf_env(data=odata) |
309 | + |
310 | + def test_disabled_bounce_does_not_change_hostname(self): |
311 | + cfg = {'hostname_bounce': {'policy': 'off'}} |
312 | + self._get_ds(self.get_ovf_env_with_dscfg('test-host', cfg)).get_data() |
313 | + self.assertEqual(0, self.set_hostname.call_count) |
314 | + |
315 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
316 | + def test_disabled_bounce_does_not_perform_bounce( |
317 | + self, perform_hostname_bounce): |
318 | + cfg = {'hostname_bounce': {'policy': 'off'}} |
319 | + self._get_ds(self.get_ovf_env_with_dscfg('test-host', cfg)).get_data() |
320 | + self.assertEqual(0, perform_hostname_bounce.call_count) |
321 | + |
322 | + def test_same_hostname_does_not_change_hostname(self): |
323 | + host_name = 'unchanged-host-name' |
324 | + self.get_hostname.return_value = host_name |
325 | + cfg = {'hostname_bounce': {'policy': 'yes'}} |
326 | + self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() |
327 | + self.assertEqual(0, self.set_hostname.call_count) |
328 | + |
329 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
330 | + def test_unchanged_hostname_does_not_perform_bounce( |
331 | + self, perform_hostname_bounce): |
332 | + host_name = 'unchanged-host-name' |
333 | + self.get_hostname.return_value = host_name |
334 | + cfg = {'hostname_bounce': {'policy': 'yes'}} |
335 | + self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() |
336 | + self.assertEqual(0, perform_hostname_bounce.call_count) |
337 | + |
338 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
339 | + def test_force_performs_bounce_regardless(self, perform_hostname_bounce): |
340 | + host_name = 'unchanged-host-name' |
341 | + self.get_hostname.return_value = host_name |
342 | + cfg = {'hostname_bounce': {'policy': 'force'}} |
343 | + self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)).get_data() |
344 | + self.assertEqual(1, perform_hostname_bounce.call_count) |
345 | + |
346 | + def test_different_hostnames_sets_hostname(self): |
347 | + expected_hostname = 'azure-expected-host-name' |
348 | + self.get_hostname.return_value = 'default-host-name' |
349 | + self._get_ds( |
350 | + self.get_ovf_env_with_dscfg(expected_hostname, {})).get_data() |
351 | + self.assertEqual(expected_hostname, |
352 | + self.set_hostname.call_args_list[0][0][0]) |
353 | + |
354 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
355 | + def test_different_hostnames_performs_bounce( |
356 | + self, perform_hostname_bounce): |
357 | + expected_hostname = 'azure-expected-host-name' |
358 | + self.get_hostname.return_value = 'default-host-name' |
359 | + self._get_ds( |
360 | + self.get_ovf_env_with_dscfg(expected_hostname, {})).get_data() |
361 | + self.assertEqual(1, perform_hostname_bounce.call_count) |
362 | + |
363 | + def test_different_hostnames_sets_hostname_back(self): |
364 | + initial_host_name = 'default-host-name' |
365 | + self.get_hostname.return_value = initial_host_name |
366 | + self._get_ds( |
367 | + self.get_ovf_env_with_dscfg('some-host-name', {})).get_data() |
368 | + self.assertEqual(initial_host_name, |
369 | + self.set_hostname.call_args_list[-1][0][0]) |
370 | + |
371 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
372 | + def test_failure_in_bounce_still_resets_host_name( |
373 | + self, perform_hostname_bounce): |
374 | + perform_hostname_bounce.side_effect = Exception |
375 | + initial_host_name = 'default-host-name' |
376 | + self.get_hostname.return_value = initial_host_name |
377 | + self._get_ds( |
378 | + self.get_ovf_env_with_dscfg('some-host-name', {})).get_data() |
379 | + self.assertEqual(initial_host_name, |
380 | + self.set_hostname.call_args_list[-1][0][0]) |
381 | + |
382 | + def test_environment_correct_for_bounce_command(self): |
383 | + interface = 'int0' |
384 | + hostname = 'my-new-host' |
385 | + old_hostname = 'my-old-host' |
386 | + self.get_hostname.return_value = old_hostname |
387 | + cfg = {'hostname_bounce': {'interface': interface, 'policy': 'force'}} |
388 | + data = self.get_ovf_env_with_dscfg(hostname, cfg) |
389 | + self._get_ds(data).get_data() |
390 | + self.assertEqual(1, self.subp.call_count) |
391 | + bounce_env = self.subp.call_args[1]['env'] |
392 | + self.assertEqual(interface, bounce_env['interface']) |
393 | + self.assertEqual(hostname, bounce_env['hostname']) |
394 | + self.assertEqual(old_hostname, bounce_env['old_hostname']) |
395 | + |
396 | + def test_default_bounce_command_used_by_default(self): |
397 | + cmd = 'default-bounce-command' |
398 | + DataSourceAzure.BUILTIN_DS_CONFIG['hostname_bounce']['command'] = cmd |
399 | + cfg = {'hostname_bounce': {'policy': 'force'}} |
400 | + data = self.get_ovf_env_with_dscfg('some-hostname', cfg) |
401 | + self._get_ds(data).get_data() |
402 | + self.assertEqual(1, self.subp.call_count) |
403 | + bounce_args = self.subp.call_args[1]['args'] |
404 | + self.assertEqual(cmd, bounce_args) |
405 | + |
406 | + @mock.patch('cloudinit.sources.DataSourceAzure.perform_hostname_bounce') |
407 | + def test_set_hostname_option_can_disable_bounce( |
408 | + self, perform_hostname_bounce): |
409 | + cfg = {'set_hostname': False, 'hostname_bounce': {'policy': 'force'}} |
410 | + data = self.get_ovf_env_with_dscfg('some-hostname', cfg) |
411 | + self._get_ds(data).get_data() |
412 | + |
413 | + self.assertEqual(0, perform_hostname_bounce.call_count) |
414 | + |
415 | + def test_set_hostname_option_can_disable_hostname_set(self): |
416 | + cfg = {'set_hostname': False, 'hostname_bounce': {'policy': 'force'}} |
417 | + data = self.get_ovf_env_with_dscfg('some-hostname', cfg) |
418 | + self._get_ds(data).get_data() |
419 | + |
420 | + self.assertEqual(0, self.set_hostname.call_count) |
421 | + |
422 | + |
423 | class TestReadAzureOvf(TestCase): |
424 | def test_invalid_xml_raises_non_azure_ds(self): |
425 | invalid_xml = "<foo>" + construct_valid_ovf_env(data={}) |
Review appreciated, but probably shouldn't be merged until vivid is out the door.