diff options
Diffstat (limited to 'cloudinit/config/cc_write_files.py')
-rw-r--r-- | cloudinit/config/cc_write_files.py | 273 |
1 files changed, 180 insertions, 93 deletions
diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 8601e707..37dae392 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -10,22 +10,25 @@ import base64 import os from textwrap import dedent -from cloudinit.config.schema import ( - get_schema_doc, validate_cloudconfig_schema) from cloudinit import log as logging -from cloudinit.settings import PER_INSTANCE from cloudinit import util - +from cloudinit.config.schema import ( + MetaSchema, + get_meta_doc, + validate_cloudconfig_schema, +) +from cloudinit.settings import PER_INSTANCE frequency = PER_INSTANCE DEFAULT_OWNER = "root:root" DEFAULT_PERMS = 0o644 -UNKNOWN_ENC = 'text/plain' +DEFAULT_DEFER = False +UNKNOWN_ENC = "text/plain" LOG = logging.getLogger(__name__) -distros = ['all'] +distros = ["all"] # The schema definition for each cloud-config module is a strict contract for # describing supported configuration parameters for each cloud-config section. @@ -34,14 +37,22 @@ distros = ['all'] # configuration. supported_encoding_types = [ - 'gz', 'gzip', 'gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64', 'b64', - 'base64'] + "gz", + "gzip", + "gz+base64", + "gzip+base64", + "gz+b64", + "gzip+b64", + "b64", + "base64", +] -schema = { - 'id': 'cc_write_files', - 'name': 'Write Files', - 'title': 'write arbitrary files', - 'description': dedent("""\ +meta: MetaSchema = { + "id": "cc_write_files", + "name": "Write Files", + "title": "write arbitrary files", + "description": dedent( + """\ Write out arbitrary content to files, optionally setting permissions. Parent folders in the path are created if absent. Content can be specified in plain text or binary. Data encoded with @@ -57,10 +68,12 @@ schema = { Do not write files under /tmp during boot because of a race with systemd-tmpfiles-clean that can cause temp files to get cleaned during the early boot process. Use /run/somedir instead to avoid race - LP:1707222."""), - 'distros': distros, - 'examples': [ - dedent("""\ + LP:1707222.""" + ), + "distros": distros, + "examples": [ + dedent( + """\ # Write out base64 encoded content to /etc/sysconfig/selinux write_files: - encoding: b64 @@ -68,16 +81,20 @@ schema = { owner: root:root path: /etc/sysconfig/selinux permissions: '0644' - """), - dedent("""\ + """ + ), + dedent( + """\ # Appending content to an existing file write_files: - content: | 15 * * * * root ship_logs path: /etc/crontab append: true - """), - dedent("""\ + """ + ), + dedent( + """\ # Provide gziped binary content write_files: - encoding: gzip @@ -85,110 +102,177 @@ schema = { H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA= path: /usr/bin/hello permissions: '0755' - """), - dedent("""\ + """ + ), + dedent( + """\ # Create an empty file on the system write_files: - path: /root/CLOUD_INIT_WAS_HERE - """)], - 'frequency': frequency, - 'type': 'object', - 'properties': { - 'write_files': { - 'type': 'array', - 'items': { - 'type': 'object', - 'properties': { - 'path': { - 'type': 'string', - 'description': dedent("""\ + """ + ), + dedent( + """\ + # Defer writing the file until after the package (Nginx) is + # installed and its user is created alongside + write_files: + - path: /etc/nginx/conf.d/example.com.conf + content: | + server { + server_name example.com; + listen 80; + root /var/www; + location / { + try_files $uri $uri/ $uri.html =404; + } + } + owner: 'nginx:nginx' + permissions: '0640' + defer: true + """ + ), + ], + "frequency": frequency, +} + +schema = { + "type": "object", + "properties": { + "write_files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": dedent( + """\ Path of the file to which ``content`` is decoded and written - """), + """ + ), }, - 'content': { - 'type': 'string', - 'default': '', - 'description': dedent("""\ + "content": { + "type": "string", + "default": "", + "description": dedent( + """\ Optional content to write to the provided ``path``. When content is present and encoding is not '%s', decode the content prior to writing. Default: **''** - """ % UNKNOWN_ENC), + """ + % UNKNOWN_ENC + ), }, - 'owner': { - 'type': 'string', - 'default': DEFAULT_OWNER, - 'description': dedent("""\ + "owner": { + "type": "string", + "default": DEFAULT_OWNER, + "description": dedent( + """\ Optional owner:group to chown on the file. Default: **{owner}** - """.format(owner=DEFAULT_OWNER)), + """.format( + owner=DEFAULT_OWNER + ) + ), }, - 'permissions': { - 'type': 'string', - 'default': oct(DEFAULT_PERMS).replace('o', ''), - 'description': dedent("""\ + "permissions": { + "type": "string", + "default": oct(DEFAULT_PERMS).replace("o", ""), + "description": dedent( + """\ Optional file permissions to set on ``path`` represented as an octal string '0###'. Default: **'{perms}'** - """.format(perms=oct(DEFAULT_PERMS).replace('o', ''))), + """.format( + perms=oct(DEFAULT_PERMS).replace("o", "") + ) + ), }, - 'encoding': { - 'type': 'string', - 'default': UNKNOWN_ENC, - 'enum': supported_encoding_types, - 'description': dedent("""\ + "encoding": { + "type": "string", + "default": UNKNOWN_ENC, + "enum": supported_encoding_types, + "description": dedent( + """\ Optional encoding type of the content. Default is **text/plain** and no content decoding is performed. Supported encoding types are: - %s.""" % ", ".join(supported_encoding_types)), + %s.""" + % ", ".join(supported_encoding_types) + ), }, - 'append': { - 'type': 'boolean', - 'default': False, - 'description': dedent("""\ + "append": { + "type": "boolean", + "default": False, + "description": dedent( + """\ Whether to append ``content`` to existing file if ``path`` exists. Default: **false**. - """), + """ + ), + }, + "defer": { + "type": "boolean", + "default": DEFAULT_DEFER, + "description": dedent( + """\ + Defer writing the file until 'final' stage, after + users were created, and packages were installed. + Default: **{defer}**. + """.format( + defer=DEFAULT_DEFER + ) + ), }, }, - 'required': ['path'], - 'additionalProperties': False + "required": ["path"], + "additionalProperties": False, }, } - } + }, } -__doc__ = get_schema_doc(schema) # Supplement python help() +__doc__ = get_meta_doc(meta, schema) # Supplement python help() def handle(name, cfg, _cloud, log, _args): - files = cfg.get('write_files') - if not files: - log.debug(("Skipping module named %s," - " no/empty 'write_files' key in configuration"), name) - return validate_cloudconfig_schema(cfg, schema) - write_files(name, files) + file_list = cfg.get("write_files", []) + filtered_files = [ + f + for f in file_list + if not util.get_cfg_option_bool(f, "defer", DEFAULT_DEFER) + ] + if not filtered_files: + log.debug( + "Skipping module named %s," + " no/empty 'write_files' key in configuration", + name, + ) + return + write_files(name, filtered_files) def canonicalize_extraction(encoding_type): if not encoding_type: - encoding_type = '' + encoding_type = "" encoding_type = encoding_type.lower().strip() - if encoding_type in ['gz', 'gzip']: - return ['application/x-gzip'] - if encoding_type in ['gz+base64', 'gzip+base64', 'gz+b64', 'gzip+b64']: - return ['application/base64', 'application/x-gzip'] + if encoding_type in ["gz", "gzip"]: + return ["application/x-gzip"] + if encoding_type in ["gz+base64", "gzip+base64", "gz+b64", "gzip+b64"]: + return ["application/base64", "application/x-gzip"] # Yaml already encodes binary data as base64 if it is given to the # yaml file as binary, so those will be automatically decoded for you. # But the above b64 is just for people that are more 'comfortable' # specifing it manually (which might be a possiblity) - if encoding_type in ['b64', 'base64']: - return ['application/base64'] + if encoding_type in ["b64", "base64"]: + return ["application/base64"] if encoding_type: - LOG.warning("Unknown encoding type %s, assuming %s", - encoding_type, UNKNOWN_ENC) + LOG.warning( + "Unknown encoding type %s, assuming %s", encoding_type, UNKNOWN_ENC + ) return [UNKNOWN_ENC] @@ -197,17 +281,20 @@ def write_files(name, files): return for (i, f_info) in enumerate(files): - path = f_info.get('path') + path = f_info.get("path") if not path: - LOG.warning("No path provided to write for entry %s in module %s", - i + 1, name) + LOG.warning( + "No path provided to write for entry %s in module %s", + i + 1, + name, + ) continue path = os.path.abspath(path) - extractions = canonicalize_extraction(f_info.get('encoding')) - contents = extract_contents(f_info.get('content', ''), extractions) - (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER)) - perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS) - omode = 'ab' if util.get_cfg_option_bool(f_info, 'append') else 'wb' + extractions = canonicalize_extraction(f_info.get("encoding")) + contents = extract_contents(f_info.get("content", ""), extractions) + (u, g) = util.extract_usergroup(f_info.get("owner", DEFAULT_OWNER)) + perms = decode_perms(f_info.get("permissions"), DEFAULT_PERMS) + omode = "ab" if util.get_cfg_option_bool(f_info, "append") else "wb" util.write_file(path, contents, omode=omode, mode=perms) util.chownbyname(path, u, g) @@ -229,20 +316,20 @@ def decode_perms(perm, default): reps.append("%o" % r) except TypeError: reps.append("%r" % r) - LOG.warning( - "Undecodable permissions %s, returning default %s", *reps) + LOG.warning("Undecodable permissions %s, returning default %s", *reps) return default def extract_contents(contents, extraction_types): result = contents for t in extraction_types: - if t == 'application/x-gzip': + if t == "application/x-gzip": result = util.decomp_gzip(result, quiet=False, decode=False) - elif t == 'application/base64': + elif t == "application/base64": result = base64.b64decode(result) elif t == UNKNOWN_ENC: pass return result + # vi: ts=4 expandtab |