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
=== added file 'cloudinit/config/cc_write_files.py'
--- cloudinit/config/cc_write_files.py 1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_write_files.py 2012-07-11 20:04:20 +0000
@@ -0,0 +1,102 @@
1# vi: ts=4 expandtab
2#
3# Copyright (C) 2012 Yahoo! Inc.
4#
5# Author: Joshua Harlow <harlowja@yahoo-inc.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3, as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import base64
20import os
21
22from cloudinit import util
23from cloudinit.settings import PER_INSTANCE
24
25frequency = PER_INSTANCE
26
27DEFAULT_OWNER = "root:root"
28DEFAULT_PERMS = 0644
29UNKNOWN_ENC = 'text/plain'
30
31
32def handle(name, cfg, _cloud, log, _args):
33 files = cfg.get('write_files')
34 if not files:
35 log.debug(("Skipping module named %s,"
36 " no/empty 'write_files' key in configuration"), name)
37 return
38 write_files(name, files, log)
39
40
41def canonicalize_extraction(compression_type, log):
42 if not compression_type:
43 compression_type = ''
44 compression_type = compression_type.lower().strip()
45 if compression_type in ['gz', 'gzip']:
46 return ['application/x-gzip']
47 if compression_type in ['gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64']:
48 return ['application/base64', 'application/x-gzip']
49 # Yaml already encodes binary data as base64 if it is given to the
50 # yaml file as binary, so those will be automatically decoded for you.
51 # But the above b64 is just for people that are more 'comfortable'
52 # specifing it manually (which might be a possiblity)
53 if compression_type in ['b64', 'base64']:
54 return ['application/base64']
55 if compression_type:
56 log.warn("Unknown compression type %s, assuming %s",
57 compression_type, UNKNOWN_ENC)
58 return [UNKNOWN_ENC]
59
60
61def write_files(name, files, log):
62 if not files:
63 return
64
65 for (i, f_info) in enumerate(files):
66 path = f_info.get('path')
67 if not path:
68 log.warn("No path provided to write for entry %s in module %s",
69 i + 1, name)
70 continue
71 path = os.path.abspath(path)
72 extractions = canonicalize_extraction(f_info.get('compression'), log)
73 contents = extract_contents(f_info.get('content', ''), extractions)
74 (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER))
75 perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS, log)
76 util.write_file(path, contents, mode=perms)
77 util.chownbyname(path, u, g)
78
79
80def decode_perms(perm, default, log):
81 try:
82 if isinstance(perm, (int, long, float)):
83 # Just 'downcast' it (if a float)
84 return int(perm)
85 else:
86 # Force to string and try octal conversion
87 return int(str(perm), 8)
88 except (TypeError, ValueError):
89 log.warn("Undecodable permissions %s, assuming %s", perm, default)
90 return default
91
92
93def extract_contents(contents, extraction_types):
94 result = str(contents)
95 for t in extraction_types:
96 if t == 'application/x-gzip':
97 result = util.decomp_gzip(result, quiet=False)
98 elif t == 'application/base64':
99 result = base64.b64decode(result)
100 elif t == UNKNOWN_ENC:
101 pass
102 return result
0103
=== modified file 'cloudinit/stages.py'
--- cloudinit/stages.py 2012-07-09 19:08:27 +0000
+++ cloudinit/stages.py 2012-07-11 20:04:20 +0000
@@ -133,18 +133,7 @@
133 if log_file:133 if log_file:
134 util.ensure_file(log_file)134 util.ensure_file(log_file)
135 if perms:135 if perms:
136 perms_parted = perms.split(':', 1)136 u, g = util.extract_usergroup(perms)
137 u = perms_parted[0]
138 if len(perms_parted) == 2:
139 g = perms_parted[1]
140 else:
141 g = ''
142 u = u.strip()
143 g = g.strip()
144 if u == "-1" or u.lower() == "none":
145 u = None
146 if g == "-1" or g.lower() == "none":
147 g = None
148 try:137 try:
149 util.chownbyname(log_file, u, g)138 util.chownbyname(log_file, u, g)
150 except OSError:139 except OSError:
151140
=== modified file 'cloudinit/user_data.py'
--- cloudinit/user_data.py 2012-06-23 05:04:37 +0000
+++ cloudinit/user_data.py 2012-07-11 20:04:20 +0000
@@ -227,7 +227,7 @@
227 raw_data = ''227 raw_data = ''
228 if not headers:228 if not headers:
229 headers = {}229 headers = {}
230 data = util.decomp_str(raw_data)230 data = util.decomp_gzip(raw_data)
231 if "mime-version:" in data[0:4096].lower():231 if "mime-version:" in data[0:4096].lower():
232 msg = email.message_from_string(data)232 msg = email.message_from_string(data)
233 for (key, val) in headers.iteritems():233 for (key, val) in headers.iteritems():
234234
=== modified file 'cloudinit/util.py'
--- cloudinit/util.py 2012-07-10 21:47:02 +0000
+++ cloudinit/util.py 2012-07-11 20:04:20 +0000
@@ -159,6 +159,10 @@
159 pass159 pass
160160
161161
162class DecompressionError(Exception):
163 pass
164
165
162def ExtendedTemporaryFile(**kwargs):166def ExtendedTemporaryFile(**kwargs):
163 fh = tempfile.NamedTemporaryFile(**kwargs)167 fh = tempfile.NamedTemporaryFile(**kwargs)
164 # Replace its unlink with a quiet version168 # Replace its unlink with a quiet version
@@ -256,13 +260,32 @@
256 return fn260 return fn
257261
258262
259def decomp_str(data):263def decomp_gzip(data, quiet=True):
260 try:264 try:
261 buf = StringIO(str(data))265 buf = StringIO(str(data))
262 with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh:266 with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh:
263 return gh.read()267 return gh.read()
264 except:268 except Exception as e:
265 return data269 if quiet:
270 return data
271 else:
272 raise DecompressionError(str(e))
273
274
275def extract_usergroup(ug_pair):
276 if not ug_pair:
277 return (None, None)
278 ug_parted = ug_pair.split(':', 1)
279 u = ug_parted[0].strip()
280 if len(ug_parted) == 2:
281 g = ug_parted[1].strip()
282 else:
283 g = None
284 if not u or u == "-1" or u.lower() == "none":
285 u = None
286 if not g or g == "-1" or g.lower() == "none":
287 g = None
288 return (u, g)
266289
267290
268def find_modules(root_dir):291def find_modules(root_dir):
269292
=== added file 'doc/examples/cloud-config-write-files.txt'
--- doc/examples/cloud-config-write-files.txt 1970-01-01 00:00:00 +0000
+++ doc/examples/cloud-config-write-files.txt 2012-07-11 20:04:20 +0000
@@ -0,0 +1,37 @@
1#cloud-config
2# vim: syntax=yaml
3#
4# This is the configuration syntax that the write_files module
5# will know how to understand, it can be given b64, b32, b16, or
6# gz (or gz+b64) encoded strings which will be decoded accordingly
7# and then written to the path that is provided.
8#
9# Note: Content strings here are truncated for example purposes.
10#
11write_files:
12- compression: b64
13 content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4...
14 owner: root:root
15 path: /etc/sysconfig/selinux
16 perms: '0644'
17- content: '
18
19 # My new /etc/sysconfig/samba file
20
21 SMBDOPTIONS="-D"
22
23 '
24 path: /etc/sysconfig/samba
25- content: !!binary |
26 f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
27 AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
28 AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
29 ....
30 path: /bin/arch
31 perms: '0555'
32- compression: gzip
33 content: !!binary |
34 H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
35 path: /usr/bin/hello
36 perms: '0755'
37