Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 18.224.32.243
# This file is part of cloud-init. See LICENSE file for license information.
import copy
import os
import re
from errno import EACCES
from typing import Optional, Type
from cloudinit import handlers
from cloudinit import log as logging
from cloudinit.helpers import Paths
from cloudinit.settings import PER_ALWAYS
from cloudinit.templater import (
MISSING_JINJA_PREFIX,
detect_template,
render_string,
)
from cloudinit.util import b64d, json_dumps, load_file, load_json
JUndefinedError: Type[Exception]
try:
from jinja2.exceptions import UndefinedError as JUndefinedError
from jinja2.lexer import operator_re
except ImportError:
# No jinja2 dependency
JUndefinedError = Exception
operator_re = re.compile(r"[-.]")
LOG = logging.getLogger(__name__)
class JinjaLoadError(Exception):
pass
class NotJinjaError(Exception):
pass
class JinjaTemplatePartHandler(handlers.Handler):
prefixes = ["## template: jinja"]
def __init__(self, paths: Paths, **_kwargs):
handlers.Handler.__init__(self, PER_ALWAYS, version=3)
self.paths = paths
self.sub_handlers = {}
for handler in _kwargs.get("sub_handlers", []):
for ctype in handler.list_types():
self.sub_handlers[ctype] = handler
def handle_part(self, data, ctype, filename, payload, frequency, headers):
if ctype in handlers.CONTENT_SIGNALS:
return
jinja_json_file = self.paths.get_runpath("instance_data_sensitive")
rendered_payload = render_jinja_payload_from_file(
payload, filename, jinja_json_file
)
if not rendered_payload:
return
subtype = handlers.type_from_starts_with(rendered_payload)
sub_handler = self.sub_handlers.get(subtype)
if not sub_handler:
LOG.warning(
"Ignoring jinja template for %s. Could not find supported"
" sub-handler for type %s",
filename,
subtype,
)
return
if sub_handler.handler_version == 3:
sub_handler.handle_part(
data, ctype, filename, rendered_payload, frequency, headers
)
elif sub_handler.handler_version == 2:
sub_handler.handle_part(
data, ctype, filename, rendered_payload, frequency
)
def render_jinja_payload_from_file(
payload, payload_fn, instance_data_file, debug=False
):
"""Render a jinja template payload sourcing variables from jinja_vars_path.
@param payload: String of jinja template content. Should begin with
## template: jinja\n.
@param payload_fn: String representing the filename from which the payload
was read used in error reporting. Generally in part-handling this is
'part-##'.
@param instance_data_file: A path to a json file containing variables that
will be used as jinja template variables.
@return: A string of jinja-rendered content with the jinja header removed.
Returns None on error.
"""
if detect_template(payload)[0] != "jinja":
raise NotJinjaError("Payload is not a jinja template")
instance_data = {}
rendered_payload = None
if not os.path.exists(instance_data_file):
raise JinjaLoadError(
"Cannot render jinja template vars. Instance data not yet"
" present at %s" % instance_data_file
)
try:
instance_data = load_json(load_file(instance_data_file))
except Exception as e:
msg = "Loading Jinja instance data failed"
if isinstance(e, (IOError, OSError)):
if e.errno == EACCES:
msg = (
"Cannot render jinja template vars. No read permission on"
" '%s'. Try sudo" % instance_data_file
)
raise JinjaLoadError(msg) from e
rendered_payload = render_jinja_payload(
payload, payload_fn, instance_data, debug
)
if not rendered_payload:
return None
return rendered_payload
def render_jinja_payload(payload, payload_fn, instance_data, debug=False):
instance_jinja_vars = convert_jinja_instance_data(
instance_data,
decode_paths=instance_data.get("base64-encoded-keys", []),
include_key_aliases=True,
)
if debug:
LOG.debug(
"Converted jinja variables\n%s", json_dumps(instance_jinja_vars)
)
try:
rendered_payload = render_string(payload, instance_jinja_vars)
except (TypeError, JUndefinedError) as e:
LOG.warning("Ignoring jinja template for %s: %s", payload_fn, str(e))
return None
warnings = [
"'%s'" % var.replace(MISSING_JINJA_PREFIX, "")
for var in re.findall(
r"%s[^\s]+" % MISSING_JINJA_PREFIX, rendered_payload
)
]
if warnings:
LOG.warning(
"Could not render jinja template variables in file '%s': %s",
payload_fn,
", ".join(warnings),
)
return rendered_payload
def get_jinja_variable_alias(orig_name: str) -> Optional[str]:
"""Return a jinja variable alias, replacing any operators with underscores.
Provide underscore-delimited key aliases to simplify dot-notation
attribute references for keys which contain operators "." or "-".
This provides for simpler short-hand jinja attribute notation
allowing one to avoid quoting keys which contain operators.
{{ ds.v1_0.config.user_network_config }} instead of
{{ ds['v1.0'].config["user.network-config"] }}.
:param orig_name: String representing a jinja variable name to scrub/alias.
:return: A string with any jinja operators replaced if needed. Otherwise,
none if no alias required.
"""
alias_name = re.sub(operator_re, "_", orig_name)
if alias_name != orig_name:
return alias_name
return None
def convert_jinja_instance_data(
data, prefix="", sep="/", decode_paths=(), include_key_aliases=False
):
"""Process instance-data.json dict for use in jinja templates.
Replace hyphens with underscores for jinja templates and decode any
base64_encoded_keys.
"""
result = {}
decode_paths = [path.replace("-", "_") for path in decode_paths]
for key, value in sorted(data.items()):
key_path = "{0}{1}{2}".format(prefix, sep, key) if prefix else key
if key_path in decode_paths:
value = b64d(value)
if isinstance(value, dict):
result[key] = convert_jinja_instance_data(
value,
key_path,
sep=sep,
decode_paths=decode_paths,
include_key_aliases=include_key_aliases,
)
if re.match(r"v\d+$", key):
# Copy values to top-level aliases
for subkey, subvalue in result[key].items():
result[subkey] = copy.deepcopy(subvalue)
else:
result[key] = value
if include_key_aliases:
alias_name = get_jinja_variable_alias(key)
if alias_name:
result[alias_name] = copy.deepcopy(result[key])
return result
|