1# Copyright (C) 2012 Yahoo! Inc. 2# 3# Author: Joshua Harlow <harlowja@yahoo-inc.com> 4# 5# This file is part of cloud-init. See LICENSE file for license information. 6 7"""Write Files: write arbitrary files""" 8 9import base64 10import os 11from textwrap import dedent 12 13from cloudinit.config.schema import ( 14 get_schema_doc, validate_cloudconfig_schema) 15from cloudinit import log as logging 16from cloudinit.settings import PER_INSTANCE 17from cloudinit import util 18 19 20frequency = PER_INSTANCE 21 22DEFAULT_OWNER = "root:root" 23DEFAULT_PERMS = 0o644 24DEFAULT_DEFER = False 25UNKNOWN_ENC = 'text/plain' 26 27LOG = logging.getLogger(__name__) 28 29distros = ['all'] 30 31# The schema definition for each cloud-config module is a strict contract for 32# describing supported configuration parameters for each cloud-config section. 33# It allows cloud-config to validate and alert users to invalid or ignored 34# configuration options before actually attempting to deploy with said 35# configuration. 36 37supported_encoding_types = [ 38 'gz', 'gzip', 'gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64', 'b64', 39 'base64'] 40 41schema = { 42 'id': 'cc_write_files', 43 'name': 'Write Files', 44 'title': 'write arbitrary files', 45 'description': dedent("""\ 46 Write out arbitrary content to files, optionally setting permissions. 47 Parent folders in the path are created if absent. 48 Content can be specified in plain text or binary. Data encoded with 49 either base64 or binary gzip data can be specified and will be decoded 50 before being written. For empty file creation, content can be omitted. 51 52 .. note:: 53 if multiline data is provided, care should be taken to ensure that it 54 follows yaml formatting standards. to specify binary data, use the yaml 55 option ``!!binary`` 56 57 .. note:: 58 Do not write files under /tmp during boot because of a race with 59 systemd-tmpfiles-clean that can cause temp files to get cleaned during 60 the early boot process. Use /run/somedir instead to avoid race 61 LP:1707222."""), 62 'distros': distros, 63 'examples': [ 64 dedent("""\ 65 # Write out base64 encoded content to /etc/sysconfig/selinux 66 write_files: 67 - encoding: b64 68 content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4... 69 owner: root:root 70 path: /etc/sysconfig/selinux 71 permissions: '0644' 72 """), 73 dedent("""\ 74 # Appending content to an existing file 75 write_files: 76 - content: | 77 15 * * * * root ship_logs 78 path: /etc/crontab 79 append: true 80 """), 81 dedent("""\ 82 # Provide gziped binary content 83 write_files: 84 - encoding: gzip 85 content: !!binary | 86 H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA= 87 path: /usr/bin/hello 88 permissions: '0755' 89 """), 90 dedent("""\ 91 # Create an empty file on the system 92 write_files: 93 - path: /root/CLOUD_INIT_WAS_HERE 94 """), 95 dedent("""\ 96 # Defer writing the file until after the package (Nginx) is 97 # installed and its user is created alongside 98 write_files: 99 - path: /etc/nginx/conf.d/example.com.conf 100 content: | 101 server { 102 server_name example.com; 103 listen 80; 104 root /var/www; 105 location / { 106 try_files $uri $uri/ $uri.html =404; 107 } 108 } 109 owner: 'nginx:nginx' 110 permissions: '0640' 111 defer: true 112 """)], 113 'frequency': frequency, 114 'type': 'object', 115 'properties': { 116 'write_files': { 117 'type': 'array', 118 'items': { 119 'type': 'object', 120 'properties': { 121 'path': { 122 'type': 'string', 123 'description': dedent("""\ 124 Path of the file to which ``content`` is decoded 125 and written 126 """), 127 }, 128 'content': { 129 'type': 'string', 130 'default': '', 131 'description': dedent("""\ 132 Optional content to write to the provided ``path``. 133 When content is present and encoding is not '%s', 134 decode the content prior to writing. Default: 135 **''** 136 """ % UNKNOWN_ENC), 137 }, 138 'owner': { 139 'type': 'string', 140 'default': DEFAULT_OWNER, 141 'description': dedent("""\ 142 Optional owner:group to chown on the file. Default: 143 **{owner}** 144 """.format(owner=DEFAULT_OWNER)), 145 }, 146 'permissions': { 147 'type': 'string', 148 'default': oct(DEFAULT_PERMS).replace('o', ''), 149 'description': dedent("""\ 150 Optional file permissions to set on ``path`` 151 represented as an octal string '0###'. Default: 152 **'{perms}'** 153 """.format(perms=oct(DEFAULT_PERMS).replace('o', ''))), 154 }, 155 'encoding': { 156 'type': 'string', 157 'default': UNKNOWN_ENC, 158 'enum': supported_encoding_types, 159 'description': dedent("""\ 160 Optional encoding type of the content. Default is 161 **text/plain** and no content decoding is 162 performed. Supported encoding types are: 163 %s.""" % ", ".join(supported_encoding_types)), 164 }, 165 'append': { 166 'type': 'boolean', 167 'default': False, 168 'description': dedent("""\ 169 Whether to append ``content`` to existing file if 170 ``path`` exists. Default: **false**. 171 """), 172 }, 173 'defer': { 174 'type': 'boolean', 175 'default': DEFAULT_DEFER, 176 'description': dedent("""\ 177 Defer writing the file until 'final' stage, after 178 users were created, and packages were installed. 179 Default: **{defer}**. 180 """.format(defer=DEFAULT_DEFER)), 181 }, 182 }, 183 'required': ['path'], 184 'additionalProperties': False 185 }, 186 } 187 } 188} 189 190__doc__ = get_schema_doc(schema) # Supplement python help() 191 192 193def handle(name, cfg, _cloud, log, _args): 194 validate_cloudconfig_schema(cfg, schema) 195 file_list = cfg.get('write_files', []) 196 filtered_files = [ 197 f for f in file_list if not util.get_cfg_option_bool(f, 198 'defer', 199 DEFAULT_DEFER) 200 ] 201 if not filtered_files: 202 log.debug(("Skipping module named %s," 203 " no/empty 'write_files' key in configuration"), name) 204 return 205 write_files(name, filtered_files) 206 207 208def canonicalize_extraction(encoding_type): 209 if not encoding_type: 210 encoding_type = '' 211 encoding_type = encoding_type.lower().strip() 212 if encoding_type in ['gz', 'gzip']: 213 return ['application/x-gzip'] 214 if encoding_type in ['gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64']: 215 return ['application/base64', 'application/x-gzip'] 216 # Yaml already encodes binary data as base64 if it is given to the 217 # yaml file as binary, so those will be automatically decoded for you. 218 # But the above b64 is just for people that are more 'comfortable' 219 # specifing it manually (which might be a possiblity) 220 if encoding_type in ['b64', 'base64']: 221 return ['application/base64'] 222 if encoding_type: 223 LOG.warning("Unknown encoding type %s, assuming %s", 224 encoding_type, UNKNOWN_ENC) 225 return [UNKNOWN_ENC] 226 227 228def write_files(name, files): 229 if not files: 230 return 231 232 for (i, f_info) in enumerate(files): 233 path = f_info.get('path') 234 if not path: 235 LOG.warning("No path provided to write for entry %s in module %s", 236 i + 1, name) 237 continue 238 path = os.path.abspath(path) 239 extractions = canonicalize_extraction(f_info.get('encoding')) 240 contents = extract_contents(f_info.get('content', ''), extractions) 241 (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER)) 242 perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS) 243 omode = 'ab' if util.get_cfg_option_bool(f_info, 'append') else 'wb' 244 util.write_file(path, contents, omode=omode, mode=perms) 245 util.chownbyname(path, u, g) 246 247 248def decode_perms(perm, default): 249 if perm is None: 250 return default 251 try: 252 if isinstance(perm, (int, float)): 253 # Just 'downcast' it (if a float) 254 return int(perm) 255 else: 256 # Force to string and try octal conversion 257 return int(str(perm), 8) 258 except (TypeError, ValueError): 259 reps = [] 260 for r in (perm, default): 261 try: 262 reps.append("%o" % r) 263 except TypeError: 264 reps.append("%r" % r) 265 LOG.warning( 266 "Undecodable permissions %s, returning default %s", *reps) 267 return default 268 269 270def extract_contents(contents, extraction_types): 271 result = contents 272 for t in extraction_types: 273 if t == 'application/x-gzip': 274 result = util.decomp_gzip(result, quiet=False, decode=False) 275 elif t == 'application/base64': 276 result = base64.b64decode(result) 277 elif t == UNKNOWN_ENC: 278 pass 279 return result 280 281# vi: ts=4 expandtab 282