Merge ~smoser/cloud-init:bug/noise-on-cmdline-url-fail into cloud-init:master

Proposed by Scott Moser
Status: Merged
Merged at revision: a1b185d0cce5064e9b36b4db7b55564e2ab1d7a8
Proposed branch: ~smoser/cloud-init:bug/noise-on-cmdline-url-fail
Merge into: cloud-init:master
Diff against target: 377 lines (+173/-97)
4 files modified
cloudinit/cmd/main.py (+103/-15)
cloudinit/util.py (+0/-44)
tests/unittests/helpers.py (+11/-5)
tests/unittests/test__init__.py (+59/-33)
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+311549@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Scott Moser (smoser) wrote :

Thanks for the review, your comments seem sane.
I really do want to get this in, as currently this is a common path for failure in maas (node can't reach region controller) and there isn't much path as to what is going on there.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index c83496c..65b15ed 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -26,6 +26,7 @@ from cloudinit import signal_handler
26from cloudinit import sources26from cloudinit import sources
27from cloudinit import stages27from cloudinit import stages
28from cloudinit import templater28from cloudinit import templater
29from cloudinit import url_helper
29from cloudinit import util30from cloudinit import util
30from cloudinit import version31from cloudinit import version
3132
@@ -129,23 +130,104 @@ def apply_reporting_cfg(cfg):
129 reporting.update_configuration(cfg.get('reporting'))130 reporting.update_configuration(cfg.get('reporting'))
130131
131132
133def parse_cmdline_url(cmdline, names=('cloud-config-url', 'url')):
134 data = util.keyval_str_to_dict(cmdline)
135 for key in names:
136 if key in data:
137 return key, data[key]
138 raise KeyError("No keys (%s) found in string '%s'" %
139 (cmdline, names))
140
141
142def attempt_cmdline_url(path, network=True, cmdline=None):
143 """Write data from url referenced in command line to path.
144
145 path: a file to write content to if downloaded.
146 network: should network access be assumed.
147 cmdline: the cmdline to parse for cloud-config-url.
148
149 This is used in MAAS datasource, in "ephemeral" (read-only root)
150 environment where the instance netboots to iscsi ro root.
151 and the entity that controls the pxe config has to configure
152 the maas datasource.
153
154 An attempt is made on network urls even in local datasource
155 for case of network set up in initramfs.
156
157 Return value is a tuple of a logger function (logging.DEBUG)
158 and a message indicating what happened.
159 """
160
161 if cmdline is None:
162 cmdline = util.get_cmdline()
163
164 try:
165 cmdline_name, url = parse_cmdline_url(cmdline)
166 except KeyError:
167 return (logging.DEBUG, "No kernel command line url found.")
168
169 path_is_local = url.startswith("file://") or url.startswith("/")
170
171 if path_is_local and os.path.exists(path):
172 if network:
173 m = ("file '%s' existed, possibly from local stage download"
174 " of command line url '%s'. Not re-writing." % (path, url))
175 level = logging.INFO
176 if path_is_local:
177 level = logging.DEBUG
178 else:
179 m = ("file '%s' existed, possibly from previous boot download"
180 " of command line url '%s'. Not re-writing." % (path, url))
181 level = logging.WARN
182
183 return (level, m)
184
185 kwargs = {'url': url, 'timeout': 10, 'retries': 2}
186 if network or path_is_local:
187 level = logging.WARN
188 kwargs['sec_between'] = 1
189 else:
190 level = logging.DEBUG
191 kwargs['sec_between'] = .1
192
193 data = None
194 header = b'#cloud-config'
195 try:
196 resp = util.read_file_or_url(**kwargs)
197 if resp.ok():
198 data = resp.contents
199 if not resp.contents.startswith(header):
200 if cmdline_name == 'cloud-config-url':
201 level = logging.WARN
202 else:
203 level = logging.INFO
204 return (
205 level,
206 "contents of '%s' did not start with %s" % (url, header))
207 else:
208 return (level,
209 "url '%s' returned code %s. Ignoring." % (url, resp.code))
210
211 except url_helper.UrlError as e:
212 return (level, "retrieving url '%s' failed: %s" % (url, e))
213
214 util.write_file(path, data, mode=0o600)
215 return (logging.INFO,
216 "wrote cloud-config data from %s='%s' to %s" %
217 (cmdline_name, url, path))
218
219
132def main_init(name, args):220def main_init(name, args):
133 deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK]221 deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK]
134 if args.local:222 if args.local:
135 deps = [sources.DEP_FILESYSTEM]223 deps = [sources.DEP_FILESYSTEM]
136224
137 if not args.local:225 early_logs = []
138 # See doc/kernel-cmdline.txt226 early_logs.append(
139 #227 attempt_cmdline_url(
140 # This is used in maas datasource, in "ephemeral" (read-only root)228 path=os.path.join("%s.d" % CLOUD_CONFIG,
141 # environment where the instance netboots to iscsi ro root.229 "91_kernel_cmdline_url.cfg"),
142 # and the entity that controls the pxe config has to configure230 network=not args.local))
143 # the maas datasource.
144 #
145 # Could be used elsewhere, only works on network based (not local).
146 root_name = "%s.d" % (CLOUD_CONFIG)
147 target_fn = os.path.join(root_name, "91_kernel_cmdline_url.cfg")
148 util.read_write_cmdline_url(target_fn)
149231
150 # Cloud-init 'init' stage is broken up into the following sub-stages232 # Cloud-init 'init' stage is broken up into the following sub-stages
151 # 1. Ensure that the init object fetches its config without errors233 # 1. Ensure that the init object fetches its config without errors
@@ -171,12 +253,14 @@ def main_init(name, args):
171 outfmt = None253 outfmt = None
172 errfmt = None254 errfmt = None
173 try:255 try:
174 LOG.debug("Closing stdin")256 early_logs.append((logging.DEBUG, "Closing stdin."))
175 util.close_stdin()257 util.close_stdin()
176 (outfmt, errfmt) = util.fixup_output(init.cfg, name)258 (outfmt, errfmt) = util.fixup_output(init.cfg, name)
177 except Exception:259 except Exception:
178 util.logexc(LOG, "Failed to setup output redirection!")260 msg = "Failed to setup output redirection!"
179 print_exc("Failed to setup output redirection!")261 util.logexc(LOG, msg)
262 print_exc(msg)
263 early_logs.append((logging.WARN, msg))
180 if args.debug:264 if args.debug:
181 # Reset so that all the debug handlers are closed out265 # Reset so that all the debug handlers are closed out
182 LOG.debug(("Logging being reset, this logger may no"266 LOG.debug(("Logging being reset, this logger may no"
@@ -190,6 +274,10 @@ def main_init(name, args):
190 # been redirected and log now configured.274 # been redirected and log now configured.
191 welcome(name, msg=w_msg)275 welcome(name, msg=w_msg)
192276
277 # re-play early log messages before logging was setup
278 for lvl, msg in early_logs:
279 LOG.log(lvl, msg)
280
193 # Stage 3281 # Stage 3
194 try:282 try:
195 init.initialize()283 init.initialize()
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 5725129..7196a7c 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1089,31 +1089,6 @@ def get_fqdn_from_hosts(hostname, filename="/etc/hosts"):
1089 return fqdn1089 return fqdn
10901090
10911091
1092def get_cmdline_url(names=('cloud-config-url', 'url'),
1093 starts=b"#cloud-config", cmdline=None):
1094 if cmdline is None:
1095 cmdline = get_cmdline()
1096
1097 data = keyval_str_to_dict(cmdline)
1098 url = None
1099 key = None
1100 for key in names:
1101 if key in data:
1102 url = data[key]
1103 break
1104
1105 if not url:
1106 return (None, None, None)
1107
1108 resp = read_file_or_url(url)
1109 # allow callers to pass starts as text when comparing to bytes contents
1110 starts = encode_text(starts)
1111 if resp.ok() and resp.contents.startswith(starts):
1112 return (key, url, resp.contents)
1113
1114 return (key, url, None)
1115
1116
1117def is_resolvable(name):1092def is_resolvable(name):
1118 """determine if a url is resolvable, return a boolean1093 """determine if a url is resolvable, return a boolean
1119 This also attempts to be resilent against dns redirection.1094 This also attempts to be resilent against dns redirection.
@@ -1475,25 +1450,6 @@ def ensure_dirs(dirlist, mode=0o755):
1475 ensure_dir(d, mode)1450 ensure_dir(d, mode)
14761451
14771452
1478def read_write_cmdline_url(target_fn):
1479 if not os.path.exists(target_fn):
1480 try:
1481 (key, url, content) = get_cmdline_url()
1482 except Exception:
1483 logexc(LOG, "Failed fetching command line url")
1484 return
1485 try:
1486 if key and content:
1487 write_file(target_fn, content, mode=0o600)
1488 LOG.debug(("Wrote to %s with contents of command line"
1489 " url %s (len=%s)"), target_fn, url, len(content))
1490 elif key and not content:
1491 LOG.debug(("Command line key %s with url"
1492 " %s had no contents"), key, url)
1493 except Exception:
1494 logexc(LOG, "Failed writing url content to %s", target_fn)
1495
1496
1497def yaml_dumps(obj, explicit_start=True, explicit_end=True):1453def yaml_dumps(obj, explicit_start=True, explicit_end=True):
1498 return yaml.safe_dump(obj,1454 return yaml.safe_dump(obj,
1499 line_break="\n",1455 line_break="\n",
diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
index cf3b46d..64e56d9 100644
--- a/tests/unittests/helpers.py
+++ b/tests/unittests/helpers.py
@@ -264,16 +264,22 @@ class HttprettyTestCase(TestCase):
264264
265class TempDirTestCase(TestCase):265class TempDirTestCase(TestCase):
266 # provide a tempdir per class, not per test.266 # provide a tempdir per class, not per test.
267 def setUp(self):267 @classmethod
268 super(TempDirTestCase, self).setUp()268 def setUpClass(cls):
269 self.tmp = tempfile.mkdtemp()269 cls.tmpd = tempfile.mkdtemp(prefix="ci-%s." % cls.__name__)
270 self.addCleanup(shutil.rmtree, self.tmp)270 return TestCase.setUpClass()
271
272 @classmethod
273 def tearDownClass(cls):
274 shutil.rmtree(cls.tmpd)
275 return TestCase.tearDownClass()
271276
272 def tmp_path(self, path):277 def tmp_path(self, path):
278 # if absolute path (starts with /), then make ./path
273 if path.startswith(os.path.sep):279 if path.startswith(os.path.sep):
274 path = "." + path280 path = "." + path
275281
276 return os.path.normpath(os.path.join(self.tmp, path))282 return os.path.normpath(os.path.join(self.tmpd, path))
277283
278284
279def populate_dir(path, files):285def populate_dir(path, files):
diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py
index 7b6f8c4..e6f4c31 100644
--- a/tests/unittests/test__init__.py
+++ b/tests/unittests/test__init__.py
@@ -1,16 +1,18 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import logging
3import os4import os
4import shutil5import shutil
5import tempfile6import tempfile
67
8from cloudinit.cmd import main
7from cloudinit import handlers9from cloudinit import handlers
8from cloudinit import helpers10from cloudinit import helpers
9from cloudinit import settings11from cloudinit import settings
10from cloudinit import url_helper12from cloudinit import url_helper
11from cloudinit import util13from cloudinit import util
1214
13from .helpers import TestCase, ExitStack, mock15from .helpers import TestCase, TempDirTestCase, ExitStack, mock
1416
1517
16class FakeModule(handlers.Handler):18class FakeModule(handlers.Handler):
@@ -170,44 +172,68 @@ class TestHandlerHandlePart(TestCase):
170 self.data, self.ctype, self.filename, self.payload)172 self.data, self.ctype, self.filename, self.payload)
171173
172174
173class TestCmdlineUrl(TestCase):175class TestCmdlineUrl(TempDirTestCase):
174 def test_invalid_content(self):176 def test_parse_cmdline_url_nokey_raises_keyerror(self):
175 url = "http://example.com/foo"177 self.assertRaises(
176 key = "mykey"178 KeyError, main.parse_cmdline_url, 'root=foo bar single')
177 payload = b"0"
178 cmdline = "ro %s=%s bar=1" % (key, url)
179179
180 with mock.patch('cloudinit.url_helper.readurl',180 def test_parse_cmdline_url_found(self):
181 return_value=url_helper.StringResponse(payload)):181 cmdline = 'root=foo bar single url=http://example.com arg1 -v'
182 self.assertEqual(182 self.assertEqual(
183 util.get_cmdline_url(names=[key], starts="xxxxxx",183 ('url', 'http://example.com'), main.parse_cmdline_url(cmdline))
184 cmdline=cmdline),
185 (key, url, None))
186184
187 def test_valid_content(self):185 @mock.patch('cloudinit.cmd.main.util.read_file_or_url')
188 url = "http://example.com/foo"186 def test_invalid_content(self, m_read):
189 key = "mykey"187 key = "cloud-config-url"
190 payload = b"xcloud-config\nmydata: foo\nbar: wark\n"188 url = 'http://example.com/foo'
191 cmdline = "ro %s=%s bar=1" % (key, url)189 cmdline = "ro %s=%s bar=1" % (key, url)
190 m_read.return_value = url_helper.StringResponse(b"unexpected blob")
192191
193 with mock.patch('cloudinit.url_helper.readurl',192 fpath = self.tmp_path("test_valid")
194 return_value=url_helper.StringResponse(payload)):193 lvl, msg = main.attempt_cmdline_url(
195 self.assertEqual(194 fpath, network=True, cmdline=cmdline)
196 util.get_cmdline_url(names=[key], starts=b"xcloud-config",195 self.assertEqual(logging.WARN, lvl)
197 cmdline=cmdline),196 self.assertIn(url, msg)
198 (key, url, payload))197 self.assertFalse(os.path.exists(fpath))
199198
200 def test_no_key_found(self):199 @mock.patch('cloudinit.cmd.main.util.read_file_or_url')
200 def test_valid_content(self, m_read):
201 url = "http://example.com/foo"201 url = "http://example.com/foo"
202 key = "mykey"202 payload = b"#cloud-config\nmydata: foo\nbar: wark\n"
203 cmdline = "ro %s=%s bar=1" % (key, url)203 cmdline = "ro %s=%s bar=1" % ('cloud-config-url', url)
204204
205 with mock.patch('cloudinit.url_helper.readurl',205 m_read.return_value = url_helper.StringResponse(payload)
206 return_value=url_helper.StringResponse(b'')):206 fpath = self.tmp_path("test_valid")
207 self.assertEqual(207 lvl, msg = main.attempt_cmdline_url(
208 util.get_cmdline_url(names=["does-not-appear"],208 fpath, network=True, cmdline=cmdline)
209 starts="#cloud-config", cmdline=cmdline),209 self.assertEqual(util.load_file(fpath, decode=False), payload)
210 (None, None, None))210 self.assertEqual(logging.INFO, lvl)
211 self.assertIn(url, msg)
212
213 @mock.patch('cloudinit.cmd.main.util.read_file_or_url')
214 def test_no_key_found(self, m_read):
215 cmdline = "ro mykey=http://example.com/foo root=foo"
216 fpath = self.tmp_path("test_no_key_found")
217 lvl, msg = main.attempt_cmdline_url(
218 fpath, network=True, cmdline=cmdline)
219
220 m_read.assert_not_called()
221 self.assertFalse(os.path.exists(fpath))
222 self.assertEqual(logging.DEBUG, lvl)
223
224 @mock.patch('cloudinit.cmd.main.util.read_file_or_url')
225 def test_exception_warns(self, m_read):
226 url = "http://example.com/foo"
227 cmdline = "ro cloud-config-url=%s root=LABEL=bar" % url
228 fpath = self.tmp_path("test_no_key_found")
229 m_read.side_effect = url_helper.UrlError(
230 cause="Unexpected Error", url="http://example.com/foo")
231
232 lvl, msg = main.attempt_cmdline_url(
233 fpath, network=True, cmdline=cmdline)
234 self.assertEqual(logging.WARN, lvl)
235 self.assertIn(url, msg)
236 self.assertFalse(os.path.exists(fpath))
211237
212238
213# vi: ts=4 expandtab239# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches