PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/jinja.py |
""" Jinja loading utils to enable a more powerful backend for jinja templates """ import itertools import logging import os.path import pprint import re import shlex import time import uuid import warnings from collections.abc import Hashable from functools import wraps from xml.dom import minidom from xml.etree.ElementTree import Element, SubElement, tostring import jinja2 from jinja2 import BaseLoader, TemplateNotFound, nodes from jinja2.environment import TemplateModule from jinja2.exceptions import TemplateRuntimeError from jinja2.ext import Extension import salt.fileclient import salt.utils.data import salt.utils.files import salt.utils.json import salt.utils.stringutils import salt.utils.url import salt.utils.yaml from salt.exceptions import TemplateError from salt.utils.decorators.jinja import jinja_filter, jinja_global, jinja_test from salt.utils.odict import OrderedDict from salt.utils.versions import Version try: from markupsafe import Markup except ImportError: # jinja < 3.1 from jinja2 import Markup # pylint: disable=no-name-in-module log = logging.getLogger(__name__) __all__ = ["SaltCacheLoader", "SerializerExtension"] GLOBAL_UUID = uuid.UUID("91633EBF-1C86-5E33-935A-28061F4B480E") JINJA_VERSION = Version(jinja2.__version__) class SaltCacheLoader(BaseLoader): """ A special jinja Template Loader for salt. Requested templates are always fetched from the server to guarantee that the file is up to date. Templates are cached like regular salt states and only loaded once per loader instance. """ def __init__( self, opts, saltenv="base", encoding="utf-8", pillar_rend=False, _file_client=None, ): self.opts = opts self.saltenv = saltenv self.encoding = encoding self.pillar_rend = pillar_rend if self.pillar_rend: if saltenv not in self.opts["pillar_roots"]: self.searchpath = [] else: self.searchpath = opts["pillar_roots"][saltenv] else: self.searchpath = [os.path.join(opts["cachedir"], "files", saltenv)] log.debug("Jinja search path: %s", self.searchpath) self.cached = [] self._file_client = _file_client self._close_file_client = _file_client is None def file_client(self): """ Return a file client. Instantiates on first call. """ # If there was no file_client passed to the class, create a cache_client # and use that. This avoids opening a new file_client every time this # class is instantiated if ( self._file_client is None or not hasattr(self._file_client, "opts") or self._file_client.opts["file_roots"] != self.opts["file_roots"] ): self._file_client = salt.fileclient.get_file_client( self.opts, self.pillar_rend ) self._close_file_client = True return self._file_client def cache_file(self, template): """ Cache a file from the salt master """ saltpath = salt.utils.url.create(template) fcl = self.file_client() return fcl.get_file(saltpath, "", True, self.saltenv) def check_cache(self, template): """ Cache a file only once """ if template not in self.cached: ret = self.cache_file(template) if ret is not False: self.cached.append(template) def get_source(self, environment, template): """ Salt-specific loader to find imported jinja files. Jinja imports will be interpreted as originating from the top of each of the directories in the searchpath when the template name does not begin with './' or '../'. When a template name begins with './' or '../' then the import will be relative to the importing file. """ # FIXME: somewhere do separator replacement: '\\' => '/' _template = template if template.split("/", 1)[0] in ("..", "."): is_relative = True else: is_relative = False # checks for relative '..' paths that step-out of file_roots if is_relative: # Starts with a relative path indicator if not environment or "tpldir" not in environment.globals: log.warning( 'Relative path "%s" cannot be resolved without an environment', template, ) raise TemplateNotFound(template) base_path = environment.globals["tpldir"] _template = os.path.normpath("/".join((base_path, _template))) if _template.split("/", 1)[0] == "..": log.warning( 'Discarded template path "%s": attempts to' " ascend outside of salt://", template, ) raise TemplateNotFound(template) # local file clients should pass the dot-expanded relative path # when it's an absolute local filesystem location if environment.globals.get("opts", {}).get( "file_client" ) == "local" and os.path.isabs(base_path): _template = os.path.relpath(_template, base_path) self.check_cache(_template) if environment and template: tpldir = os.path.dirname(_template).replace("\\", "/") tplfile = _template if is_relative: tpldir = environment.globals.get("tpldir", tpldir) tplfile = template tpldata = { "tplfile": tplfile, "tpldir": "." if tpldir == "" else tpldir, "tpldot": tpldir.replace("/", "."), } environment.globals.update(tpldata) if _template in self.cached or os.path.exists(_template): # pylint: disable=cell-var-from-loop for spath in self.searchpath: filepath = os.path.join(spath, _template) try: with salt.utils.files.fopen(filepath, "rb") as ifile: contents = ifile.read().decode(self.encoding) mtime = os.path.getmtime(filepath) def uptodate(): try: return os.path.getmtime(filepath) == mtime except OSError: return False return contents, filepath, uptodate except OSError: # there is no file under current path continue # pylint: enable=cell-var-from-loop # there is no template file within searchpaths raise TemplateNotFound(template) def destroy(self): if self._close_file_client is False: return if self._file_client is None: return file_client = self._file_client self._file_client = None try: file_client.destroy() except AttributeError: # PillarClient and LocalClient objects do not have a destroy method pass def __enter__(self): self.file_client() return self def __exit__(self, *args): self.destroy() class PrintableDict(OrderedDict): """ Ensures that dict str() and repr() are YAML friendly. .. code-block:: python mapping = OrderedDict([('a', 'b'), ('c', None)]) print mapping # OrderedDict([('a', 'b'), ('c', None)]) decorated = PrintableDict(mapping) print decorated # {'a': 'b', 'c': None} """ def __str__(self): output = [] for key, value in self.items(): if isinstance(value, str): # keeps quotes around strings output.append(f"{key!r}: {value!r}") else: # let default output output.append(f"{key!r}: {value!s}") return "{" + ", ".join(output) + "}" def __repr__(self): # pylint: disable=W0221 output = [] for key, value in self.items(): # Raw string formatter required here because this is a repr # function. output.append(f"{key!r}: {value!r}") return "{" + ", ".join(output) + "}" # Additional globals @jinja_global("raise") def jinja_raise(msg): raise TemplateError(msg) # Additional tests @jinja_test("match") def test_match(txt, rgx, ignorecase=False, multiline=False): """Returns true if a sequence of chars matches a pattern.""" flag = 0 if ignorecase: flag |= re.I if multiline: flag |= re.M compiled_rgx = re.compile(rgx, flag) return True if compiled_rgx.match(txt) else False @jinja_test("equalto") def test_equalto(value, other): """Returns true if two values are equal.""" return value == other # Additional filters @jinja_filter("skip") def skip_filter(data): """ Suppress data output .. code-block:: yaml {% my_string = "foo" %} {{ my_string|skip }} will be rendered as empty string, """ return "" @jinja_filter("sequence") def ensure_sequence_filter(data): """ Ensure sequenced data. **sequence** ensure that parsed data is a sequence .. code-block:: jinja {% set my_string = "foo" %} {% set my_list = ["bar", ] %} {% set my_dict = {"baz": "qux"} %} {{ my_string|sequence|first }} {{ my_list|sequence|first }} {{ my_dict|sequence|first }} will be rendered as: .. code-block:: yaml foo bar baz """ if not isinstance(data, (list, tuple, set, dict)): return [data] return data @jinja_filter("to_bool") def to_bool(val): """ Returns the logical value. .. code-block:: jinja {{ 'yes' | to_bool }} will be rendered as: .. code-block:: text True """ if val is None: return False if isinstance(val, bool): return val if isinstance(val, (str, (str,))): return val.lower() in ("yes", "1", "true") if isinstance(val, int): return val > 0 if not isinstance(val, Hashable): return len(val) > 0 return False @jinja_filter("indent") def indent(s, width=4, first=False, blank=False, indentfirst=None): """ A ported version of the "indent" filter containing a fix for indenting Markup objects. If the minion has Jinja version 2.11 or newer, the "indent" filter from upstream will be used, and this one will be ignored. """ if indentfirst is not None: warnings.warn( "The 'indentfirst' argument is renamed to 'first' and will" " be removed in Jinja 3.0.", DeprecationWarning, stacklevel=2, ) first = indentfirst indention = " " * width newline = "\n" if isinstance(s, Markup): indention = Markup(indention) newline = Markup(newline) s += newline # this quirk is necessary for splitlines method if blank: rv = (newline + indention).join(s.splitlines()) else: lines = s.splitlines() rv = lines.pop(0) if lines: rv += newline + newline.join( indention + line if line else line for line in lines ) if first: rv = indention + rv return rv @jinja_filter("tojson") def tojson(val, indent=None, **options): """ Implementation of tojson filter (only present in Jinja 2.9 and later). Unlike the Jinja built-in filter, this allows arbitrary options to be passed in to the underlying JSON library. """ options.setdefault("ensure_ascii", True) if indent is not None: options["indent"] = indent return ( salt.utils.json.dumps(val, **options) .replace("<", "\\u003c") .replace(">", "\\u003e") .replace("&", "\\u0026") .replace("'", "\\u0027") ) @jinja_filter("quote") def quote(txt): """ Wraps a text around quotes. .. code-block:: jinja {% set my_text = 'my_text' %} {{ my_text | quote }} will be rendered as: .. code-block:: text 'my_text' """ return shlex.quote(txt) @jinja_filter() def regex_escape(value): return re.escape(value) @jinja_filter("regex_search") def regex_search(txt, rgx, ignorecase=False, multiline=False): """ Searches for a pattern in the text. .. code-block:: jinja {% set my_text = 'abcd' %} {{ my_text | regex_search('^(.*)BC(.*)$', ignorecase=True) }} will be rendered as: .. code-block:: text ('a', 'd') """ flag = 0 if ignorecase: flag |= re.I if multiline: flag |= re.M obj = re.search(rgx, txt, flag) if not obj: return return obj.groups() @jinja_filter("regex_match") def regex_match(txt, rgx, ignorecase=False, multiline=False): """ Searches for a pattern in the text. .. code-block:: jinja {% set my_text = 'abcd' %} {{ my_text | regex_match('^(.*)BC(.*)$', ignorecase=True) }} will be rendered as: .. code-block:: text ('a', 'd') """ flag = 0 if ignorecase: flag |= re.I if multiline: flag |= re.M obj = re.match(rgx, txt, flag) if not obj: return return obj.groups() @jinja_filter("regex_replace") def regex_replace(txt, rgx, val, ignorecase=False, multiline=False): r""" Searches for a pattern and replaces with a sequence of characters. .. code-block:: jinja {% set my_text = 'lets replace spaces' %} {{ my_text | regex_replace('\s+', '__') }} will be rendered as: .. code-block:: text lets__replace__spaces """ flag = 0 if ignorecase: flag |= re.I if multiline: flag |= re.M compiled_rgx = re.compile(rgx, flag) return compiled_rgx.sub(val, txt) @jinja_filter("uuid") def uuid_(val): """ Returns a UUID corresponding to the value passed as argument. .. code-block:: jinja {{ 'example' | uuid }} will be rendered as: .. code-block:: text f4efeff8-c219-578a-bad7-3dc280612ec8 """ return str(uuid.uuid5(GLOBAL_UUID, salt.utils.stringutils.to_str(val))) ### List-related filters @jinja_filter() def unique(values): """ Removes duplicates from a list. .. code-block:: jinja {% set my_list = ['a', 'b', 'c', 'a', 'b'] -%} {{ my_list | unique }} will be rendered as: .. code-block:: text ['a', 'b', 'c'] """ ret = None if isinstance(values, Hashable): ret = set(values) else: ret = [] for value in values: if value not in ret: ret.append(value) return ret @jinja_filter("min") def lst_min(obj): """ Returns the min value. .. code-block:: jinja {% set my_list = [1,2,3,4] -%} {{ my_list | min }} will be rendered as: .. code-block:: text 1 """ return min(obj) @jinja_filter("max") def lst_max(obj): """ Returns the max value. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | max }} will be rendered as: .. code-block:: text 4 """ return max(obj) @jinja_filter("avg") def lst_avg(lst): """ Returns the average value of a list. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | avg }} will be rendered as: .. code-block:: yaml 2.5 """ if not isinstance(lst, Hashable): return float(sum(lst) / len(lst)) return float(lst) @jinja_filter("union") def union(lst1, lst2): """ Returns the union of two lists. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | union([2, 4, 6]) }} will be rendered as: .. code-block:: text [1, 2, 3, 4, 6] """ if isinstance(lst1, Hashable) and isinstance(lst2, Hashable): return set(lst1) | set(lst2) return unique(lst1 + lst2) @jinja_filter("intersect") def intersect(lst1, lst2): """ Returns the intersection of two lists. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | intersect([2, 4, 6]) }} will be rendered as: .. code-block:: text [2, 4] """ if isinstance(lst1, Hashable) and isinstance(lst2, Hashable): return set(lst1) & set(lst2) return unique([ele for ele in lst1 if ele in lst2]) @jinja_filter("difference") def difference(lst1, lst2): """ Returns the difference of two lists. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | difference([2, 4, 6]) }} will be rendered as: .. code-block:: text [1, 3, 6] """ if isinstance(lst1, Hashable) and isinstance(lst2, Hashable): return set(lst1) - set(lst2) return unique([ele for ele in lst1 if ele not in lst2]) @jinja_filter("symmetric_difference") def symmetric_difference(lst1, lst2): """ Returns the symmetric difference of two lists. .. code-block:: jinja {% my_list = [1,2,3,4] -%} {{ set my_list | symmetric_difference([2, 4, 6]) }} will be rendered as: .. code-block:: text [1, 3] """ if isinstance(lst1, Hashable) and isinstance(lst2, Hashable): return set(lst1) ^ set(lst2) return unique( [ele for ele in union(lst1, lst2) if ele not in intersect(lst1, lst2)] ) @jinja_filter("method_call") def method_call(obj, f_name, *f_args, **f_kwargs): return getattr(obj, f_name, lambda *args, **kwargs: None)(*f_args, **f_kwargs) try: pass_context = jinja2.pass_context except AttributeError: # Old and deprecated method pass_context = jinja2.contextfunction @pass_context def show_full_context(ctx): return salt.utils.data.simple_types_filter( {key: value for key, value in ctx.items()} ) class SerializerExtension(Extension): ''' Yaml and Json manipulation. **Format filters** Allows jsonifying or yamlifying any data structure. For example, this dataset: .. code-block:: python data = { 'foo': True, 'bar': 42, 'baz': [1, 2, 3], 'qux': 2.0 } .. code-block:: jinja yaml = {{ data|yaml }} json = {{ data|json }} python = {{ data|python }} xml = {{ {'root_node': data}|xml }} will be rendered as:: yaml = {bar: 42, baz: [1, 2, 3], foo: true, qux: 2.0} json = {"baz": [1, 2, 3], "foo": true, "bar": 42, "qux": 2.0} python = {'bar': 42, 'baz': [1, 2, 3], 'foo': True, 'qux': 2.0} xml = """<<?xml version="1.0" ?> <root_node bar="42" foo="True" qux="2.0"> <baz>1</baz> <baz>2</baz> <baz>3</baz> </root_node>""" The yaml filter takes an optional flow_style parameter to control the default-flow-style parameter of the YAML dumper. .. code-block:: jinja {{ data|yaml(False) }} will be rendered as: .. code-block:: yaml bar: 42 baz: - 1 - 2 - 3 foo: true qux: 2.0 **Load filters** Strings and variables can be deserialized with **load_yaml** and **load_json** tags and filters. It allows one to manipulate data directly in templates, easily: .. code-block:: jinja {%- set yaml_src = "{foo: it works}"|load_yaml %} {%- set json_src = '{"bar": "for real"}'|load_json %} Dude, {{ yaml_src.foo }} {{ json_src.bar }}! will be rendered as:: Dude, it works for real! **Load tags** Salt implements ``load_yaml`` and ``load_json`` tags. They work like the `import tag`_, except that the document is also deserialized. Syntaxes are ``{% load_yaml as [VARIABLE] %}[YOUR DATA]{% endload %}`` and ``{% load_json as [VARIABLE] %}[YOUR DATA]{% endload %}`` For example: .. code-block:: jinja {% load_yaml as yaml_src %} foo: it works {% endload %} {% load_json as json_src %} { "bar": "for real" } {% endload %} Dude, {{ yaml_src.foo }} {{ json_src.bar }}! will be rendered as:: Dude, it works for real! **Import tags** External files can be imported and made available as a Jinja variable. .. code-block:: jinja {% import_yaml "myfile.yml" as myfile %} {% import_json "defaults.json" as defaults %} {% import_text "completeworksofshakespeare.txt" as poems %} **Catalog** ``import_*`` and ``load_*`` tags will automatically expose their target variable to import. This feature makes catalog of data to handle. for example: .. code-block:: jinja # doc1.sls {% load_yaml as var1 %} foo: it works {% endload %} {% load_yaml as var2 %} bar: for real {% endload %} .. code-block:: jinja # doc2.sls {% from "doc1.sls" import var1, var2 as local2 %} {{ var1.foo }} {{ local2.bar }} ** Escape Filters ** .. versionadded:: 2017.7.0 Allows escaping of strings so they can be interpreted literally by another function. For example: .. code-block:: jinja regex_escape = {{ 'https://example.com?foo=bar%20baz' | regex_escape }} will be rendered as:: regex_escape = https\\:\\/\\/example\\.com\\?foo\\=bar\\%20baz ** Set Theory Filters ** .. versionadded:: 2017.7.0 Performs set math using Jinja filters. For example: .. code-block:: jinja unique = {{ ['foo', 'foo', 'bar'] | unique }} will be rendered as:: unique = ['foo', 'bar'] ** Salt State Parameter Format Filters ** .. versionadded:: 3005 Renders a formatted multi-line YAML string from a Python dictionary. Each key/value pair in the dictionary will be added as a single-key dictionary to a list that will then be sent to the YAML formatter. For example: .. code-block:: jinja {% set thing_params = { "name": "thing", "changes": True, "warnings": "OMG! Stuff is happening!" } %} thing: test.configurable_test_state: {{ thing_params | dict_to_sls_yaml_params | indent }} will be rendered as:: .. code-block:: yaml thing: test.configurable_test_state: - name: thing - changes: true - warnings: OMG! Stuff is happening! .. _`import tag`: https://jinja.palletsprojects.com/en/2.11.x/templates/#import ''' tags = { "load_yaml", "load_json", "import_yaml", "import_json", "load_text", "import_text", "profile", } def __init__(self, environment): super().__init__(environment) self.environment.filters.update( { "yaml": self.format_yaml, "json": self.format_json, "xml": self.format_xml, "python": self.format_python, "load_yaml": self.load_yaml, "load_json": self.load_json, "load_text": self.load_text, "dict_to_sls_yaml_params": self.dict_to_sls_yaml_params, "combinations": itertools.combinations, "combinations_with_replacement": itertools.combinations_with_replacement, "compress": itertools.compress, "permutations": itertools.permutations, "product": itertools.product, "zip": zip, "zip_longest": itertools.zip_longest, } ) if self.environment.finalize is None: self.environment.finalize = self.finalizer else: finalizer = self.environment.finalize @wraps(finalizer) def wrapper(self, data): return finalizer(self.finalizer(data)) self.environment.finalize = wrapper def finalizer(self, data): """ Ensure that printed mappings are YAML friendly. """ def explore(data): if isinstance(data, (dict, OrderedDict)): return PrintableDict( [(key, explore(value)) for key, value in data.items()] ) elif isinstance(data, (list, tuple, set)): return data.__class__([explore(value) for value in data]) return data return explore(data) def format_json(self, value, sort_keys=True, indent=None): json_txt = salt.utils.json.dumps( value, sort_keys=sort_keys, indent=indent ).strip() try: return Markup(json_txt) except UnicodeDecodeError: return Markup(salt.utils.stringutils.to_unicode(json_txt)) def format_yaml(self, value, flow_style=True): yaml_txt = salt.utils.yaml.safe_dump( value, default_flow_style=flow_style ).strip() if yaml_txt.endswith("\n..."): yaml_txt = yaml_txt[: len(yaml_txt) - 4] try: return Markup(yaml_txt) except UnicodeDecodeError: return Markup(salt.utils.stringutils.to_unicode(yaml_txt)) def format_xml(self, value): """Render a formatted multi-line XML string from a complex Python data structure. Supports tag attributes and nested dicts/lists. :param value: Complex data structure representing XML contents :returns: Formatted XML string rendered with newlines and indentation :rtype: str """ def normalize_iter(value): if isinstance(value, (list, tuple)): if isinstance(value[0], str): xmlval = value else: xmlval = [] elif isinstance(value, dict): xmlval = list(value.items()) else: raise TemplateRuntimeError( "Value is not a dict or list. Cannot render as XML" ) return xmlval def recurse_tree(xmliter, element=None): sub = None for tag, attrs in xmliter: if isinstance(attrs, list): for attr in attrs: recurse_tree(((tag, attr),), element) elif element is not None: sub = SubElement(element, tag) else: sub = Element(tag) if isinstance(attrs, (str, int, bool, float)): sub.text = str(attrs) continue if isinstance(attrs, dict): sub.attrib = { attr: str(val) for attr, val in attrs.items() if not isinstance(val, (dict, list)) } for tag, val in [ item for item in normalize_iter(attrs) if isinstance(item[1], (dict, list)) ]: recurse_tree(((tag, val),), sub) return sub return Markup( minidom.parseString( tostring(recurse_tree(normalize_iter(value))) ).toprettyxml(indent=" ") ) def format_python(self, value): return Markup(pprint.pformat(value).strip()) def load_yaml(self, value): if isinstance(value, TemplateModule): value = str(value) try: return salt.utils.data.decode(salt.utils.yaml.safe_load(value)) except salt.utils.yaml.YAMLError as exc: msg = "Encountered error loading yaml: " try: # Reported line is off by one, add 1 to correct it line = exc.problem_mark.line + 1 buf = exc.problem_mark.buffer problem = exc.problem except AttributeError: # No context information available in the exception, fall back # to the stringified version of the exception. msg += str(exc) else: msg += f"{problem}\n" msg += salt.utils.stringutils.get_context( buf, line, marker=" <======================" ) raise TemplateRuntimeError(msg) except AttributeError: raise TemplateRuntimeError(f"Unable to load yaml from {value}") def load_json(self, value): if isinstance(value, TemplateModule): value = str(value) try: return salt.utils.json.loads(value) except (ValueError, TypeError, AttributeError): raise TemplateRuntimeError(f"Unable to load json from {value}") def load_text(self, value): if isinstance(value, TemplateModule): value = str(value) return value _load_parsers = {"load_yaml", "load_json", "load_text"} _import_parsers = {"import_yaml", "import_json", "import_text"} def parse(self, parser): if parser.stream.current.value in self._load_parsers: return self.parse_load(parser) elif parser.stream.current.value in self._import_parsers: return self.parse_import( parser, parser.stream.current.value.split("_", 1)[1] ) elif parser.stream.current.value == "profile": return self.parse_profile(parser) parser.fail( "Unknown format " + parser.stream.current.value, parser.stream.current.lineno, ) # pylint: disable=E1120,E1121 def parse_profile(self, parser): lineno = next(parser.stream).lineno parser.stream.expect("name:as") label = parser.parse_expression() body = parser.parse_statements(["name:endprofile"], drop_needle=True) return self._parse_profile_block(parser, label, "profile block", body, lineno) def _create_profile_id(self, parser): return f"_salt_profile_{parser.free_identifier().name}" def _profile_start(self, label, source): return (label, source, time.time()) def _profile_end(self, label, source, previous_time): log.profile( "Time (in seconds) to render %s '%s': %s", source, label, time.time() - previous_time, ) def _parse_profile_block(self, parser, label, source, body, lineno): profile_id = self._create_profile_id(parser) ret = ( [ nodes.Assign( nodes.Name(profile_id, "store").set_lineno(lineno), self.call_method( "_profile_start", dyn_args=nodes.List([label, nodes.Const(source)]).set_lineno( lineno ), ).set_lineno(lineno), ).set_lineno(lineno), ] + body + [ nodes.ExprStmt( self.call_method( "_profile_end", dyn_args=nodes.Name(profile_id, "load") ), ).set_lineno(lineno), ] ) return ret def parse_load(self, parser): filter_name = parser.stream.current.value lineno = next(parser.stream).lineno if filter_name not in self.environment.filters: parser.fail(f"Unable to parse {filter_name}", lineno) parser.stream.expect("name:as") target = parser.parse_assign_target() macro_name = "_" + parser.free_identifier().name macro_body = parser.parse_statements(("name:endload",), drop_needle=True) return [ nodes.Macro(macro_name, [], [], macro_body).set_lineno(lineno), nodes.Assign( target, nodes.Filter( nodes.Call( nodes.Name(macro_name, "load").set_lineno(lineno), [], [], None, None, ).set_lineno(lineno), filter_name, [], [], None, None, ).set_lineno(lineno), ).set_lineno(lineno), ] def parse_import(self, parser, converter): import_node = parser.parse_import() target = import_node.target lineno = import_node.lineno body = [ import_node, nodes.Assign( nodes.Name(target, "store").set_lineno(lineno), nodes.Filter( nodes.Name(target, "load").set_lineno(lineno), f"load_{converter}", [], [], None, None, ).set_lineno(lineno), ).set_lineno(lineno), ] return self._parse_profile_block( parser, import_node.template, f"import_{converter}", body, lineno ) def dict_to_sls_yaml_params(self, value, flow_style=False): """ .. versionadded:: 3005 Render a formatted multi-line YAML string from a Python dictionary. Each key/value pair in the dictionary will be added as a single-key dictionary to a list that will then be sent to the YAML formatter. :param value: Python dictionary representing Salt state parameters :param flow_style: Setting flow_style to False will enforce indentation mode :returns: Formatted SLS YAML string rendered with newlines and indentation """ return self.format_yaml( [{key: val} for key, val in value.items()], flow_style=flow_style )