Merge ~chad.smith/cloud-init:set-hostname-before-network into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- set-hostname-before-network
- Merge into master
Status: | Merged |
---|---|
Approved by: | Scott Moser |
Approved revision: | 148b8ab1ae0a21bb9e58a94818a234d0aaaf3548 |
Merge reported by: | Chad Smith |
Merged at revision: | 133ad2cb327ad17b7b81319fac8f9f14577c04df |
Proposed branch: | ~chad.smith/cloud-init:set-hostname-before-network |
Merge into: | cloud-init:master |
Diff against target: |
668 lines (+449/-22) 9 files modified
cloudinit/cloud.py (+3/-2) cloudinit/cmd/main.py (+25/-0) cloudinit/cmd/tests/test_main.py (+161/-0) cloudinit/config/cc_set_hostname.py (+35/-6) cloudinit/sources/__init__.py (+17/-4) cloudinit/sources/tests/test_init.py (+69/-1) cloudinit/tests/test_util.py (+74/-0) cloudinit/util.py (+12/-5) tests/unittests/test_handler/test_handler_set_hostname.py (+53/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email: mp+339720@code.launchpad.net |
Commit message
set_hostname: When present in metadata, set it before network bringup.
When instance meta-data provides hostname information, run
cc_set_hostname in the init-local or init-net stage before network
comes up.
Prevent an initial DHCP request which leaks the stock cloud-image default
hostname before the meta-data provided hostname was processed.
A leaked cloud-image hostname adversely affects Dynamic DNS which
would reallocate 'ubuntu' hostname in DNS to every instance brought up by
cloud-init. These instances would only update DNS to the cloud-init
configured hostname upon DHCP lease renewal.
This branch extends the get_hostname methods in datasource, cloud and
util to limit results to metadata_only to avoid extra cost of querying
the distro for hostname information if metadata does not provide that
information.
LP: #1746455
Description of the change
See commit message
Server Team CI bot (server-team-bot) wrote : | # |
Scott Moser (smoser) : | # |
Ryan Harper (raharper) wrote : | # |
I've a bit of a quibble with the description. Mostly because cloud-init has _no_ control over how DNS or DHCP servers react to dhcp client requests coming from an instance.
What we (cloud-init) can do is set instance specified hostnames as _soon_ as we know it, which may be of help to some situations where Dynamic DNS via dhcp client hostname value.
This is an environment specific issue and having cloud-init set hostname as soon as possible does not ensure that there won't be issues with DNS, DHCP outside of cloud-init control.
Some inline comments too.
- 8211b97... by Chad Smith
-
lints and flake8
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:2baccbbf6c4
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:
https:/
Chad Smith (chad.smith) : | # |
Chad Smith (chad.smith) : | # |
Scott Moser (smoser) wrote : | # |
My only comment at this point is wheter or not we should bother with calling the config module and writing the marker file and such at that point.
the other option is to just set it.
The other thoughts (i guess i did have more than one)
a.) we have had issues with setting hostname not being possible early in boot due to that being a systemd service (hostnamectl) .
b.) we want to somehow make sure that we only set this hostname on first instance boot. We don't want to overwrite what a user configured manually.
I think that your use of config module probably handles that, but just want to write those thoughts down.
Ryan Harper (raharper) wrote : | # |
Sorry for the noise on the get_hostname part; I missed it getting sent through the datasource object.
- 6bd8875... by Chad Smith
-
SethostnameError is now raised by cc_set_hostname. Handle that exception in init-local main. Call cc_set_hostname if hostname changed in init-net.
- 37d5b61... by Chad Smith
-
UserDataProcess class now returns a tuple of (processed_
messages, processing_errors). Datsources now requery userdata or vendordata on previous errors instead of using the cache. - 227b353... by Chad Smith
-
flakes
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:2b8f6fa52c9
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:
https:/
Scott Moser (smoser) wrote : | # |
hm...
Does UserDataProcess
If so, we're absolutely changing their contract. Now they'd be running without a network.
I'm really sorry this is so hard. :-(
- 6ebb0d1... by Chad Smith
-
address review comments
- cc8a6ec... by Chad Smith
-
revert UserDataProcess changes
- 92d62c6... by Chad Smith
-
don't call update in local stage as it tries to process userdata. Only react to metadata in local stage
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:254d7a22e42
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:
https:/
- e51081d... by Chad Smith
-
unit tests for cc_set_hostname and get_hostname_fqdn
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:cf810a94cbf
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:
https:/
- 29fc8fe... by Chad Smith
-
add unit test for SetHostnameError
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:711843e2e24
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:
https:/
Scott Moser (smoser) wrote : | # |
Some questions inline.
I think your commit message also needs updating to address the
fact that you're (I htink) only looking at meta-data, not user-data.
Specifically:
"When instance metadata/user-data provides hostname information,"
- 4a3814d... by Chad Smith
-
address review: fixup incorrect docstring comments on metadata_only parameter, comment about different uses of set-hostname artifact file versus prevvious-hostname used by cc_update_hostname
- d9ff4f2... by Chad Smith
-
add unit tests for main_init calling set_hostname on metadata
- 148b8ab... by Chad Smith
-
flakes
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:fe9ba4daf59
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:
https:/
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:148b8ab1ae0
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:
https:/
Scott Moser (smoser) wrote : | # |
Approved. I'd like to get this into bionic tomorrow.
Chad Smith (chad.smith) wrote : | # |
An upstream commit landed for this bug.
To view that commit see the following URL:
https:/
Preview Diff
1 | diff --git a/cloudinit/cloud.py b/cloudinit/cloud.py | |||
2 | index ba61678..6d12c43 100644 | |||
3 | --- a/cloudinit/cloud.py | |||
4 | +++ b/cloudinit/cloud.py | |||
5 | @@ -78,8 +78,9 @@ class Cloud(object): | |||
6 | 78 | def get_locale(self): | 78 | def get_locale(self): |
7 | 79 | return self.datasource.get_locale() | 79 | return self.datasource.get_locale() |
8 | 80 | 80 | ||
11 | 81 | def get_hostname(self, fqdn=False): | 81 | def get_hostname(self, fqdn=False, metadata_only=False): |
12 | 82 | return self.datasource.get_hostname(fqdn=fqdn) | 82 | return self.datasource.get_hostname( |
13 | 83 | fqdn=fqdn, metadata_only=metadata_only) | ||
14 | 83 | 84 | ||
15 | 84 | def device_name_to_device(self, name): | 85 | def device_name_to_device(self, name): |
16 | 85 | return self.datasource.device_name_to_device(name) | 86 | return self.datasource.device_name_to_device(name) |
17 | diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py | |||
18 | index fcddd75..3f2dbb9 100644 | |||
19 | --- a/cloudinit/cmd/main.py | |||
20 | +++ b/cloudinit/cmd/main.py | |||
21 | @@ -40,6 +40,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE, | |||
22 | 40 | 40 | ||
23 | 41 | from cloudinit import atomic_helper | 41 | from cloudinit import atomic_helper |
24 | 42 | 42 | ||
25 | 43 | from cloudinit.config import cc_set_hostname | ||
26 | 43 | from cloudinit.dhclient_hook import LogDhclient | 44 | from cloudinit.dhclient_hook import LogDhclient |
27 | 44 | 45 | ||
28 | 45 | 46 | ||
29 | @@ -352,6 +353,11 @@ def main_init(name, args): | |||
30 | 352 | 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", |
31 | 353 | mode, name, iid, init.is_new_instance()) | 354 | mode, name, iid, init.is_new_instance()) |
32 | 354 | 355 | ||
33 | 356 | if mode == sources.DSMODE_LOCAL: | ||
34 | 357 | # Before network comes up, set any configured hostname to allow | ||
35 | 358 | # dhcp clients to advertize this hostname to any DDNS services | ||
36 | 359 | # LP: #1746455. | ||
37 | 360 | _maybe_set_hostname(init, stage='local', retry_stage='network') | ||
38 | 355 | init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL)) | 361 | init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL)) |
39 | 356 | 362 | ||
40 | 357 | if mode == sources.DSMODE_LOCAL: | 363 | if mode == sources.DSMODE_LOCAL: |
41 | @@ -368,6 +374,7 @@ def main_init(name, args): | |||
42 | 368 | init.setup_datasource() | 374 | init.setup_datasource() |
43 | 369 | # update fully realizes user-data (pulling in #include if necessary) | 375 | # update fully realizes user-data (pulling in #include if necessary) |
44 | 370 | init.update() | 376 | init.update() |
45 | 377 | _maybe_set_hostname(init, stage='init-net', retry_stage='modules:config') | ||
46 | 371 | # Stage 7 | 378 | # Stage 7 |
47 | 372 | try: | 379 | try: |
48 | 373 | # Attempt to consume the data per instance. | 380 | # Attempt to consume the data per instance. |
49 | @@ -681,6 +688,24 @@ def status_wrapper(name, args, data_d=None, link_d=None): | |||
50 | 681 | return len(v1[mode]['errors']) | 688 | return len(v1[mode]['errors']) |
51 | 682 | 689 | ||
52 | 683 | 690 | ||
53 | 691 | def _maybe_set_hostname(init, stage, retry_stage): | ||
54 | 692 | """Call set-hostname if metadata, vendordata or userdata provides it. | ||
55 | 693 | |||
56 | 694 | @param stage: String representing current stage in which we are running. | ||
57 | 695 | @param retry_stage: String represented logs upon error setting hostname. | ||
58 | 696 | """ | ||
59 | 697 | cloud = init.cloudify() | ||
60 | 698 | (hostname, _fqdn) = util.get_hostname_fqdn( | ||
61 | 699 | init.cfg, cloud, metadata_only=True) | ||
62 | 700 | if hostname: # meta-data or user-data hostname content | ||
63 | 701 | try: | ||
64 | 702 | cc_set_hostname.handle('set-hostname', init.cfg, cloud, LOG, None) | ||
65 | 703 | except cc_set_hostname.SetHostnameError as e: | ||
66 | 704 | LOG.debug( | ||
67 | 705 | 'Failed setting hostname in %s stage. Will' | ||
68 | 706 | ' retry in %s stage. Error: %s.', stage, retry_stage, str(e)) | ||
69 | 707 | |||
70 | 708 | |||
71 | 684 | def main_features(name, args): | 709 | def main_features(name, args): |
72 | 685 | sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n') | 710 | sys.stdout.write('\n'.join(sorted(version.FEATURES)) + '\n') |
73 | 686 | 711 | ||
74 | diff --git a/cloudinit/cmd/tests/test_main.py b/cloudinit/cmd/tests/test_main.py | |||
75 | 687 | new file mode 100644 | 712 | new file mode 100644 |
76 | index 0000000..dbe421c | |||
77 | --- /dev/null | |||
78 | +++ b/cloudinit/cmd/tests/test_main.py | |||
79 | @@ -0,0 +1,161 @@ | |||
80 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
81 | 2 | |||
82 | 3 | from collections import namedtuple | ||
83 | 4 | import copy | ||
84 | 5 | import os | ||
85 | 6 | from six import StringIO | ||
86 | 7 | |||
87 | 8 | from cloudinit.cmd import main | ||
88 | 9 | from cloudinit.util import ( | ||
89 | 10 | ensure_dir, load_file, write_file, yaml_dumps) | ||
90 | 11 | from cloudinit.tests.helpers import ( | ||
91 | 12 | FilesystemMockingTestCase, wrap_and_call) | ||
92 | 13 | |||
93 | 14 | mypaths = namedtuple('MyPaths', 'run_dir') | ||
94 | 15 | myargs = namedtuple('MyArgs', 'debug files force local reporter subcommand') | ||
95 | 16 | |||
96 | 17 | |||
97 | 18 | class TestMain(FilesystemMockingTestCase): | ||
98 | 19 | |||
99 | 20 | with_logs = True | ||
100 | 21 | |||
101 | 22 | def setUp(self): | ||
102 | 23 | super(TestMain, self).setUp() | ||
103 | 24 | self.new_root = self.tmp_dir() | ||
104 | 25 | self.cloud_dir = self.tmp_path('var/lib/cloud/', dir=self.new_root) | ||
105 | 26 | os.makedirs(self.cloud_dir) | ||
106 | 27 | self.replicateTestRoot('simple_ubuntu', self.new_root) | ||
107 | 28 | self.cfg = { | ||
108 | 29 | 'datasource_list': ['None'], | ||
109 | 30 | 'runcmd': ['ls /etc'], # test ALL_DISTROS | ||
110 | 31 | 'system_info': {'paths': {'cloud_dir': self.cloud_dir, | ||
111 | 32 | 'run_dir': self.new_root}}, | ||
112 | 33 | 'write_files': [ | ||
113 | 34 | { | ||
114 | 35 | 'path': '/etc/blah.ini', | ||
115 | 36 | 'content': 'blah', | ||
116 | 37 | 'permissions': 0o755, | ||
117 | 38 | }, | ||
118 | 39 | ], | ||
119 | 40 | 'cloud_init_modules': ['write-files', 'runcmd'], | ||
120 | 41 | } | ||
121 | 42 | cloud_cfg = yaml_dumps(self.cfg) | ||
122 | 43 | ensure_dir(os.path.join(self.new_root, 'etc', 'cloud')) | ||
123 | 44 | self.cloud_cfg_file = os.path.join( | ||
124 | 45 | self.new_root, 'etc', 'cloud', 'cloud.cfg') | ||
125 | 46 | write_file(self.cloud_cfg_file, cloud_cfg) | ||
126 | 47 | self.patchOS(self.new_root) | ||
127 | 48 | self.patchUtils(self.new_root) | ||
128 | 49 | self.stderr = StringIO() | ||
129 | 50 | self.patchStdoutAndStderr(stderr=self.stderr) | ||
130 | 51 | |||
131 | 52 | def test_main_init_run_net_stops_on_file_no_net(self): | ||
132 | 53 | """When no-net file is present, main_init does not process modules.""" | ||
133 | 54 | stop_file = os.path.join(self.cloud_dir, 'data', 'no-net') # stop file | ||
134 | 55 | write_file(stop_file, '') | ||
135 | 56 | cmdargs = myargs( | ||
136 | 57 | debug=False, files=None, force=False, local=False, reporter=None, | ||
137 | 58 | subcommand='init') | ||
138 | 59 | (item1, item2) = wrap_and_call( | ||
139 | 60 | 'cloudinit.cmd.main', | ||
140 | 61 | {'util.close_stdin': True, | ||
141 | 62 | 'netinfo.debug_info': 'my net debug info', | ||
142 | 63 | 'util.fixup_output': ('outfmt', 'errfmt')}, | ||
143 | 64 | main.main_init, 'init', cmdargs) | ||
144 | 65 | # We should not run write_files module | ||
145 | 66 | self.assertFalse( | ||
146 | 67 | os.path.exists(os.path.join(self.new_root, 'etc/blah.ini')), | ||
147 | 68 | 'Unexpected run of write_files module produced blah.ini') | ||
148 | 69 | self.assertEqual([], item2) | ||
149 | 70 | # Instancify is called | ||
150 | 71 | instance_id_path = 'var/lib/cloud/data/instance-id' | ||
151 | 72 | self.assertFalse( | ||
152 | 73 | os.path.exists(os.path.join(self.new_root, instance_id_path)), | ||
153 | 74 | 'Unexpected call to datasource.instancify produced instance-id') | ||
154 | 75 | expected_logs = [ | ||
155 | 76 | "Exiting. stop file ['{stop_file}'] existed\n".format( | ||
156 | 77 | stop_file=stop_file), | ||
157 | 78 | 'my net debug info' # netinfo.debug_info | ||
158 | 79 | ] | ||
159 | 80 | for log in expected_logs: | ||
160 | 81 | self.assertIn(log, self.stderr.getvalue()) | ||
161 | 82 | |||
162 | 83 | def test_main_init_run_net_runs_modules(self): | ||
163 | 84 | """Modules like write_files are run in 'net' mode.""" | ||
164 | 85 | cmdargs = myargs( | ||
165 | 86 | debug=False, files=None, force=False, local=False, reporter=None, | ||
166 | 87 | subcommand='init') | ||
167 | 88 | (item1, item2) = wrap_and_call( | ||
168 | 89 | 'cloudinit.cmd.main', | ||
169 | 90 | {'util.close_stdin': True, | ||
170 | 91 | 'netinfo.debug_info': 'my net debug info', | ||
171 | 92 | 'util.fixup_output': ('outfmt', 'errfmt')}, | ||
172 | 93 | main.main_init, 'init', cmdargs) | ||
173 | 94 | self.assertEqual([], item2) | ||
174 | 95 | # Instancify is called | ||
175 | 96 | instance_id_path = 'var/lib/cloud/data/instance-id' | ||
176 | 97 | self.assertEqual( | ||
177 | 98 | 'iid-datasource-none\n', | ||
178 | 99 | os.path.join(load_file( | ||
179 | 100 | os.path.join(self.new_root, instance_id_path)))) | ||
180 | 101 | # modules are run (including write_files) | ||
181 | 102 | self.assertEqual( | ||
182 | 103 | 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini'))) | ||
183 | 104 | expected_logs = [ | ||
184 | 105 | 'network config is disabled by fallback', # apply_network_config | ||
185 | 106 | 'my net debug info', # netinfo.debug_info | ||
186 | 107 | 'no previous run detected' | ||
187 | 108 | ] | ||
188 | 109 | for log in expected_logs: | ||
189 | 110 | self.assertIn(log, self.stderr.getvalue()) | ||
190 | 111 | |||
191 | 112 | def test_main_init_run_net_calls_set_hostname_when_metadata_present(self): | ||
192 | 113 | """When local-hostname metadata is present, call cc_set_hostname.""" | ||
193 | 114 | self.cfg['datasource'] = { | ||
194 | 115 | 'None': {'metadata': {'local-hostname': 'md-hostname'}}} | ||
195 | 116 | cloud_cfg = yaml_dumps(self.cfg) | ||
196 | 117 | write_file(self.cloud_cfg_file, cloud_cfg) | ||
197 | 118 | cmdargs = myargs( | ||
198 | 119 | debug=False, files=None, force=False, local=False, reporter=None, | ||
199 | 120 | subcommand='init') | ||
200 | 121 | |||
201 | 122 | def set_hostname(name, cfg, cloud, log, args): | ||
202 | 123 | self.assertEqual('set-hostname', name) | ||
203 | 124 | updated_cfg = copy.deepcopy(self.cfg) | ||
204 | 125 | updated_cfg.update( | ||
205 | 126 | {'def_log_file': '/var/log/cloud-init.log', | ||
206 | 127 | 'log_cfgs': [], | ||
207 | 128 | 'syslog_fix_perms': ['syslog:adm', 'root:adm', 'root:wheel'], | ||
208 | 129 | 'vendor_data': {'enabled': True, 'prefix': []}}) | ||
209 | 130 | updated_cfg.pop('system_info') | ||
210 | 131 | |||
211 | 132 | self.assertEqual(updated_cfg, cfg) | ||
212 | 133 | self.assertEqual(main.LOG, log) | ||
213 | 134 | self.assertIsNone(args) | ||
214 | 135 | |||
215 | 136 | (item1, item2) = wrap_and_call( | ||
216 | 137 | 'cloudinit.cmd.main', | ||
217 | 138 | {'util.close_stdin': True, | ||
218 | 139 | 'netinfo.debug_info': 'my net debug info', | ||
219 | 140 | 'cc_set_hostname.handle': {'side_effect': set_hostname}, | ||
220 | 141 | 'util.fixup_output': ('outfmt', 'errfmt')}, | ||
221 | 142 | main.main_init, 'init', cmdargs) | ||
222 | 143 | self.assertEqual([], item2) | ||
223 | 144 | # Instancify is called | ||
224 | 145 | instance_id_path = 'var/lib/cloud/data/instance-id' | ||
225 | 146 | self.assertEqual( | ||
226 | 147 | 'iid-datasource-none\n', | ||
227 | 148 | os.path.join(load_file( | ||
228 | 149 | os.path.join(self.new_root, instance_id_path)))) | ||
229 | 150 | # modules are run (including write_files) | ||
230 | 151 | self.assertEqual( | ||
231 | 152 | 'blah', load_file(os.path.join(self.new_root, 'etc/blah.ini'))) | ||
232 | 153 | expected_logs = [ | ||
233 | 154 | 'network config is disabled by fallback', # apply_network_config | ||
234 | 155 | 'my net debug info', # netinfo.debug_info | ||
235 | 156 | 'no previous run detected' | ||
236 | 157 | ] | ||
237 | 158 | for log in expected_logs: | ||
238 | 159 | self.assertIn(log, self.stderr.getvalue()) | ||
239 | 160 | |||
240 | 161 | # vi: ts=4 expandtab | ||
241 | diff --git a/cloudinit/config/cc_set_hostname.py b/cloudinit/config/cc_set_hostname.py | |||
242 | index aa3dfe5..3d2b2da 100644 | |||
243 | --- a/cloudinit/config/cc_set_hostname.py | |||
244 | +++ b/cloudinit/config/cc_set_hostname.py | |||
245 | @@ -32,22 +32,51 @@ will be used. | |||
246 | 32 | hostname: <fqdn/hostname> | 32 | hostname: <fqdn/hostname> |
247 | 33 | """ | 33 | """ |
248 | 34 | 34 | ||
249 | 35 | import os | ||
250 | 36 | |||
251 | 37 | |||
252 | 38 | from cloudinit.atomic_helper import write_json | ||
253 | 35 | from cloudinit import util | 39 | from cloudinit import util |
254 | 36 | 40 | ||
255 | 37 | 41 | ||
256 | 42 | class SetHostnameError(Exception): | ||
257 | 43 | """Raised when the distro runs into an exception when setting hostname. | ||
258 | 44 | |||
259 | 45 | This may happen if we attempt to set the hostname early in cloud-init's | ||
260 | 46 | init-local timeframe as certain services may not be running yet. | ||
261 | 47 | """ | ||
262 | 48 | pass | ||
263 | 49 | |||
264 | 50 | |||
265 | 38 | def handle(name, cfg, cloud, log, _args): | 51 | def handle(name, cfg, cloud, log, _args): |
266 | 39 | if util.get_cfg_option_bool(cfg, "preserve_hostname", False): | 52 | if util.get_cfg_option_bool(cfg, "preserve_hostname", False): |
267 | 40 | log.debug(("Configuration option 'preserve_hostname' is set," | 53 | log.debug(("Configuration option 'preserve_hostname' is set," |
268 | 41 | " not setting the hostname in module %s"), name) | 54 | " not setting the hostname in module %s"), name) |
269 | 42 | return | 55 | return |
270 | 43 | |||
271 | 44 | (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) | 56 | (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud) |
272 | 57 | # Check for previous successful invocation of set-hostname | ||
273 | 58 | |||
274 | 59 | # set-hostname artifact file accounts for both hostname and fqdn | ||
275 | 60 | # deltas. As such, it's format is different than cc_update_hostname's | ||
276 | 61 | # previous-hostname file which only contains the base hostname. | ||
277 | 62 | # TODO consolidate previous-hostname and set-hostname artifact files and | ||
278 | 63 | # distro._read_hostname implementation so we only validate one artifact. | ||
279 | 64 | prev_fn = os.path.join(cloud.get_cpath('data'), "set-hostname") | ||
280 | 65 | prev_hostname = {} | ||
281 | 66 | if os.path.exists(prev_fn): | ||
282 | 67 | prev_hostname = util.load_json(util.load_file(prev_fn)) | ||
283 | 68 | hostname_changed = (hostname != prev_hostname.get('hostname') or | ||
284 | 69 | fqdn != prev_hostname.get('fqdn')) | ||
285 | 70 | if not hostname_changed: | ||
286 | 71 | log.debug('No hostname changes. Skipping set-hostname') | ||
287 | 72 | return | ||
288 | 73 | log.debug("Setting the hostname to %s (%s)", fqdn, hostname) | ||
289 | 45 | try: | 74 | try: |
290 | 46 | log.debug("Setting the hostname to %s (%s)", fqdn, hostname) | ||
291 | 47 | cloud.distro.set_hostname(hostname, fqdn) | 75 | cloud.distro.set_hostname(hostname, fqdn) |
296 | 48 | except Exception: | 76 | except Exception as e: |
297 | 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) |
298 | 50 | hostname) | 78 | util.logexc(log, msg) |
299 | 51 | raise | 79 | raise SetHostnameError("%s: %s" % (msg, e)) |
300 | 80 | write_json(prev_fn, {'hostname': hostname, 'fqdn': fqdn}) | ||
301 | 52 | 81 | ||
302 | 53 | # vi: ts=4 expandtab | 82 | # vi: ts=4 expandtab |
303 | diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py | |||
304 | index a05ca2f..df0b374 100644 | |||
305 | --- a/cloudinit/sources/__init__.py | |||
306 | +++ b/cloudinit/sources/__init__.py | |||
307 | @@ -276,21 +276,34 @@ class DataSource(object): | |||
308 | 276 | return "iid-datasource" | 276 | return "iid-datasource" |
309 | 277 | return str(self.metadata['instance-id']) | 277 | return str(self.metadata['instance-id']) |
310 | 278 | 278 | ||
312 | 279 | def get_hostname(self, fqdn=False, resolve_ip=False): | 279 | def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
313 | 280 | """Get hostname or fqdn from the datasource. Look it up if desired. | ||
314 | 281 | |||
315 | 282 | @param fqdn: Boolean, set True to return hostname with domain. | ||
316 | 283 | @param resolve_ip: Boolean, set True to attempt to resolve an ipv4 | ||
317 | 284 | address provided in local-hostname meta-data. | ||
318 | 285 | @param metadata_only: Boolean, set True to avoid looking up hostname | ||
319 | 286 | if meta-data doesn't have local-hostname present. | ||
320 | 287 | |||
321 | 288 | @return: hostname or qualified hostname. Optionally return None when | ||
322 | 289 | metadata_only is True and local-hostname data is not available. | ||
323 | 290 | """ | ||
324 | 280 | defdomain = "localdomain" | 291 | defdomain = "localdomain" |
325 | 281 | defhost = "localhost" | 292 | defhost = "localhost" |
326 | 282 | domain = defdomain | 293 | domain = defdomain |
327 | 283 | 294 | ||
328 | 284 | if not self.metadata or 'local-hostname' not in self.metadata: | 295 | if not self.metadata or 'local-hostname' not in self.metadata: |
329 | 296 | if metadata_only: | ||
330 | 297 | return None | ||
331 | 285 | # this is somewhat questionable really. | 298 | # this is somewhat questionable really. |
332 | 286 | # the cloud datasource was asked for a hostname | 299 | # the cloud datasource was asked for a hostname |
333 | 287 | # and didn't have one. raising error might be more appropriate | 300 | # and didn't have one. raising error might be more appropriate |
334 | 288 | # but instead, basically look up the existing hostname | 301 | # but instead, basically look up the existing hostname |
335 | 289 | toks = [] | 302 | toks = [] |
336 | 290 | hostname = util.get_hostname() | 303 | hostname = util.get_hostname() |
340 | 291 | fqdn = util.get_fqdn_from_hosts(hostname) | 304 | hosts_fqdn = util.get_fqdn_from_hosts(hostname) |
341 | 292 | if fqdn and fqdn.find(".") > 0: | 305 | if hosts_fqdn and hosts_fqdn.find(".") > 0: |
342 | 293 | toks = str(fqdn).split(".") | 306 | toks = str(hosts_fqdn).split(".") |
343 | 294 | elif hostname and hostname.find(".") > 0: | 307 | elif hostname and hostname.find(".") > 0: |
344 | 295 | toks = str(hostname).split(".") | 308 | toks = str(hostname).split(".") |
345 | 296 | elif hostname: | 309 | elif hostname: |
346 | diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py | |||
347 | index af15115..5065083 100644 | |||
348 | --- a/cloudinit/sources/tests/test_init.py | |||
349 | +++ b/cloudinit/sources/tests/test_init.py | |||
350 | @@ -7,7 +7,7 @@ import stat | |||
351 | 7 | from cloudinit.helpers import Paths | 7 | from cloudinit.helpers import Paths |
352 | 8 | from cloudinit.sources import ( | 8 | from cloudinit.sources import ( |
353 | 9 | INSTANCE_JSON_FILE, DataSource) | 9 | INSTANCE_JSON_FILE, DataSource) |
355 | 10 | from cloudinit.tests.helpers import CiTestCase, skipIf | 10 | from cloudinit.tests.helpers import CiTestCase, skipIf, mock |
356 | 11 | from cloudinit.user_data import UserDataProcessor | 11 | from cloudinit.user_data import UserDataProcessor |
357 | 12 | from cloudinit import util | 12 | from cloudinit import util |
358 | 13 | 13 | ||
359 | @@ -108,6 +108,74 @@ class TestDataSource(CiTestCase): | |||
360 | 108 | self.assertEqual('userdata_raw', datasource.userdata_raw) | 108 | self.assertEqual('userdata_raw', datasource.userdata_raw) |
361 | 109 | self.assertEqual('vendordata_raw', datasource.vendordata_raw) | 109 | self.assertEqual('vendordata_raw', datasource.vendordata_raw) |
362 | 110 | 110 | ||
363 | 111 | def test_get_hostname_strips_local_hostname_without_domain(self): | ||
364 | 112 | """Datasource.get_hostname strips metadata local-hostname of domain.""" | ||
365 | 113 | tmp = self.tmp_dir() | ||
366 | 114 | datasource = DataSourceTestSubclassNet( | ||
367 | 115 | self.sys_cfg, self.distro, Paths({'run_dir': tmp})) | ||
368 | 116 | self.assertTrue(datasource.get_data()) | ||
369 | 117 | self.assertEqual( | ||
370 | 118 | 'test-subclass-hostname', datasource.metadata['local-hostname']) | ||
371 | 119 | self.assertEqual('test-subclass-hostname', datasource.get_hostname()) | ||
372 | 120 | datasource.metadata['local-hostname'] = 'hostname.my.domain.com' | ||
373 | 121 | self.assertEqual('hostname', datasource.get_hostname()) | ||
374 | 122 | |||
375 | 123 | def test_get_hostname_with_fqdn_returns_local_hostname_with_domain(self): | ||
376 | 124 | """Datasource.get_hostname with fqdn set gets qualified hostname.""" | ||
377 | 125 | tmp = self.tmp_dir() | ||
378 | 126 | datasource = DataSourceTestSubclassNet( | ||
379 | 127 | self.sys_cfg, self.distro, Paths({'run_dir': tmp})) | ||
380 | 128 | self.assertTrue(datasource.get_data()) | ||
381 | 129 | datasource.metadata['local-hostname'] = 'hostname.my.domain.com' | ||
382 | 130 | self.assertEqual( | ||
383 | 131 | 'hostname.my.domain.com', datasource.get_hostname(fqdn=True)) | ||
384 | 132 | |||
385 | 133 | def test_get_hostname_without_metadata_uses_system_hostname(self): | ||
386 | 134 | """Datasource.gethostname runs util.get_hostname when no metadata.""" | ||
387 | 135 | tmp = self.tmp_dir() | ||
388 | 136 | datasource = DataSourceTestSubclassNet( | ||
389 | 137 | self.sys_cfg, self.distro, Paths({'run_dir': tmp})) | ||
390 | 138 | self.assertEqual({}, datasource.metadata) | ||
391 | 139 | mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' | ||
392 | 140 | with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: | ||
393 | 141 | with mock.patch(mock_fqdn) as m_fqdn: | ||
394 | 142 | m_gethost.return_value = 'systemhostname.domain.com' | ||
395 | 143 | m_fqdn.return_value = None # No maching fqdn in /etc/hosts | ||
396 | 144 | self.assertEqual('systemhostname', datasource.get_hostname()) | ||
397 | 145 | self.assertEqual( | ||
398 | 146 | 'systemhostname.domain.com', | ||
399 | 147 | datasource.get_hostname(fqdn=True)) | ||
400 | 148 | |||
401 | 149 | def test_get_hostname_without_metadata_returns_none(self): | ||
402 | 150 | """Datasource.gethostname returns None when metadata_only and no MD.""" | ||
403 | 151 | tmp = self.tmp_dir() | ||
404 | 152 | datasource = DataSourceTestSubclassNet( | ||
405 | 153 | self.sys_cfg, self.distro, Paths({'run_dir': tmp})) | ||
406 | 154 | self.assertEqual({}, datasource.metadata) | ||
407 | 155 | mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' | ||
408 | 156 | with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: | ||
409 | 157 | with mock.patch(mock_fqdn) as m_fqdn: | ||
410 | 158 | self.assertIsNone(datasource.get_hostname(metadata_only=True)) | ||
411 | 159 | self.assertIsNone( | ||
412 | 160 | datasource.get_hostname(fqdn=True, metadata_only=True)) | ||
413 | 161 | self.assertEqual([], m_gethost.call_args_list) | ||
414 | 162 | self.assertEqual([], m_fqdn.call_args_list) | ||
415 | 163 | |||
416 | 164 | def test_get_hostname_without_metadata_prefers_etc_hosts(self): | ||
417 | 165 | """Datasource.gethostname prefers /etc/hosts to util.get_hostname.""" | ||
418 | 166 | tmp = self.tmp_dir() | ||
419 | 167 | datasource = DataSourceTestSubclassNet( | ||
420 | 168 | self.sys_cfg, self.distro, Paths({'run_dir': tmp})) | ||
421 | 169 | self.assertEqual({}, datasource.metadata) | ||
422 | 170 | mock_fqdn = 'cloudinit.sources.util.get_fqdn_from_hosts' | ||
423 | 171 | with mock.patch('cloudinit.sources.util.get_hostname') as m_gethost: | ||
424 | 172 | with mock.patch(mock_fqdn) as m_fqdn: | ||
425 | 173 | m_gethost.return_value = 'systemhostname.domain.com' | ||
426 | 174 | m_fqdn.return_value = 'fqdnhostname.domain.com' | ||
427 | 175 | self.assertEqual('fqdnhostname', datasource.get_hostname()) | ||
428 | 176 | self.assertEqual('fqdnhostname.domain.com', | ||
429 | 177 | datasource.get_hostname(fqdn=True)) | ||
430 | 178 | |||
431 | 111 | def test_get_data_write_json_instance_data(self): | 179 | def test_get_data_write_json_instance_data(self): |
432 | 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.""" |
433 | 113 | tmp = self.tmp_dir() | 181 | tmp = self.tmp_dir() |
434 | diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py | |||
435 | index c3e2e40..d30643d 100644 | |||
436 | --- a/cloudinit/tests/test_util.py | |||
437 | +++ b/cloudinit/tests/test_util.py | |||
438 | @@ -16,6 +16,25 @@ MOUNT_INFO = [ | |||
439 | 16 | ] | 16 | ] |
440 | 17 | 17 | ||
441 | 18 | 18 | ||
442 | 19 | class FakeCloud(object): | ||
443 | 20 | |||
444 | 21 | def __init__(self, hostname, fqdn): | ||
445 | 22 | self.hostname = hostname | ||
446 | 23 | self.fqdn = fqdn | ||
447 | 24 | self.calls = [] | ||
448 | 25 | |||
449 | 26 | def get_hostname(self, fqdn=None, metadata_only=None): | ||
450 | 27 | myargs = {} | ||
451 | 28 | if fqdn is not None: | ||
452 | 29 | myargs['fqdn'] = fqdn | ||
453 | 30 | if metadata_only is not None: | ||
454 | 31 | myargs['metadata_only'] = metadata_only | ||
455 | 32 | self.calls.append(myargs) | ||
456 | 33 | if fqdn: | ||
457 | 34 | return self.fqdn | ||
458 | 35 | return self.hostname | ||
459 | 36 | |||
460 | 37 | |||
461 | 19 | class TestUtil(CiTestCase): | 38 | class TestUtil(CiTestCase): |
462 | 20 | 39 | ||
463 | 21 | def test_parse_mount_info_no_opts_no_arg(self): | 40 | def test_parse_mount_info_no_opts_no_arg(self): |
464 | @@ -67,3 +86,58 @@ class TestShellify(CiTestCase): | |||
465 | 67 | "'echo' 'hi' 'sis'", ""]), | 86 | "'echo' 'hi' 'sis'", ""]), |
466 | 68 | util.shellify(["echo hi mom", ["echo", "hi dad"], | 87 | util.shellify(["echo hi mom", ["echo", "hi dad"], |
467 | 69 | ('echo', 'hi', 'sis')])) | 88 | ('echo', 'hi', 'sis')])) |
468 | 89 | |||
469 | 90 | |||
470 | 91 | class TestGetHostnameFqdn(CiTestCase): | ||
471 | 92 | |||
472 | 93 | def test_get_hostname_fqdn_from_only_cfg_fqdn(self): | ||
473 | 94 | """When cfg only has the fqdn key, derive hostname and fqdn from it.""" | ||
474 | 95 | hostname, fqdn = util.get_hostname_fqdn( | ||
475 | 96 | cfg={'fqdn': 'myhost.domain.com'}, cloud=None) | ||
476 | 97 | self.assertEqual('myhost', hostname) | ||
477 | 98 | self.assertEqual('myhost.domain.com', fqdn) | ||
478 | 99 | |||
479 | 100 | def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): | ||
480 | 101 | """When cfg has both fqdn and hostname keys, return them.""" | ||
481 | 102 | hostname, fqdn = util.get_hostname_fqdn( | ||
482 | 103 | cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None) | ||
483 | 104 | self.assertEqual('other', hostname) | ||
484 | 105 | self.assertEqual('myhost.domain.com', fqdn) | ||
485 | 106 | |||
486 | 107 | def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): | ||
487 | 108 | """When cfg has only hostname key which represents a fqdn, use that.""" | ||
488 | 109 | hostname, fqdn = util.get_hostname_fqdn( | ||
489 | 110 | cfg={'hostname': 'myhost.domain.com'}, cloud=None) | ||
490 | 111 | self.assertEqual('myhost', hostname) | ||
491 | 112 | self.assertEqual('myhost.domain.com', fqdn) | ||
492 | 113 | |||
493 | 114 | def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): | ||
494 | 115 | """When cfg has a hostname without a '.' query cloud.get_hostname.""" | ||
495 | 116 | mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') | ||
496 | 117 | hostname, fqdn = util.get_hostname_fqdn( | ||
497 | 118 | cfg={'hostname': 'myhost'}, cloud=mycloud) | ||
498 | 119 | self.assertEqual('myhost', hostname) | ||
499 | 120 | self.assertEqual('cloudhost.mycloud.com', fqdn) | ||
500 | 121 | self.assertEqual( | ||
501 | 122 | [{'fqdn': True, 'metadata_only': False}], mycloud.calls) | ||
502 | 123 | |||
503 | 124 | def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): | ||
504 | 125 | """When cfg has neither hostname nor fqdn cloud.get_hostname.""" | ||
505 | 126 | mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') | ||
506 | 127 | hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud) | ||
507 | 128 | self.assertEqual('cloudhost', hostname) | ||
508 | 129 | self.assertEqual('cloudhost.mycloud.com', fqdn) | ||
509 | 130 | self.assertEqual( | ||
510 | 131 | [{'fqdn': True, 'metadata_only': False}, | ||
511 | 132 | {'metadata_only': False}], mycloud.calls) | ||
512 | 133 | |||
513 | 134 | def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): | ||
514 | 135 | """Calls to cloud.get_hostname pass the metadata_only parameter.""" | ||
515 | 136 | mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') | ||
516 | 137 | hostname, fqdn = util.get_hostname_fqdn( | ||
517 | 138 | cfg={}, cloud=mycloud, metadata_only=True) | ||
518 | 139 | self.assertEqual( | ||
519 | 140 | [{'fqdn': True, 'metadata_only': True}, | ||
520 | 141 | {'metadata_only': True}], mycloud.calls) | ||
521 | 142 | |||
522 | 143 | # vi: ts=4 expandtab | ||
523 | diff --git a/cloudinit/util.py b/cloudinit/util.py | |||
524 | index 083a8ef..4504f05 100644 | |||
525 | --- a/cloudinit/util.py | |||
526 | +++ b/cloudinit/util.py | |||
527 | @@ -1025,9 +1025,16 @@ def dos2unix(contents): | |||
528 | 1025 | return contents.replace('\r\n', '\n') | 1025 | return contents.replace('\r\n', '\n') |
529 | 1026 | 1026 | ||
530 | 1027 | 1027 | ||
534 | 1028 | def get_hostname_fqdn(cfg, cloud): | 1028 | def get_hostname_fqdn(cfg, cloud, metadata_only=False): |
535 | 1029 | # 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. |
536 | 1030 | # then fall back to data from cloud | 1030 | |
537 | 1031 | @param cfg: Dictionary of merged user-data configuration (from init.cfg). | ||
538 | 1032 | @param cloud: Cloud instance from init.cloudify(). | ||
539 | 1033 | @param metadata_only: Boolean, set True to only query cloud meta-data, | ||
540 | 1034 | returning None if not present in meta-data. | ||
541 | 1035 | @return: a Tuple of strings <hostname>, <fqdn>. Values can be none when | ||
542 | 1036 | metadata_only is True and no cfg or metadata provides hostname info. | ||
543 | 1037 | """ | ||
544 | 1031 | if "fqdn" in cfg: | 1038 | if "fqdn" in cfg: |
545 | 1032 | # user specified a fqdn. Default hostname then is based off that | 1039 | # user specified a fqdn. Default hostname then is based off that |
546 | 1033 | fqdn = cfg['fqdn'] | 1040 | fqdn = cfg['fqdn'] |
547 | @@ -1041,11 +1048,11 @@ def get_hostname_fqdn(cfg, cloud): | |||
548 | 1041 | else: | 1048 | else: |
549 | 1042 | # no fqdn set, get fqdn from cloud. | 1049 | # no fqdn set, get fqdn from cloud. |
550 | 1043 | # get hostname from cfg if available otherwise cloud | 1050 | # get hostname from cfg if available otherwise cloud |
552 | 1044 | fqdn = cloud.get_hostname(fqdn=True) | 1051 | fqdn = cloud.get_hostname(fqdn=True, metadata_only=metadata_only) |
553 | 1045 | if "hostname" in cfg: | 1052 | if "hostname" in cfg: |
554 | 1046 | hostname = cfg['hostname'] | 1053 | hostname = cfg['hostname'] |
555 | 1047 | else: | 1054 | else: |
557 | 1048 | hostname = cloud.get_hostname() | 1055 | hostname = cloud.get_hostname(metadata_only=metadata_only) |
558 | 1049 | return (hostname, fqdn) | 1056 | return (hostname, fqdn) |
559 | 1050 | 1057 | ||
560 | 1051 | 1058 | ||
561 | diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/test_handler/test_handler_set_hostname.py | |||
562 | index abdc17e..d09ec23 100644 | |||
563 | --- a/tests/unittests/test_handler/test_handler_set_hostname.py | |||
564 | +++ b/tests/unittests/test_handler/test_handler_set_hostname.py | |||
565 | @@ -11,6 +11,7 @@ from cloudinit.tests import helpers as t_help | |||
566 | 11 | 11 | ||
567 | 12 | from configobj import ConfigObj | 12 | from configobj import ConfigObj |
568 | 13 | import logging | 13 | import logging |
569 | 14 | import os | ||
570 | 14 | import shutil | 15 | import shutil |
571 | 15 | from six import BytesIO | 16 | from six import BytesIO |
572 | 16 | import tempfile | 17 | import tempfile |
573 | @@ -19,14 +20,18 @@ LOG = logging.getLogger(__name__) | |||
574 | 19 | 20 | ||
575 | 20 | 21 | ||
576 | 21 | class TestHostname(t_help.FilesystemMockingTestCase): | 22 | class TestHostname(t_help.FilesystemMockingTestCase): |
577 | 23 | |||
578 | 24 | with_logs = True | ||
579 | 25 | |||
580 | 22 | def setUp(self): | 26 | def setUp(self): |
581 | 23 | super(TestHostname, self).setUp() | 27 | super(TestHostname, self).setUp() |
582 | 24 | self.tmp = tempfile.mkdtemp() | 28 | self.tmp = tempfile.mkdtemp() |
583 | 29 | util.ensure_dir(os.path.join(self.tmp, 'data')) | ||
584 | 25 | self.addCleanup(shutil.rmtree, self.tmp) | 30 | self.addCleanup(shutil.rmtree, self.tmp) |
585 | 26 | 31 | ||
586 | 27 | def _fetch_distro(self, kind): | 32 | def _fetch_distro(self, kind): |
587 | 28 | cls = distros.fetch(kind) | 33 | cls = distros.fetch(kind) |
589 | 29 | paths = helpers.Paths({}) | 34 | paths = helpers.Paths({'cloud_dir': self.tmp}) |
590 | 30 | return cls(kind, {}, paths) | 35 | return cls(kind, {}, paths) |
591 | 31 | 36 | ||
592 | 32 | def test_write_hostname_rhel(self): | 37 | def test_write_hostname_rhel(self): |
593 | @@ -34,7 +39,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): | |||
594 | 34 | 'hostname': 'blah.blah.blah.yahoo.com', | 39 | 'hostname': 'blah.blah.blah.yahoo.com', |
595 | 35 | } | 40 | } |
596 | 36 | distro = self._fetch_distro('rhel') | 41 | distro = self._fetch_distro('rhel') |
598 | 37 | paths = helpers.Paths({}) | 42 | paths = helpers.Paths({'cloud_dir': self.tmp}) |
599 | 38 | ds = None | 43 | ds = None |
600 | 39 | cc = cloud.Cloud(ds, paths, {}, distro, None) | 44 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
601 | 40 | self.patchUtils(self.tmp) | 45 | self.patchUtils(self.tmp) |
602 | @@ -51,7 +56,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): | |||
603 | 51 | 'hostname': 'blah.blah.blah.yahoo.com', | 56 | 'hostname': 'blah.blah.blah.yahoo.com', |
604 | 52 | } | 57 | } |
605 | 53 | distro = self._fetch_distro('debian') | 58 | distro = self._fetch_distro('debian') |
607 | 54 | paths = helpers.Paths({}) | 59 | paths = helpers.Paths({'cloud_dir': self.tmp}) |
608 | 55 | ds = None | 60 | ds = None |
609 | 56 | cc = cloud.Cloud(ds, paths, {}, distro, None) | 61 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
610 | 57 | self.patchUtils(self.tmp) | 62 | self.patchUtils(self.tmp) |
611 | @@ -65,7 +70,7 @@ class TestHostname(t_help.FilesystemMockingTestCase): | |||
612 | 65 | 'hostname': 'blah.blah.blah.suse.com', | 70 | 'hostname': 'blah.blah.blah.suse.com', |
613 | 66 | } | 71 | } |
614 | 67 | distro = self._fetch_distro('sles') | 72 | distro = self._fetch_distro('sles') |
616 | 68 | paths = helpers.Paths({}) | 73 | paths = helpers.Paths({'cloud_dir': self.tmp}) |
617 | 69 | ds = None | 74 | ds = None |
618 | 70 | cc = cloud.Cloud(ds, paths, {}, distro, None) | 75 | cc = cloud.Cloud(ds, paths, {}, distro, None) |
619 | 71 | self.patchUtils(self.tmp) | 76 | self.patchUtils(self.tmp) |
620 | @@ -74,4 +79,48 @@ class TestHostname(t_help.FilesystemMockingTestCase): | |||
621 | 74 | contents = util.load_file(distro.hostname_conf_fn) | 79 | contents = util.load_file(distro.hostname_conf_fn) |
622 | 75 | self.assertEqual('blah', contents.strip()) | 80 | self.assertEqual('blah', contents.strip()) |
623 | 76 | 81 | ||
624 | 82 | def test_multiple_calls_skips_unchanged_hostname(self): | ||
625 | 83 | """Only new hostname or fqdn values will generate a hostname call.""" | ||
626 | 84 | distro = self._fetch_distro('debian') | ||
627 | 85 | paths = helpers.Paths({'cloud_dir': self.tmp}) | ||
628 | 86 | ds = None | ||
629 | 87 | cc = cloud.Cloud(ds, paths, {}, distro, None) | ||
630 | 88 | self.patchUtils(self.tmp) | ||
631 | 89 | cc_set_hostname.handle( | ||
632 | 90 | 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, []) | ||
633 | 91 | contents = util.load_file("/etc/hostname") | ||
634 | 92 | self.assertEqual('hostname1', contents.strip()) | ||
635 | 93 | cc_set_hostname.handle( | ||
636 | 94 | 'cc_set_hostname', {'hostname': 'hostname1.me.com'}, cc, LOG, []) | ||
637 | 95 | self.assertIn( | ||
638 | 96 | 'DEBUG: No hostname changes. Skipping set-hostname\n', | ||
639 | 97 | self.logs.getvalue()) | ||
640 | 98 | cc_set_hostname.handle( | ||
641 | 99 | 'cc_set_hostname', {'hostname': 'hostname2.me.com'}, cc, LOG, []) | ||
642 | 100 | contents = util.load_file("/etc/hostname") | ||
643 | 101 | self.assertEqual('hostname2', contents.strip()) | ||
644 | 102 | self.assertIn( | ||
645 | 103 | 'Non-persistently setting the system hostname to hostname2', | ||
646 | 104 | self.logs.getvalue()) | ||
647 | 105 | |||
648 | 106 | def test_error_on_distro_set_hostname_errors(self): | ||
649 | 107 | """Raise SetHostnameError on exceptions from distro.set_hostname.""" | ||
650 | 108 | distro = self._fetch_distro('debian') | ||
651 | 109 | |||
652 | 110 | def set_hostname_error(hostname, fqdn): | ||
653 | 111 | raise Exception("OOPS on: %s" % fqdn) | ||
654 | 112 | |||
655 | 113 | distro.set_hostname = set_hostname_error | ||
656 | 114 | paths = helpers.Paths({'cloud_dir': self.tmp}) | ||
657 | 115 | ds = None | ||
658 | 116 | cc = cloud.Cloud(ds, paths, {}, distro, None) | ||
659 | 117 | self.patchUtils(self.tmp) | ||
660 | 118 | with self.assertRaises(cc_set_hostname.SetHostnameError) as ctx_mgr: | ||
661 | 119 | cc_set_hostname.handle( | ||
662 | 120 | 'somename', {'hostname': 'hostname1.me.com'}, cc, LOG, []) | ||
663 | 121 | self.assertEqual( | ||
664 | 122 | 'Failed to set the hostname to hostname1.me.com (hostname1):' | ||
665 | 123 | ' OOPS on: hostname1.me.com', | ||
666 | 124 | str(ctx_mgr.exception)) | ||
667 | 125 | |||
668 | 77 | # vi: ts=4 expandtab | 126 | # vi: ts=4 expandtab |
FAILED: Continuous integration, rev:a015a634f18 594be2bcf78824e b64311d1d52845 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 790/
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 790/rebuild
https:/