Merge lp:~harlowja/cloud-init/cloud-file-write into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
Status: Merged
Merged at revision: 598
Proposed branch: lp:~harlowja/cloud-init/cloud-file-write
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 236 lines (+167/-16)
5 files modified
cloudinit/config/cc_write_files.py (+102/-0)
cloudinit/stages.py (+1/-12)
cloudinit/user_data.py (+1/-1)
cloudinit/util.py (+26/-3)
doc/examples/cloud-config-write-files.txt (+37/-0)
To merge this branch: bzr merge lp:~harlowja/cloud-init/cloud-file-write
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+114417@code.launchpad.net

Description of the change

cloud-config support for file writing

Fixes LP: #1012854 by implementing file writing, adjusts
other code to have user/group parsing in util instead
of in stages.py, renames decomp_str to decomp_gzip since
it is more meaningful when named that (as thats all it can
decompress).

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

Joshua, Thanks for the new feature.
A couple things:
 * I think we might as well just require 'compression' to be 'gzip', rather than supporting anything from is_true. having more things just makes more work in the future, and its no harder for someone to produce 'compression: gzip' than 'compression: 1'
 * it seems like the code right now only base64 decodes if the content is set to compressed. I'd like to do that seperately. It maybe that someone wants to simply produce base64 for all files they send through, rather than only for gzipped.
 * examples added in /doc would be nice.

Other than that, I think it looks great.

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

Sounds good to me.

597. By Joshua Harlow

Adjust the decoding of the files that are given so that a canonicalize
happens first, which will examine the incoming encoding, and decide the
neccasary decoding types needed to get the final resultant string and
then use these normalized decoding types to actually do the final decode.

Also change the name of the config key that is looked up to 'write_files'
since 'files' is pretty generic and could have clashes with other modules.

Add an example that shows how to use this in the different encoding formats
that are supported.

598. By Joshua Harlow

Fix log message after 'write_files' key change.

599. By Joshua Harlow

Update the write files with the new content/compression handling.

Adjust the examples file to reflect this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cloudinit/config/cc_write_files.py'
2--- cloudinit/config/cc_write_files.py 1970-01-01 00:00:00 +0000
3+++ cloudinit/config/cc_write_files.py 2012-07-11 20:04:20 +0000
4@@ -0,0 +1,102 @@
5+# vi: ts=4 expandtab
6+#
7+# Copyright (C) 2012 Yahoo! Inc.
8+#
9+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
10+#
11+# This program is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License version 3, as
13+# published by the Free Software Foundation.
14+#
15+# This program is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# You should have received a copy of the GNU General Public License
21+# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
23+import base64
24+import os
25+
26+from cloudinit import util
27+from cloudinit.settings import PER_INSTANCE
28+
29+frequency = PER_INSTANCE
30+
31+DEFAULT_OWNER = "root:root"
32+DEFAULT_PERMS = 0644
33+UNKNOWN_ENC = 'text/plain'
34+
35+
36+def handle(name, cfg, _cloud, log, _args):
37+ files = cfg.get('write_files')
38+ if not files:
39+ log.debug(("Skipping module named %s,"
40+ " no/empty 'write_files' key in configuration"), name)
41+ return
42+ write_files(name, files, log)
43+
44+
45+def canonicalize_extraction(compression_type, log):
46+ if not compression_type:
47+ compression_type = ''
48+ compression_type = compression_type.lower().strip()
49+ if compression_type in ['gz', 'gzip']:
50+ return ['application/x-gzip']
51+ if compression_type in ['gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64']:
52+ return ['application/base64', 'application/x-gzip']
53+ # Yaml already encodes binary data as base64 if it is given to the
54+ # yaml file as binary, so those will be automatically decoded for you.
55+ # But the above b64 is just for people that are more 'comfortable'
56+ # specifing it manually (which might be a possiblity)
57+ if compression_type in ['b64', 'base64']:
58+ return ['application/base64']
59+ if compression_type:
60+ log.warn("Unknown compression type %s, assuming %s",
61+ compression_type, UNKNOWN_ENC)
62+ return [UNKNOWN_ENC]
63+
64+
65+def write_files(name, files, log):
66+ if not files:
67+ return
68+
69+ for (i, f_info) in enumerate(files):
70+ path = f_info.get('path')
71+ if not path:
72+ log.warn("No path provided to write for entry %s in module %s",
73+ i + 1, name)
74+ continue
75+ path = os.path.abspath(path)
76+ extractions = canonicalize_extraction(f_info.get('compression'), log)
77+ contents = extract_contents(f_info.get('content', ''), extractions)
78+ (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER))
79+ perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS, log)
80+ util.write_file(path, contents, mode=perms)
81+ util.chownbyname(path, u, g)
82+
83+
84+def decode_perms(perm, default, log):
85+ try:
86+ if isinstance(perm, (int, long, float)):
87+ # Just 'downcast' it (if a float)
88+ return int(perm)
89+ else:
90+ # Force to string and try octal conversion
91+ return int(str(perm), 8)
92+ except (TypeError, ValueError):
93+ log.warn("Undecodable permissions %s, assuming %s", perm, default)
94+ return default
95+
96+
97+def extract_contents(contents, extraction_types):
98+ result = str(contents)
99+ for t in extraction_types:
100+ if t == 'application/x-gzip':
101+ result = util.decomp_gzip(result, quiet=False)
102+ elif t == 'application/base64':
103+ result = base64.b64decode(result)
104+ elif t == UNKNOWN_ENC:
105+ pass
106+ return result
107
108=== modified file 'cloudinit/stages.py'
109--- cloudinit/stages.py 2012-07-09 19:08:27 +0000
110+++ cloudinit/stages.py 2012-07-11 20:04:20 +0000
111@@ -133,18 +133,7 @@
112 if log_file:
113 util.ensure_file(log_file)
114 if perms:
115- perms_parted = perms.split(':', 1)
116- u = perms_parted[0]
117- if len(perms_parted) == 2:
118- g = perms_parted[1]
119- else:
120- g = ''
121- u = u.strip()
122- g = g.strip()
123- if u == "-1" or u.lower() == "none":
124- u = None
125- if g == "-1" or g.lower() == "none":
126- g = None
127+ u, g = util.extract_usergroup(perms)
128 try:
129 util.chownbyname(log_file, u, g)
130 except OSError:
131
132=== modified file 'cloudinit/user_data.py'
133--- cloudinit/user_data.py 2012-06-23 05:04:37 +0000
134+++ cloudinit/user_data.py 2012-07-11 20:04:20 +0000
135@@ -227,7 +227,7 @@
136 raw_data = ''
137 if not headers:
138 headers = {}
139- data = util.decomp_str(raw_data)
140+ data = util.decomp_gzip(raw_data)
141 if "mime-version:" in data[0:4096].lower():
142 msg = email.message_from_string(data)
143 for (key, val) in headers.iteritems():
144
145=== modified file 'cloudinit/util.py'
146--- cloudinit/util.py 2012-07-10 21:47:02 +0000
147+++ cloudinit/util.py 2012-07-11 20:04:20 +0000
148@@ -159,6 +159,10 @@
149 pass
150
151
152+class DecompressionError(Exception):
153+ pass
154+
155+
156 def ExtendedTemporaryFile(**kwargs):
157 fh = tempfile.NamedTemporaryFile(**kwargs)
158 # Replace its unlink with a quiet version
159@@ -256,13 +260,32 @@
160 return fn
161
162
163-def decomp_str(data):
164+def decomp_gzip(data, quiet=True):
165 try:
166 buf = StringIO(str(data))
167 with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh:
168 return gh.read()
169- except:
170- return data
171+ except Exception as e:
172+ if quiet:
173+ return data
174+ else:
175+ raise DecompressionError(str(e))
176+
177+
178+def extract_usergroup(ug_pair):
179+ if not ug_pair:
180+ return (None, None)
181+ ug_parted = ug_pair.split(':', 1)
182+ u = ug_parted[0].strip()
183+ if len(ug_parted) == 2:
184+ g = ug_parted[1].strip()
185+ else:
186+ g = None
187+ if not u or u == "-1" or u.lower() == "none":
188+ u = None
189+ if not g or g == "-1" or g.lower() == "none":
190+ g = None
191+ return (u, g)
192
193
194 def find_modules(root_dir):
195
196=== added file 'doc/examples/cloud-config-write-files.txt'
197--- doc/examples/cloud-config-write-files.txt 1970-01-01 00:00:00 +0000
198+++ doc/examples/cloud-config-write-files.txt 2012-07-11 20:04:20 +0000
199@@ -0,0 +1,37 @@
200+#cloud-config
201+# vim: syntax=yaml
202+#
203+# This is the configuration syntax that the write_files module
204+# will know how to understand, it can be given b64, b32, b16, or
205+# gz (or gz+b64) encoded strings which will be decoded accordingly
206+# and then written to the path that is provided.
207+#
208+# Note: Content strings here are truncated for example purposes.
209+#
210+write_files:
211+- compression: b64
212+ content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4...
213+ owner: root:root
214+ path: /etc/sysconfig/selinux
215+ perms: '0644'
216+- content: '
217+
218+ # My new /etc/sysconfig/samba file
219+
220+ SMBDOPTIONS="-D"
221+
222+ '
223+ path: /etc/sysconfig/samba
224+- content: !!binary |
225+ f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
226+ AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
227+ AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
228+ ....
229+ path: /bin/arch
230+ perms: '0555'
231+- compression: gzip
232+ content: !!binary |
233+ H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
234+ path: /usr/bin/hello
235+ perms: '0755'
236+