1515"""Set of utility functions for working with virtual machines."""
1616
1717
18+ import collections
1819import contextlib
1920import enum
2021import logging
3637from perfkitbenchmarker import errors
3738from perfkitbenchmarker import log_util
3839from perfkitbenchmarker import temp_dir
40+ import yaml
3941
4042FLAGS = flags .FLAGS
4143# Using logger rather than logging.info to avoid stack_level problems.
@@ -925,11 +927,48 @@ def DictionaryToEnvString(dictionary, joiner=' '):
925927 )
926928
927929
930+ def WriteTemporaryFile (
931+ file_contents : str ,
932+ origin : str = '' ,
933+ should_log_file : bool = True ,
934+ stack_level : int = 1 ,
935+ ) -> str :
936+ """Writes the string file_contents to a temporary file.
937+
938+ Args:
939+ file_contents: string. The contents of the file to write.
940+ origin: str. The origin of the file, used for logging.
941+ should_log_file: bool. Whether to log the file before writing.
942+ stack_level: int. The stack level to use for logging.
943+
944+ Returns:
945+ The name of the temporary file.
946+ """
947+ stack_level += 1
948+ prefix = 'pkb-'
949+ with NamedTemporaryFile (
950+ prefix = prefix , dir = GetTempDir (), delete = False , mode = 'w'
951+ ) as tf :
952+ if origin :
953+ origin = f'from { origin } '
954+ if should_log_file :
955+ logging .info (
956+ 'Writing temporary file %s to name %s with contents:\n %s' ,
957+ origin ,
958+ tf .name ,
959+ file_contents ,
960+ stacklevel = stack_level ,
961+ )
962+ tf .write (file_contents )
963+ tf .close ()
964+ return tf .name
965+
966+
928967def RenderTemplate (
929968 template_path ,
930969 context ,
931- should_log_file : bool = False ,
932970 trim_spaces : bool = False ,
971+ ** logging_kwargs : dict [str , Any ],
933972) -> str :
934973 """Renders a local Jinja2 template and returns its file name.
935974
@@ -938,8 +977,8 @@ def RenderTemplate(
938977 Args:
939978 template_path: string. Local path to jinja2 template.
940979 context: dict. Variables to pass to the Jinja2 template during rendering.
941- should_log_file: bool. Whether to log the file after rendering.
942980 trim_spaces: bool. Value for both trim_blocks and lstrip_blocks.
981+ **logging_kwargs: dict. Keyword arguments passed to WriteTemporaryFile.
943982
944983 Raises:
945984 jinja2.UndefinedError: if template contains variables not present in
@@ -951,24 +990,14 @@ def RenderTemplate(
951990 rendered_template = ReadAndRenderJinja2Template (
952991 template_path , trim_spaces , ** context
953992 )
954- prefix = 'pkb-' + os .path .basename (template_path )
955- with NamedTemporaryFile (
956- prefix = prefix , dir = GetTempDir (), delete = False , mode = 'w'
957- ) as tf :
958- if should_log_file :
959- logging .info (
960- 'Rendered template from %s to %s with full text:\n %s' ,
961- template_path ,
962- tf .name ,
963- rendered_template ,
964- stacklevel = 2 ,
965- )
966- tf .write (rendered_template )
967- tf .close ()
968- return tf .name
993+ IncrementStackLevel (** logging_kwargs )
994+ return WriteTemporaryFile (
995+ rendered_template ,
996+ origin = f'jinja2 template { template_path } ' ,
997+ ** logging_kwargs ,
998+ )
969999
9701000
971- @staticmethod
9721001def ReadAndRenderJinja2Template (
9731002 file_path : str , trim_spaces : bool = False , ** kwargs
9741003) -> str :
@@ -1003,3 +1032,100 @@ def ReadLocalFile(filename: str) -> str:
10031032 file_path = posixpath .join (GetTempDir (), filename )
10041033 stdout , _ , _ = IssueCommand (['cat' , file_path ])
10051034 return stdout
1035+
1036+
1037+ def RecursiveDict () -> dict [str , Any ]:
1038+ """Creates a new dictionary with auto nesting keys.
1039+
1040+ See https://stackoverflow.com/a/10218517/2528472.
1041+
1042+ Returns:
1043+ A new dictionary that automatically sets the value for any nested missing
1044+ keys.
1045+ """
1046+ return collections .defaultdict (RecursiveDict )
1047+
1048+
1049+ def ReadYamlAsDicts (
1050+ file_contents : str
1051+ ) -> list [dict [str , Any ]]:
1052+ """Reads file contents & converts it to a list of recursive YAML doc dicts.
1053+
1054+ Args:
1055+ file_contents: The yaml string.
1056+
1057+ Returns:
1058+ The various YAML documents as a list of recursive dictionaries.
1059+ """
1060+ yaml_docs = yaml .safe_load_all (file_contents )
1061+ return_yamls = []
1062+ for yaml_doc in yaml_docs :
1063+ return_yamls .append (ConvertToDictType (yaml_doc , RecursiveDict ))
1064+ return return_yamls
1065+
1066+
1067+ def ConvertToDictType (yaml_doc : Any , dict_lambda : Any ) -> dict [str , Any ] | Any :
1068+ """Converts a YAML document to the given dictionary type.
1069+
1070+ In particular a recursive defaultdict can be more easily accessed with e.g.
1071+ my_dict['a']['b'] = value rather than my_dict.setdefault('a', {})['b'] =
1072+ value.
1073+
1074+ Args:
1075+ yaml_doc: The YAML document to convert.
1076+ dict_lambda: A constructor for the dictionary type to convert to.
1077+
1078+ Returns:
1079+ The remade dictionary.
1080+ """
1081+ if not isinstance (yaml_doc , dict ) and not isinstance (yaml_doc , list ):
1082+ return yaml_doc
1083+
1084+ def _ConvertPossiblyEmptyValue (value : Any ) -> Any | None :
1085+ """Converts a given value to the dictionary type, or None if empty."""
1086+ if not bool (value ) and value != 0 :
1087+ return None
1088+ converted_value = ConvertToDictType (value , dict_lambda )
1089+ if not bool (converted_value ) and converted_value != 0 :
1090+ return None
1091+ return converted_value
1092+
1093+ yaml_list = []
1094+ if isinstance (yaml_doc , list ):
1095+ for item in yaml_doc :
1096+ converted_value = _ConvertPossiblyEmptyValue (item )
1097+ if converted_value is None :
1098+ continue
1099+ yaml_list .append (converted_value )
1100+ return yaml_list
1101+ yaml_dict = dict_lambda ()
1102+ for key , value in yaml_doc .items ():
1103+ converted_value = _ConvertPossiblyEmptyValue (value )
1104+ if converted_value is None :
1105+ continue
1106+ yaml_dict [key ] = converted_value
1107+ return yaml_dict
1108+
1109+
1110+ def WriteYaml (
1111+ yaml_dicts : list [dict [str , Any ]], ** kwargs
1112+ ) -> str :
1113+ """Writes yaml to a file & returns the name of that file.
1114+
1115+ Args:
1116+ yaml_dicts: A list of YAML documents.
1117+ **kwargs: Keyword arguments passed to file writing.
1118+
1119+ Returns:
1120+ Names of the written file.
1121+ """
1122+ normal_dicts = []
1123+ # Convert back to a normal dict because yaml.dump otherwise adds random
1124+ # "dictitems:" keys & other python artifacts.
1125+ for yaml_dict in yaml_dicts :
1126+ normal_dicts .append (ConvertToDictType (yaml_dict , dict ))
1127+ manifest = yaml .dump_all (normal_dicts )
1128+ IncrementStackLevel (** kwargs )
1129+ return WriteTemporaryFile (
1130+ manifest , origin = 'yaml' , ** kwargs
1131+ )
0 commit comments