Blame cloudinit/handlers/jinja_template.py

Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
from errno import EACCES
Packit Service a04d08
import os
Packit Service a04d08
import re
Packit Service a04d08
Packit Service a04d08
try:
Packit Service a04d08
    from jinja2.exceptions import UndefinedError as JUndefinedError
Packit Service a04d08
except ImportError:
Packit Service a04d08
    # No jinja2 dependency
Packit Service a04d08
    JUndefinedError = Exception
Packit Service a04d08
Packit Service a04d08
from cloudinit import handlers
Packit Service a04d08
from cloudinit import log as logging
Packit Service a04d08
from cloudinit.sources import INSTANCE_JSON_FILE
Packit Service a04d08
from cloudinit.templater import render_string, MISSING_JINJA_PREFIX
Packit Service a04d08
from cloudinit.util import b64d, load_file, load_json, json_dumps
Packit Service a04d08
Packit Service a04d08
from cloudinit.settings import PER_ALWAYS
Packit Service a04d08
Packit Service a04d08
LOG = logging.getLogger(__name__)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
class JinjaTemplatePartHandler(handlers.Handler):
Packit Service a04d08
Packit Service a04d08
    prefixes = ['## template: jinja']
Packit Service a04d08
Packit Service a04d08
    def __init__(self, paths, **_kwargs):
Packit Service a04d08
        handlers.Handler.__init__(self, PER_ALWAYS, version=3)
Packit Service a04d08
        self.paths = paths
Packit Service a04d08
        self.sub_handlers = {}
Packit Service a04d08
        for handler in _kwargs.get('sub_handlers', []):
Packit Service a04d08
            for ctype in handler.list_types():
Packit Service a04d08
                self.sub_handlers[ctype] = handler
Packit Service a04d08
Packit Service a04d08
    def handle_part(self, data, ctype, filename, payload, frequency, headers):
Packit Service a04d08
        if ctype in handlers.CONTENT_SIGNALS:
Packit Service a04d08
            return
Packit Service a04d08
        jinja_json_file = os.path.join(self.paths.run_dir, INSTANCE_JSON_FILE)
Packit Service a04d08
        rendered_payload = render_jinja_payload_from_file(
Packit Service a04d08
            payload, filename, jinja_json_file)
Packit Service a04d08
        if not rendered_payload:
Packit Service a04d08
            return
Packit Service a04d08
        subtype = handlers.type_from_starts_with(rendered_payload)
Packit Service a04d08
        sub_handler = self.sub_handlers.get(subtype)
Packit Service a04d08
        if not sub_handler:
Packit Service a04d08
            LOG.warning(
Packit Service a04d08
                'Ignoring jinja template for %s. Could not find supported'
Packit Service a04d08
                ' sub-handler for type %s', filename, subtype)
Packit Service a04d08
            return
Packit Service a04d08
        if sub_handler.handler_version == 3:
Packit Service a04d08
            sub_handler.handle_part(
Packit Service a04d08
                data, ctype, filename, rendered_payload, frequency, headers)
Packit Service a04d08
        elif sub_handler.handler_version == 2:
Packit Service a04d08
            sub_handler.handle_part(
Packit Service a04d08
                data, ctype, filename, rendered_payload, frequency)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def render_jinja_payload_from_file(
Packit Service a04d08
        payload, payload_fn, instance_data_file, debug=False):
Packit Service a04d08
    """Render a jinja template payload sourcing variables from jinja_vars_path.
Packit Service a04d08
Packit Service a04d08
    @param payload: String of jinja template content. Should begin with
Packit Service a04d08
        ## template: jinja\n.
Packit Service a04d08
    @param payload_fn: String representing the filename from which the payload
Packit Service a04d08
        was read used in error reporting. Generally in part-handling this is
Packit Service a04d08
        'part-##'.
Packit Service a04d08
    @param instance_data_file: A path to a json file containing variables that
Packit Service a04d08
        will be used as jinja template variables.
Packit Service a04d08
Packit Service a04d08
    @return: A string of jinja-rendered content with the jinja header removed.
Packit Service a04d08
        Returns None on error.
Packit Service a04d08
    """
Packit Service a04d08
    instance_data = {}
Packit Service a04d08
    rendered_payload = None
Packit Service a04d08
    if not os.path.exists(instance_data_file):
Packit Service a04d08
        raise RuntimeError(
Packit Service a04d08
            'Cannot render jinja template vars. Instance data not yet'
Packit Service a04d08
            ' present at %s' % instance_data_file)
Packit Service a04d08
    try:
Packit Service a04d08
        instance_data = load_json(load_file(instance_data_file))
Packit Service a04d08
    except (IOError, OSError) as e:
Packit Service a04d08
        if e.errno == EACCES:
Packit Service a04d08
            raise RuntimeError(
Packit Service a04d08
                'Cannot render jinja template vars. No read permission on'
Packit Service 751c4a
                " '%s'. Try sudo" % instance_data_file
Packit Service 751c4a
            ) from e
Packit Service a04d08
Packit Service a04d08
    rendered_payload = render_jinja_payload(
Packit Service a04d08
        payload, payload_fn, instance_data, debug)
Packit Service a04d08
    if not rendered_payload:
Packit Service a04d08
        return None
Packit Service a04d08
    return rendered_payload
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def render_jinja_payload(payload, payload_fn, instance_data, debug=False):
Packit Service a04d08
    instance_jinja_vars = convert_jinja_instance_data(
Packit Service a04d08
        instance_data,
Packit Service a04d08
        decode_paths=instance_data.get('base64-encoded-keys', []))
Packit Service a04d08
    if debug:
Packit Service a04d08
        LOG.debug('Converted jinja variables\n%s',
Packit Service a04d08
                  json_dumps(instance_jinja_vars))
Packit Service a04d08
    try:
Packit Service a04d08
        rendered_payload = render_string(payload, instance_jinja_vars)
Packit Service a04d08
    except (TypeError, JUndefinedError) as e:
Packit Service a04d08
        LOG.warning(
Packit Service a04d08
            'Ignoring jinja template for %s: %s', payload_fn, str(e))
Packit Service a04d08
        return None
Packit Service a04d08
    warnings = [
Packit Service a04d08
        "'%s'" % var.replace(MISSING_JINJA_PREFIX, '')
Packit Service a04d08
        for var in re.findall(
Packit Service a04d08
            r'%s[^\s]+' % MISSING_JINJA_PREFIX, rendered_payload)]
Packit Service a04d08
    if warnings:
Packit Service a04d08
        LOG.warning(
Packit Service a04d08
            "Could not render jinja template variables in file '%s': %s",
Packit Service a04d08
            payload_fn, ', '.join(warnings))
Packit Service a04d08
    return rendered_payload
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def convert_jinja_instance_data(data, prefix='', sep='/', decode_paths=()):
Packit Service a04d08
    """Process instance-data.json dict for use in jinja templates.
Packit Service a04d08
Packit Service a04d08
    Replace hyphens with underscores for jinja templates and decode any
Packit Service a04d08
    base64_encoded_keys.
Packit Service a04d08
    """
Packit Service a04d08
    result = {}
Packit Service a04d08
    decode_paths = [path.replace('-', '_') for path in decode_paths]
Packit Service a04d08
    for key, value in sorted(data.items()):
Packit Service a04d08
        if '-' in key:
Packit Service a04d08
            # Standardize keys for use in #cloud-config/shell templates
Packit Service a04d08
            key = key.replace('-', '_')
Packit Service a04d08
        key_path = '{0}{1}{2}'.format(prefix, sep, key) if prefix else key
Packit Service a04d08
        if key_path in decode_paths:
Packit Service a04d08
            value = b64d(value)
Packit Service a04d08
        if isinstance(value, dict):
Packit Service a04d08
            result[key] = convert_jinja_instance_data(
Packit Service a04d08
                value, key_path, sep=sep, decode_paths=decode_paths)
Packit Service a04d08
            if re.match(r'v\d+', key):
Packit Service a04d08
                # Copy values to top-level aliases
Packit Service a04d08
                for subkey, subvalue in result[key].items():
Packit Service a04d08
                    result[subkey] = subvalue
Packit Service a04d08
        else:
Packit Service a04d08
            result[key] = value
Packit Service a04d08
    return result
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab