|
|
# vim: fileencoding=utf-8
|
|
|
#
|
|
|
from pprint import pprint
|
|
|
from ..utils.package import (
|
|
|
PackageAtomParser,
|
|
|
PackageCreator,
|
|
|
Package,
|
|
|
PackageNotFound,
|
|
|
PackageAtomName,
|
|
|
Version,
|
|
|
NonePackage,
|
|
|
)
|
|
|
from ..utils.files import (
|
|
|
join_paths,
|
|
|
write_file,
|
|
|
read_file_lines,
|
|
|
FilesError,
|
|
|
check_directory_link,
|
|
|
read_link,
|
|
|
Process,
|
|
|
get_target_from_link,
|
|
|
get_directory_contents,
|
|
|
)
|
|
|
from .template_engine import (
|
|
|
TemplateEngine,
|
|
|
Variables,
|
|
|
ConditionFailed,
|
|
|
ParametersProcessor,
|
|
|
DIR, FILE,
|
|
|
ParametersContainer,
|
|
|
)
|
|
|
from calculate.variables.datavars import (
|
|
|
StringType,
|
|
|
ListType,
|
|
|
NamespaceNode,
|
|
|
VariableNode,
|
|
|
TableType,
|
|
|
VariableNotFoundError
|
|
|
)
|
|
|
from typing import (
|
|
|
Union,
|
|
|
Dict,
|
|
|
List,
|
|
|
Tuple,
|
|
|
Iterator,
|
|
|
Optional,
|
|
|
Callable
|
|
|
)
|
|
|
from calculate.variables.loader import Datavars
|
|
|
from .format.base_format import Format, FormatError
|
|
|
from ..utils.io_module import IOModule
|
|
|
from collections import OrderedDict, abc
|
|
|
from contextlib import contextmanager
|
|
|
from ..utils.mount import Mounts
|
|
|
import hashlib
|
|
|
import fnmatch
|
|
|
import shutil
|
|
|
import errno
|
|
|
import stat
|
|
|
import glob
|
|
|
import copy
|
|
|
import os
|
|
|
|
|
|
|
|
|
# Наверное временно.
|
|
|
CALCULATE_VERSION = Version('4.0')
|
|
|
|
|
|
|
|
|
class TemplateExecutorError(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class TemplateTypeConflict(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class TemplateCollisionError(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
|
class CalculateConfigFile:
|
|
|
'''Класс для работы с файлом /var/lib/calculate/config.'''
|
|
|
def __init__(self, cl_config_path: str = '/var/lib/calculate/config',
|
|
|
cl_chroot_path: str = '/'):
|
|
|
self.chroot_path: str = cl_chroot_path
|
|
|
|
|
|
self.cl_config_path: str = cl_config_path
|
|
|
|
|
|
self._config_dictionary: OrderedDict = self._get_cl_config_dictionary()
|
|
|
|
|
|
self._unsaved_changes: bool = False
|
|
|
|
|
|
def __contains__(self, file_path: str) -> bool:
|
|
|
file_path = self._remove_chroot(file_path)
|
|
|
return file_path in self._config_dictionary
|
|
|
|
|
|
def _get_cl_config_dictionary(self) -> OrderedDict:
|
|
|
'''Метод для загрузки словаря файла /var/lib/calculate/config.'''
|
|
|
config_dictionary = OrderedDict()
|
|
|
|
|
|
if os.path.exists(self.cl_config_path):
|
|
|
if os.path.isdir(self.cl_config_path):
|
|
|
raise TemplateExecutorError(
|
|
|
"directory instead calculate config file in: {}".
|
|
|
format(self.cl_config_path))
|
|
|
else:
|
|
|
write_file(self.cl_config_path).close()
|
|
|
return config_dictionary
|
|
|
|
|
|
try:
|
|
|
config_file_lines = read_file_lines(self.cl_config_path)
|
|
|
except FilesError as error:
|
|
|
raise TemplateExecutorError(
|
|
|
"cannot read calculate config file in: {0}. Reason: {1}".
|
|
|
format(self.cl_config_path, str(error)))
|
|
|
|
|
|
# TODO Продумать проверку корректности найденного файла.
|
|
|
for file_line in config_file_lines:
|
|
|
filename, md5_sum = file_line.split(' ')
|
|
|
config_dictionary.update({filename: md5_sum})
|
|
|
|
|
|
self._unsaved_changes = False
|
|
|
return config_dictionary
|
|
|
|
|
|
def set_files_md5(self, file_path: str, file_md5: str) -> None:
|
|
|
'''Метод для установки в config соответствия файла некоторой
|
|
|
контрольной сумме.'''
|
|
|
file_path = self._remove_chroot(file_path)
|
|
|
self._config_dictionary[file_path] = file_md5
|
|
|
self._unsaved_changes = True
|
|
|
|
|
|
def remove_file(self, file_path: str) -> None:
|
|
|
'''Метод для удаления файла из config.'''
|
|
|
file_path = self._remove_chroot(file_path)
|
|
|
if file_path in self._config_dictionary:
|
|
|
self._config_dictionary.pop(file_path)
|
|
|
self._unsaved_changes = True
|
|
|
|
|
|
def compare_md5(self, file_path: str, file_md5: str) -> None:
|
|
|
'''Метод для сравнения хэш-суммы из config и некоторой заданной.'''
|
|
|
file_path = self._remove_chroot(file_path)
|
|
|
if file_path in self._config_dictionary:
|
|
|
return self._config_dictionary[file_path] == file_md5
|
|
|
else:
|
|
|
return False
|
|
|
|
|
|
def save_changes(self) -> None:
|
|
|
'''Метод для записи изменений, внессенных в файл config.'''
|
|
|
if not self._unsaved_changes:
|
|
|
return
|
|
|
config_file = write_file(self.cl_config_path)
|
|
|
|
|
|
for file_name, file_md5 in self._config_dictionary.items():
|
|
|
config_file.write('{} {}\n'.format(file_name, file_md5))
|
|
|
config_file.close()
|
|
|
|
|
|
self._unsaved_changes = False
|
|
|
|
|
|
def _remove_chroot(self, file_path: str) -> str:
|
|
|
'''Метод для удаления корневого пути из указанного пути.'''
|
|
|
if self.chroot_path != '/' and file_path.startswith(self.chroot_path):
|
|
|
file_path = file_path[len(self.chroot_path):]
|
|
|
|
|
|
return file_path
|
|
|
|
|
|
|
|
|
class TemplateWrapper:
|
|
|
'''Класс связывающий шаблон с целевым файлом и определяющий параметры
|
|
|
наложения шаблона, обусловленные состоянием целевого файла.'''
|
|
|
type_checks: Dict[int,
|
|
|
Callable[[str], bool]] = {DIR: os.path.isdir,
|
|
|
FILE: os.path.isfile}
|
|
|
|
|
|
_protected_is_set: bool = False
|
|
|
_protected_set: set = set()
|
|
|
_unprotected_set: set = set()
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
if not cls._protected_is_set:
|
|
|
# Устанавливаем значения PROTECTED, если не заданы.
|
|
|
if 'chroot_path' in kwargs:
|
|
|
chroot_path = kwargs['chroot_path']
|
|
|
else:
|
|
|
chroot_path = '/'
|
|
|
cls._set_protected(chroot_path)
|
|
|
return super().__new__(cls)
|
|
|
|
|
|
def __init__(
|
|
|
self, target_file_path: str,
|
|
|
parameters: ParametersContainer,
|
|
|
template_type: int,
|
|
|
template_path: str,
|
|
|
template_text: str = '',
|
|
|
target_package: Optional[Package] = None,
|
|
|
chroot_path: str = '/',
|
|
|
config_archive_path: str = '/var/lib/calculate/config-archive',
|
|
|
dbpkg: bool = True,
|
|
|
pkg_autosave: bool = False):
|
|
|
self.target_path: str = target_file_path
|
|
|
self.template_path: str = template_path
|
|
|
self.chroot_path: str = chroot_path
|
|
|
self.config_archive_path: str = config_archive_path
|
|
|
|
|
|
self.target_package_name: Union[PackageAtomName, None] = None
|
|
|
self.package_atom_parser: PackageAtomParser = PackageAtomParser(
|
|
|
chroot_path=self.chroot_path)
|
|
|
|
|
|
self._pkg_autosave: bool = pkg_autosave
|
|
|
|
|
|
# Вспомогательный флаг, включается, если по целевому пути лежит файл,
|
|
|
# для которого не определился никакой пакет.
|
|
|
self.target_without_package: bool = False
|
|
|
|
|
|
self.parameters: ParametersContainer = parameters
|
|
|
|
|
|
self.output_path: str = self.target_path
|
|
|
self.input_path: Union[str, None] = None
|
|
|
|
|
|
self.template_type: int = template_type
|
|
|
self.template_text: str = template_text
|
|
|
|
|
|
self.contents_matching: bool = True
|
|
|
self.ca_is_missed: bool = False
|
|
|
|
|
|
# Флаг, указывающий, что нужно удалить файл из target_path перед
|
|
|
# применением шаблона.
|
|
|
self.remove_original: bool = False
|
|
|
|
|
|
# Флаг, указывающий, что целевой путь был изменен.
|
|
|
self.target_path_is_changed: bool = False
|
|
|
|
|
|
# Флаг, указывающий, что файл по целевому пути является ссылкой.
|
|
|
self.target_is_link: bool = False
|
|
|
|
|
|
# Пакет, к которому относится файл.
|
|
|
self.target_package: Package = target_package
|
|
|
|
|
|
# Флаг, разрешающий работу с CONTENTS. Если False, то выключает
|
|
|
# protected для всех файлов блокирует все операции с CONTENTS и ._cfg.
|
|
|
self.dbpkg: bool = dbpkg
|
|
|
|
|
|
# Флаг, указывающий, что файл является PROTECTED.
|
|
|
self.protected: bool = False
|
|
|
|
|
|
# Временный флаг для определения того, является ли шаблон userspace.
|
|
|
self.is_userspace: bool = False
|
|
|
|
|
|
self.format_class: Union[Format, None] = None
|
|
|
|
|
|
if self.parameters.run or self.parameters.exec:
|
|
|
# Если есть параметр run или exec, то кроме текста шаблона ничего
|
|
|
# не нужно.
|
|
|
return
|
|
|
|
|
|
if self.parameters.append in {'join', 'before', 'after', 'replace'}:
|
|
|
# Получаем класс соответствующего формата файла.
|
|
|
if self.parameters.format:
|
|
|
self.format_class = ParametersProcessor.\
|
|
|
available_formats[self.parameters.format]
|
|
|
elif self.template_type is FILE:
|
|
|
# TODO Здесь будет детектор форматов. Когда-нибудь.
|
|
|
raise TemplateExecutorError("'format' parameter is not set"
|
|
|
" file template.")
|
|
|
|
|
|
# Если по этому пути что-то есть -- проверяем тип этого.
|
|
|
if os.path.exists(target_file_path):
|
|
|
for file_type, checker in self.type_checks.items():
|
|
|
if checker(target_file_path):
|
|
|
self.target_type = file_type
|
|
|
break
|
|
|
|
|
|
self.target_is_link = os.path.islink(target_file_path)
|
|
|
|
|
|
# Если установлен параметр mirror и есть параметр source,
|
|
|
# содержащий несуществующий путь -- удаляем целевой файл.
|
|
|
if (self.parameters.source is True and self.parameters.mirror and
|
|
|
not self.format_class.EXECUTABLE):
|
|
|
self.remove_original = True
|
|
|
else:
|
|
|
self.target_type = None
|
|
|
|
|
|
if self.format_class is not None and self.format_class.EXECUTABLE:
|
|
|
# Если формат исполняемый -- проверяем, существует ли директория,
|
|
|
# из которой будет выполняться шаблон.
|
|
|
if self.target_type is None:
|
|
|
# Если не существует -- создаем ее.
|
|
|
if not os.path.exists(os.path.dirname(self.target_path)):
|
|
|
os.makedirs(os.path.dirname(self.target_path))
|
|
|
|
|
|
# Если есть параметр package, определяем по нему пакет.
|
|
|
if self.parameters.package:
|
|
|
self.target_package_name = self.parameters.package
|
|
|
if (self.target_package is None or
|
|
|
self.target_package.package_name !=
|
|
|
self.target_package_name):
|
|
|
self.target_package = Package(self.parameters.package,
|
|
|
chroot_path=self.chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
return
|
|
|
|
|
|
self._check_type_conflicts()
|
|
|
|
|
|
self._check_package_collision()
|
|
|
|
|
|
self._check_user_changes()
|
|
|
|
|
|
# if self.target_type is not None and self.contents_matching:
|
|
|
# # Удаляем целевой файл, если append = 'replace'
|
|
|
# if (self.parameters.append and
|
|
|
# self.parameters.append == "replace"):
|
|
|
# self.remove_original = True
|
|
|
|
|
|
def _check_type_conflicts(self) -> None:
|
|
|
'''Метод для проверки конфликтов типов.'''
|
|
|
if self.parameters.append == 'link':
|
|
|
if self.parameters.force:
|
|
|
if os.path.exists(self.parameters.source):
|
|
|
self.remove_original = True
|
|
|
elif self.target_is_link:
|
|
|
if self.template_type != self.target_type:
|
|
|
raise TemplateTypeConflict(
|
|
|
"the target is a link to {} while the template"
|
|
|
"is {} and has append = 'link'".
|
|
|
format('directory' if self.template_type ==
|
|
|
DIR else 'file',
|
|
|
'file' if self.template_type ==
|
|
|
DIR else 'directory'))
|
|
|
else:
|
|
|
self.remove_original = True
|
|
|
elif self.target_type == DIR:
|
|
|
raise TemplateTypeConflict("the target is a directory while "
|
|
|
"the template has append = 'link'")
|
|
|
elif self.target_type == FILE:
|
|
|
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:
|
|
|
if self.parameters.force:
|
|
|
self.remove_original = True
|
|
|
elif not self.parameters.append == "remove":
|
|
|
try:
|
|
|
link_source = check_directory_link(
|
|
|
self.target_path,
|
|
|
chroot_path=self.chroot_path)
|
|
|
self.target_path = link_source
|
|
|
self.target_path_is_changed = True
|
|
|
except FilesError as error:
|
|
|
raise TemplateExecutorError("files error: {}".
|
|
|
format(str(error)))
|
|
|
|
|
|
elif self.template_type == FILE:
|
|
|
if self.parameters.force:
|
|
|
if self.target_type == DIR:
|
|
|
self.remove_original = True
|
|
|
elif self.target_is_link and self.target_type == FILE:
|
|
|
try:
|
|
|
link_source = read_link(self.target_path)
|
|
|
self.target_path = get_target_from_link(
|
|
|
self.target_path,
|
|
|
link_source,
|
|
|
chroot_path=self.chroot_path)
|
|
|
self.target_path_is_changed = True
|
|
|
except FilesError as error:
|
|
|
raise TemplateExecutorError("files error: {}".
|
|
|
format(str(error)))
|
|
|
elif self.target_is_link:
|
|
|
if not self.parameters.append == "remove":
|
|
|
if self.target_type == DIR:
|
|
|
raise TemplateTypeConflict("the target file is a link"
|
|
|
" to a directory while the"
|
|
|
" template is a file")
|
|
|
else:
|
|
|
raise TemplateTypeConflict("the target file is a link"
|
|
|
" to a file while the"
|
|
|
" template is a file")
|
|
|
elif self.target_type == DIR:
|
|
|
raise TemplateTypeConflict("the target file is a directory"
|
|
|
" while the template is a file")
|
|
|
|
|
|
def _check_package_collision(self) -> None:
|
|
|
'''Метод для проверки на предмет коллизии, то есть конфликта пакета
|
|
|
шаблона и целевого файла.'''
|
|
|
if self.parameters.package:
|
|
|
parameter_package = self.parameters.package
|
|
|
else:
|
|
|
parameter_package = None
|
|
|
|
|
|
if self.target_type is not None:
|
|
|
try:
|
|
|
file_package = self.package_atom_parser.get_file_package(
|
|
|
self.target_path)
|
|
|
except PackageNotFound:
|
|
|
file_package = None
|
|
|
self.target_without_package = True
|
|
|
else:
|
|
|
file_package = None
|
|
|
|
|
|
# Если для шаблона и целевого файла никаким образом не удается
|
|
|
# определить пакет и есть параметр append -- шаблон пропускаем.
|
|
|
if parameter_package is None and file_package is None:
|
|
|
if (self.parameters.append and self.parameters.append != 'skip'
|
|
|
and not self.parameters.handler):
|
|
|
raise TemplateCollisionError(
|
|
|
"'package' parameter is not defined for"
|
|
|
" template with 'append' parameter")
|
|
|
else:
|
|
|
return
|
|
|
elif parameter_package is None:
|
|
|
self.target_package_name = file_package
|
|
|
|
|
|
elif file_package is None:
|
|
|
if self.parameters.handler:
|
|
|
raise TemplateCollisionError((
|
|
|
"The template is a handler while target"
|
|
|
" file belongs {} package").format(
|
|
|
file_package.atom
|
|
|
))
|
|
|
self.target_package_name = parameter_package
|
|
|
|
|
|
elif file_package != parameter_package and self.template_type != DIR:
|
|
|
target_name = self._compare_packages(parameter_package,
|
|
|
file_package)
|
|
|
if (target_name is not None and self.target_package is not None
|
|
|
and self.target_package.package_name == target_name):
|
|
|
target_name = self.target_package.package_name
|
|
|
|
|
|
if target_name is None or target_name.slot_specified:
|
|
|
raise TemplateCollisionError((
|
|
|
"The template package is {0} while target"
|
|
|
" file package is {1}").format(
|
|
|
parameter_package.atom,
|
|
|
file_package.atom
|
|
|
))
|
|
|
if (self.target_package is None or
|
|
|
self.target_package_name != self.target_package.package_name):
|
|
|
self.target_package = Package(self.target_package_name,
|
|
|
chroot_path=self.chroot_path,
|
|
|
autosave=True)
|
|
|
# Теперь перемещаем файл в пакет со старшей версией.
|
|
|
self.source_package = Package(file_package,
|
|
|
chroot_path=self.chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
removed = self.source_package.remove_file(self.target_path)
|
|
|
|
|
|
for file_path, file_info in removed.items():
|
|
|
if file_info['type'] == 'dir':
|
|
|
self.target_package.add_dir(file_path)
|
|
|
elif file_info['type'] == 'obj':
|
|
|
self.target_package.add_obj(file_path,
|
|
|
file_md5=file_info['md5'],
|
|
|
mtime=file_info['mtime'])
|
|
|
elif file_info['type'] == 'sym':
|
|
|
self.target_package.add_sym(
|
|
|
file_path,
|
|
|
target_path=file_info['target'],
|
|
|
mtime=file_info['mtime'])
|
|
|
else:
|
|
|
self.target_package_name = parameter_package
|
|
|
|
|
|
if (self.target_package is None or
|
|
|
self.target_package_name != self.target_package.package_name):
|
|
|
self.target_package = Package(self.target_package_name,
|
|
|
chroot_path=self.chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
|
|
|
def _compare_packages(self, lpackage: PackageAtomName,
|
|
|
rpackage: PackageAtomName
|
|
|
) -> Union[None, PackageAtomName]:
|
|
|
'''Метод, сравнивающий пакеты по их именам, возвращает старший, если
|
|
|
пакеты с одинаковыми именами, но разными версиями, или None, если их
|
|
|
имена и категории не равны.'''
|
|
|
if lpackage.category != rpackage.category:
|
|
|
return None
|
|
|
|
|
|
if lpackage.name != rpackage.name:
|
|
|
return None
|
|
|
|
|
|
if lpackage.version > rpackage.version:
|
|
|
return lpackage
|
|
|
else:
|
|
|
return rpackage
|
|
|
|
|
|
def _check_user_changes(self) -> None:
|
|
|
'''Метод для проверки наличия пользовательских изменений в
|
|
|
конфигурационных файлах.'''
|
|
|
# Эта проверка только для файлов.
|
|
|
if self.template_type != FILE:
|
|
|
return
|
|
|
|
|
|
# Проверим, является ли файл защищенным.
|
|
|
# Сначала проверяем по переменной CONFIG_PROTECT.
|
|
|
if self.dbpkg and not self.parameters.handler:
|
|
|
for protected_path in self._protected_set:
|
|
|
if self.target_path.startswith(protected_path):
|
|
|
self.protected = True
|
|
|
break
|
|
|
|
|
|
# Затем по переменной CONFIG_PROTECT_MASK.
|
|
|
for unprotected_path in self._unprotected_set:
|
|
|
if self.target_path.startswith(unprotected_path):
|
|
|
self.protected = False
|
|
|
break
|
|
|
else:
|
|
|
self.protected = False
|
|
|
|
|
|
# Собираем список имеющихся ._cfg файлов.
|
|
|
cfg_pattern = os.path.join(os.path.dirname(self.target_path),
|
|
|
"._cfg????_{}".format(
|
|
|
os.path.basename(self.target_path)))
|
|
|
self.cfg_list = glob.glob(cfg_pattern)
|
|
|
self.cfg_list.sort()
|
|
|
|
|
|
# Путь к архивной версии файла.
|
|
|
self.archive_path = self._get_archive_path(self.target_path)
|
|
|
|
|
|
self.contents_matching = (self.parameters.autoupdate)
|
|
|
|
|
|
if not self.protected:
|
|
|
self.contents_matching = True
|
|
|
|
|
|
elif self.parameters.unbound:
|
|
|
# Если присутствует unbound, то просто модифицируем файл и
|
|
|
# удаляем его из CONTENTS.
|
|
|
self.contents_matching = True
|
|
|
|
|
|
elif self.target_type is None:
|
|
|
# Если целевой файл отсутствует.
|
|
|
if (self.target_path in self.target_package and
|
|
|
not self.parameters.autoupdate):
|
|
|
# Проверка -- был ли файл удален.
|
|
|
self.contents_matching = False
|
|
|
pass
|
|
|
else:
|
|
|
self.contents_matching = True
|
|
|
|
|
|
elif self.target_without_package:
|
|
|
# Если файл по целевому пути не относится к какому-либо пакету.
|
|
|
# self.contents_matching = False
|
|
|
pass
|
|
|
elif self.target_type is DIR and self.parameters.force:
|
|
|
self.contents_matching = True
|
|
|
elif not self.contents_matching:
|
|
|
# Если файл есть и он относится к текущему пакету.
|
|
|
# Если по каким-то причинам уже нужно считать, что хэш-суммы
|
|
|
# совпадают -- в дальнейшей проверке нет необходимости.
|
|
|
# target_md5 = self.target_package.get_md5(self.target_path)
|
|
|
self.contents_matching = self.target_package.check_contents_data(
|
|
|
self.target_path,
|
|
|
symlink=self.target_is_link)
|
|
|
|
|
|
# Если по целевому пути файл не относящийся к какому-либо пакету и
|
|
|
# присутствует параметр autoupdate -- удаляем этот файл.
|
|
|
if (self.target_without_package and
|
|
|
(self.parameters.autoupdate or self.parameters.force)):
|
|
|
self.remove_original = True
|
|
|
|
|
|
# Определяем пути входных и выходных файлов.
|
|
|
if self.contents_matching:
|
|
|
# Приоритет отдаем пути из параметра source.
|
|
|
if self.parameters.source:
|
|
|
self.input_path = self.parameters.source
|
|
|
elif self.cfg_list and not self.parameters.unbound:
|
|
|
if os.path.exists(self.archive_path):
|
|
|
self.input_path = self.archive_path
|
|
|
else:
|
|
|
self.input_path = self.target_path
|
|
|
self.ca_is_missed = True
|
|
|
else:
|
|
|
self.input_path = self.target_path
|
|
|
|
|
|
self.output_path = self.target_path
|
|
|
else:
|
|
|
# Приоритет отдаем пути из параметра source.
|
|
|
if self.parameters.source:
|
|
|
self.input_path = self.parameters.source
|
|
|
else:
|
|
|
if os.path.exists(self.archive_path):
|
|
|
self.input_path = self.archive_path
|
|
|
else:
|
|
|
self.input_path = self.target_path
|
|
|
self.ca_is_missed = True
|
|
|
|
|
|
self.output_path = self._get_cfg_path(self.target_path)
|
|
|
|
|
|
def _get_archive_path(self, file_path: str) -> str:
|
|
|
'''Метод для получения пути к архивной версии указанного файла.'''
|
|
|
if self.chroot_path != "/" and file_path.startswith(self.chroot_path):
|
|
|
file_path = file_path[len(self.chroot_path):]
|
|
|
return join_paths(self.config_archive_path, file_path)
|
|
|
|
|
|
def _get_cfg_path(self, file_path: str) -> str:
|
|
|
'''Метод для получения пути для создания нового ._cfg????_ файла.'''
|
|
|
if self.cfg_list:
|
|
|
last_cfg_name = os.path.basename(self.cfg_list[-1])
|
|
|
|
|
|
slice_value = len('._cfg')
|
|
|
cfg_number = int(last_cfg_name[slice_value: slice_value + 4]) + 1
|
|
|
cfg_number = str(cfg_number)
|
|
|
else:
|
|
|
cfg_number = '0'
|
|
|
|
|
|
if len(cfg_number) < 4:
|
|
|
cfg_number = '0' * (4 - len(cfg_number)) + cfg_number
|
|
|
new_cfg_name = "._cfg{}_{}".format(cfg_number,
|
|
|
os.path.basename(file_path))
|
|
|
new_cfg_path = os.path.join(os.path.dirname(file_path), new_cfg_name)
|
|
|
|
|
|
return new_cfg_path
|
|
|
|
|
|
def remove_from_contents(self) -> None:
|
|
|
'''Метод для удаления целевого файла из CONTENTS.'''
|
|
|
if self.target_package is None:
|
|
|
return
|
|
|
|
|
|
if self.template_type == DIR:
|
|
|
self.target_package.remove_dir(self.target_path)
|
|
|
elif self.template_type == FILE:
|
|
|
self.target_package.remove_obj(self.target_path)
|
|
|
|
|
|
def clear_dir_contents(self) -> None:
|
|
|
'''Метод для удаления из CONTENTS всего содержимого директории после
|
|
|
применения append = "clear".'''
|
|
|
if self.template_type == DIR and self.target_package is not None:
|
|
|
self.target_package.clear_dir(self.target_path)
|
|
|
|
|
|
def add_to_contents(self, file_md5: Optional[str] = None) -> None:
|
|
|
'''Метод для добавления целевого файла в CONTENTS.'''
|
|
|
if self.target_package is None:
|
|
|
return
|
|
|
|
|
|
# В подавляющем большинстве случаев берем хэш-сумму из выходного файла,
|
|
|
# но если по какой-то причине выходного файла нет -- пытаемся
|
|
|
# по целевому. Такое поведение маловероятно, но, наверное, стоит
|
|
|
# учесть возможность такой ситуации.
|
|
|
if os.path.exists(self.output_path):
|
|
|
source_path = self.output_path
|
|
|
else:
|
|
|
source_path = self.target_path
|
|
|
|
|
|
if self.parameters.append == 'link':
|
|
|
self.target_package.add_sym(source_path,
|
|
|
self.parameters.source)
|
|
|
elif self.template_type == DIR:
|
|
|
self.target_package.add_dir(source_path)
|
|
|
|
|
|
elif self.template_type == FILE:
|
|
|
self.target_package.add_obj(source_path, file_md5=file_md5)
|
|
|
|
|
|
def update_contents_from_list(self, changed_list: dict) -> None:
|
|
|
'''Метод для изменения CONTENTS по списку измененных файлов.'''
|
|
|
print("UPDATE CONTENTS FROM LIST")
|
|
|
if self.target_package is None:
|
|
|
return
|
|
|
|
|
|
for file_path, mode in changed_list.items():
|
|
|
if mode in {"M", "N"}:
|
|
|
if os.path.islink(file_path):
|
|
|
self.target_package.add_sym(file_path)
|
|
|
|
|
|
elif os.path.isdir(file_path):
|
|
|
self.target_package.add_dir(file_path)
|
|
|
|
|
|
elif os.path.isfile(file_path):
|
|
|
self.target_package.add_obj(file_path)
|
|
|
elif mode == "D":
|
|
|
if os.path.islink(file_path) or os.path.isfile(file_path):
|
|
|
self.target_package.remove_obj(file_path)
|
|
|
|
|
|
elif os.path.isdir(file_path):
|
|
|
self.target_package.add_dir(file_path)
|
|
|
|
|
|
@classmethod
|
|
|
def _set_protected(cls, chroot_path: str) -> None:
|
|
|
'''Метод для получения множества защищенных директорий.'''
|
|
|
cls._protected_set = set()
|
|
|
cls._unprotected_set = set()
|
|
|
|
|
|
cls._protected_set.add(join_paths(chroot_path, '/etc'))
|
|
|
|
|
|
config_protect_env = os.environ.get('CONFIG_PROTECT', False)
|
|
|
if config_protect_env:
|
|
|
for protected_path in config_protect_env.split():
|
|
|
protected_path = join_paths(chroot_path,
|
|
|
protected_path.strip())
|
|
|
cls._protected_set.add(protected_path)
|
|
|
|
|
|
config_protect_mask_env = os.environ.get('CONFIG_PROTECT_MASK', False)
|
|
|
if config_protect_mask_env:
|
|
|
for unprotected_path in config_protect_mask_env.split():
|
|
|
unprotected_path = join_paths(chroot_path,
|
|
|
unprotected_path.strip())
|
|
|
cls._unprotected_set.add(unprotected_path)
|
|
|
|
|
|
cls._protected_is_set = True
|
|
|
|
|
|
def save_changes(self) -> None:
|
|
|
'''Метод для сохранения изменений внесенных в CONTENTS.'''
|
|
|
if self.target_package:
|
|
|
self.target_package.remove_empty_directories()
|
|
|
self.target_package.write_contents_file()
|
|
|
|
|
|
|
|
|
class TemplateExecutor:
|
|
|
'''Класс исполнительного модуля.'''
|
|
|
def __init__(self,
|
|
|
datavars_module: Union[Datavars, Variables] = Variables(),
|
|
|
chroot_path: str = '/',
|
|
|
cl_config_archive: str = '/var/lib/calculate/config-archive',
|
|
|
cl_config_path: str = '/var/lib/calculate/config',
|
|
|
execute_archive_path: str = '/var/lib/calculate/.execute/',
|
|
|
dbpkg: bool = True,
|
|
|
pkg_autosave: bool = False):
|
|
|
# TODO добавить список измененных файлов.
|
|
|
self.datavars_module: Union[Datavars, Variables] = datavars_module
|
|
|
|
|
|
self.chroot_path: str = chroot_path
|
|
|
|
|
|
# Объект для проверки файловых систем. Пока не инициализируем.
|
|
|
self.mounts: Union[Mounts, None] = None
|
|
|
|
|
|
# Директория для хранения полученных при обработке exec скриптов.
|
|
|
self.execute_archive_path: str = execute_archive_path
|
|
|
self.execute_files: OrderedDict = OrderedDict()
|
|
|
|
|
|
self.dbpkg: bool = dbpkg
|
|
|
self._pkg_autosave: bool = pkg_autosave
|
|
|
|
|
|
# Словарь с измененными файлами и статусами их изменений.
|
|
|
self.changed_files: dict = {}
|
|
|
|
|
|
# Список целевых путей измененных файлов. Нужен для корректного
|
|
|
# формирования calculate-заголовка.
|
|
|
self.processed_targets: list = []
|
|
|
|
|
|
# TODO разобраться с этим.
|
|
|
# Значения параметров по умолчанию, пока не используются.
|
|
|
self.directory_default_parameters: dict =\
|
|
|
ParametersProcessor.directory_default_parameters
|
|
|
self.file_default_parameters: dict =\
|
|
|
ParametersProcessor.file_default_parameters
|
|
|
|
|
|
# Отображение имен действий для директорий на методы их реализующие.
|
|
|
self.directory_appends: dict = {
|
|
|
'join': self._append_join_directory,
|
|
|
'remove': self._append_remove_directory,
|
|
|
'skip': self._append_skip_directory,
|
|
|
'clear': self._append_clear_directory,
|
|
|
'link': self._append_link_directory,
|
|
|
'replace': self._append_replace_directory}
|
|
|
|
|
|
# Отображение имен действий для файлов на методы их реализующие.
|
|
|
self.file_appends: dict = {'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: set = ParametersProcessor.available_formats
|
|
|
self.calculate_config_file: CalculateConfigFile = CalculateConfigFile(
|
|
|
cl_config_path=cl_config_path,
|
|
|
cl_chroot_path=chroot_path)
|
|
|
self.cl_config_archive_path: str = cl_config_archive
|
|
|
Format.CALCULATE_VERSION = CALCULATE_VERSION
|
|
|
|
|
|
@property
|
|
|
def available_appends(self) -> set:
|
|
|
'''Метод для получения множества возможных значений append.'''
|
|
|
appends_set = set(self.directory_appends.keys()).union(
|
|
|
set(self.file_appends.keys()))
|
|
|
|
|
|
return appends_set
|
|
|
|
|
|
def execute_template(self, target_path: str,
|
|
|
parameters: ParametersContainer, template_type: int,
|
|
|
template_path: str, template_text: str = '',
|
|
|
save_changes: bool = True,
|
|
|
target_package: Optional[Package] = None) -> dict:
|
|
|
'''Метод для запуска выполнения шаблонов.'''
|
|
|
# Словарь с данными о результате работы исполнительного метода.
|
|
|
self.executor_output = {'target_path': None,
|
|
|
'stdout': None,
|
|
|
'stderr': None,
|
|
|
'warnings': []}
|
|
|
if parameters.append == 'skip':
|
|
|
return self.executor_output
|
|
|
|
|
|
try:
|
|
|
template_object = TemplateWrapper(
|
|
|
target_path, parameters,
|
|
|
template_type,
|
|
|
template_path,
|
|
|
template_text=template_text,
|
|
|
target_package=target_package,
|
|
|
chroot_path=self.chroot_path,
|
|
|
config_archive_path=self.cl_config_archive_path,
|
|
|
dbpkg=self.dbpkg,
|
|
|
pkg_autosave=self._pkg_autosave)
|
|
|
|
|
|
except TemplateTypeConflict as error:
|
|
|
raise TemplateExecutorError("type conflict: {}".format(str(error)))
|
|
|
|
|
|
except TemplateCollisionError as error:
|
|
|
raise TemplateExecutorError("collision: {}".format(str(error)))
|
|
|
|
|
|
# Удаляем оригинал, если это необходимо из-за наличия force или по
|
|
|
# другим причинам.
|
|
|
if template_object.remove_original:
|
|
|
if template_object.target_type == DIR:
|
|
|
self._remove_directory(template_object.target_path)
|
|
|
else:
|
|
|
self._remove_file(template_object.target_path)
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.remove_from_contents()
|
|
|
template_object.target_type = None
|
|
|
|
|
|
# Если был включен mirror, то после удаления файла завершаем
|
|
|
# выполнение шаблона.
|
|
|
if template_object.parameters.mirror:
|
|
|
# if save_changes:
|
|
|
# template_object.save_changes()
|
|
|
return self.executor_output
|
|
|
|
|
|
template_object.target_type = None
|
|
|
|
|
|
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:
|
|
|
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](
|
|
|
template_object)
|
|
|
|
|
|
# Сохраняем изменения в CONTENTS внесенные согласно шаблону.
|
|
|
# if save_changes:
|
|
|
# template_object.save_changes()
|
|
|
|
|
|
# Возвращаем целевой путь, если он был изменен, или
|
|
|
# None если не был изменен.
|
|
|
if template_object.target_path_is_changed:
|
|
|
self.executor_output['target_path'] =\
|
|
|
template_object.target_path
|
|
|
|
|
|
if template_object.ca_is_missed:
|
|
|
self.executor_output['warnings'].append(
|
|
|
"archive file is missed,"
|
|
|
" target file was used instead")
|
|
|
|
|
|
return self.executor_output
|
|
|
|
|
|
def save_changes(self) -> None:
|
|
|
'''Метод для сохранения чего-нибудь после выполнения всех шаблонов.'''
|
|
|
# Пока сохраняем только получившееся содержимое config-файла.
|
|
|
self.calculate_config_file.save_changes()
|
|
|
|
|
|
def _append_join_directory(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия для append = "join", если шаблон --
|
|
|
директория. Создает директорию, если ее нет.'''
|
|
|
if template_object.target_type is None:
|
|
|
self._create_directory(template_object)
|
|
|
|
|
|
# Корректируем список измененных файлов.
|
|
|
if template_object.target_path in self.changed_files:
|
|
|
self.changed_files[template_object.target_path] = 'M'
|
|
|
else:
|
|
|
self.changed_files[template_object.target_path] = 'N'
|
|
|
else:
|
|
|
if template_object.parameters.chmod:
|
|
|
self._chmod_directory(template_object.target_path,
|
|
|
template_object.parameters.chmod)
|
|
|
if template_object.parameters.chown:
|
|
|
self._chown_directory(template_object.target_path,
|
|
|
template_object.parameters.chown)
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.add_to_contents()
|
|
|
|
|
|
def _append_remove_directory(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия для append = "remove", если шаблон --
|
|
|
директория. Удаляет директорию со всем содержимым, если она есть.'''
|
|
|
if template_object.target_type is not None:
|
|
|
changed = [template_object.target_path]
|
|
|
changed.extend(get_directory_contents(template_object.target_path))
|
|
|
self._remove_directory(template_object.target_path)
|
|
|
|
|
|
# Добавляем все содержимое директории в список измененных файлов.
|
|
|
for file_path in changed:
|
|
|
if file_path in self.changed_files:
|
|
|
if self.changed_files[file_path] == 'N':
|
|
|
self.changed_files.pop(file_path)
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.remove_from_contents()
|
|
|
|
|
|
def _append_skip_directory(self,
|
|
|
template_object: TemplateWrapper) -> None:
|
|
|
pass
|
|
|
|
|
|
def _append_clear_directory(self,
|
|
|
template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия для append = "clear", если шаблон --
|
|
|
директория. Удаляет все содержимое директории, если она есть.'''
|
|
|
if template_object.target_type is not None:
|
|
|
dir_contents = get_directory_contents(template_object.target_path)
|
|
|
self._clear_directory(template_object.target_path)
|
|
|
|
|
|
# Добавляем все содержимое директории в список изменных файлов.
|
|
|
for file_path in dir_contents:
|
|
|
if file_path in self.changed_files:
|
|
|
if self.changed_files[file_path] == 'N':
|
|
|
self.changed_files.pop(file_path)
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
|
|
|
# Меняем права и владельца очищенной директории, если это
|
|
|
# необходимо.
|
|
|
if template_object.parameters.chmod:
|
|
|
self._chmod_directory(template_object.target_path,
|
|
|
template_object.parameters.chmod)
|
|
|
|
|
|
if template_object.parameters.chown:
|
|
|
self._chown_directory(template_object.target_path,
|
|
|
template_object.parameters.chown)
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.clear_dir_contents()
|
|
|
|
|
|
def _append_link_directory(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия для append = "link", если шаблон --
|
|
|
директория. Создает ссылку на директорию, если она есть.'''
|
|
|
self._link_directory(template_object.parameters.source,
|
|
|
template_object.target_path)
|
|
|
|
|
|
# Корректируем список измененных файлов.
|
|
|
if template_object.target_path in self.changed_files:
|
|
|
self.changed_files[template_object.target_path] = 'M'
|
|
|
else:
|
|
|
self.changed_files[template_object.target_path] = 'N'
|
|
|
|
|
|
# Меняем права и владельца файла, на который указывает ссылка.
|
|
|
if template_object.parameters.chmod:
|
|
|
self._chmod_directory(template_object.parameters.source,
|
|
|
template_object.parameters.chmod)
|
|
|
|
|
|
if template_object.parameters.chown:
|
|
|
self._chown_directory(template_object.parameters.source,
|
|
|
template_object.parameters.chown)
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.add_to_contents()
|
|
|
|
|
|
def _append_replace_directory(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия для append = "replace", если шаблон --
|
|
|
директория. Очищает директорию или создает, если ее нет.'''
|
|
|
if template_object.target_type is None:
|
|
|
self._create_directory(template_object)
|
|
|
if self.dbpkg:
|
|
|
template_object.add_to_contents()
|
|
|
|
|
|
# Корректируем список измененных файлов.
|
|
|
if template_object.target_path in self.changed_files:
|
|
|
self.changed_files[template_object.target_path] = 'M'
|
|
|
else:
|
|
|
self.changed_files[template_object.target_path] = 'N'
|
|
|
else:
|
|
|
dir_contents = get_directory_contents(template_object.target_path)
|
|
|
self._clear_directory(template_object.target_path)
|
|
|
|
|
|
# Добавляем все содержимое директории в список измененных файлов.
|
|
|
for file_path in dir_contents:
|
|
|
if file_path in self.changed_files:
|
|
|
if self.changed_files[file_path] == 'N':
|
|
|
self.changed_files.pop(file_path)
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.clear_dir_contents()
|
|
|
|
|
|
def _append_join_file(self, template_object: TemplateWrapper,
|
|
|
join_before: bool = False, replace: bool = False
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия при append = "join", если шаблон -- файл.
|
|
|
Объединяет шаблон с целевым файлом.'''
|
|
|
output_path = template_object.output_path
|
|
|
|
|
|
input_file_md5 = None
|
|
|
output_file_md5 = None
|
|
|
|
|
|
template_format = template_object.format_class
|
|
|
|
|
|
# Задаемся значениями chmod и chown в зависимости от наличия или
|
|
|
# отсутствия файла, принадлежности его пакету и наличия значений
|
|
|
# параметров по умолчанию.
|
|
|
chmod = self._get_chmod(template_object)
|
|
|
chown = self._get_chown(template_object)
|
|
|
|
|
|
if template_format.EXECUTABLE or template_object.contents_matching:
|
|
|
# Действия, если целевой файл не имеет пользовательских изменений
|
|
|
# или если он исполнительный.
|
|
|
# Парсим текст шаблона используя его формат.
|
|
|
if (not template_object.template_text.strip()
|
|
|
and template_object.parameters.source
|
|
|
and template_object.parameters.append == "replace"
|
|
|
and template_object.parameters.format == "raw"):
|
|
|
# Если шаблон пуст, параметром source задан входной файл,
|
|
|
# и при этом формат шаблона raw и append = 'replace'
|
|
|
# значит этот шаблон предназначен для копирования.
|
|
|
if template_object.target_type is not None:
|
|
|
self._remove_file(template_object.target_path)
|
|
|
output_file_md5 = self._copy_from_source(template_object,
|
|
|
chown=chown,
|
|
|
chmod=chmod)
|
|
|
elif not template_object.format_class.EXECUTABLE:
|
|
|
parsed_template = template_format(
|
|
|
template_object.template_text,
|
|
|
template_object.template_path,
|
|
|
ignore_comments=True,
|
|
|
chroot_path=self.chroot_path)
|
|
|
# Действия для шаблонов не являющихся исполнительными.
|
|
|
output_paths = [output_path]
|
|
|
|
|
|
# Если целевой файл защищен, а шаблон не userspace.
|
|
|
if (template_object.protected
|
|
|
and not template_object.is_userspace):
|
|
|
# Тогда также обновляем архив.
|
|
|
output_paths.append(template_object.archive_path)
|
|
|
|
|
|
input_text = self._get_input_text(template_object,
|
|
|
replace=replace)
|
|
|
if (replace and template_object.target_type is not None and
|
|
|
os.path.exists(output_path)):
|
|
|
self._clear_file(output_path)
|
|
|
|
|
|
# Если шаблон не исполнительный разбираем входной текст.
|
|
|
parsed_input = template_format(
|
|
|
input_text,
|
|
|
template_object.template_path,
|
|
|
add_header=True,
|
|
|
join_before=join_before,
|
|
|
already_changed=(template_object.target_path
|
|
|
in self.processed_targets),
|
|
|
parameters=template_object.parameters)
|
|
|
parsed_input.join_template(parsed_template)
|
|
|
|
|
|
# Результат наложения шаблона.
|
|
|
output_text = parsed_input.document_text
|
|
|
|
|
|
# Удаляем форматный объект входного файла.
|
|
|
# del(parsed_input)
|
|
|
output_file_md5 = hashlib.md5(output_text.encode()).hexdigest()
|
|
|
if input_text:
|
|
|
input_file_md5 = hashlib.md5(
|
|
|
input_text.encode()).hexdigest()
|
|
|
|
|
|
for save_path in output_paths:
|
|
|
if not os.path.exists(os.path.dirname(save_path)):
|
|
|
self._create_directory(
|
|
|
template_object,
|
|
|
path_to_create=os.path.dirname(save_path))
|
|
|
with open(save_path, 'w') as output_file:
|
|
|
output_file.write(output_text)
|
|
|
|
|
|
# Меняем права доступа и владельца всех сохраняемых файлов,
|
|
|
# если это необходимо.
|
|
|
if chown:
|
|
|
self._chown_file(save_path, chown)
|
|
|
if chmod:
|
|
|
self._chmod_file(save_path, chmod)
|
|
|
elif template_object.format_class.EXECUTABLE:
|
|
|
parsed_template = template_format(
|
|
|
template_object.template_text,
|
|
|
template_object.template_path,
|
|
|
parameters=template_object.parameters,
|
|
|
datavars=self.datavars_module)
|
|
|
changed_files = parsed_template.execute_format(
|
|
|
template_object.target_path,
|
|
|
chroot_path=self.chroot_path)
|
|
|
# Удаляем форматный объект входного файла.
|
|
|
# del(parsed_template)
|
|
|
|
|
|
# Если исполняемый формат выдал список измененных файлов для
|
|
|
# изменения CONTENTS и при этом задан пакет -- обновляем
|
|
|
# CONTENTS.
|
|
|
for file_path, status in changed_files.items():
|
|
|
if status == 'M':
|
|
|
if file_path in self.changed_files:
|
|
|
if self.changed_files[file_path] == 'D':
|
|
|
self.changed_files[file_path] == 'N'
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'M'
|
|
|
elif status == 'D':
|
|
|
if file_path in self.changed_files:
|
|
|
if self.changed_files[file_path] == 'M':
|
|
|
self.changed_files[file_path] == 'D'
|
|
|
if self.changed_files[file_path] == 'N':
|
|
|
self.changed_files.pop(file_path)
|
|
|
else:
|
|
|
self.changed_files[file_path] = 'D'
|
|
|
|
|
|
# Если в ходе работы формат выкидывал предупреждения --
|
|
|
# добавляем их в список предупреждений.
|
|
|
if parsed_template.warnings:
|
|
|
self.executor_output['warnings'].extend(
|
|
|
parsed_template.warnings)
|
|
|
|
|
|
if (self.dbpkg and changed_files and
|
|
|
template_object.target_package and
|
|
|
template_object.format_class.FORMAT != 'contents'):
|
|
|
template_object.update_contents_from_list(changed_files)
|
|
|
return
|
|
|
|
|
|
if input_file_md5 != output_file_md5:
|
|
|
if template_object.target_type is None:
|
|
|
self.changed_files[
|
|
|
template_object.target_path] = 'N'
|
|
|
else:
|
|
|
self.changed_files[
|
|
|
template_object.target_path] = 'M'
|
|
|
|
|
|
if self.dbpkg:
|
|
|
self._remove_cfg_files(template_object)
|
|
|
if template_object.parameters.unbound:
|
|
|
template_object.remove_from_contents()
|
|
|
else:
|
|
|
template_object.add_to_contents(
|
|
|
file_md5=output_file_md5)
|
|
|
else:
|
|
|
input_text = self._get_input_text(template_object,
|
|
|
replace=replace)
|
|
|
parsed_input = template_format(
|
|
|
input_text,
|
|
|
template_object.template_path,
|
|
|
add_header=True,
|
|
|
join_before=join_before,
|
|
|
already_changed=False,
|
|
|
parameters=template_object.parameters)
|
|
|
parsed_template = template_format(template_object.template_text,
|
|
|
template_object.template_path,
|
|
|
ignore_comments=True)
|
|
|
parsed_input.join_template(parsed_template)
|
|
|
|
|
|
# Результат наложения шаблона.
|
|
|
output_text = parsed_input.document_text
|
|
|
|
|
|
# Удаляем форматный объект входного файла.
|
|
|
del(parsed_input)
|
|
|
output_file_md5 = hashlib.md5(output_text.encode()).hexdigest()
|
|
|
|
|
|
if not self.calculate_config_file.compare_md5(
|
|
|
template_object.target_path,
|
|
|
output_file_md5):
|
|
|
if not os.path.exists(os.path.dirname(output_path)):
|
|
|
self._create_directory(
|
|
|
template_object,
|
|
|
path_to_create=os.path.dirname(output_path))
|
|
|
|
|
|
with open(output_path, 'w') as output_file:
|
|
|
output_file.write(output_text)
|
|
|
|
|
|
# Меняем права доступа и владельца ._cfg????_ файлов, если
|
|
|
# это необходимо.
|
|
|
if chown:
|
|
|
self._chown_file(output_path, chown)
|
|
|
if chmod:
|
|
|
self._chmod_file(output_path, chmod)
|
|
|
|
|
|
# Обновляем CL.
|
|
|
self.calculate_config_file.set_files_md5(
|
|
|
template_object.target_path,
|
|
|
output_file_md5)
|
|
|
|
|
|
self.changed_files[template_object.target_path] = 'C'
|
|
|
# Обновляем CONTENTS.
|
|
|
template_object.add_to_contents(file_md5=output_file_md5)
|
|
|
else:
|
|
|
# Действия если CL совпало. Hичего не делаем.
|
|
|
pass
|
|
|
|
|
|
def _get_input_text(self, template_object: TemplateWrapper,
|
|
|
replace: bool = False) -> str:
|
|
|
'''Метод для получения текста входного файла.'''
|
|
|
# TODO Сделать это как-нибудь получше
|
|
|
input_path = template_object.input_path
|
|
|
|
|
|
if (not replace and (template_object.target_type is not None
|
|
|
or template_object.parameters.source)):
|
|
|
# Если целевой файл есть и нет параметра replace --
|
|
|
# используем текст целевого файла.
|
|
|
if (not input_path.startswith(self.cl_config_archive_path)
|
|
|
or os.path.exists(input_path)):
|
|
|
# Если входной файл просто не из архива, или из архива
|
|
|
# и при этом существует -- используем его
|
|
|
try:
|
|
|
with open(input_path, 'r') as input_file:
|
|
|
input_text = input_file.read()
|
|
|
return input_text
|
|
|
except UnicodeDecodeError:
|
|
|
raise TemplateExecutorError("can not join binary files.")
|
|
|
return ''
|
|
|
|
|
|
def _remove_cfg_files(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод для удаления всех ._cfg-файлов и хэш-сумм добавленных в
|
|
|
config-файл.'''
|
|
|
# Убираем все ._cfg файлы.
|
|
|
if template_object.cfg_list:
|
|
|
for cfg_file_path in template_object.cfg_list:
|
|
|
self._remove_file(cfg_file_path)
|
|
|
|
|
|
# Убираем целевой файл из CL.
|
|
|
self.calculate_config_file.remove_file(template_object.target_path)
|
|
|
|
|
|
def _copy_from_source(self, template_object: TemplateWrapper,
|
|
|
chown: Optional[dict] = None,
|
|
|
chmod: Optional[int] = None) -> str:
|
|
|
'''Метод для копирования файла, указанного в source.'''
|
|
|
output_path = template_object.output_path
|
|
|
source_path = template_object.input_path
|
|
|
|
|
|
output_directory = os.path.dirname(output_path)
|
|
|
if not os.path.exists(output_directory):
|
|
|
self._create_directory(template_object,
|
|
|
path_to_create=output_directory)
|
|
|
shutil.copy(source_path, output_path)
|
|
|
|
|
|
if chown:
|
|
|
self._chown_file(template_object.output_path, chown)
|
|
|
if chmod:
|
|
|
self._chmod_file(template_object.output_path, chmod)
|
|
|
|
|
|
with open(output_path, "rb") as output_file:
|
|
|
output_file_data = output_file.read()
|
|
|
output_file_md5 = hashlib.md5(output_file_data).hexdigest()
|
|
|
return output_file_md5
|
|
|
|
|
|
def _get_chmod(self, template_object: TemplateWrapper) -> int:
|
|
|
chmod = template_object.parameters.chmod
|
|
|
if not chmod:
|
|
|
if (template_object.target_type is not None and
|
|
|
not template_object.target_without_package):
|
|
|
chmod = self._get_file_mode(template_object.target_path)
|
|
|
else:
|
|
|
chmod = self.file_default_parameters.get('chmod', False)
|
|
|
return chmod
|
|
|
|
|
|
def _get_chown(self, template_object: TemplateWrapper) -> dict:
|
|
|
chown = template_object.parameters.chown
|
|
|
if not chown:
|
|
|
if (template_object.target_type is not None and
|
|
|
not template_object.target_without_package):
|
|
|
chown = self._get_file_owner(template_object.target_path)
|
|
|
else:
|
|
|
chown = self.file_default_parameters.get('chown', False)
|
|
|
return chown
|
|
|
|
|
|
def _append_after_file(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия при append = "after", если шаблон --
|
|
|
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
|
|
|
в конец файла и в конец каждой секции файла.'''
|
|
|
self._append_join_file(template_object, join_before=False)
|
|
|
|
|
|
def _append_before_file(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия при append = "after", если шаблон --
|
|
|
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
|
|
|
в начало файла и в начало каждой секции файла.'''
|
|
|
self._append_join_file(template_object, join_before=True)
|
|
|
|
|
|
def _append_skip_file(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия при append = "skip". Пока никаких
|
|
|
действий.'''
|
|
|
pass
|
|
|
|
|
|
def _append_replace_file(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия при append = "replace", если шаблон --
|
|
|
файл. Очищает файл и затем накладывает на него шаблон.'''
|
|
|
self._append_join_file(template_object, replace=True)
|
|
|
|
|
|
def _append_remove_file(self, template_object: TemplateWrapper
|
|
|
) -> None:
|
|
|
'''Метод описывающий действия при append = "remove", если шаблон --
|
|
|
файл. Удаляет файл.'''
|
|
|
if template_object.target_type is not None:
|
|
|
self._remove_file(template_object.target_path)
|
|
|
|
|
|
if template_object.target_path in self.changed_files:
|
|
|
if self.changed_files[template_object.target_path] == 'N':
|
|
|
self.changed_files.pop(template_object.target_path)
|
|
|
elif self.changed_files[template_object.target_path] == 'M':
|
|
|
self.changed_files[template_object.target_path] = 'D'
|
|
|
else:
|
|
|
self.changed_files[template_object.target_path] = 'D'
|
|
|
if self.dbpkg:
|
|
|
template_object.remove_from_contents()
|
|
|
|
|
|
def _append_clear_file(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия при append = "clear", если шаблон --
|
|
|
файл. Очищает файл.'''
|
|
|
if template_object.target_type is not None:
|
|
|
self._clear_file(template_object.target_path)
|
|
|
|
|
|
# Меняем владельца и права доступа к очищенному файлу, если нужно.
|
|
|
if template_object.parameters.chown:
|
|
|
self._chown_file(template_object.target_path,
|
|
|
template_object.parameters.chown)
|
|
|
|
|
|
if template_object.parameters.chmod:
|
|
|
self._chmod_file(template_object.target_path,
|
|
|
template_object.parameters.chmod)
|
|
|
|
|
|
# Корректируем список измененных файлов.
|
|
|
if template_object.target_path not in self.changed_files:
|
|
|
self.changed_files[template_object.target_path] = 'M'
|
|
|
|
|
|
if self.dbpkg:
|
|
|
template_object.add_to_contents()
|
|
|
|
|
|
def _append_link_file(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод описывающий действия при append = "link", если шаблон --
|
|
|
файл. Создает ссылку на файл, указанный в параметре source.'''
|
|
|
input_path = template_object.input_path
|
|
|
output_path = template_object.output_path
|
|
|
|
|
|
if template_object.contents_matching:
|
|
|
output_paths = [output_path]
|
|
|
# Если целевой файл защищен, а шаблон не userspace,
|
|
|
# тогда также обновляем архив.
|
|
|
if (template_object.protected
|
|
|
and not template_object.is_userspace):
|
|
|
output_paths.append(template_object.archive_path)
|
|
|
|
|
|
for link_path in output_paths:
|
|
|
if not os.path.exists(os.path.dirname(link_path)):
|
|
|
self._create_directory(
|
|
|
template_object,
|
|
|
path_to_create=os.path.dirname(link_path))
|
|
|
elif os.path.exists(link_path):
|
|
|
os.unlink(link_path)
|
|
|
self._link_file(input_path, link_path)
|
|
|
|
|
|
# Корректируем список измененных файлов.
|
|
|
if template_object.target_path not in self.changed_files:
|
|
|
self.changed_files[template_object.target_path] = 'N'
|
|
|
else:
|
|
|
self.changed_files[template_object.target_path] = 'M'
|
|
|
|
|
|
if self.dbpkg:
|
|
|
# Убираем целевой файл из CL.
|
|
|
self.calculate_config_file.remove_file(
|
|
|
template_object.target_path)
|
|
|
if template_object.parameters.unbound:
|
|
|
template_object.remove_from_contents()
|
|
|
else:
|
|
|
template_object.add_to_contents()
|
|
|
else:
|
|
|
source_path_md5 = self._get_source_hash(input_path)
|
|
|
if not self.calculate_config_file.compare_md5(
|
|
|
template_object.target_path,
|
|
|
source_path_md5
|
|
|
):
|
|
|
if not os.path.exists(os.path.dirname(output_path)):
|
|
|
self._create_directory(
|
|
|
template_object,
|
|
|
path_to_create=os.path.dirname(output_path))
|
|
|
elif os.path.exists(output_path):
|
|
|
os.unlink(output_path)
|
|
|
self._link_file(input_path, output_path)
|
|
|
|
|
|
self.changed_files[template_object.target_path] = 'C'
|
|
|
if self.dbpkg:
|
|
|
template_object.add_to_contents()
|
|
|
|
|
|
# Обновляем CL.
|
|
|
self.calculate_config_file.set_files_md5(
|
|
|
template_object.target_path,
|
|
|
source_path_md5)
|
|
|
self.changed_files[template_object.target_path] = 'C'
|
|
|
# Обновляем CONTENTS.
|
|
|
template_object.add_to_contents()
|
|
|
else:
|
|
|
# Действия если CL совпало. Hичего не делаем.
|
|
|
pass
|
|
|
|
|
|
# Меняем права и владельца файла, на который указывает ссылка.
|
|
|
if template_object.parameters.chmod:
|
|
|
self._chmod_file(input_path, template_object.parameters.chmod)
|
|
|
|
|
|
if template_object.parameters.chown:
|
|
|
self._chown_file(input_path, template_object.parameters.chown)
|
|
|
|
|
|
def _get_source_hash(self, source_path: str) -> str:
|
|
|
if (self.chroot_path != "/" and
|
|
|
source_path.startswith(self.chroot_path)):
|
|
|
source_path = source_path[len(self.chroot_path):]
|
|
|
if not source_path.startswith("/"):
|
|
|
source_path = "/" + source_path
|
|
|
return hashlib.md5(source_path.encode()).hexdigest()
|
|
|
|
|
|
def _create_directory(self, template_object: TemplateWrapper,
|
|
|
path_to_create: Optional[str] = None) -> None:
|
|
|
'''Метод для создания директории и, при необходимости, изменения
|
|
|
владельца и доступа все директорий на пути к целевой.'''
|
|
|
if path_to_create is None:
|
|
|
target_path = template_object.target_path
|
|
|
else:
|
|
|
target_path = path_to_create
|
|
|
|
|
|
template_parameters = template_object.parameters
|
|
|
|
|
|
# Если файл есть, но указан chmod или chown -- используем их.
|
|
|
if os.access(target_path, os.F_OK):
|
|
|
if template_parameters.chmod:
|
|
|
self._chmod_directory(target_path, template_parameters.chmod)
|
|
|
|
|
|
if template_parameters.chown:
|
|
|
self._chown_directory(target_path, template_parameters.chown)
|
|
|
return
|
|
|
|
|
|
directories_to_create = [target_path]
|
|
|
directory_path = os.path.dirname(target_path)
|
|
|
|
|
|
# Составляем список путей к директориям, которые нужно создать.
|
|
|
while not os.access(directory_path, os.F_OK) and directory_path:
|
|
|
directories_to_create.append(directory_path)
|
|
|
directory_path = os.path.dirname(directory_path)
|
|
|
|
|
|
# получаем информацию о владельце и правах доступа ближайшей
|
|
|
# существующей директории.
|
|
|
chmod = template_parameters.chmod
|
|
|
if not chmod:
|
|
|
chmod = self._get_file_mode(directory_path)
|
|
|
|
|
|
chown = template_parameters.chown
|
|
|
if not chown:
|
|
|
chown = self._get_file_owner(directory_path)
|
|
|
|
|
|
directories_to_create.reverse()
|
|
|
|
|
|
# создаем директории.
|
|
|
for create_path in directories_to_create:
|
|
|
try:
|
|
|
os.mkdir(create_path)
|
|
|
|
|
|
# Для каждой созданной директории меняем права и владельца
|
|
|
# если это необходимо.
|
|
|
if chmod:
|
|
|
self._chmod_directory(create_path, chmod)
|
|
|
|
|
|
if chown:
|
|
|
self._chown_directory(create_path, chown)
|
|
|
|
|
|
except OSError as error:
|
|
|
raise TemplateExecutorError(
|
|
|
'Failed to create directory: {}, reason: {}'.
|
|
|
format(create_path, str(error)))
|
|
|
|
|
|
def _remove_directory(self, target_path: str) -> None:
|
|
|
'''Метод для удаления директории.'''
|
|
|
if os.path.exists(target_path):
|
|
|
if os.path.isdir(target_path):
|
|
|
try:
|
|
|
if os.path.islink(target_path):
|
|
|
os.unlink(target_path)
|
|
|
else:
|
|
|
shutil.rmtree(target_path)
|
|
|
return
|
|
|
except Exception as error:
|
|
|
raise TemplateExecutorError(
|
|
|
("Failed to delete the directory: {0},"
|
|
|
" reason: {1}").format(target_path,
|
|
|
str(error)))
|
|
|
else:
|
|
|
error_message = "target file is not directory"
|
|
|
else:
|
|
|
error_message = "target file does not exist"
|
|
|
|
|
|
raise TemplateExecutorError(("Failed to delete the directory: {0},"
|
|
|
"reason: {1}").format(target_path,
|
|
|
error_message))
|
|
|
|
|
|
def _clear_directory(self, target_path: str) -> None:
|
|
|
'''Метод для очистки содержимого целевой директории.'''
|
|
|
if os.path.exists(target_path):
|
|
|
if os.path.isdir(target_path):
|
|
|
# Удаляем все содержимое директории.
|
|
|
for node in os.scandir(target_path):
|
|
|
if node.is_dir():
|
|
|
self._remove_directory(node.path)
|
|
|
else:
|
|
|
self._remove_file(node.path)
|
|
|
return
|
|
|
else:
|
|
|
error_message = "target file is not directory"
|
|
|
else:
|
|
|
error_message = "target directory does not exist"
|
|
|
|
|
|
raise TemplateExecutorError(("failed to clear directory: {},"
|
|
|
" reason: {}").format(target_path,
|
|
|
error_message))
|
|
|
|
|
|
def _link_directory(self, source: str, target_path: str,
|
|
|
force: bool = False) -> None:
|
|
|
'''Метод для создания по целевому пути ссылки на директорию
|
|
|
расположенную на пути, указанном в source.'''
|
|
|
try:
|
|
|
os.symlink(source, target_path, target_is_directory=True)
|
|
|
except OSError as error:
|
|
|
raise TemplateExecutorError(
|
|
|
"failed to create symlink: {0} -> {1}, reason: {2}".
|
|
|
format(target_path, source, str(error)))
|
|
|
|
|
|
def _remove_file(self, target_path: str) -> None:
|
|
|
'''Метод для удаления файлов.'''
|
|
|
if os.path.exists(target_path):
|
|
|
if os.path.isfile(target_path):
|
|
|
if os.path.islink(target_path):
|
|
|
try:
|
|
|
os.unlink(target_path)
|
|
|
return
|
|
|
except OSError as error:
|
|
|
error_message = str(error)
|
|
|
|
|
|
try:
|
|
|
os.remove(target_path)
|
|
|
return
|
|
|
except OSError as error:
|
|
|
error_message = str(error)
|
|
|
else:
|
|
|
error_message = 'target is not a file'
|
|
|
elif os.path.islink(target_path):
|
|
|
try:
|
|
|
os.unlink(target_path)
|
|
|
return
|
|
|
except OSError as error:
|
|
|
error_message = str(error)
|
|
|
else:
|
|
|
error_message = 'target file does not exist'
|
|
|
|
|
|
raise TemplateExecutorError(("failed to remove the file: {0},"
|
|
|
"reason: {1}").format(target_path,
|
|
|
error_message))
|
|
|
|
|
|
def _clear_file(self, target_path: str) -> None:
|
|
|
'''Метод для очистки файлов.'''
|
|
|
if os.path.exists(target_path):
|
|
|
if os.path.isfile(target_path):
|
|
|
try:
|
|
|
with open(target_path, 'w') as f:
|
|
|
f.truncate(0)
|
|
|
return
|
|
|
except IOError as error:
|
|
|
error_message = str(error)
|
|
|
else:
|
|
|
error_message = 'target is not a file'
|
|
|
else:
|
|
|
error_message = 'target file does not exist'
|
|
|
|
|
|
raise TemplateExecutorError(("failed to clear the file: {0},"
|
|
|
"reason: {1}").format(target_path,
|
|
|
error_message))
|
|
|
|
|
|
def _link_file(self, source: str, target_path: str) -> None:
|
|
|
'''Метод для создания по целевому пути ссылки на файл расположенный на
|
|
|
пути, указанному в source.'''
|
|
|
try:
|
|
|
os.symlink(source, target_path)
|
|
|
except OSError as error:
|
|
|
raise TemplateExecutorError(
|
|
|
"failed to create symlink to the file: {0} -> {1}, reason: {2}".
|
|
|
format(target_path, source, str(error)))
|
|
|
|
|
|
def _run_template(self, template_object: TemplateWrapper) -> None:
|
|
|
'''Метод для сохранения текста шаблонов, который должен быть исполнен
|
|
|
интерпретатором указанным в run прямо во время обработки шаблонов.'''
|
|
|
text_to_run = template_object.template_text
|
|
|
interpreter = template_object.parameters.run
|
|
|
|
|
|
if template_object.template_type == FILE:
|
|
|
cwd_path = os.path.dirname(template_object.target_path)
|
|
|
else:
|
|
|
cwd_path = template_object.target_path
|
|
|
|
|
|
if not os.path.exists(cwd_path):
|
|
|
raise TemplateExecutorError(("can not run template, directory from"
|
|
|
" target path does not exist: {}").
|
|
|
format(template_object.target_path))
|
|
|
elif not os.path.isdir(cwd_path):
|
|
|
raise TemplateExecutorError(("can not exec template, {} is not a"
|
|
|
" directory.").format(cwd_path))
|
|
|
|
|
|
try:
|
|
|
run_process = Process(interpreter, cwd=cwd_path)
|
|
|
run_process.write(text_to_run)
|
|
|
|
|
|
if run_process.readable:
|
|
|
stdout = run_process.read()
|
|
|
if stdout:
|
|
|
self.executor_output['stdout'] = stdout
|
|
|
|
|
|
if run_process.readable_errors:
|
|
|
stderr = run_process.read_error()
|
|
|
if 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) -> None:
|
|
|
'''Метод для сохранения текста шаблонов, который должен быть исполнен
|
|
|
интерпретатором указанным в exec после выполнения всех прочих шаблонов.
|
|
|
'''
|
|
|
text_to_run = template_object.template_text
|
|
|
interpreter = template_object.parameters.exec
|
|
|
|
|
|
if template_object.template_type == FILE:
|
|
|
cwd_path = os.path.dirname(template_object.target_path)
|
|
|
else:
|
|
|
cwd_path = template_object.target_path
|
|
|
|
|
|
if not os.path.exists(cwd_path):
|
|
|
raise TemplateExecutorError(
|
|
|
("can not exec template, directory from"
|
|
|
" target path does not exist: {}").
|
|
|
format(cwd_path))
|
|
|
elif not os.path.isdir(cwd_path):
|
|
|
raise TemplateExecutorError(("can not exec template, {} is not a"
|
|
|
" directory.").format(cwd_path))
|
|
|
|
|
|
# Получаем путь к директории для хранения файлов .execute.
|
|
|
if (self.chroot_path != '/' and not
|
|
|
self.execute_archive_path.startswith(self.chroot_path)):
|
|
|
self.execute_archive_path = join_paths(
|
|
|
self.chroot_path,
|
|
|
self.execute_archive_path)
|
|
|
|
|
|
# Если директория уже существует получаем номер очередного файла для
|
|
|
# exec по номеру последнего.
|
|
|
exec_number = 0
|
|
|
if not self.execute_files:
|
|
|
if os.path.exists(self.execute_archive_path):
|
|
|
exec_files_list = os.listdir(self.execute_archive_path)
|
|
|
if exec_files_list:
|
|
|
exec_number = int(exec_files_list[-1][-4:])
|
|
|
exec_number = str(exec_number + 1)
|
|
|
else:
|
|
|
exec_number = str(len(self.execute_files) + 1)
|
|
|
|
|
|
# Получаем название нового exec_???? файла.
|
|
|
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(self.execute_archive_path,
|
|
|
exec_file_name)
|
|
|
|
|
|
exec_file = write_file(exec_file_path)
|
|
|
exec_file.write(text_to_run)
|
|
|
exec_file.close()
|
|
|
|
|
|
self.execute_files[exec_file_path] = {'interpreter': interpreter,
|
|
|
'cwd_path': cwd_path,
|
|
|
'template_path':
|
|
|
template_object.template_path}
|
|
|
|
|
|
def execute_file(self, interpreter: str, exec_path: str,
|
|
|
cwd_path: str) -> dict:
|
|
|
"""Метод для выполнения скриптов сохраненных в результате обработки
|
|
|
шаблонов с параметром exec. Скрипт всегда удаляется вне зависимости
|
|
|
от успешности его выполнения."""
|
|
|
exec_output = {'stdout': None, 'stderr': None}
|
|
|
with open(exec_path, 'r') as exec_file:
|
|
|
script_text = exec_file.read()
|
|
|
os.remove(exec_path)
|
|
|
|
|
|
try:
|
|
|
run_process = Process(interpreter, cwd=cwd_path)
|
|
|
run_process.write(script_text)
|
|
|
|
|
|
if run_process.readable:
|
|
|
stdout = run_process.read()
|
|
|
if stdout:
|
|
|
exec_output['stdout'] = stdout
|
|
|
|
|
|
if run_process.readable_errors:
|
|
|
stderr = run_process.read_error()
|
|
|
if stderr:
|
|
|
exec_output['stderr'] = stderr
|
|
|
return exec_output
|
|
|
except FilesError as error:
|
|
|
raise TemplateExecutorError(("can not run template using the"
|
|
|
" interpreter '{}', reason: {}").
|
|
|
format(interpreter, str(error)))
|
|
|
|
|
|
def _chown_directory(self, target_path: str, chown_value: dict
|
|
|
) -> None:
|
|
|
"""Метод для смены владельца директории."""
|
|
|
try:
|
|
|
if os.path.exists(target_path):
|
|
|
os.chown(target_path, chown_value['uid'], chown_value['gid'])
|
|
|
else:
|
|
|
raise TemplateExecutorError(
|
|
|
'The target directory does not exist: {0}'.
|
|
|
format(target_path))
|
|
|
except (OSError, Exception) as error:
|
|
|
if not self._check_os_error(error, target_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'Can not chown file: {0} to {1}, reason: {2}'.
|
|
|
format(target_path, self._translate_uid_gid(
|
|
|
chown_value['uid'],
|
|
|
chown_value['gid']),
|
|
|
str(error)))
|
|
|
|
|
|
def _chmod_directory(self, target_path: str, chmod_value: int) -> None:
|
|
|
'''Метод для смены прав доступа к директории.'''
|
|
|
if isinstance(chmod_value, tuple) and not chmod_value[1]:
|
|
|
chmod_value = chmod_value[0]
|
|
|
try:
|
|
|
if os.path.exists(target_path):
|
|
|
if isinstance(chmod_value, int):
|
|
|
os.chmod(target_path, chmod_value)
|
|
|
else:
|
|
|
chmod_value = self._use_chmod_x_mask(chmod_value)
|
|
|
os.chmod(target_path, chmod_value)
|
|
|
else:
|
|
|
raise TemplateExecutorError(
|
|
|
'The target directory does not exist: {0}'.
|
|
|
format(target_path))
|
|
|
except (OSError, Exception) as error:
|
|
|
if not self._check_os_error(error, target_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'Can not chmod directory: {0}, reason: {1}'.
|
|
|
format(target_path, str(error)))
|
|
|
|
|
|
def _chown_file(self, target_path: str, chown_value: dict) -> None:
|
|
|
'''Метод для смены владельца файла.'''
|
|
|
try:
|
|
|
if os.path.exists(target_path):
|
|
|
os.lchown(target_path, chown_value['uid'], chown_value['gid'])
|
|
|
else:
|
|
|
raise TemplateExecutorError(
|
|
|
'The target file does not exist: {0}'.
|
|
|
format(target_path))
|
|
|
except (OSError, Exception) as error:
|
|
|
if not self._check_os_error(error, target_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'Can not chown file: {0} to {1}, reason: {2}'.
|
|
|
format(target_path, self._translate_uid_gid(
|
|
|
chown_value['uid'],
|
|
|
chown_value['gid']),
|
|
|
str(error)))
|
|
|
|
|
|
def _chmod_file(self, target_path: str, chmod_value: int) -> None:
|
|
|
'''Метод для смены прав доступа к директории.'''
|
|
|
try:
|
|
|
if not os.path.exists(target_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'The target file does not exist: {0}'.
|
|
|
format(target_path))
|
|
|
if isinstance(chmod_value, int):
|
|
|
os.chmod(target_path, chmod_value)
|
|
|
else:
|
|
|
chmod_value = self._use_chmod_x_mask(
|
|
|
chmod_value,
|
|
|
current_mode=self._get_file_mode(
|
|
|
target_path))
|
|
|
os.chmod(target_path, chmod_value)
|
|
|
except (OSError, Exception) as error:
|
|
|
if not self._check_os_error(error, target_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'Can not chmod file: {0}, reason: {1}'.
|
|
|
format(target_path, str(error)))
|
|
|
|
|
|
def _use_chmod_x_mask(self, chmod: Tuple[int, int],
|
|
|
current_mode: Optional[int] = None) -> int:
|
|
|
'''Метод для наложения X-маски, необходимой для получения значения
|
|
|
chmod, c учетом возможности наличия в нем значения "X".'''
|
|
|
chmod, x_mask = chmod
|
|
|
|
|
|
if not x_mask:
|
|
|
return chmod
|
|
|
|
|
|
if current_mode is None:
|
|
|
return chmod ^ x_mask
|
|
|
else:
|
|
|
return chmod ^ (current_mode & x_mask)
|
|
|
|
|
|
def _get_file_mode(self, file_path: str) -> int:
|
|
|
'''Метод для получения прав доступа для указанного файла.'''
|
|
|
if not os.path.exists(file_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'The file to get mode does not exist: {0}'.
|
|
|
format(file_path))
|
|
|
file_stat = os.stat(file_path)
|
|
|
return stat.S_IMODE(file_stat.st_mode)
|
|
|
|
|
|
def _get_file_owner(self, file_path: str) -> dict:
|
|
|
'''Метод для получения uid и gid значений для владельца указанного
|
|
|
файла.'''
|
|
|
if not os.path.exists(file_path):
|
|
|
raise TemplateExecutorError(
|
|
|
'The file to get owner does not exist: {0}'.
|
|
|
format(file_path))
|
|
|
file_stat = os.stat(file_path)
|
|
|
return {'uid': file_stat.st_uid, 'gid': file_stat.st_gid}
|
|
|
|
|
|
def _translate_uid_gid(self, uid: int, gid: int) -> str:
|
|
|
'''Метод для получения из uid и gid имен пользователя и группы при,
|
|
|
необходимых для выдачи сообщения об ошибке при попытке chown.'''
|
|
|
import pwd
|
|
|
import grp
|
|
|
|
|
|
try:
|
|
|
if self.chroot_path == '/':
|
|
|
user_name = pwd.getpwuid(uid).pw_name
|
|
|
else:
|
|
|
user_name = self._get_user_name_from_uid(uid)
|
|
|
except (TypeError, KeyError):
|
|
|
user_name = str(uid)
|
|
|
|
|
|
try:
|
|
|
if self.chroot_path == '/':
|
|
|
group_name = grp.getgrgid(gid).gr_name
|
|
|
else:
|
|
|
group_name = self._get_group_name_from_gid(gid)
|
|
|
except (TypeError, KeyError):
|
|
|
group_name = str(gid)
|
|
|
|
|
|
return '{0}:{1}'.format(user_name, group_name)
|
|
|
|
|
|
def _get_user_name_from_uid(self, uid: int) -> str:
|
|
|
'''Метод для получения имени пользователя по его uid.'''
|
|
|
passwd_file_path = os.path.join(self.chroot_path, 'etc/passwd')
|
|
|
passwd_dictionary = dict()
|
|
|
if os.path.exists(passwd_file_path):
|
|
|
with open(passwd_file_path, 'r') as passwd_file:
|
|
|
for line in passwd_file:
|
|
|
if line.startswith('#'):
|
|
|
continue
|
|
|
|
|
|
passwd_line = line.split(':')
|
|
|
line_uid = int(passwd_line[2])
|
|
|
line_username = passwd_line[0]
|
|
|
|
|
|
if line_uid and line_username:
|
|
|
passwd_dictionary[line_uid] = line_username
|
|
|
|
|
|
if uid in passwd_dictionary:
|
|
|
return passwd_dictionary[uid]
|
|
|
else:
|
|
|
return str(uid)
|
|
|
|
|
|
def _get_group_name_from_gid(self, gid: int) -> str:
|
|
|
'''Метод для получения названия группы по его gid.'''
|
|
|
group_file_path = os.path.join(self.chroot_path, 'etc/group')
|
|
|
group_dictionary = dict()
|
|
|
if os.path.exists(group_file_path):
|
|
|
with open(group_file_path, 'r') as group_file:
|
|
|
for line in group_file:
|
|
|
if line.startswith('#'):
|
|
|
continue
|
|
|
|
|
|
group_line = line.split(':')
|
|
|
line_gid = int(group_line[2])
|
|
|
line_group = group_line[0]
|
|
|
|
|
|
if line_gid and line_group:
|
|
|
group_dictionary[line_gid] = line_group
|
|
|
|
|
|
if gid in group_dictionary:
|
|
|
return group_dictionary[gid]
|
|
|
else:
|
|
|
return str(gid)
|
|
|
|
|
|
def _check_os_error(self, error: Exception, path_to_check: str) -> bool:
|
|
|
'''Метод для проверки причины, по которой не удалось изменить владельца
|
|
|
или права доступа файла.'''
|
|
|
if hasattr(error, 'errno') and error.errno == errno.EPERM:
|
|
|
if self._is_vfat(path_to_check):
|
|
|
return True
|
|
|
|
|
|
return hasattr(error, 'errno') and error.errno == errno.EACCES and\
|
|
|
'var/calculate/remote' in path_to_check
|
|
|
|
|
|
def _is_vfat(self, path_to_check: str) -> bool:
|
|
|
'''Метод, проверяющий является ли файловая система vfat. Нужно для того,
|
|
|
чтобы знать о возможности применения chown, chmod и т.д.'''
|
|
|
# Инициализируем объект для проверки примонтированных файловых систем.
|
|
|
if self.mounts is None:
|
|
|
self.mounts = Mounts()
|
|
|
|
|
|
# Проверяем файловую систему на пути.
|
|
|
fstab_info = self.mounts.get_from_fstab(what=self.mounts.TYPE,
|
|
|
where=self.mounts.DIR,
|
|
|
is_in=path_to_check)[0]
|
|
|
return fstab_info in {'vfat', 'ntfs-3g', 'ntfs'}
|
|
|
|
|
|
|
|
|
class DirectoryTree:
|
|
|
'''Класс реализующий дерево каталогов для пакета.'''
|
|
|
def __init__(self, base_directory: str):
|
|
|
self.base_directory = base_directory
|
|
|
self._tree = {}
|
|
|
|
|
|
def update_tree(self, tree: dict) -> None:
|
|
|
'''Метод, инициирующий наложение заданного дерева каталогов на данный
|
|
|
экземпляр дерева.'''
|
|
|
self._update(self._tree, tree)
|
|
|
|
|
|
def _update(self, original_tree: dict, tree: dict) -> dict:
|
|
|
'''Метод для рекурсивного наложения одного дерева на другое.'''
|
|
|
for parent, child in tree.items():
|
|
|
if isinstance(child, abc.Mapping):
|
|
|
original_tree[parent] = self._update(original_tree.get(parent,
|
|
|
dict()),
|
|
|
child)
|
|
|
else:
|
|
|
original_tree[parent] = child
|
|
|
return original_tree
|
|
|
|
|
|
def show_tree(self) -> None:
|
|
|
pprint(self._tree)
|
|
|
|
|
|
def get_directory_tree(self, directory: str) -> "DirectoryTree":
|
|
|
'''Метод для получения нового дерева из ветви данного дерева,
|
|
|
соответствующей некоторому каталогу, содержащемуся в корне данного
|
|
|
дерева.'''
|
|
|
directory_tree = DirectoryTree(os.path.join(self.base_directory,
|
|
|
directory))
|
|
|
if directory in self._tree:
|
|
|
directory_tree._tree = self._tree[directory]
|
|
|
return directory_tree
|
|
|
|
|
|
def __getitem__(self, name: str) -> Union[None, dict]:
|
|
|
if name in self._tree:
|
|
|
return self._tree[name]
|
|
|
else:
|
|
|
return None
|
|
|
|
|
|
def __setitem__(self, name: str, value: Union[None, dict]) -> None:
|
|
|
self._tree[name] = value
|
|
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
|
if self._tree is not None:
|
|
|
return iter(self._tree.keys())
|
|
|
else:
|
|
|
return iter([])
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
return '<DirectoryTree: {}>'.format(self._tree)
|
|
|
|
|
|
def __bool__(self) -> bool:
|
|
|
return bool(self._tree)
|
|
|
|
|
|
|
|
|
class DirectoryProcessor:
|
|
|
'''Класс обработчика директорий шаблонов.'''
|
|
|
def __init__(self, action: str,
|
|
|
datavars_module: Union[Datavars, Variables] = Variables(),
|
|
|
install: Union[str, PackageAtomName] = '',
|
|
|
output_module: IOModule = IOModule(),
|
|
|
dbpkg: bool = True, pkg_autosave: bool = True,
|
|
|
namespace: Optional[NamespaceNode] = None, **groups: dict):
|
|
|
if isinstance(action, list):
|
|
|
self.action = action
|
|
|
else:
|
|
|
self.action = [action]
|
|
|
|
|
|
self.output: IOModule = output_module
|
|
|
self.datavars_module: Variables = datavars_module
|
|
|
self._namespace: NamespaceNode = namespace
|
|
|
self._pkg_autosave: bool = pkg_autosave
|
|
|
|
|
|
# Корневая директория.
|
|
|
self.cl_chroot_path: str
|
|
|
if 'cl_chroot_path' in datavars_module.main:
|
|
|
self.cl_chroot_path = datavars_module.main.cl_chroot_path
|
|
|
else:
|
|
|
self.cl_chroot_path = '/'
|
|
|
|
|
|
# Корневой путь шаблонов.
|
|
|
self.templates_root: str
|
|
|
if 'cl_root_path' in datavars_module.main:
|
|
|
self.templates_root = join_paths(self.cl_chroot_path,
|
|
|
datavars_module.main.cl_root_path)
|
|
|
else:
|
|
|
self.templates_root = self.cl_chroot_path
|
|
|
|
|
|
self.cl_ignore_files: List[str] = self._get_cl_ignore_files()
|
|
|
|
|
|
# Путь к файлу config с хэш-суммами файлов, для которых уже
|
|
|
# предлагались изменения.
|
|
|
self.cl_config_path: str
|
|
|
if 'cl_config_path' in datavars_module.main:
|
|
|
self.cl_config_path = self._add_chroot_path(
|
|
|
self.datavars_module.main.cl_config_path)
|
|
|
else:
|
|
|
self.cl_config_path = self._add_chroot_path(
|
|
|
'/var/lib/calculate/config')
|
|
|
|
|
|
# Путь к директории config-archive для хранения оригинальной ветки
|
|
|
# конфигурационных файлов.
|
|
|
self.cl_config_archive: str
|
|
|
if 'cl_config_archive' in datavars_module.main:
|
|
|
self.cl_config_archive = self._add_chroot_path(
|
|
|
self.datavars_module.main.cl_config_archive)
|
|
|
else:
|
|
|
self.cl_config_archive = self._add_chroot_path(
|
|
|
'/var/lib/calculate/config-archive')
|
|
|
|
|
|
# Путь к директории .execute для хранения хранения файлов скриптов,
|
|
|
# полученных из шаблонов с параметром exec.
|
|
|
self.cl_exec_dir_path: str
|
|
|
if 'cl_exec_dir_path' in datavars_module.main:
|
|
|
self.cl_exec_dir_path = self._add_chroot_path(
|
|
|
self.datavars_module.main.cl_exec_dir_path)
|
|
|
else:
|
|
|
self.cl_exec_dir_path = self._add_chroot_path(
|
|
|
'/var/lib/calculate/.execute/')
|
|
|
|
|
|
# Инициализируем исполнительный модуль.
|
|
|
self.template_executor: TemplateExecutor = TemplateExecutor(
|
|
|
datavars_module=self.datavars_module,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
cl_config_archive=self.cl_config_archive,
|
|
|
cl_config_path=self.cl_config_path,
|
|
|
execute_archive_path=self.cl_exec_dir_path,
|
|
|
dbpkg=dbpkg,
|
|
|
pkg_autosave=self._pkg_autosave)
|
|
|
|
|
|
# Разбираем atom имена пакетов, указанных для групп пакетов.
|
|
|
if groups:
|
|
|
for group_name, package_name in groups.items():
|
|
|
if isinstance(package_name, list):
|
|
|
for atom in package_name:
|
|
|
self._add_package_to_group(group_name, atom)
|
|
|
else:
|
|
|
self._add_package_to_group(group_name, package_name)
|
|
|
|
|
|
# Создаем переменную для указания текущих шаблонов, если ее еще нет.
|
|
|
self._make_current_template_var()
|
|
|
|
|
|
# Инициализируем шаблонизатор.
|
|
|
self.template_engine: TemplateEngine = TemplateEngine(
|
|
|
datavars_module=self.datavars_module,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
appends_set=self.template_executor.available_appends)
|
|
|
|
|
|
# Разбираем atom имя пакета, для которого накладываем шаблоны.
|
|
|
self.for_package: Union[PackageAtomName, None] = None
|
|
|
if install:
|
|
|
if isinstance(install, PackageAtomName):
|
|
|
self.for_package = install
|
|
|
elif isinstance(install, str):
|
|
|
try:
|
|
|
self.for_package = self.template_engine.\
|
|
|
parameters_processor.check_postparse_package(
|
|
|
install)
|
|
|
except ConditionFailed as error:
|
|
|
# ConditionFailed потому что для проверки значения пакета,
|
|
|
# используется тот же метод, что проверяет параметр package
|
|
|
# в шаблонах, а в них этот параметр играет роль условия.
|
|
|
self.output.set_error(str(error))
|
|
|
return
|
|
|
self.template_engine.for_package = self.for_package
|
|
|
|
|
|
# Получаем список директорий шаблонов.
|
|
|
# TODO переменная список.
|
|
|
self.template_paths: List[str]
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
var_type = self.datavars_module.main[
|
|
|
'cl_template_path'].variable_type
|
|
|
else:
|
|
|
var_type = StringType
|
|
|
|
|
|
if var_type is StringType:
|
|
|
self.template_paths = (self.datavars_module.
|
|
|
main.cl_template_path.split(','))
|
|
|
elif var_type is ListType:
|
|
|
self.template_paths = self.datavars_module.main.cl_template_path
|
|
|
|
|
|
# Список обработанных пакетов.
|
|
|
self.processed_packages = set()
|
|
|
|
|
|
# Список пакетов, взятый из значений параметра merge.
|
|
|
self.packages_to_merge = set()
|
|
|
|
|
|
# Словарь для хранения деревьев директорий для различных пакетов.
|
|
|
self.packages_file_trees = OrderedDict()
|
|
|
|
|
|
# Список обработчиков.
|
|
|
self._handlers: Dict[tuple] = {}
|
|
|
self._handlers_queue: List[str] = []
|
|
|
self._handling: Union[str, None] = None
|
|
|
|
|
|
def _get_cl_ignore_files(self) -> List[str]:
|
|
|
'''Метод для получения из соответствующей переменной списка паттернов
|
|
|
для обнаружения игнорируемых в ходе обработки шаблонов файлов.'''
|
|
|
if 'cl_ignore_files' in self.datavars_module.main:
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
var_type = self.datavars_module.main[
|
|
|
'cl_ignore_files'].variable_type
|
|
|
else:
|
|
|
var_type = StringType
|
|
|
cl_ignore_files = self.datavars_module.main.cl_ignore_files
|
|
|
cl_ignore_files_list = []
|
|
|
|
|
|
if var_type is StringType:
|
|
|
for pattern in cl_ignore_files.split(','):
|
|
|
cl_ignore_files_list.append(pattern.strip())
|
|
|
elif var_type is ListType:
|
|
|
cl_ignore_files_list = cl_ignore_files
|
|
|
return cl_ignore_files_list
|
|
|
else:
|
|
|
return []
|
|
|
|
|
|
def _add_chroot_path(self, path_to_add: str) -> str:
|
|
|
'''Метод для добавления корневого пути к заданному пути, если таковой
|
|
|
задан и отсутствует в заданном пути.'''
|
|
|
if (self.cl_chroot_path != '/' and
|
|
|
not path_to_add.startswith(self.cl_chroot_path)):
|
|
|
return join_paths(self.cl_chroot_path, path_to_add)
|
|
|
else:
|
|
|
return path_to_add
|
|
|
|
|
|
def _add_package_to_group(self, group_name: str,
|
|
|
package_atom: str) -> None:
|
|
|
try:
|
|
|
groups_namespace = self.datavars_module.main.cl.groups
|
|
|
except (VariableNotFoundError, AttributeError):
|
|
|
namespaces = ['cl', 'groups']
|
|
|
groups_namespace = self.datavars_module.main
|
|
|
for namespace in namespaces:
|
|
|
if namespace in groups_namespace:
|
|
|
groups_namespace = groups_namespace[namespace]
|
|
|
else:
|
|
|
if isinstance(groups_namespace, NamespaceNode):
|
|
|
groups_namespace = NamespaceNode(
|
|
|
namespace,
|
|
|
parent=groups_namespace)
|
|
|
else:
|
|
|
groups_namespace[namespace] = Variables()
|
|
|
groups_namespace = groups_namespace[namespace]
|
|
|
|
|
|
atom_dict = PackageAtomParser.parse_atom_name(package_atom)
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
if group_name not in groups_namespace:
|
|
|
VariableNode(group_name, groups_namespace,
|
|
|
variable_type=TableType,
|
|
|
source=[atom_dict])
|
|
|
else:
|
|
|
group_var = groups_namespace[group_name]
|
|
|
group_table = group_var.get_value().get_table()
|
|
|
check_result = self.check_existance_in_group(atom_dict,
|
|
|
group_table)
|
|
|
if check_result == -2:
|
|
|
group_table.append(atom_dict)
|
|
|
elif check_result >= 0:
|
|
|
group_table[check_result] = atom_dict
|
|
|
|
|
|
if check_result != -1:
|
|
|
group_var.source = group_table
|
|
|
else:
|
|
|
if group_name not in groups_namespace:
|
|
|
groups_namespace[group_name] = [atom_dict]
|
|
|
else:
|
|
|
group_table = groups_namespace[group_name]
|
|
|
check_result = self.check_existance_in_group(atom_dict,
|
|
|
group_table)
|
|
|
if check_result == -2:
|
|
|
group_table.append(atom_dict)
|
|
|
elif check_result >= 0:
|
|
|
group_table[check_result] = atom_dict
|
|
|
|
|
|
if check_result != -1:
|
|
|
groups_namespace[group_name] = group_table
|
|
|
|
|
|
def check_existance_in_group(self, atom_dict: dict, group: list) -> bool:
|
|
|
'''Метод для проверки наличия в таблице групп указанного пакета, а
|
|
|
также для сравнения данных о пакете из таблицы и из атом словаря.
|
|
|
Возвращает:
|
|
|
-2 -- если пакета нет;
|
|
|
-1 -- если присутствует и полностью совпадает;
|
|
|
>=0 -- если имеющиеся в таблице данные о пакете должены быть заменены
|
|
|
на указанные. В этом случае возвращается индекс старых данных в
|
|
|
таблице. '''
|
|
|
index = 0
|
|
|
|
|
|
for group_atom in group:
|
|
|
if (atom_dict['category'] != group_atom['category'] and
|
|
|
atom_dict['name'] != group_atom['name']):
|
|
|
index += 1
|
|
|
continue
|
|
|
|
|
|
if (atom_dict['slot'] is not None and
|
|
|
group_atom['slot'] is not None):
|
|
|
if atom_dict['slot'] != group_atom['slot']:
|
|
|
return -2
|
|
|
|
|
|
if atom_dict['version'] is not None:
|
|
|
if (group_atom['version'] is None or
|
|
|
atom_dict['version'] != group_atom['version']):
|
|
|
return index
|
|
|
|
|
|
return -1
|
|
|
return -2
|
|
|
|
|
|
def _make_current_template_var(self) -> None:
|
|
|
var_path = ['main', 'cl']
|
|
|
namespace = self.datavars_module
|
|
|
|
|
|
for field in var_path:
|
|
|
if field not in namespace:
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
namespace = NamespaceNode(field, parent=namespace)
|
|
|
else:
|
|
|
namespace[field] = Variables()
|
|
|
namespace = namespace[field]
|
|
|
else:
|
|
|
namespace = namespace[field]
|
|
|
|
|
|
if 'current_template' not in namespace:
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
VariableNode('current_template', namespace,
|
|
|
variable_type=StringType, source="")
|
|
|
else:
|
|
|
namespace['current_template'] = ""
|
|
|
|
|
|
def process_template_directories(self) -> None:
|
|
|
'''Метод для обхода шаблонов, содержащихся в каталогах из
|
|
|
main.cl_template.path.'''
|
|
|
# Режим заполнения очередей директорий пакетов, необходимых для более
|
|
|
# быстрой обработки параметра merge.
|
|
|
self.fill_trees = bool(self.for_package)
|
|
|
|
|
|
if self.for_package:
|
|
|
if self.for_package is NonePackage:
|
|
|
package = self.for_package
|
|
|
else:
|
|
|
package = Package(self.for_package,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
else:
|
|
|
package = None
|
|
|
|
|
|
self.base_directory: str
|
|
|
for directory_path in self.template_paths:
|
|
|
self.base_directory = directory_path.strip()
|
|
|
entries = os.scandir(self.base_directory)
|
|
|
|
|
|
files: list = []
|
|
|
directories: list = []
|
|
|
|
|
|
for node in entries:
|
|
|
if node.is_file():
|
|
|
if (node.name != ".calculate_directory" and
|
|
|
self._check_file_name(node.name)):
|
|
|
files.append(node.name)
|
|
|
elif node.is_dir():
|
|
|
directories.append(node.path)
|
|
|
|
|
|
self.directory_tree = {}
|
|
|
self._run_template_from_base_directory(
|
|
|
files, self.base_directory,
|
|
|
package=package,
|
|
|
directory_tree=self.directory_tree)
|
|
|
|
|
|
for directory_path in directories:
|
|
|
self.directory_tree = {}
|
|
|
parameters = ParametersContainer()
|
|
|
if self._namespace:
|
|
|
parameters.set_inheritable({'env': {self._namespace}})
|
|
|
self._walk_directory_tree(directory_path,
|
|
|
self.templates_root,
|
|
|
parameters,
|
|
|
directory_tree=self.directory_tree,
|
|
|
package=package)
|
|
|
|
|
|
# Теперь когда дерево заполнено, можно выключить этот режим.
|
|
|
self.fill_trees = False
|
|
|
|
|
|
if self.for_package:
|
|
|
self.output.set_info('Processing packages from merge parameter...')
|
|
|
self.processed_packages.add(self.for_package)
|
|
|
self._merge_packages()
|
|
|
|
|
|
if self._handlers_queue:
|
|
|
self._execute_handlers()
|
|
|
|
|
|
if self.template_executor.execute_files:
|
|
|
self._run_exec_files()
|
|
|
|
|
|
self.template_executor.save_changes()
|
|
|
PackageCreator.save_all()
|
|
|
return self.template_executor.changed_files
|
|
|
|
|
|
def _run_template_from_base_directory(self, template_names: List[str],
|
|
|
base_directory: str,
|
|
|
package: Optional[Package] = None,
|
|
|
directory_tree: Optional[dict] = None
|
|
|
) -> None:
|
|
|
'''Метод для запуска шаблонов файлов находящихся в базовой директории.
|
|
|
'''
|
|
|
self.template_engine.change_directory(base_directory)
|
|
|
|
|
|
for template_name in template_names:
|
|
|
template_path = join_paths(base_directory, template_name)
|
|
|
target_path = join_paths(self.templates_root, template_name)
|
|
|
|
|
|
parameters = ParametersContainer()
|
|
|
if self._namespace:
|
|
|
parameters.set_inheritable({'env': {self._namespace}})
|
|
|
template_text = self._parse_template(parameters,
|
|
|
template_name,
|
|
|
FILE, base_directory)
|
|
|
if template_text is False:
|
|
|
continue
|
|
|
|
|
|
if not self._check_package_and_action(
|
|
|
parameters,
|
|
|
template_path,
|
|
|
directory_tree=directory_tree):
|
|
|
if parameters.handler:
|
|
|
# Если директория шаблонов является обработчиком --
|
|
|
# добавляем ее в словарь обработчиков и пропускаем ее.
|
|
|
self._handlers.update({parameters.handler:
|
|
|
(FILE, template_path)})
|
|
|
self._update_package_tree(parameters.package,
|
|
|
directory_tree)
|
|
|
continue
|
|
|
|
|
|
# Если есть параметр merge добавляем его содержимое в список
|
|
|
# пакетов для последующей обработки.
|
|
|
if self.for_package and parameters.merge:
|
|
|
for pkg in parameters.merge:
|
|
|
if pkg not in self.processed_packages:
|
|
|
self.packages_to_merge.add(pkg)
|
|
|
|
|
|
# Если присутствует параметр notify, в котором указаны хэндлеры для
|
|
|
# последующего выполнения -- добавляем их в очередь.
|
|
|
if parameters.notify:
|
|
|
for handler_id in parameters.notify:
|
|
|
if handler_id not in self._handlers_queue:
|
|
|
self._handlers_queue.append(handler_id)
|
|
|
|
|
|
# Корректируем путь к целевому файлу.
|
|
|
target_path = self._make_target_path(target_path,
|
|
|
template_name,
|
|
|
parameters)
|
|
|
if parameters.package:
|
|
|
template_package = Package(parameters.package,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
else:
|
|
|
template_package = package
|
|
|
|
|
|
# Дефолтные значения разные для упрощения написания шаблонов.
|
|
|
if not parameters.run and not parameters.exec:
|
|
|
if not parameters.format:
|
|
|
parameters.set_parameter({'format': 'raw'})
|
|
|
if not parameters.append:
|
|
|
if parameters.format == "raw":
|
|
|
parameters.set_parameter({'append': 'replace'})
|
|
|
else:
|
|
|
parameters.set_parameter({'append': 'join'})
|
|
|
|
|
|
if parameters.append != 'skip':
|
|
|
# Выполняем действия, указанные в шаблоне.
|
|
|
self._execute_template(target_path, parameters,
|
|
|
FILE, template_path,
|
|
|
template_text=template_text,
|
|
|
package=template_package)
|
|
|
|
|
|
def _execute_handlers(self) -> None:
|
|
|
'''Метод для запуска обработчиков добавленных в очередь обработчиков
|
|
|
с помощью параметра notify.'''
|
|
|
self.output.set_info('Processing handlers...')
|
|
|
index = 0
|
|
|
|
|
|
while index < len(self._handlers_queue):
|
|
|
handler = self._handlers_queue[index]
|
|
|
index += 1
|
|
|
if handler in self._handlers:
|
|
|
handler_type, handler_path = self._handlers[handler]
|
|
|
else:
|
|
|
self.output.set_warning(
|
|
|
f"Handler '{handler}' is not found.")
|
|
|
continue
|
|
|
|
|
|
with self._start_handling(handler):
|
|
|
if handler_type is DIR:
|
|
|
self.directory_tree = {}
|
|
|
parameters = ParametersContainer()
|
|
|
if self._namespace:
|
|
|
parameters.set_inheritable({'env': {self._namespace}})
|
|
|
|
|
|
self._walk_directory_tree(
|
|
|
handler_path,
|
|
|
self.templates_root,
|
|
|
parameters,
|
|
|
directory_tree=self.directory_tree)
|
|
|
elif handler_type is FILE:
|
|
|
handler_dir, handler_name = os.path.split(handler_path)
|
|
|
self.template_engine.change_directory(handler_dir)
|
|
|
parameters = ParametersContainer()
|
|
|
if self._namespace:
|
|
|
parameters.set_inheritable({'env': {self._namespace}})
|
|
|
|
|
|
handler_text = self._parse_template(parameters,
|
|
|
handler_name,
|
|
|
FILE, handler_dir)
|
|
|
if handler_text is False:
|
|
|
continue
|
|
|
|
|
|
if parameters.notify:
|
|
|
for handler_id in parameters.notify:
|
|
|
if handler_id not in self._handlers_queue:
|
|
|
self._handlers_queue.append(handler_id)
|
|
|
|
|
|
# Корректируем путь к целевому файлу.
|
|
|
target_file_path = self._make_target_path(
|
|
|
self.templates_root,
|
|
|
handler_name,
|
|
|
parameters)
|
|
|
|
|
|
if not parameters.run and not parameters.exec:
|
|
|
if not parameters.format:
|
|
|
parameters.set_parameter({'format': 'raw'})
|
|
|
if not parameters.append:
|
|
|
if parameters.format == "raw":
|
|
|
parameters.set_parameter({'append': 'replace'})
|
|
|
else:
|
|
|
parameters.set_parameter({'append': 'join'})
|
|
|
|
|
|
# Выполняем действия, указанные в обработчике.
|
|
|
self._execute_template(target_file_path, parameters,
|
|
|
FILE, handler_path,
|
|
|
template_text=handler_text)
|
|
|
|
|
|
def _merge_packages(self) -> None:
|
|
|
'''Метод для выполнения шаблонов относящихся к пакетам, указанным во
|
|
|
всех встреченных значениях параметра merge.'''
|
|
|
not_merged_packages = []
|
|
|
self.for_package = None
|
|
|
|
|
|
while self.packages_to_merge:
|
|
|
if self.for_package is None:
|
|
|
# Если список найденных пакетов пройден, но пакеты для
|
|
|
# настройки еще есть -- заново проходимся по списку.
|
|
|
for package in self.packages_to_merge.difference(
|
|
|
self.packages_file_trees):
|
|
|
self.output.set_warning(
|
|
|
("Warning: package '{0}' not found for"
|
|
|
" action{1} '{2}'.").format(
|
|
|
str(package),
|
|
|
's' if len(self.action) > 1 else '',
|
|
|
', '.join(self.action)))
|
|
|
self.packages_to_merge.remove(package)
|
|
|
if not self.packages_to_merge:
|
|
|
break
|
|
|
founded_packages = iter(self.packages_file_trees.keys())
|
|
|
|
|
|
self.for_package = next(founded_packages, None)
|
|
|
if (self.for_package is None
|
|
|
or self.for_package not in self.packages_to_merge):
|
|
|
continue
|
|
|
|
|
|
self.template_engine.for_package = self.for_package
|
|
|
|
|
|
if self.for_package not in self.packages_file_trees:
|
|
|
self.output.set_error(
|
|
|
"Error: package '{0}' not found for action{1} '{2}'.".
|
|
|
format(self.for_package,
|
|
|
's' if len(self.action) > 1 else '',
|
|
|
', '.join(self.action)))
|
|
|
not_merged_packages.append(self.for_package)
|
|
|
continue
|
|
|
|
|
|
package = Package(self.for_package,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
|
|
|
for directory_name in self.packages_file_trees[self.for_package]:
|
|
|
directory_tree = self.packages_file_trees[self.for_package].\
|
|
|
get_directory_tree(directory_name)
|
|
|
|
|
|
parameters = ParametersContainer()
|
|
|
if self._namespace:
|
|
|
parameters.set_inheritable({'env': {self._namespace}})
|
|
|
self._walk_directory_tree(directory_tree.base_directory,
|
|
|
self.templates_root,
|
|
|
parameters,
|
|
|
directory_tree=directory_tree,
|
|
|
package=package)
|
|
|
|
|
|
self.processed_packages.add(self.for_package)
|
|
|
self.packages_to_merge.remove(self.for_package)
|
|
|
|
|
|
if not_merged_packages:
|
|
|
self.output.set_error('Packages {} is not merged.'.
|
|
|
format(','.join(not_merged_packages)))
|
|
|
else:
|
|
|
self.output.set_success('All packages are merged.')
|
|
|
|
|
|
def _run_exec_files(self) -> None:
|
|
|
'''Метод для выполнения скриптов, полученных в результате обработки
|
|
|
шаблонов с параметром exec.'''
|
|
|
for exec_file_path, exec_info in\
|
|
|
self.template_executor.execute_files.items():
|
|
|
try:
|
|
|
output = self.template_executor.execute_file(
|
|
|
exec_info['interpreter'],
|
|
|
exec_file_path,
|
|
|
exec_info['cwd_path'])
|
|
|
|
|
|
if output['stdout'] is not None:
|
|
|
self.output.set_info("stdout from template: {}:\n{}\n".
|
|
|
format(exec_info['template_path'],
|
|
|
output['stdout']))
|
|
|
|
|
|
if output['stderr'] is not None:
|
|
|
self.output.set_error("stderr from template: {}:\n{}\n".
|
|
|
format(exec_info['template_path'],
|
|
|
output['stderr']))
|
|
|
except TemplateExecutorError as error:
|
|
|
self.output.set_error(str(error))
|
|
|
|
|
|
def _walk_directory_tree(self, current_directory_path: str,
|
|
|
current_target_path: str,
|
|
|
directory_parameters: ParametersContainer,
|
|
|
directory_tree: Union[dict, DirectoryTree] = {},
|
|
|
package: Optional[Package] = None) -> None:
|
|
|
'''Метод для рекурсивного обхода директорий с шаблонами, а также, при
|
|
|
необходимости, заполнения деревьев директорий шаблонов, с помощью
|
|
|
которых далее выполняются шаблоны пакетов из merge.'''
|
|
|
directory_name = os.path.basename(current_directory_path)
|
|
|
|
|
|
# Если включено заполнение дерева создаем пустой словарь для сбора
|
|
|
# содержимого текущей директории.
|
|
|
if self.fill_trees:
|
|
|
directory_tree[directory_name] = {}
|
|
|
|
|
|
self.template_engine.change_directory(current_directory_path)
|
|
|
template_directories, template_files = self._scan_directory(
|
|
|
current_directory_path)
|
|
|
template_files = sorted(template_files)
|
|
|
|
|
|
# обрабатываем в первую очередь шаблон директории.
|
|
|
if '.calculate_directory' in template_files:
|
|
|
template_files.remove('.calculate_directory')
|
|
|
template_text = self._parse_template(directory_parameters,
|
|
|
'.calculate_directory',
|
|
|
DIR, current_directory_path)
|
|
|
if template_text is False:
|
|
|
directory_tree = {}
|
|
|
return
|
|
|
|
|
|
# directory_parameters.print_parameters_for_debug()
|
|
|
|
|
|
# Корректируем путь к целевой директории.
|
|
|
current_target_path = self._make_target_path(current_target_path,
|
|
|
directory_name,
|
|
|
directory_parameters)
|
|
|
|
|
|
# Если нужно заполнять дерево директорий, отправляем в метод для
|
|
|
# проверки параметров package и action текущее дерево.
|
|
|
if (self._handling is None
|
|
|
and not self._check_package_and_action(
|
|
|
directory_parameters,
|
|
|
current_directory_path,
|
|
|
directory_tree=(directory_tree if
|
|
|
self.fill_trees else None))):
|
|
|
if directory_parameters.handler:
|
|
|
# Если директория шаблонов является обработчиком и параметр
|
|
|
# handler -- не унаследован, что свидетельствует о том,
|
|
|
# что директория не расположена внутри другой
|
|
|
# директории-обработчика добавляем ее в словарь
|
|
|
# обработчиков и пропускаем ее.
|
|
|
self._handlers.update({directory_parameters.handler:
|
|
|
(DIR, current_directory_path)})
|
|
|
if self.fill_trees:
|
|
|
directory_tree = {}
|
|
|
elif self.fill_trees:
|
|
|
# Если проверка не пройдена и включено заполнение дерева,
|
|
|
# то, используя нынешнее состояние дерева директорий,
|
|
|
# обновляем дерево пакета текущего шаблона директории.
|
|
|
self._update_package_tree(directory_parameters.package,
|
|
|
directory_tree[directory_name])
|
|
|
# Перед выходом из директории очищаем текущий уровень
|
|
|
# дерева.
|
|
|
directory_tree = {}
|
|
|
return
|
|
|
|
|
|
# Хэндлеры, вложенные в другие хэндлеры не разрешены.
|
|
|
if self._handling is not None and directory_parameters.handler:
|
|
|
if directory_parameters.handler != self._handling:
|
|
|
self.output.set_error("'handler' parameter is not"
|
|
|
" available in other handler"
|
|
|
f" '{self._handling}'")
|
|
|
return
|
|
|
|
|
|
# Если есть параметр merge -- сохраняем присутствующие в нем пакеты
|
|
|
# для последующей обработки.
|
|
|
if self.for_package and directory_parameters.merge:
|
|
|
for pkg in directory_parameters.merge:
|
|
|
if pkg not in self.processed_packages:
|
|
|
self.packages_to_merge.add(pkg)
|
|
|
|
|
|
# Если присутствует параметр notify, в котором указаны хэндлеры для
|
|
|
# последующего выполнения -- добавляем их в очередь.
|
|
|
if directory_parameters.notify:
|
|
|
for handler_id in directory_parameters.notify:
|
|
|
if handler_id not in self._handlers_queue:
|
|
|
self._handlers_queue.append(handler_id)
|
|
|
|
|
|
# Если присутствует параметр package -- проверяем, изменился ли он
|
|
|
# и был ли задан до этого. Если не был задан или изменился, меняем
|
|
|
# текущий пакет ветки шаблонов на этот.
|
|
|
if directory_parameters.package:
|
|
|
if (package is None or
|
|
|
package.package_name != directory_parameters.package):
|
|
|
package = Package(directory_parameters.package,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
else:
|
|
|
# Если .calculate_directory отсутствует -- создаем директорию,
|
|
|
# используя унаследованные параметры и имя самой директории.
|
|
|
# Для того чтобы директория была создана, просто добавляем параметр
|
|
|
# append = join.
|
|
|
template_text = ''
|
|
|
current_target_path = os.path.join(current_target_path,
|
|
|
directory_name)
|
|
|
|
|
|
# Для директорий по умолчанию append = join.
|
|
|
if not directory_parameters.append:
|
|
|
directory_parameters.set_parameter({'append': 'join'})
|
|
|
|
|
|
# Выполняем наложение шаблона.
|
|
|
current_target_path = self._execute_template(
|
|
|
current_target_path,
|
|
|
directory_parameters, DIR,
|
|
|
current_directory_path,
|
|
|
template_text=template_text,
|
|
|
package=package)
|
|
|
if not current_target_path:
|
|
|
directory_tree = {}
|
|
|
return
|
|
|
|
|
|
# Далее обрабатываем файлы шаблонов хранящихся в директории.
|
|
|
# Если в данный момент обходим дерево -- берем список файлов и
|
|
|
# директорий из него.
|
|
|
if not self.fill_trees and directory_tree:
|
|
|
template_directories, template_files =\
|
|
|
self._get_files_and_dirs_from_tree(template_files,
|
|
|
template_directories,
|
|
|
directory_tree)
|
|
|
|
|
|
# Просто псевдоним, чтобы меньше путаницы было далее.
|
|
|
template_parameters = directory_parameters
|
|
|
|
|
|
# Обрабатываем файлы шаблонов.
|
|
|
for template_name in template_files:
|
|
|
# Удаляем все параметры, которые не наследуются и используем
|
|
|
# полученный контейнер для сбора параметров файлов шаблонов.
|
|
|
template_parameters.remove_not_inheritable()
|
|
|
template_path = os.path.join(current_directory_path,
|
|
|
template_name)
|
|
|
|
|
|
# Обрабатываем файл шаблона шаблонизатором.
|
|
|
template_text = self._parse_template(template_parameters,
|
|
|
template_name,
|
|
|
FILE, current_directory_path)
|
|
|
if template_text is False:
|
|
|
continue
|
|
|
|
|
|
# template_parameters.print_parameters_for_debug()
|
|
|
|
|
|
# Если находимся на стадии заполнения дерева директорий --
|
|
|
# проверяем параметры package и action с заполнением дерева.
|
|
|
if (self._handling is None
|
|
|
and not self._check_package_and_action(
|
|
|
template_parameters,
|
|
|
template_path,
|
|
|
directory_tree=(directory_tree[directory_name] if
|
|
|
self.fill_trees else None))):
|
|
|
if template_parameters.handler:
|
|
|
# Если директория шаблонов является обработчиком --
|
|
|
# добавляем ее в словарь обработчиков и пропускаем ее.
|
|
|
self._handlers.update({template_parameters.handler:
|
|
|
(FILE, template_path)})
|
|
|
|
|
|
# * * * ПРИДУМАТЬ ОПТИМИЗАЦИЮ * * *
|
|
|
# TODO Потому что накладывать дерево каждый раз, когда
|
|
|
# обнаружены файлы какого-то пакета не рационально.
|
|
|
# Обновляем дерево директорий для данного пакета, если
|
|
|
# происходит его заполнение.
|
|
|
if self.fill_trees:
|
|
|
self._update_package_tree(template_parameters.package,
|
|
|
directory_tree[directory_name])
|
|
|
directory_tree[directory_name] = {}
|
|
|
continue
|
|
|
|
|
|
if self._handling is not None and template_parameters.handler:
|
|
|
if template_parameters.handler != self._handling:
|
|
|
self.output.set_error("'handler' parameter is not"
|
|
|
" available in other handler"
|
|
|
f" '{self._handling}'")
|
|
|
continue
|
|
|
|
|
|
# Если есть параметр merge добавляем его содержимое в список
|
|
|
# пакетов для последующей обработки.
|
|
|
if self.for_package and template_parameters.merge:
|
|
|
for pkg in template_parameters.merge:
|
|
|
if pkg not in self.processed_packages:
|
|
|
self.packages_to_merge.add(pkg)
|
|
|
|
|
|
# Если присутствует параметр notify, в котором указаны хэндлеры для
|
|
|
# последующего выполнения -- добавляем их в очередь.
|
|
|
if template_parameters.notify:
|
|
|
for handler_id in template_parameters.notify:
|
|
|
if handler_id not in self._handlers_queue:
|
|
|
self._handlers_queue.append(handler_id)
|
|
|
|
|
|
# Корректируем путь к целевому файлу.
|
|
|
target_file_path = self._make_target_path(current_target_path,
|
|
|
template_name,
|
|
|
template_parameters)
|
|
|
|
|
|
# Создаем объект пакета для файлов шаблонов этой директории.
|
|
|
template_package = package
|
|
|
if template_parameters.package:
|
|
|
if (template_package is None or
|
|
|
package.package_name != template_parameters.package):
|
|
|
template_package = Package(template_parameters.package,
|
|
|
chroot_path=self.cl_chroot_path,
|
|
|
autosave=self._pkg_autosave)
|
|
|
|
|
|
if not template_parameters.run and not template_parameters.exec:
|
|
|
if not template_parameters.format:
|
|
|
template_parameters.set_parameter({'format': 'raw'})
|
|
|
if not template_parameters.append:
|
|
|
if template_parameters.format == "raw":
|
|
|
template_parameters.set_parameter(
|
|
|
{'append': 'replace'})
|
|
|
else:
|
|
|
template_parameters.set_parameter({'append': 'join'})
|
|
|
|
|
|
if template_parameters.append != 'skip':
|
|
|
# Выполняем действия, указанные в шаблоне.
|
|
|
target_file_path = self._execute_template(
|
|
|
target_file_path,
|
|
|
template_parameters,
|
|
|
FILE, template_path,
|
|
|
template_text=template_text,
|
|
|
package=template_package)
|
|
|
if not target_file_path:
|
|
|
continue
|
|
|
|
|
|
# Проходимся далее по директориям.
|
|
|
for directory in template_directories:
|
|
|
if self.fill_trees:
|
|
|
self._walk_directory_tree(
|
|
|
directory, current_target_path,
|
|
|
directory_parameters.get_inheritables(),
|
|
|
directory_tree=directory_tree[directory_name],
|
|
|
package=package)
|
|
|
directory_tree[directory_name] = {}
|
|
|
else:
|
|
|
if isinstance(directory, DirectoryTree):
|
|
|
# Если директории взяты из дерева -- путь к директории
|
|
|
# соответствует корню каждой взятой ветви дерева.
|
|
|
directory_path = directory.base_directory
|
|
|
else:
|
|
|
directory_path = directory
|
|
|
|
|
|
self._walk_directory_tree(
|
|
|
directory_path, current_target_path,
|
|
|
directory_parameters.get_inheritables(),
|
|
|
package=package)
|
|
|
|
|
|
if self.fill_trees:
|
|
|
directory_tree = {}
|
|
|
|
|
|
def _scan_directory(self, directory_path: str
|
|
|
) -> Tuple[List[str], List[str]]:
|
|
|
'''Метод для получения и фильтрования списка файлов и директорий,
|
|
|
содержащихся в директории шаблонов.'''
|
|
|
template_files = []
|
|
|
template_directories = []
|
|
|
|
|
|
entries = os.scandir(directory_path)
|
|
|
|
|
|
for node in entries:
|
|
|
if not self._check_file_name(node.name):
|
|
|
continue
|
|
|
|
|
|
if node.is_symlink():
|
|
|
self.output.set_warning(
|
|
|
'symlink: {0} is ignored in the template directory: {1}'.
|
|
|
format(node.path, directory_path))
|
|
|
continue
|
|
|
elif node.is_dir():
|
|
|
template_directories.append(node.path)
|
|
|
elif node.is_file():
|
|
|
template_files.append(node.name)
|
|
|
|
|
|
return template_directories, template_files
|
|
|
|
|
|
def _check_file_name(self, filename: str) -> bool:
|
|
|
'''Метод для проверки соответствия имени файла содержимому переменной
|
|
|
main.cl_ignore_files.'''
|
|
|
for pattern in self.cl_ignore_files:
|
|
|
if fnmatch.fnmatch(filename, pattern):
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
def _get_files_and_dirs_from_tree(self, template_files: List[str],
|
|
|
template_directories: List[str],
|
|
|
directory_tree: DirectoryTree
|
|
|
) -> Tuple[List[str],
|
|
|
List[DirectoryTree]]:
|
|
|
'''Метод для получения списков файлов и директорий из дерева
|
|
|
директорий.'''
|
|
|
tree_files = []
|
|
|
tree_directories = []
|
|
|
|
|
|
for template in directory_tree:
|
|
|
if template in template_files:
|
|
|
tree_files.append(template)
|
|
|
else:
|
|
|
next_directory_tree =\
|
|
|
directory_tree.get_directory_tree(template)
|
|
|
tree_directories.append(next_directory_tree)
|
|
|
|
|
|
return tree_directories, tree_files
|
|
|
|
|
|
def _make_target_path(self, target_path: str, template_name: str,
|
|
|
parameters: ParametersContainer) -> str:
|
|
|
'''Метод для получения пути к целевому файлу с учетом наличия
|
|
|
параметров name, path и append = skip.'''
|
|
|
# Если есть непустой параметр name -- меняем имя шаблона.
|
|
|
if parameters.name is not False and parameters.name != '':
|
|
|
template_name = parameters.name
|
|
|
|
|
|
# Если для шаблона задан путь -- меняем путь к директории шаблона.
|
|
|
if parameters.path:
|
|
|
target_path = join_paths(self.templates_root,
|
|
|
parameters.path)
|
|
|
|
|
|
# Если параметр append не равен skip -- добавляем имя шаблона к
|
|
|
# целевому пути.
|
|
|
if not parameters.append == 'skip':
|
|
|
target_path = os.path.join(target_path,
|
|
|
template_name)
|
|
|
return target_path
|
|
|
|
|
|
def _parse_template(self, parameters: ParametersContainer,
|
|
|
template_name: str,
|
|
|
template_type: int,
|
|
|
template_directory: str) -> Union[str, bool]:
|
|
|
'''Метод для разбора шаблонов, получения значений их параметров и их
|
|
|
текста после отработки шаблонизитора.'''
|
|
|
if template_type == DIR:
|
|
|
template_path = template_directory
|
|
|
else:
|
|
|
template_path = join_paths(template_directory, template_name)
|
|
|
|
|
|
if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
|
|
|
self.datavars_module.main.cl['current_template'].set(template_path)
|
|
|
else:
|
|
|
self.datavars_module.main.cl['current_template'] = template_path
|
|
|
|
|
|
try:
|
|
|
self.template_engine.process_template(template_name,
|
|
|
template_type,
|
|
|
parameters=parameters)
|
|
|
return self.template_engine.template_text
|
|
|
except ConditionFailed as error:
|
|
|
self.output.set_warning('{0}. Template: {1}'.
|
|
|
format(str(error),
|
|
|
template_path))
|
|
|
return False
|
|
|
except Exception as error:
|
|
|
self.output.set_error('Template error: {0}. Template: {1}'.
|
|
|
format(str(error),
|
|
|
template_path))
|
|
|
return False
|
|
|
|
|
|
def _execute_template(self, target_path: str,
|
|
|
parameters: ParametersContainer,
|
|
|
template_type: int,
|
|
|
template_path: str,
|
|
|
template_text: str = '',
|
|
|
package: Optional[Package] = None
|
|
|
) -> Union[bool, str]:
|
|
|
'''Метод для наложения шаблонов и обработки информации полученной после
|
|
|
наложения.'''
|
|
|
try:
|
|
|
output = self.template_executor.execute_template(
|
|
|
target_path,
|
|
|
parameters,
|
|
|
template_type,
|
|
|
template_path,
|
|
|
template_text=template_text,
|
|
|
target_package=package)
|
|
|
# Если во время выполнения шаблона был изменен целевой путь,
|
|
|
# например, из-за ссылки на директорию в source -- обновляем
|
|
|
# целевой путь.
|
|
|
if output['target_path'] is not None:
|
|
|
target_path = output['target_path']
|
|
|
|
|
|
if output['warnings']:
|
|
|
if not isinstance(output['warnings'], list):
|
|
|
output['warnings'] = [output['warnings']]
|
|
|
for warning in output['warnings']:
|
|
|
self.output.set_warning(f"{warning}."
|
|
|
f" Template: {template_path}")
|
|
|
|
|
|
# Если есть вывод от параметра run -- выводим как info.
|
|
|
if output['stdout'] is not None:
|
|
|
self.output.set_info("stdout from template: {}:\n{}\n".format(
|
|
|
template_path,
|
|
|
output['stdout']))
|
|
|
|
|
|
# Если есть ошибки от параметра run -- выводим их как error.
|
|
|
if output['stderr'] is not None:
|
|
|
self.output.set_error("stderr from template: {}:\n{}\n".
|
|
|
format(template_path,
|
|
|
output['stderr']))
|
|
|
# Если run выполнен с ошибками -- пропускаем директорию.
|
|
|
return False
|
|
|
|
|
|
except TemplateExecutorError as error:
|
|
|
self.output.set_error('Template execution error: {}. Template: {}'.
|
|
|
format(str(error),
|
|
|
template_path))
|
|
|
return False
|
|
|
except FormatError as error:
|
|
|
if error.executable:
|
|
|
msg = 'Format execution error: {}. Template: {}'
|
|
|
else:
|
|
|
msg = 'Format joining error: {}. Template: {}'
|
|
|
self.output.set_error(msg.format(str(error), template_path))
|
|
|
return False
|
|
|
|
|
|
if template_type == DIR:
|
|
|
self.output.set_success('Processed directory: {}'.
|
|
|
format(template_path))
|
|
|
else:
|
|
|
self.output.set_success('Processed template: {}'.
|
|
|
format(template_path))
|
|
|
return target_path
|
|
|
|
|
|
def _update_package_tree(self, package: Package,
|
|
|
current_level_tree: Union[None, dict]) -> None:
|
|
|
'''Метод для обновления деревьев директорий пакетов, необходимых для
|
|
|
обработки шаблонов пакетов из значения параметра merge.'''
|
|
|
# Если текущему уровню соответствует заглушка None или он содержит
|
|
|
# файлы, то есть не пустой -- тогда есть смысл обновлять.
|
|
|
if current_level_tree is None or current_level_tree:
|
|
|
if package in self.packages_file_trees:
|
|
|
# Если для данного пакета уже есть дерево --
|
|
|
# накладываем на него текущее.
|
|
|
self.packages_file_trees[package].update_tree(
|
|
|
copy.deepcopy(self.directory_tree)
|
|
|
)
|
|
|
else:
|
|
|
# Если для данного пакета еще нет дерева --
|
|
|
# копируем для него текущее.
|
|
|
directory_tree = DirectoryTree(self.base_directory)
|
|
|
directory_tree.update_tree(copy.deepcopy(self.directory_tree))
|
|
|
self.packages_file_trees[package] = directory_tree
|
|
|
|
|
|
def _check_package_and_action(self, parameters: ParametersContainer,
|
|
|
template_path: str,
|
|
|
directory_tree: Optional[dict] = None
|
|
|
) -> bool:
|
|
|
'''Метод для проверки параметров action и package во время обработки
|
|
|
каталогов с шаблонами. Если среди аргументов указано также
|
|
|
дерево каталогов, то в случае несовпадения значений package для файла
|
|
|
или директории, им в дереве присваивается значение None.'''
|
|
|
if parameters.handler:
|
|
|
return False
|
|
|
|
|
|
if parameters.append != 'skip' or parameters.action:
|
|
|
if not parameters.action:
|
|
|
self.output.set_warning(
|
|
|
("Action parameter is not set for template:"
|
|
|
" {0}").format(template_path))
|
|
|
return False
|
|
|
elif parameters.action.startswith('!'):
|
|
|
action_matching = (parameters.action[1:].strip() not in
|
|
|
self.action)
|
|
|
elif parameters.action not in self.action:
|
|
|
action_matching = False
|
|
|
else:
|
|
|
action_matching = True
|
|
|
|
|
|
if not action_matching:
|
|
|
self.output.set_warning(
|
|
|
("Action parameter value '{0}' does not match its"
|
|
|
" current value{1} '{2}'. Template: {3}").format(
|
|
|
parameters.action,
|
|
|
's' if len(self.action) > 1
|
|
|
else '',
|
|
|
', '.join(self.action),
|
|
|
template_path))
|
|
|
return False
|
|
|
|
|
|
if self.for_package:
|
|
|
# if not parameters.package:
|
|
|
# if self.for_package is not NonePackage:
|
|
|
# self.output.set_warning(
|
|
|
# "'package' parameter is not defined. Template: {}".
|
|
|
# format(template_path))
|
|
|
|
|
|
if parameters.package != self.for_package:
|
|
|
if directory_tree is not None:
|
|
|
# Если есть дерево, которое собирается для текущих шаблонов
|
|
|
# и параметр package шаблона не совпадает с текущим,
|
|
|
# ставим заглушку, в качестве которой используется None.
|
|
|
template_name = os.path.basename(template_path)
|
|
|
directory_tree[template_name] = None
|
|
|
self.output.set_warning(
|
|
|
("'package' parameter value '{0}' does not "
|
|
|
"match its current target package '{1}'. "
|
|
|
"Template: {2}").
|
|
|
format(parameters.package.atom,
|
|
|
self.for_package.atom,
|
|
|
template_path)
|
|
|
)
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
@contextmanager
|
|
|
def _start_handling(self, handler_id: str):
|
|
|
'''Метод для перевода обработчика каталогов в режим обработки
|
|
|
хэндлеров.'''
|
|
|
try:
|
|
|
self._handling = handler_id
|
|
|
yield self
|
|
|
finally:
|
|
|
self._handling = None
|
|
|
|
|
|
@contextmanager
|
|
|
def _set_current_package(self, package: Package):
|
|
|
'''Метод для указания в шаблонизаторе пакета, для которого в данный
|
|
|
момент проводим настройку. Пока не используется.'''
|
|
|
try:
|
|
|
last_package = self.template_engine.for_package
|
|
|
self.template_engine.for_package = package
|
|
|
yield self
|
|
|
finally:
|
|
|
self.template_engine.for_package = last_package
|