Merge lp:~oddbloke/cloud-init/lp1375252 into lp:~cloud-init-dev/cloud-init/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
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+256291@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Dan Watkins (oddbloke) wrote :

Review appreciated, but probably shouldn't be merged until vivid is out the door.

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={})