From 8229f8c677ce58d27df75e82e92c2d2b155855b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=94=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=81?= Date: Thu, 28 May 2020 18:05:03 +0300 Subject: [PATCH] Added support for 'run' and 'exec' parameters. Implementation of the files appends is almost done. --- calculate/templates/format/diff_format.py | 3 +- calculate/templates/format/patch_format.py | 2 +- calculate/templates/format/procmail_format.py | 3 +- calculate/templates/format/proftpd_format.py | 1 + .../templates/format/xml_gconf_format.py | 3 +- calculate/templates/format/xml_xfce_format.py | 4 +- calculate/templates/template_engine.py | 87 ++++-- calculate/utils/files.py | 71 ++++- template_action_draft.py | 260 ++++++++++++++---- tests/templates/testfiles/test_root/etc/file | 3 + .../test-category/test-package-1.0/CONTENTS | 2 +- 11 files changed, 347 insertions(+), 92 deletions(-) create mode 100644 tests/templates/testfiles/test_root/etc/file diff --git a/calculate/templates/format/diff_format.py b/calculate/templates/format/diff_format.py index e2baa32..80a3177 100644 --- a/calculate/templates/format/diff_format.py +++ b/calculate/templates/format/diff_format.py @@ -10,7 +10,8 @@ class DiffFormat(BaseFormat): FORMAT = 'diff' EXECUTABLE = True - def __init__(self, document_text: str): + def __init__(self, document_text: str, + join_before=False): self._patch_text = document_text self._root_path = '' self._last_level = 0 diff --git a/calculate/templates/format/patch_format.py b/calculate/templates/format/patch_format.py index db28650..862b258 100644 --- a/calculate/templates/format/patch_format.py +++ b/calculate/templates/format/patch_format.py @@ -14,7 +14,7 @@ class PatchFormat(BaseFormat): FORMAT_PARAMETERS = {'multiline', 'dotall', 'comment'} def __init__(self, document_text: str, multiline=False, dotall=False, - comment_symbol=''): + comment_symbol='', join_before=False): processing_methods = OrderedDict() super().__init__(processing_methods) diff --git a/calculate/templates/format/procmail_format.py b/calculate/templates/format/procmail_format.py index b8b0181..2503c33 100644 --- a/calculate/templates/format/procmail_format.py +++ b/calculate/templates/format/procmail_format.py @@ -19,7 +19,7 @@ class ProcmailFormat(BaseFormat): def __init__(self, document_text: str, ignore_comments=False, - join_before=True, + join_before=False, comment_symbol=''): processing_methods = [self._parse_comment_line, self._parse_parameter_line, @@ -28,6 +28,7 @@ class ProcmailFormat(BaseFormat): super().__init__(processing_methods) self._ignore_comments = ignore_comments self._comments_processing = True + self._join_before = join_before self._last_comments_list = [] diff --git a/calculate/templates/format/proftpd_format.py b/calculate/templates/format/proftpd_format.py index 2af4688..a5d82a0 100644 --- a/calculate/templates/format/proftpd_format.py +++ b/calculate/templates/format/proftpd_format.py @@ -42,6 +42,7 @@ class ProFTPDFormat(BaseFormat): self._ignore_comments = ignore_comments self._need_finish = True self._comments_processing = True + self._join_before = join_before self._section_stack = [] self._actions_stack = [] diff --git a/calculate/templates/format/xml_gconf_format.py b/calculate/templates/format/xml_gconf_format.py index dfd467b..b7b5812 100644 --- a/calculate/templates/format/xml_gconf_format.py +++ b/calculate/templates/format/xml_gconf_format.py @@ -21,7 +21,7 @@ class XMLGConfFormat(BaseFormat): cls._initialize_parser() return super().__new__(cls) - def __init__(self, document_text: str): + def __init__(self, document_text: str, join_before=False): processing_methods = OrderedDict({'gconf': self._gconf, 'entry': self._entry, 'dir': self._dir, @@ -34,6 +34,7 @@ class XMLGConfFormat(BaseFormat): super().__init__(processing_methods) self._initialize_parser() + self._join_before = join_before self._parse_xml_to_dictionary(document_text) diff --git a/calculate/templates/format/xml_xfce_format.py b/calculate/templates/format/xml_xfce_format.py index 90e4890..5d9bbec 100644 --- a/calculate/templates/format/xml_xfce_format.py +++ b/calculate/templates/format/xml_xfce_format.py @@ -21,13 +21,15 @@ class XMLXfceFormat(BaseFormat): cls._initialize_parser() return super().__new__(cls) - def __init__(self, document_text: str, ignore_comments=False): + def __init__(self, document_text: str, ignore_comments=False, + join_before=False): processing_methods = OrderedDict({'channel': self._channel, 'property': self._property, 'value': self._value, 'unknown': self._unknown}) super().__init__(processing_methods) self._initialize_parser() + self._join_before = join_before if document_text == '': self._document_dictionary = OrderedDict() diff --git a/calculate/templates/template_engine.py b/calculate/templates/template_engine.py index c96e606..35ad6e4 100644 --- a/calculate/templates/template_engine.py +++ b/calculate/templates/template_engine.py @@ -15,7 +15,8 @@ import os from ..utils.package import PackageAtomParser, PackageAtomError, NOTEXIST,\ Version -from ..utils.files import join_paths +from ..utils.files import join_paths, check_directory_link, check_command,\ + FilesError # Типы шаблона: директория или файл. @@ -134,7 +135,9 @@ class ParametersProcessor: self.postparse_checkers_list = OrderedDict({ 'append': self.check_postparse_append, 'source': self.check_postparse_source, - 'autoupdate': self.check_postparse_autoupdate}) + 'autoupdate': self.check_postparse_autoupdate, + 'run': self.check_postparse_run, + 'exec': self.check_postparse_exec}) # Если параметр является наследуемым только при некоторых условиях -- # указываем здесь эти условия. @@ -279,28 +282,34 @@ class ParametersProcessor: "'format' parameter value is not available") def check_stop_parameter(self, parameter_value): - if parameter_value and isinstance(parameter_value, str): - return parameter_value - else: - raise IncorrectParameter("'stop' parameter value is not correct") + if not parameter_value and isinstance(parameter_value, bool): + raise IncorrectParameter("'stop' parameter value is empty") + return parameter_value def check_start_parameter(self, parameter_value): - if parameter_value and isinstance(parameter_value, str): - return parameter_value - else: - raise IncorrectParameter("'start' parameter value is not correct") + if not parameter_value and isinstance(parameter_value, bool): + raise IncorrectParameter("'start' parameter value is empty") + return parameter_value def check_run_parameter(self, parameter_value): - if parameter_value and isinstance(parameter_value, str): - return parameter_value - else: - raise IncorrectParameter("'run' parameter value is not correct") + if not parameter_value and isinstance(parameter_value, bool): + raise IncorrectParameter("'run' parameter value is empty") + try: + interpreter_path = check_command(parameter_value) + except FilesError as error: + raise IncorrectParameter("interpreter from 'run' parameter not" + " found") + return interpreter_path def check_exec_parameter(self, parameter_value): - if parameter_value and isinstance(parameter_value, str): - return parameter_value - else: - raise IncorrectParameter("'exec' parameter value is not correct") + if not parameter_value and isinstance(parameter_value, bool): + raise IncorrectParameter("'exec' parameter value is empty") + try: + interpreter_path = check_command(parameter_value) + except FilesError as error: + raise IncorrectParameter("interpreter from 'exec' parameter not" + " found") + return interpreter_path def check_chown_parameter(self, parameter_value): if not parameter_value or isinstance(parameter_value, bool): @@ -331,6 +340,9 @@ class ParametersProcessor: raise IncorrectParameter("'chmod' parameter value is not correct") def check_source_parameter(self, parameter_value): + if not parameter_value or isinstance(parameter_value, bool): + raise IncorrectParameter("'source' parameter value is empty") + if self.chroot_path != '/': real_path = join_paths(self.chroot_path, parameter_value) else: @@ -342,15 +354,18 @@ class ParametersProcessor: source_file_type = DIR if os.path.isdir(real_path) else FILE - if not parameter_value or isinstance(parameter_value, bool): - raise IncorrectParameter("'source' parameter value is empty") - - if (self._parameters_container.append == 'link' and - self.template_type != source_file_type): + # Проверяем, совпадают ли типы шаблона и файла, указанного в source + if (self.template_type != source_file_type): raise IncorrectParameter( "the type of the 'source' file does not match" " the type of the template file") + # Проверяем, не является ли файл из source зацикленной ссылкой. + if (source_file_type == DIR and os.path.islink(real_path) + and not check_directory_link(real_path)): + raise IncorrectParameter( + "the link from 'source' parameter is cycled") + return os.path.normpath(real_path) def check_env_parameter(self, parameter_value): @@ -395,6 +410,32 @@ class ParametersProcessor: raise IncorrectParameter("append = 'link' without source " "parameter.") + if self._parameters_container.run: + raise IncorrectParameter("'append' parameter is not 'compatible' " + "with the 'run' parameter") + + if self._parameters_container.exec: + raise IncorrectParameter("'append' parameter is not 'compatible' " + "with the 'exec' parameter") + + def check_postparse_run(self, parameter_value): + if self._parameters_container.append: + raise IncorrectParameter("'run' parameter is not 'compatible' " + "with the 'append' parameter") + + if self._parameters_container.exec: + raise IncorrectParameter("'run' parameter is not 'compatible' " + "with the 'exec' parameter") + + def check_postparse_exec(self, parameter_value): + if self._parameters_container.append: + raise IncorrectParameter("'exec' parameter is not 'compatible' " + "with the 'append' parameter") + + if self._parameters_container.run: + raise IncorrectParameter("'exec' parameter is not 'compatible' " + "with the 'run' parameter") + def check_postparse_source(self, parameter_value): # Если файл по пути source не существует, но присутствует параметр # mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть diff --git a/calculate/utils/files.py b/calculate/utils/files.py index 3c3df91..5ad42b4 100644 --- a/calculate/utils/files.py +++ b/calculate/utils/files.py @@ -38,7 +38,7 @@ class KeyboardInputProcess(): class Process(): - '''Обертка для работы с процессами.''' + '''Класс-обертка для работы с процессами.''' STDOUT = STDOUT PIPE = PIPE @@ -94,6 +94,7 @@ class Process(): return self.stdin_handler def _open_process(self): + '''Метод для открытия процесса.''' try: piped_stdin = self._stdin._get_stdout() self._process = Popen(self._command, @@ -128,6 +129,7 @@ class Process(): raise FilesError('Can not open process.') def close(self): + '''Метод для закрытия процесса.''' if self._opened: if self._process.stdin: self.stdin_handler.close() @@ -135,6 +137,7 @@ class Process(): self._opened = False def write(self, data): + '''Метод для записи данных в stdin процесса.''' if not self._opened: self._open_process() self._is_read = False @@ -149,6 +152,7 @@ class Process(): raise FilesError(str(error)) def read(self): + '''Метод для чтения данных из stdout процесса.''' if not self._opened and not self._writable: self._open_process() @@ -173,6 +177,8 @@ class Process(): raise def read_error(self): + '''Метод для чтения ошибок, появившихся при выполнении процесса, из его + stderr.''' self.read() if not self._error_cache: try: @@ -182,6 +188,7 @@ class Process(): return self._error_cache def kill(self): + '''Метод для удаления процесса, если он работает.''' if self._opened: self._process.kill() @@ -198,22 +205,28 @@ class Process(): @property def writable(self): + '''Метод для проверки возможности записи данных в во входной поток + процесса.''' return self._writable @property def readable(self): + '''Метод для проверки возможности чтения вывода процесса.''' return self._readable @property def readable_errors(self): + '''Метод для проверки возможности чтения ошибок.''' return self._readable_errors def return_code(self): + '''Метод возвращающий код возвращенный процессом.''' self.read() return self._process.returncode @property def shell_command(self): + '''Метод для получения эквивалентной консольной команды.''' command = ' '.join(self._command) previous_commands = self._stdin.shell_command if previous_commands == '': @@ -222,13 +235,17 @@ class Process(): return ' | '.join([previous_commands, command]) def success(self): + '''Метод для проверки успешности выполнения процесса.''' return self.return_code() == 0 def failed(self): + '''Метод для проверки неуспешности выполнения процесса.''' return self.return_code() != 0 class ProgramPathCache: + '''Класс, для поиска и кэширования путей к исполнительным файлам различных + команд.''' def __init__(self): self._cache = {} @@ -256,7 +273,8 @@ class ProgramPathCache: get_program_path = ProgramPathCache() -def check_utils(*utils): +def check_command(*utils): + '''Функция для проверки наличия той или иной команды системе.''' output = [] for util in utils: util_path = get_program_path(util) @@ -271,6 +289,7 @@ def check_utils(*utils): def join_paths(*paths): + '''Функция для объединения путей. Объединяет также абсолютные пути.''' if len(paths) == 1: return next(iter(paths)) @@ -288,9 +307,12 @@ def join_paths(*paths): def read_link(file_path): + '''Функция для получения целевого пути символьной ссылки.''' try: if path.exists(file_path): return os.readlink(file_path) + else: + return None except (OSError, IOError) as error: mod, lineno = get_traceback_caller(*sys.exc_info()) FilesError("link read error, {}({}:{})". @@ -298,6 +320,7 @@ def read_link(file_path): def read_file(file_path): + '''Функция для чтения файлов, возвращает текст файла.''' try: if path.exists(file_path): with open(file_path, 'r') as opened_file: @@ -309,6 +332,8 @@ def read_file(file_path): def write_file(file_path): + '''Функция для открытия и записи файлов. Создает директории на пути к + целевому файлу если это необходимо. Возвращает файловый объект.''' directory_path = path.dirname(file_path) if not path.exists(directory_path): os.makedirs(directory_path) @@ -317,6 +342,7 @@ def write_file(file_path): def read_file_lines(file_name, grab=False): + '''Функция для чтения файлов построчно.''' try: if path.exists(file_name): for file_line in open(file_name, 'r'): @@ -377,6 +403,47 @@ def make_directory(directory_path, force=False): return False +def check_directory_link(link_path): + '''Метод для проверки наличия зацикливающихся ссылок и их корректности в + целом. В случае успешной проверки возвращает целевой путь ссылки.''' + link_target = read_link(link_path) + if link_target is None: + # Ссылка не существует. + return False + + if not os.path.isdir(link_target): + # Ссылка не на директорию. + return False + + linked_path = os.path.abspath(link_target) + + # Добавляем / к концу пути, чтобы показать, что это путь к директории. + if linked_path[-1] != '/': + linked_path = linked_path + '/' + + # Пути, которые нужно проверить. + to_check = [linked_path] + # Целевые пути из встреченных ссылок. + linked_paths = {linked_path} + + while to_check: + current_directory = to_check.pop() + for entry in os.scandir(current_directory): + # Обходим только директории и ссылки на директории. + if not entry.is_dir(): + continue + if entry.is_symlink(): + linked_path = read_link(entry.path) + if linked_path in linked_paths: + return False + linked_paths.add(linked_path) + to_check.append(linked_path) + else: + to_check.append(entry.path) + + return link_target + + class RealFS(GenericFS): def __init__(self, prefix='/'): self.prefix = prefix diff --git a/template_action_draft.py b/template_action_draft.py index 6c7b40a..29bcf9e 100644 --- a/template_action_draft.py +++ b/template_action_draft.py @@ -3,7 +3,8 @@ from calculate.templates.template_engine import TemplateEngine, Variables,\ from calculate.templates.template_processor import TemplateAction from calculate.utils.package import PackageAtomParser, Package, PackageNotFound from calculate.utils.files import write_file, read_link, read_file_lines,\ - FilesError, join_paths + FilesError, join_paths,\ + check_directory_link, Process from calculate.utils.mount import Mounts from collections import OrderedDict import hashlib @@ -12,25 +13,32 @@ import glob import shutil import os -template_text = '''{% calculate append = 'link', source = '/etc/dir/dir_2' -%} +template_text = '''{% calculate append = 'join' -%} {% calculate package = 'test-category/test-package', format = 'samba' -%} [section one] parameter_1 = {{ vars_1.value_1 }} !parameter_2 ''' +template_to_run = '''{% calculate run = "/usr/bin/python" -%} +with open('etc/dir/file.conf', 'r') as input_file: + print(input_file.read()) +''' + backup_template_text = '''{% calculate append = 'join', format = 'samba', -autoupdate, package = 'test-category/test-package' -%} +package = 'test-category/test-package' -%} [section one] parameter_1 = value parameter_2 = value_2 [section two] other_parameter = other_value +[!section_name] ''' APPENDS_SET = TemplateAction().available_appends -vars_1 = Variables({'value_1': 'value_1'}) +vars_1 = Variables({'value_1': 'value_1', 'value_2': 'value_to_print', + 'value_3': 5}) DATAVARS_MODULE = Variables({'vars_1': vars_1}) CHROOT_PATH = os.path.join(os.getcwd(), 'tests/templates/testfiles/test_root') @@ -43,6 +51,7 @@ template_engine = TemplateEngine(datavars_module=DATAVARS_MODULE, chroot_path=CHROOT_PATH) target_path = os.path.join(CHROOT_PATH, 'etc/dir/file.conf') +run_target_path = os.path.join(CHROOT_PATH, 'file_to_run.py') class TemplateExecutorError(Exception): @@ -196,13 +205,19 @@ class TemplateWrapper: # Временный флаг для определения того, является ли шаблон userspace. self.is_userspace = False - # Получаем класс соответствующего формата файла. - if self.parameters.format: - self.format_class = ParametersProcessor.\ - available_formats[self.parameters.format] - else: - # Здесь будет детектор форматов. - pass + if self.parameters.run or self.parameters.exec: + # Если есть параметр run или exec, то кроме текста шаблона ничего + # не нужно. + return + + if self.parameters.append in {'join', 'before', 'after'}: + # Получаем класс соответствующего формата файла. + if self.parameters.format: + self.format_class = ParametersProcessor.\ + available_formats[self.parameters.format] + else: + # Здесь будет детектор форматов. + pass # Если по этому пути что-то есть -- проверяем конфликты. if os.path.exists(target_file_path): @@ -243,27 +258,26 @@ class TemplateWrapper: if self.parameters.force: self.remove_original = True elif self.target_type == DIR: - raise TemplateTypeConflict( - "The target is a directory while the" - " template has append = 'link'.") + raise TemplateTypeConflict("the target is a directory while " + "the template has append = 'link'") else: - raise TemplateTypeConflict( - "The target is a file while the" - " template has append = 'link'.") + raise TemplateTypeConflict("the target is a file while the" + " template has append = 'link'") elif self.template_type == DIR: if self.target_type == FILE: if self.parameters.force: self.remove_original = True else: - raise TemplateTypeConflict("The target is a file while the" - " template is a directory.") - elif self.target_is_link and self.target_type == DIR: + raise TemplateTypeConflict("the target is a file while the" + " template is a directory") + elif self.target_is_link: if self.parameters.force: self.remove_original = True else: try: - self.target_path = read_link(self.target_path) + self.target_path = check_directory_link( + self.target_path) except FilesError as error: raise TemplateExecutorError("files error: {}". format(str(error))) @@ -279,8 +293,8 @@ class TemplateWrapper: raise TemplateExecutorError("files error: {}". format(str(error))) elif self.target_type == DIR: - raise TemplateTypeConflict("The target file is a directory" - " while the template is a file.") + raise TemplateTypeConflict("the target file is a directory" + " while the template is a file") def check_package_collision(self): '''Проверка на предмет коллизии, то есть конфликта пакета шаблона и @@ -433,11 +447,9 @@ class TemplateWrapper: def remove_from_contents(self): '''Метод для удаления целевого файла из CONTENTS.''' - print('let s remove') if self.template_type == DIR: self.target_package.remove_dir(self.target_path) elif self.template_type == FILE: - print('remove as file') self.target_package.remove_obj(self.target_path) def clear_dir_contents(self): @@ -483,8 +495,6 @@ class TemplateWrapper: def save_changes(self): '''Метод для сохранения изменений внесенных в CONTENTS.''' if self.target_package: - print('saving CONTENTS: {}'.format( - self.target_package.contents_dictionary.keys())) self.target_package.remove_empty_directories() self.target_package.write_contents_file() @@ -492,11 +502,14 @@ class TemplateWrapper: class TemplateExecutor: def __init__(self, datavars_module=Variables(), chroot_path='/', cl_config_archive='/var/lib/calculate/config-archive', - cl_config_path='/var/lib/calculate/config'): + cl_config_path='/var/lib/calculate/config', + exec_dir_path='/var/lib/calculate/.execute/'): self.datavars_module = datavars_module self.chroot_path = chroot_path + self.exec_files_directory = '/var/lib/calculate/.execute/' + self.directory_default_parameters =\ ParametersProcessor.directory_default_parameters self.file_default_parameters =\ @@ -509,7 +522,14 @@ class TemplateExecutor: 'link': self._append_link_directory, 'replace': self._append_replace_directory} - self.file_appends = {'join': self._append_join_file} + self.file_appends = {'join': self._append_join_file, + 'after': self._append_after_file, + 'before': self._append_before_file, + 'replace': self._append_replace_file, + 'remove': self._append_remove_file, + 'skip': self._append_skip_file, + 'clear': self._append_clear_file, + 'link': self._append_link_file} self.formats_classes = ParametersProcessor.available_formats self.calculate_config_file = CalculateConfigFile( @@ -530,21 +550,24 @@ class TemplateExecutor: def execute_template(self, target_path, parameters, template_type, template_text=''): '''Метод для запуска выполнения шаблонов.''' + self.executor_output = {'target_path': None, + 'stdout': None, + 'stderr': None, + 'exec_file': None} + try: template_object = TemplateWrapper(target_path, parameters, template_type, template_text=template_text) except TemplateTypeConflict as error: - print('Error: {}'.format(str(error))) - return + raise TemplateExecutorError("type conflict: {}".format(str(error))) + except TemplateCollisionError as error: - print('Error: {}'.format(str(error))) - return + raise TemplateExecutorError("collision: {}".format(str(error))) # Удаляем оригинал, если это необходимо из-за наличия force или по # другим причинам. if template_object.remove_original: - print('remove original') if template_object.target_type == DIR: self._remove_directory(template_object.target_path) else: @@ -557,25 +580,35 @@ class TemplateExecutor: return template_object.target_type = None - print('input path: {}'.format(template_object.input_path)) - print('output path: {}'.format(template_object.output_path)) - - # (!) Добавить поддержку run, execute и т.д. - if template_object.template_type == DIR: - self.directory_appends[template_object.parameters.append]( + if template_object.parameters.run: + # Если есть параметр run -- запускаем текст шаблона. + self._run_template(template_object) + elif template_object.parameters.exec: + # Если есть параметр exec -- запускаем текст шаблона после + # обработки всех шаблонов. + self._exec_template(template_object) + elif template_object.parameters.append: + print('Using append: {}'.format(template_object.parameters.append)) + print('input path: {}'.format(template_object.input_path)) + print('output path: {}'.format(template_object.output_path)) + + if template_object.template_type == DIR: + self.directory_appends[template_object.parameters.append]( template_object) - elif template_object.template_type == FILE: - self.file_appends[template_object.parameters.append]( + elif template_object.template_type == FILE: + self.file_appends[template_object.parameters.append]( template_object) - # Сохраняем изменения в CONTENTS внесенные согласно шаблону. - print('it is time to save changes') - template_object.save_changes() - # Возвращаем целевой путь, если он был изменен, или None если не был. - if template_object.target_path_is_changed: - return template_object.target_path - else: - return + # Сохраняем изменения в CONTENTS внесенные согласно шаблону. + template_object.save_changes() + + # Возвращаем целевой путь, если он был изменен, или + # None если не был изменен. + if template_object.target_path_is_changed: + self.executor_output['target_path'] =\ + template_object.target_path + + return self.executor_output def save_changes(self): '''Метод для сохранения чего-нибудь после выполнения всех шаблонов.''' @@ -628,7 +661,8 @@ class TemplateExecutor: self._clear_directory(template_object.target_path) template_object.clear_dir_contents() - def _append_join_file(self, template_object: TemplateWrapper): + def _append_join_file(self, template_object: TemplateWrapper, + join_before=False, replace=False): '''Метод описывающий действия при append = "join", если шаблон -- файл. Объединяет шаблон с целевым файлом.''' input_path = template_object.input_path @@ -644,14 +678,15 @@ class TemplateExecutor: if template_object.protected and not template_object.is_userspace: output_paths.append(template_object.archive_path) - if template_object.target_type is not None: + if template_object.target_type is not None and not replace: with open(input_path, 'r') as input_file: input_text = input_file.read() else: input_text = '' - parsed_input = template_format(input_text) + parsed_input = template_format(input_text, join_before=join_before) - parsed_template = template_format(template_object.template_text) + parsed_template = template_format(template_object.template_text, + join_before=join_before) parsed_input.join_template(parsed_template) @@ -683,20 +718,19 @@ class TemplateExecutor: # Убираем целевой файл из CL. self.calculate_config_file.remove_file(template_object.target_path) - print('is contents update is needed') # Обновляем CONTENTS. if template_object.protected: - print('needed') if template_object.parameters.unbound: - print('remove') template_object.remove_from_contents() else: - print('add') template_object.add_to_contents(file_md5=output_text_md5) else: - with open(input_path, 'r') as input_file: - input_text = input_file.read() - parsed_input = template_format(input_text) + if template_object.target_type is not None and not replace: + with open(input_path, 'r') as input_file: + input_text = input_file.read() + parsed_input = template_format(input_text) + else: + input_text = '' parsed_template = template_format(template_object.template_text) parsed_input.join_template(parsed_template) @@ -732,6 +766,39 @@ class TemplateExecutor: # Обновляем CONTENTS. template_object.add_to_contents(file_md5=output_text_md5) + def _append_after_file(self, template_object: TemplateWrapper): + self._append_join_file(self, template_object, join_before=False) + + def _append_before_file(self, template_object: TemplateWrapper): + self._append_join_file(self, template_object, join_before=True) + + def _append_skip_file(self, template_object: TemplateWrapper): + pass + + def _append_replace_file(self, template_object: TemplateWrapper): + self._append_join_file(template_object, replace=True) + + def _append_remove_file(self, template_object: TemplateWrapper): + if template_object.target_type is not None: + self._remove_file(template_object.target_path) + + template_object.remove_from_contents() + + def _append_clear_file(self, template_object: TemplateWrapper): + if template_object.target_type is not None: + self._clear_file(template_object.target_path) + else: + open(template_object.target_path, 'w').close() + + template_object.add_to_contents() + + def _append_link_file(self, template_object: TemplateWrapper): + if template_object.target_type is not None: + self._link_file(template_object.target_path, + template_object.parameters.source) + + template_object.add_to_contents() + def _create_directory(self, template_object: TemplateWrapper): '''Метод для создания директории и, при необходимости, изменения владельца и доступа все директорий на пути к целевой.''' @@ -920,6 +987,63 @@ class TemplateExecutor: raise TemplateExecutorError('Can not chmod file: {0}, reason: {1}'. format(target_path, str(error))) + def _run_template(self, template_object: TemplateWrapper): + '''Метод для сохранения текста шаблонов, который должен быть исполнен + интерпретатором указанным в run прямо во время обработки шаблонов.''' + text_to_run = template_object.template_text + interpreter = template_object.parameters.run + + try: + run_process = Process(interpreter, cwd=self.chroot_path) + run_process.write(text_to_run) + + if run_process.readable: + stdout = run_process.read() + if stdout: + print("Run output:\n{}".format(stdout)) + self.executor_output['stdout'] = stdout + + if run_process.readable_errors: + stderr = run_process.read_error() + if stderr: + print("Run errors:\n{}".format(stderr)) + self.executor_output['stderr'] = stderr + + except FilesError as error: + raise TemplateExecutorError(("can not run template using the" + " interpreter '{}', reason: {}"). + format(interpreter, str(error))) + + def _exec_template(self, template_object: TemplateWrapper): + '''Метод для сохранения текста шаблонов, который должен быть исполнен + интерпретатором указанным в exec после выполнения всех прочих шаблонов. + ''' + text_to_run = template_object.template_text + interpreter = template_object.parameters.exec + if (self.chroot_path != '/' and not + self.exec_files_directory.startswith(self.chroot_path)): + exec_files_directory = join_paths(self.chroot_path, + '/var/lib/calculate/.execute/') + + exec_number = 0 + if os.path.exists(exec_files_directory): + exec_files_list = os.listdir(exec_files_directory) + if exec_files_list: + exec_number = int(exec_files_list[-1][-4:]) + exec_number = str(exec_number + 1) + + if len(exec_number) < 4: + exec_number = '0' * (4 - len(exec_number)) + exec_number + exec_file_name = 'exec_{}'.format(exec_number) + exec_file_path = join_paths(exec_files_directory, + exec_file_name) + + exec_file = write_file(exec_file_path) + exec_file.write(text_to_run) + exec_file.close() + + self.executor_output['exec_file'] = {interpreter: exec_file_name} + def get_file_info(self, path, info='all'): file_stat = os.stat(path) if info == 'all': @@ -1005,3 +1129,17 @@ template_executor_obj.execute_template(target_path, template_parameters, FILE, template_text=template_text) template_executor_obj.save_changes() + +input() +template_engine.process_template_from_string(template_to_run, FILE) +template_parameters = template_engine.parameters +template_text = template_engine.template_text +template_executor_obj = TemplateExecutor( + datavars_module=DATAVARS_MODULE, + chroot_path=CHROOT_PATH, + cl_config_archive=CL_CONFIG_ARCHIVE_PATH, + cl_config_path=CL_CONFIG_PATH) +template_executor_obj.execute_template(target_path, + template_parameters, + FILE, template_text=template_text) +template_executor_obj.save_changes() diff --git a/tests/templates/testfiles/test_root/etc/file b/tests/templates/testfiles/test_root/etc/file new file mode 100644 index 0000000..a5af483 --- /dev/null +++ b/tests/templates/testfiles/test_root/etc/file @@ -0,0 +1,3 @@ +# Source file +[section_name] + rare_parameter = eternal_value diff --git a/tests/templates/testfiles/test_root/var/db/pkg/test-category/test-package-1.0/CONTENTS b/tests/templates/testfiles/test_root/var/db/pkg/test-category/test-package-1.0/CONTENTS index a47ff84..553374f 100644 --- a/tests/templates/testfiles/test_root/var/db/pkg/test-category/test-package-1.0/CONTENTS +++ b/tests/templates/testfiles/test_root/var/db/pkg/test-category/test-package-1.0/CONTENTS @@ -1,3 +1,3 @@ dir /etc dir /etc/dir -obj /etc/dir/file.conf 0b87fea7f5b65cac5012baa2bf647e72 1590588845 +obj /etc/dir/file.conf 0b87fea7f5b65cac5012baa2bf647e72 1590678156