Merge lp:~harlowja/cloud-init/write-files-fetch-from-somewhere into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow
Status: Rejected
Rejected by: Scott Moser
Proposed branch: lp:~harlowja/cloud-init/write-files-fetch-from-somewhere
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 218 lines (+132/-9)
3 files modified
cloudinit/config/cc_write_files.py (+27/-3)
cloudinit/url_helper.py (+20/-6)
tests/unittests/test_handler/test_handler_write_files.py (+85/-0)
To merge this branch: bzr merge lp:~harlowja/cloud-init/write-files-fetch-from-somewhere
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
Scott Moser Needs Fixing
Review via email: mp+254816@code.launchpad.net
To post a comment you must log in.
1091. By Joshua Harlow

Fix reading of files for py3.x

util.read_file_or_url needs to have a __str__ that
returns the right type for py3.x; so make sure that if
binary data is read that it is decoded.

1092. By Joshua Harlow

Only do the decoding on py3.x

1093. By Joshua Harlow

Some contents/text tweaks and handler tests

Revision history for this message
Scott Moser (smoser) wrote :

I think better to require user to be explicit that this is a remote file.
I don't like the interpretation of content. The problem being, if it seems quite likely that some config file that someone would want to 'write-files' of might start with 'http://'.
If they try to send such a content, then cloud-init would interpret that content and pull the url.

So, in short, I'd rather:
write_files:
 - path: /myfile-from-remote
   content_url: http://www.yahoo.com
 - path: /odd-use-of-cloud-init-for-copy
   content_url: file:///usr/bin/true
 - path: /myfile-old-write_files
   content: "Hi Mom"

Than:
write_files:
 - path: /myfile-from-remote
   content: http://www.yahoo.com

in the event of an entry having both 'content_url' and 'content', I guess I'd warn and use the 'content' field.

Agree/ disagree?

review: Needs Fixing
Revision history for this message
Joshua Harlow (harlowja) wrote :

Thats fine with me; I was debating whether to try to interpret content or not and am fine either way; will adjust that soon.

1094. By Joshua Harlow

Avoid fetching automagically and add test for this

1095. By Joshua Harlow

Fixs log entry/output

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:1095
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~harlowja/cloud-init/write-files-fetch-from-somewhere/+merge/254816/+edit-commit-message

https://server-team-jenkins.canonical.com/job/cloud-init-ci/47/
Executed test runs:
    None: https://server-team-jenkins.canonical.com/job/lp-vote-on-merge/21/console

Click here to trigger a rebuild:
https://server-team-jenkins.canonical.com/job/cloud-init-ci/47/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Hi,

Josh, Please re-submit this. I like the functionality. Please re-submit in git.

Thank you for taking the time to contribute to cloud-init. Cloud-init has moved its revision control system to git. As a result, we are marking all bzr merge proposals as 'rejected'. If you would like to re-submit this proposal for review, please do so by following the current HACKING documentation at http://cloudinit.readthedocs.io/en/latest/topics/hacking.html .

I'm going to mark this as 'merged', because I think it is actually
upstream at
https://git.launchpad.net/cloud-init/commit/?id=79236a629f1

Unmerged revisions

1095. By Joshua Harlow

Fixs log entry/output

1094. By Joshua Harlow

Avoid fetching automagically and add test for this

1093. By Joshua Harlow

Some contents/text tweaks and handler tests

1092. By Joshua Harlow

Only do the decoding on py3.x

1091. By Joshua Harlow

Fix reading of files for py3.x

util.read_file_or_url needs to have a __str__ that
returns the right type for py3.x; so make sure that if
binary data is read that it is decoded.

1090. By Joshua Harlow

Fix extra space

1089. By Joshua Harlow

Allow write_files to fetch content

Instead of requring write_files to be always provided
all the data (which can be big/large blobs); allow it to
fetch that data from either a local file or a http/https
url instead (saving precious user-data space).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/config/cc_write_files.py'
2--- cloudinit/config/cc_write_files.py 2015-01-26 17:41:04 +0000
3+++ cloudinit/config/cc_write_files.py 2015-04-07 22:54:32 +0000
4@@ -25,6 +25,13 @@
5
6 frequency = PER_INSTANCE
7
8+# Content with these prefix(s) will be read/fetched externally to grab its
9+# contents (which will replace any provided contents...)
10+READ_EXTERNAL_PREFIXES = [
11+ "http://",
12+ 'file://',
13+ "https://",
14+]
15 DEFAULT_OWNER = "root:root"
16 DEFAULT_PERMS = 0o644
17 UNKNOWN_ENC = 'text/plain'
18@@ -62,7 +69,6 @@
19 def write_files(name, files, log):
20 if not files:
21 return
22-
23 for (i, f_info) in enumerate(files):
24 path = f_info.get('path')
25 if not path:
26@@ -70,8 +76,26 @@
27 i + 1, name)
28 continue
29 path = os.path.abspath(path)
30+ pre_content = f_info.get('content', '')
31+ fetch_external = util.is_true(f_info.get('fetch_external', False))
32+ if fetch_external:
33+ # Verify prefixes that we will try to fetch...
34+ prefix_match = False
35+ for p in READ_EXTERNAL_PREFIXES:
36+ if pre_content.startswith(p):
37+ prefix_match = True
38+ break
39+ if not prefix_match:
40+ log.warn("Unable to read from external content '%s' (for"
41+ " entry %s) that does not start with one"
42+ " of %s prefixes", pre_content, i + 1,
43+ READ_EXTERNAL_PREFIXES)
44+ continue
45+ else:
46+ pre_content = util.read_file_or_url(pre_content)
47+ pre_content = pre_content.contents
48 extractions = canonicalize_extraction(f_info.get('encoding'), log)
49- contents = extract_contents(f_info.get('content', ''), extractions)
50+ contents = extract_contents(pre_content, extractions)
51 (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER))
52 perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS, log)
53 util.write_file(path, contents, mode=perms)
54@@ -92,7 +116,7 @@
55
56
57 def extract_contents(contents, extraction_types):
58- result = str(contents)
59+ result = contents
60 for t in extraction_types:
61 if t == 'application/x-gzip':
62 result = util.decomp_gzip(result, quiet=False)
63
64=== modified file 'cloudinit/url_helper.py'
65--- cloudinit/url_helper.py 2015-03-02 20:48:42 +0000
66+++ cloudinit/url_helper.py 2015-04-07 22:54:32 +0000
67@@ -92,11 +92,18 @@
68 # read_file_or_url can return this or that object and the
69 # 'user' of those objects will not need to know the difference.
70 class StringResponse(object):
71- def __init__(self, contents, code=200):
72+ def __init__(self, contents, code=200, encoding='utf-8'):
73 self.code = code
74 self.headers = {}
75- self.contents = contents
76 self.url = None
77+ self.encoding = encoding
78+ # Get the contents and text in the right encoding(s)...
79+ if not isinstance(contents, six.text_type):
80+ self.contents = contents
81+ self.text = contents.decode(encoding)
82+ else:
83+ self.text = contents
84+ self.contents = contents.encode(encoding)
85
86 def ok(self, *args, **kwargs):
87 if self.code != 200:
88@@ -104,12 +111,14 @@
89 return True
90
91 def __str__(self):
92- return self.contents
93+ """The text/unicode contents."""
94+ return self.text
95
96
97 class FileResponse(StringResponse):
98- def __init__(self, path, contents, code=200):
99- StringResponse.__init__(self, contents, code=code)
100+ def __init__(self, path, contents, code=200, encoding='utf-8'):
101+ StringResponse.__init__(self, contents,
102+ code=code, encoding=encoding)
103 self.url = path
104
105
106@@ -119,8 +128,13 @@
107
108 @property
109 def contents(self):
110+ """The binary contents."""
111 return self._response.content
112
113+ def text(self):
114+ """The text/unicode contents."""
115+ return self._response.text
116+
117 @property
118 def url(self):
119 return self._response.url
120@@ -143,7 +157,7 @@
121 return self._response.status_code
122
123 def __str__(self):
124- return self._response.text
125+ return self.text
126
127
128 class UrlError(IOError):
129
130=== added file 'tests/unittests/test_handler/test_handler_write_files.py'
131--- tests/unittests/test_handler/test_handler_write_files.py 1970-01-01 00:00:00 +0000
132+++ tests/unittests/test_handler/test_handler_write_files.py 2015-04-07 22:54:32 +0000
133@@ -0,0 +1,85 @@
134+from cloudinit import util
135+
136+from cloudinit.config import cc_write_files
137+
138+from .. import helpers
139+
140+import grp
141+import logging
142+import os
143+import pwd
144+import shutil
145+import tempfile
146+
147+LOG = logging.getLogger(__name__)
148+
149+hp = helpers.import_httpretty()
150+
151+
152+def get_owner():
153+ user = pwd.getpwuid(os.getuid())[0]
154+ gid = pwd.getpwnam(user).pw_gid
155+ group = grp.getgrgid(gid).gr_name
156+ return "%s:%s" % (user, group)
157+
158+
159+class TestWriteFiles(helpers.FilesystemMockingTestCase):
160+ def setUp(self):
161+ super(TestWriteFiles, self).setUp()
162+ self.tmp = tempfile.mkdtemp()
163+ self.addCleanup(shutil.rmtree, self.tmp)
164+
165+ def test_local_write(self):
166+ path = os.path.join(self.tmp, 'stuff.txt')
167+ files = [
168+ {
169+ 'path': path,
170+ 'content': 'blahblah',
171+ 'permissions': '0755',
172+ 'owner': get_owner(),
173+ },
174+ ]
175+ cfg = {
176+ 'write_files': files,
177+ }
178+ cc_write_files.handle('write_files', cfg, None, LOG, [])
179+ blob = util.load_file(path)
180+ self.assertEqual("blahblah", blob)
181+
182+ @hp.activate
183+ def test_remote_read(self):
184+ hp.register_uri(hp.GET, 'http://www.yahoo.com',
185+ status=200, body='yahooooooo!')
186+ path = os.path.join(self.tmp, 'stuff.txt')
187+ files = [
188+ {
189+ 'path': path,
190+ 'content': 'http://www.yahoo.com',
191+ 'permissions': '0755',
192+ 'owner': get_owner(),
193+ 'fetch_external': 1,
194+ },
195+ ]
196+ cfg = {
197+ 'write_files': files,
198+ }
199+ cc_write_files.handle('write_files', cfg, None, LOG, [])
200+ blob = util.load_file(path)
201+ self.assertEqual("yahooooooo!", blob)
202+
203+ def test_skipped_remote_read(self):
204+ path = os.path.join(self.tmp, 'stuff.txt')
205+ files = [
206+ {
207+ 'path': path,
208+ 'content': 'blah://www.yahoo.com',
209+ 'permissions': '0755',
210+ 'owner': get_owner(),
211+ 'fetch_external': 1,
212+ },
213+ ]
214+ cfg = {
215+ 'write_files': files,
216+ }
217+ cc_write_files.handle('write_files', cfg, None, LOG, [])
218+ self.assertFalse(os.path.isfile(path))