diff --git a/calculate/templates/format/templates/__init__.py b/calculate/templates/format/templates/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/calculate/templates/template_processor.py b/calculate/templates/template_processor.py
new file mode 100644
index 0000000..ad6af22
--- /dev/null
+++ b/calculate/templates/template_processor.py
@@ -0,0 +1,746 @@
+import os
+import sys
+import re
+import stat
+import shutil
+from .template_engine import TemplateEngine, Variables, ConditionFailed
+from ..utils.io_module import IOModule
+from collections import OrderedDict
+from ..utils.mount import Mounts
+
+
+class TemplateAction:
+ def __init__(self, output_module=IOModule(), base_directory='/'):
+ self.output = output_module
+ self.mounts = None
+ self.base_directory = base_directory
+ self.exec_list = OrderedDict()
+
+ self.DIRECTORY_APPENDS = {
+ 'remove': self.remove_directory,
+ 'clear': self.clear_directory,
+ 'replace': self.remove_directory,
+ 'link': self.link_directory,
+ 'join': self.join_directory
+ }
+
+ self.FILE_APPENDS = {
+ 'replace': self.replace_file,
+ 'remove': self.remove_file,
+ 'clear': self.clear_file,
+ 'link': self.link_file,
+ 'join': self.join_file,
+ 'before': self.before_file,
+ 'after': self.after_file
+ }
+
+ def process_template_directory(self, target_path, template_path,
+ parameters={}):
+ self.template_path = template_path
+
+ # разбираем общие параметры шаблона.
+ self.force = parameters.pop('force', False)
+ self.autoupdate = parameters.pop('autoupdate', False)
+
+ self.source = parameters.pop('source', False)
+ if self.source and parameters['append'] != 'link':
+ self.output.set_error(("'source' parameter is set without "
+ "'append = link' in template {}").
+ format(self.template_path))
+ return False
+
+ self.chown = parameters.get('chown', False)
+ if not self.get_chown_values(self.chown):
+ return False
+
+ self.chmod = parameters.get('chmod', False)
+
+ def join_directory(self, target_path, template_path):
+ if os.access(target_path, os.F_OK):
+ return True
+ directory = target_path
+ directories_to_create = []
+ while not os.access(directory, os.F_OK) and directory:
+ directory = os.path.split(directory)[0]
+ directories_to_create.append(directory)
+ try:
+ current_mode, current_uid, current_gid = self.get_file_info(
+ target_path,
+ 'all')
+ except OSError:
+ self.output.set_error("No access to the directory: {}".
+ format(target_path))
+ return False
+ if self.chmode:
+ mode = self.chmode
+ else:
+ mode = current_mode
+
+ if self.chown['uid']:
+ uid = self.chown['uid']
+ else:
+ uid = current_uid
+
+ if self.chown['gid']:
+ gid = self.chown['gid']
+ else:
+ gid = current_gid
+
+ directories_to_create.reverse()
+ for directory in directories_to_create:
+ try:
+ if mode:
+ os.mkdir(directory)
+ os.chmod(directory, mode)
+ else:
+ os.mkdir(directory)
+ self.chown_directory(directory)
+ except OSError:
+ self.output.set_error('failed to create directory: {}'.
+ format(directory))
+ return False
+ return True
+
+ def remove_directory(self, target_path):
+ if os.path.isdir(target_path) or os.path.exists(target_path):
+ try:
+ shutil.rmtree(target_path)
+ return True
+ except Exception:
+ self.output.set_error("failed to delete the directory: {}".
+ format(target_path))
+ return False
+ else:
+ self.output.set_error("failed to delete the directory: {}".
+ format(target_path))
+ self.output.set_error(
+ "target file is not directory or not exists.".
+ format(target_path))
+ return False
+
+ def clear_directory(self, target_path):
+ if os.path.isdir(target_path) and os.path.exists(target_path):
+ for node in os.scandir(target_path):
+ if node.is_dir():
+ if self.remove_directory(node.path):
+ return True
+ else:
+ self.output.set_error('Failed to delete directory: {}'.
+ format(target_path))
+ else:
+ try:
+ os.remove(node.path)
+ return True
+ except OSError:
+ self.output.set_error('Failed to delete directory: {}'.
+ format(target_path))
+ else:
+ self.output.set_error('failed to delete directory: {}'.
+ format(target_path))
+ self.output.set_error(
+ 'target file is not directory or does not exist: {}'.
+ format(target_path))
+
+
+ def link_directory(self, target_path):
+ if not self.source:
+ self.output.set_error(("'source' parameter is not defined for "
+ "'append = link' in template {}").
+ format(self.template_path))
+ return False
+
+ def replace_directory(self, target_path):
+ pass
+
+ def process_template_file(self, target_path, template_path,
+ template_text='', parameters={}):
+ self.template_text = template_text
+ self.template_path = template_path
+
+ # разбираем общие параметры шаблона.
+ self.autoupdate = parameters.pop('autoupdate', False)
+ self.force = parameters.pop('force', False)
+ self.mirror = parameters.pop('mirror', False)
+ self.protected = parameters.pop('protected', False)
+
+ self.format = parameters.pop('format', False)
+ self.source = parameters.pop('source', False)
+ self.run = parameters.pop('run', False)
+ self.exec = parameters.pop('exec', False)
+ self.chmod = parameters.pop('chmod', False)
+ self.chown = parameters.pop('chown', False)
+
+ if (self.source and parameters['append'] not in
+ {'link', 'join', 'after', 'before'}):
+ self.output.set_error(("'source' parameter is set without"
+ " a suitable 'append' parameter"
+ " in template {}").
+ format(self.template_path))
+ return False
+
+ try:
+ append_value = parameters.pop('append', False)
+ if not append_value:
+ self.output("'append' parameter is not defined in template {}".
+ format(self.template_path))
+ return False
+ self.append = self.DIRECTORY_APPENDS[append_value]
+ except KeyError:
+ self.output.set_error("Unknown 'append' value in template {}".
+ format(self.template_path))
+ return False
+
+ # получаем uid и gid значения из параметра chown.
+ if self.chown:
+ self.chown = self.get_chown_values()
+ if not self.chown:
+ return False
+
+ # временный параметр format будет заменен на автоопределение формата.
+ if not self.format:
+ self.output.set_error(
+ "'format' parameter is not defined in template {}".
+ format(self.template_path)
+ )
+ return False
+
+ def join_file(self, target_path):
+ pass
+
+ def before_file(self, target_path):
+ pass
+
+ def after_file(self, target_path):
+ pass
+
+ def replace_file(self, target_path):
+ pass
+
+ def remove_file(self, target_path):
+ try:
+ if os.path.islink(target_path):
+ try:
+ os.unlink(target_path)
+ return True
+ except OSError:
+ self.output.set_error('Template error: {}'.
+ format(target_path))
+ self.output.set_error('Failed to delete the link: {}'.
+ format(target_path))
+ return False
+ if os.path.isfile(target_path):
+ try:
+ os.remove(target_path)
+ return True
+ except OSError:
+ self.output.set_error('Template error: {}'.
+ format(target_path))
+ self.output.set_error('Failed to delete the file: {}'.
+ format(target_path))
+ finally:
+ pass
+
+ def clear_file(self, target_path):
+ try:
+ with open(target_path, 'w') as f:
+ f.truncate(0)
+ except IOError:
+ self.output.set_error("Template error: {}".
+ format(self.template_path))
+ self.output.set_error("Failed to clear the file: {}".
+ format(target_path))
+ return False
+
+ def link_file(self, target_path):
+ pass
+
+ def get_parameter_names(self):
+ '''Временная замена механизма получения множества
+ поддерживаемых форматами параметров.'''
+ parameters_set = {'name', 'path', 'append', 'chmod', 'chown',
+ 'autoupdate', 'env', 'link', 'force', 'symbolic',
+ 'format', 'comment', 'protected', 'mirror', 'run',
+ 'exec', 'env', 'stretch', 'convert', 'dotall',
+ 'multiline', 'dconf', 'package', 'merge',
+ 'postmerge', 'action', 'rebuild'}
+
+ return parameters_set
+
+ # возможно, создать отдельный класс для хранения имен имеющихся в системе.
+ def get_chown_values(self, chown: str):
+ """Получить значения uid и gid из параметра chown."""
+ if chown and ':' in chown:
+ user_name, group_name = chown.split(':')
+ import pwd
+
+ try:
+ if self.base_directory == '/':
+ uid = pwd.getpwnam(user_name).pw_uid
+ else:
+ uid = self.get_uid_from_passwd(user_name)
+ except (KeyError, TypeError):
+ self.output.set_error(
+ "There is no such user in the system: {}".
+ format(user_name))
+ self.output.set_error(
+ "Wrong 'chown' value '{0}' in the template: {1}".
+ format(chown, self.template_path))
+ return False
+
+ import grp
+
+ try:
+ if self.base_directory == '/':
+ gid = grp.getgrnam(group_name).pw_gid
+ else:
+ gid = self.get_gid_from_group(group_name)
+ except (KeyError, TypeError):
+ self.output.set_error(
+ "There is no such group in the system: {}".
+ format(group_name))
+ self.output.set_error(
+ "Wrong 'chown' value '{0}' in the template: {1}".
+ format(chown, self.template_path))
+ return False
+ return {'uid': uid, 'gid': gid}
+ else:
+ self.output.set_error(
+ "Wrong 'chown' value '{0}' in the template: {1}".
+ format(chown, self.template_path))
+ return False
+
+ def get_uid_from_passwd(self, user_name: str):
+ """Взять uid из chroot passwd файла."""
+ passwd_file_path = os.path.join(self.base_directory, 'etc/passwd')
+ passwd_dictionary = []
+ 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_item = tuple(line.split(':')[0:3:2])
+ if (len(passwd_item) > 1 and passwd_item[0]
+ and passwd_item[0]):
+ passwd_dictionary.append(passwd_item)
+ passwd_dictionary = dict(passwd_dictionary)
+ return int(passwd_dictionary[user_name])
+ else:
+ self.output.set_error("passwd file was not found in {}".
+ format(passwd_file_path))
+ return False
+
+ def get_gid_from_group(self, group_name: str):
+ """Взять gid из chroot group файла."""
+ group_file_path = os.path.join(self.base_directory, 'etc/group')
+ group_dictionary = []
+ 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_item = tuple(line.split(':')[0:3:2])
+ if len(group_item) > 1 and group_item[0] and group_item[1]:
+ group_dictionary.append(group_item)
+ group_dictionary = dict(group_dictionary)
+ if group_name in group_dictionary:
+ return int(group_dictionary[group_name])
+ else:
+ self.output.set_error("'{0}' gid was not found in {1}".
+ format(group_name, group_file_path))
+ else:
+ self.output.set_error("group file was not found in {}".
+ format(group_file_path))
+ return False
+
+ def chown_directory(self, target_path):
+ """Сменить владельца директории."""
+ try:
+ os.chown(target_path, self.chown['uid'], self.chown['gid'])
+ except (OSError, Exception) as error:
+ # возможно потребуются дополнительные проверки.
+ self.output.set_error('Can not chown directory: {}'.
+ format(target_path))
+ return False
+
+ def chmod_directory(self, target_path):
+ """Сменить права доступа к директории."""
+ try:
+ os.chmod(target_path, self.chmod)
+ except (OSError, Exception) as error:
+ # возможно потребуются дополнительные проверки.
+ self.output.set_error('Can not chmod directory: {}'.
+ format(target_path))
+ return False
+
+ def chown_file(self, target_path, check_existation=True):
+ """Сменить владельца файла."""
+ try:
+ if check_existation and not os.path.exists(target_path):
+ open(target_path, 'w').close()
+ os.lchown(target_path, self.chown['uid'], self.chown['gid'])
+ return True
+ except (OSError, Exception) as error:
+ # возможно потребуются дополнительные проверки.
+ self.output.set_error('Can not chown file: {}'.format(target_path))
+ return False
+
+ def chmod_file(self, target_path, check_existation=True):
+ """Сменить права доступа к директории."""
+ try:
+ if check_existation and not os.path.exists(target_path):
+ open(target_path, 'w').close()
+ os.chmod(target_path, self.chmod)
+ return True
+ except (OSError, Exception) as error:
+ # возможно потребуются дополнительные проверки.
+ self.output.set_error('Can not chmod file: {}'.format(target_path))
+ return False
+
+ def get_file_info(self, path, info='all'):
+ file_stat = os.stat(path)
+ if info == 'all':
+ return stat.S_IMODE(file_stat.st_mode), file_stat.st_uid,\
+ file_stat.st_gid
+ if info == 'mode':
+ return stat.S_IMODE(file_stat.st_mode)
+ if info == 'owner':
+ return file_stat.st_uid, file_stat.st_gid
+
+ def is_vfat(self, path):
+ if self.mounts is None:
+ self.mounts = Mounts()
+ if self.mounts.get_from_fstab(what=self.mounts.TYPE,
+ where=self.mounts.DIR,
+ is_in=path) in ('vfat', 'ntfs-3g',
+ 'ntfs'):
+ return True
+ return False
+
+ def check_os_error(self, error, path):
+ if hasattr(error, 'errno') and error.errno == os.errno.EPERM:
+ if self.is_vfat(path):
+ return True
+ if hasattr(error, 'errno') and error.errno == os.errno.EACCES and\
+ 'var/calculate/remote' in path:
+ return True
+ return False
+
+
+class FormatDetector:
+ pass
+
+
+class DirectoryProcessor:
+ chmod_regex = re.compile(r'\d{3}')
+
+ def __init__(self, action, datavars_module=Variables(), package='',
+ output_module=IOModule()):
+ self.INHERITABLE_PARAMETERS = {'chmod', 'chown', 'autoupdate',
+ 'env', 'package', 'action'}
+ self.datavars_module = datavars_module
+ self.output = output_module
+ self.template_action = TemplateAction(output_module=self.output)
+
+ self.action = action
+ self.chroot_path = datavars_module.main.cl_chroot.path
+ self.parameters_set = self.template_action.get_parameter_names()
+ self.template_engine = TemplateEngine(
+ datavars_module=datavars_module,
+ parameters_set=self.parameters_set
+ )
+ self.template_paths = (self.datavars_module.
+ main.cl_template.path.split(','))
+
+ self.for_package = package
+ self.processed_packages = []
+
+ self.packages_to_merge = []
+ self.packages_file_paths = {}
+
+ def process_template_directories(self):
+ # Проходим каталоги из main.cl_template.path
+ for directory_path in self.template_paths:
+ directory_path = directory_path.strip()
+ entries = os.scandir(directory_path)
+ for node in entries:
+ self.walk_directory_tree(node.path,
+ self.chroot_path,
+ {}, set())
+
+ if self.for_package:
+ self.output.set_info('Processing packages from merge parameter...')
+ self.processed_packages.append(self.for_package)
+ self.merge_packages()
+
+ def merge_packages(self):
+ not_merged_packages = []
+ while self.packages_to_merge:
+ self.for_package = self.packages_to_merge.pop()
+ if self.for_package not in self.packages_file_paths:
+ self.output.set_error(
+ "Error: package '{0}' not found for action '{1}'.".
+ format(self.for_package, self.action)
+ )
+ not_merged_packages.append(self.for_package)
+ continue
+ for template_path in self.packages_file_paths[self.for_package]:
+ base_directory, queue = self.get_directories_queue(
+ template_path
+ )
+ first_directory = queue.pop()
+ first_directory_path = os.path.join(base_directory,
+ first_directory)
+ self.walk_directory_tree(first_directory_path,
+ self.chroot_path, {}, set(),
+ directories_queue=queue)
+
+ self.processed_packages.append(self.for_package)
+ if not_merged_packages:
+ self.output.set_error('Packages {} is not merged.'.
+ format(','.join(self.packages_to_merge)))
+ else:
+ self.output.set_success('All packages are merged...')
+
+ def get_directories_queue(self, path):
+ directories_queue = []
+ for base_dir in self.template_paths:
+ base_dir = base_dir.strip()
+ if path.startswith(base_dir):
+ base_directory = base_dir
+ break
+
+ while path != base_directory:
+ path, dir_name = os.path.split(path)
+ directories_queue.append(dir_name)
+
+ return base_directory, directories_queue
+
+ def walk_directory_tree(self, current_directory_path, target_path,
+ directory_parameters, directory_env,
+ directories_queue=[]):
+ template_files = []
+ template_directories = []
+ directory_name = os.path.basename(current_directory_path)
+
+ current_env = directory_env.copy()
+ current_target_path = target_path
+
+ directory_parameters = directory_parameters.copy()
+
+ entries = os.scandir(current_directory_path)
+ self.template_engine.change_directory(current_directory_path)
+ for node in entries:
+ # Временное, вместо этого добавить переменную ignore_files.
+ if node.name.endswith('.swp'):
+ continue
+ if node.is_dir():
+ template_directories.append(node.path)
+ elif node.is_file():
+ template_files.append(node.name)
+
+ # обрабатываем в первую очередь шаблон директории.
+ if '.calculate_directory' in template_files:
+ template_files.remove('.calculate_directory')
+
+ try:
+ self.template_engine.process_template('.calculate_directory',
+ env=current_env)
+ except ConditionFailed:
+ self.output.set_warning('Condition failed in {}'.
+ format(current_directory_path))
+ self.output.console_output.print_default('\n')
+ return
+ except Exception as error:
+ self.output.set_error('Template error: {}'.
+ format(str(error)))
+ return
+
+ directory_parameters.update(self.template_engine.parameters)
+
+ # Если есть параметр name -- меняем имя текущего каталога.
+ if 'name' in directory_parameters:
+ directory_name = directory_parameters['name']
+
+ # Если есть параметр path -- меняем текущий путь к целевому
+ # каталогу.
+ if 'path' in directory_parameters:
+ current_target_path = directory_parameters['path']
+
+ if directory_parameters.get('append', None) != 'skip':
+ current_target_path = os.path.join(current_target_path,
+ directory_name)
+ if directory_parameters.get('action', None) != self.action:
+ self.output.set_error(
+ 'Action parameter not found or not relevant: {}'.
+ format(directory_name))
+ self.output.console_output.print_default('\n')
+ return
+
+ # Если шаблоны обрабатываются для одного пакета -- проверяем
+ # параметр package и собираем пути к директориям других
+ # пакетов, необходимые для merge.
+ if self.for_package:
+ if 'package' not in directory_parameters:
+ self.output.set_warning(
+ "'package' is not defined for {}".
+ format(current_directory_path)
+ )
+ return
+ elif directory_parameters['package'] != self.for_package:
+ package_name = directory_parameters['package']
+ if package_name in self.packages_file_paths:
+ self.packages_file_paths[package_name].append(
+ current_directory_path
+ )
+ else:
+ self.packages_file_paths[package_name] =\
+ [current_directory_path]
+ self.output.set_warning(
+ "'package' parameter is not actual in {}".
+ format(current_directory_path)
+ )
+ return
+
+ # Если параметр env содержит несуществующий модуль -- шаблон не
+ # выполняем.
+ if 'env' in directory_parameters:
+ env_to_check = directory_parameters['env'].split(',')
+ for name in env_to_check:
+ if name.strip() not in self.datavars_module:
+ return
+ del(directory_parameters['env'])
+
+ # Если есть параметр merge -- собираем указанные в нем пакеты
+ # в списке имен пакетов для последующей обработки.
+ if self.for_package and 'merge' in directory_parameters:
+ merge_list = directory_parameters['merge'].split(',')
+ del(directory_parameters['merge'])
+ for package in merge_list:
+ self.packages_to_merge.append(package.strip())
+
+ # Выполняем действие с директорией.
+ self.output.set_success('Processing directory: {}'.
+ format(current_directory_path))
+ self.output.console_output.print_default(
+ 'Directory parameters: {}\n'.
+ format(directory_parameters)
+ )
+ self.output.set_info(
+ 'Actions using template directory. Target: {}'.
+ format(current_target_path)
+ )
+
+ # Оставляем только наследуемые параметры.
+ directory_parameters = {name: value for name, value in
+ directory_parameters.items()
+ if name in self.INHERITABLE_PARAMETERS}
+ if 'chmod' in directory_parameters:
+ match = self.chmod_regex.search(
+ str(directory_parameters['chmod'])
+ )
+ if match:
+ del(directory_parameters['chmod'])
+
+ if directories_queue:
+ next_node = directories_queue.pop()
+ if next_node in template_files:
+ template_files = [next_node]
+ template_directories = []
+ else:
+ next_directory_path = os.path.join(current_directory_path,
+ next_node)
+ template_files = []
+ template_directories = [next_directory_path]
+
+ # обрабатываем файлы шаблонов хранящихся в директории.
+ for template_name in template_files:
+ template_parameters = {}
+ template_parameters = directory_parameters.copy()
+ try:
+ self.template_engine.process_template(template_name,
+ env=current_env.copy())
+ except ConditionFailed:
+ self.output.set_warning('Condition failed for: {}'.
+ format(template_name))
+ continue
+ except Exception as error:
+ self.output.set_error('Template error: {}'.
+ format(str(error)))
+ continue
+
+ template_parameters.update(self.template_engine.parameters)
+ template_text = self.template_engine.template_text
+
+ if template_parameters.get('action', None) != self.action:
+ self.output.set_warning(
+ 'Action parameter not found or not relevant.'
+ )
+ continue
+
+ if 'action' in template_parameters:
+ del(template_parameters['action'])
+
+ if self.for_package:
+ if 'package' not in template_parameters:
+ self.output.set_warning("'package' is not defined for {}".
+ format(template_name))
+ continue
+ elif template_parameters['package'] != self.for_package:
+ package_name = template_parameters['package']
+ template_path = os.path.join(current_directory_path,
+ template_name)
+ if package_name in self.packages_file_paths:
+ self.packages_file_paths[package_name].append(
+ template_path
+ )
+ else:
+ self.packages_file_paths[package_name] =\
+ [template_path]
+ self.output.set_warning(
+ "'package' parameter is not actual in {}".
+ format(template_name)
+ )
+ continue
+
+ if 'merge' in template_parameters:
+ merge_list = template_parameters['merge'].split(',')
+ del(template_parameters['merge'])
+ for package_name in merge_list:
+ if package_name not in self.processed_packages:
+ self.packages_to_merge.append(package_name.strip())
+
+ # Если параметр env содержит несуществующий модуль -- шаблон не
+ # выполняем.
+ if 'env' in template_parameters:
+ env_to_check = template_parameters['env'].split(',')
+ for name in env_to_check:
+ if name.strip() not in self.datavars_module:
+ continue
+ del(template_parameters['env'])
+
+ if 'name' in template_parameters:
+ template_name = template_parameters['name']
+
+ if 'path' in template_parameters:
+ target_file_path = os.path.join(template_parameters['path'],
+ template_name)
+ else:
+ target_file_path = os.path.join(current_target_path,
+ template_name)
+
+ # Выполняем действие с использованием шаблона.
+ self.output.set_success('Processing template: {}...'.
+ format(template_name))
+ self.output.console_output.print_default(
+ 'With parameters: {}\n'.
+ format(template_parameters)
+ )
+ self.output.set_info('Target file: {}.\nTemplate text:'.
+ format(target_file_path))
+ self.output.console_output.print_default(template_text + '\n\n')
+
+ # проходимся далее по директориям.
+ for directory_path in template_directories:
+ self.walk_directory_tree(directory_path, current_target_path,
+ directory_parameters, current_env,
+ directories_queue=directories_queue)
diff --git a/calculate/utils/device.py b/calculate/utils/device.py
new file mode 100644
index 0000000..6527639
--- /dev/null
+++ b/calculate/utils/device.py
@@ -0,0 +1,631 @@
+import os
+import re
+from . import files
+from .tools import Cachable, GenericFS, unique
+from time import sleep
+from pprint import pprint
+
+
+def get_uuid_dict(reverse=False, devices=()):
+ '''Получить словарь со значениями UUID блочных устройств.'''
+ blkid_process = files.Process('/sbin/blkid', '-s', 'UUID',
+ '-c', '/dev/null', *devices)
+ re_split = re.compile('^([^:]+):.*UUID="([^"]+)"', re.S)
+
+ output_items = {}
+ search_results = []
+ lines = blkid_process.read_lines()
+ for line in lines:
+ search_result = re_split.search(line)
+ if search_result:
+ search_results.append(search_result.groups())
+
+ output_items = (("UUID={}".format(uuid), udev.get_devname(name=dev,
+ fallback=dev))
+ for dev, uuid in search_results)
+ if reverse:
+ return {v: k for k, v in output_items}
+ else:
+ return {k: v for k, v in output_items}
+
+
+def find_device_by_partition(device):
+ '''Найти устройство, к которому относится данный раздел.'''
+ info = udev.get_device_info(name=device)
+ if info.get('DEVTYPE', '') != 'partition':
+ return ''
+ parent_path = os.path.dirname(info.get('DEVPATH', ''))
+ if parent_path:
+ device_info = udev.get_device_info(path=parent_path)
+ return device_info.get('DEVNAME', '')
+ return False
+
+
+def count_partitions(device_name):
+ '''Посчитать количество разделов данного устройства.'''
+ syspath = udev.get_device_info(name=device_name).get('DEVPATH', '')
+ if not syspath:
+ return 0
+ device_name = os.path.basename(syspath)
+ return len([x for x in sysfs.listdir(syspath) if x.startswith(device_name)])
+
+
+def get_lspci_output(filter_name=None, short_info=False):
+ '''Функция для чтения вывода lspci. Возвращает словарь с идентификаторами
+ устройств и информацией о них.'''
+ re_data = re.compile(r'(\S+)\s"([^"]+)"\s+"([^"]+)"\s+"([^"]+)"', re.S)
+ lspci_column_names = ('type', 'vendor', 'name')
+ if filter_name:
+ if callable(filter_name):
+ filter_function = filter_name
+ else:
+ def filter_function(input):
+ return filter_name in input
+ else:
+ def filter_function(input):
+ return input
+ lspci_path = files.check_utils('/usr/sbin/lspci')
+ lspci_output = files.Process(lspci_path, '-m').read_lines()
+ output = {}
+ lspci_output = list(filter(filter_function, lspci_output))
+ for line in lspci_output:
+ search_result = re_data.search(line)
+ if search_result:
+ search_result = search_result.groups()
+ device_id = search_result[0]
+ output[device_id] = {key: value for key, value in zip(
+ lspci_column_names,
+ search_result[1:])}
+ return output
+
+
+class LvmCommand:
+ '''Класс для работы с командой lvm и выполнения с ее помощью различных
+ действий.'''
+ @property
+ def lvm_command(self):
+ '''Возвращает кешированный путь к lvm.'''
+ return files.get_program_path('/sbin/lvm')
+
+ def get_physical_extent_size(self):
+ if not self.lvm_command:
+ return ''
+ pv_data = files.Process(self.lvm_command, 'lvmconfig', '--type',
+ 'full', 'allocation/physical_extent_size')
+ if pv_data.success():
+ return pv_data.read()
+ return ''
+
+ def get_pvdisplay_output(self, option):
+ '''Получить вывод pv_display c указанной option.'''
+ if not self.lvm_command:
+ return ''
+ pv_data = files.Process(self.lvm_command, 'pvdisplay', '-C', '-o',
+ option, '--noh')
+ if pv_data.success():
+ return pv_data.read()
+ return False
+
+ def vg_scan(self):
+ '''Найти существующие в системе группы томов.'''
+ if self.lvm_command:
+ return files.Process(self.lvm_command, 'vgscan').success()
+ return False
+
+ def vg_change(self):
+ '''Изменить атрибуты группы томов.'''
+ if self.lvm_command:
+ failed = files.Process('vgchange', '-ay').failed()
+ failed |= files.Process('vgchange', '--refresh').failed()
+ return not failed
+ return False
+
+ def lv_change(self, groups):
+ '''Изменить атрибуты логического тома.'''
+ if self.lvm_command:
+ failed = False
+ for group in groups:
+ failed |= files.Process('lvchange', '-ay', group).failed()
+ failed |= files.Process('lvchange', '--refresh',
+ group).failed()
+ return failed
+ return False
+
+ def execute(self, *command):
+ '''Выполнить указанную LVM команду.'''
+ if self.lvm_command:
+ return files.Process(self.lvm_command, *command).success()
+ return False
+
+ def double_execute(self, *command):
+ '''Выполнить дважды указанную LVM команду.'''
+ if not self.execute(*command):
+ sleep(2)
+ return self.execute(*command)
+ return False
+
+ def remove_lv(self, vg, lv):
+ '''Удалить указанный логических томов из системы.'''
+ return self.double_execute('lvremove', '{0}/{1}'.format(vg, lv), '-f')
+
+ def remove_vg(self, vg):
+ '''Удалить указанную группу томов из системы.'''
+ return self.double_execute('vgremove', vg, '-f')
+
+ def remove_pv(self, pv):
+ '''Удалить LVM метку с физического тома.'''
+ return self.double_execute('pvremove', pv, '-ffy')
+
+
+class Lvm(Cachable):
+ '''Класс для получения информации о lvm.'''
+ def __init__(self, lvm_command):
+ super().__init__()
+ self.lvm_command = lvm_command
+
+ def get_pvdisplay_full(self):
+ '''Получить полный вывод команды pvdisplay.'''
+ get_pvdisplay_output = self.get_pvdisplay_output(
+ 'vg_name,lv_name,pv_name'
+ ).strip()
+ if get_pvdisplay_output:
+ output = (line.split() for line in
+ get_pvdisplay_output.split('\n'))
+ return [tuple(y.strip() for y in x)
+ for x in output if x and len(x) == 3]
+
+ @property
+ def get_volume_groups(self):
+ '''Получить имеющиеся в системе группы томов.'''
+ return sorted({vg for vg, lv, pv in self.get_pvdisplay_full()})
+
+ @Cachable.method_cached()
+ def get_physical_extent_size(self):
+ '''Получить размер физического диапазона.'''
+ return self.lvm_command.get_physical_extent_size()
+
+ @Cachable.method_cached()
+ def get_pvdisplay_output(self, option):
+ '''Получить с помощью pvdisplay информацию .'''
+ return self.lvm_command.get_pvdisplay_output(option)
+
+ def get_used_partitions(self, vg_condition, lv_condition):
+ '''Получить испоьзуемые разделы.'''
+ return list(sorted({part for vg, lv, part in self.get_pvdisplay_full()
+ if vg == vg_condition and lv == lv_condition}))
+
+ def refresh(self):
+ if not os.environ.get('EBUILD_PHASE', False):
+ self.lvm_command.vg_scan()
+ self.lvm_command.vg_change()
+ self.lvm_command.lv_change(lvm)
+
+ def remove_lv(self, vg, lv):
+ return self.lvm_command.remove_lv(vg, lv)
+
+ def remove_vg(self, vg):
+ return self.lvm_command.remove_vg(vg)
+
+ def remove_pv(self, pv):
+ return self.lvm_command.remove_pv(pv)
+
+
+class MdadmCommand:
+ '''Класс для работы с командой mdadm.'''
+ @property
+ def mdadm_command(self):
+ '''Возвращает кешированный путь к mdamd.'''
+ return files.get_program_path('/sbin/mdadm')
+
+ def stop_raid(self, raid_device):
+ '''Остановить устройство из RAID-массива.'''
+ if not self.mdadm_command:
+ return False
+ return files.Process(self.mdadm_command, '-S', raid_device).success()
+
+ def zero_superblock(self, device):
+ '''Затереть superblock для составляющей RAID-массива.'''
+ if not self.mdadm_command:
+ return False
+ return files.Process(self.mdadm_command, '--zero_superblock',
+ device).success()
+
+
+class RAID:
+ '''Класс для работы с RAID-массивами.'''
+ def __init__(self, mdadm_command):
+ self.mdadm_command = mdadm_command
+
+ def get_devices_info(self, raid_device):
+ '''Получить информацию о RAID-массиве.'''
+ device = udev.get_device_info(path=raid_device)
+ if udev.is_raid(device):
+ for raid_block in sysfs.glob(raid_device, 'md/dev-*', 'block'):
+ yield udev.get_device_info(sysfs.join_path(raid_block))
+
+ def get_devices(self, raid_device, path_name='DEVPATH'):
+ '''Получить устройства /dev, из которых состоит RAID-массив.
+ Не возвращает список этих устройств для раздела сделанного для RAID
+ (/dev/md0p1).'''
+ for device_info in self.get_devices_info(raid_device):
+ device_name = device_info.get(path_name, '')
+ if device_name:
+ yield device_name
+
+ def get_devices_syspath(self, raid_device):
+ '''Получить sysfs пути устройств, из которых состоит RAID-массив.'''
+ for device in self.get_devices(raid_device, path_name='DEVPATH'):
+ yield device
+
+ def remove_raid(self, raid_name):
+ '''Удалить RAID-массив.'''
+ raid_parts = list(self.get_devices(udev.get_syspath(name=raid_name)))
+ failed = False
+ failed |= not (self.mdadm_command.stop_raid(raid_name) or
+ self.mdadm_command.stop_raid(raid_name))
+ for device in raid_parts:
+ failed |= not (self.mdadm_command.zero_superblock(device) or
+ self.mdadm_command.zero_superblock(device))
+ return not failed
+
+
+class DeviceFS(GenericFS):
+ '''Базовый класс для классов предназначенных для работы с sysfs и /dev'''
+ def __init__(self, filesystem=None):
+ if isinstance(filesystem, GenericFS):
+ self.filesystem = filesystem
+ else:
+ self.filesystem = files.RealFS('/')
+
+ def join_path(self, *paths):
+ output = os.path.join('/', *(_path[1:] if _path.startswith('/')
+ else _path for _path in paths))
+ return output
+
+ def exists(self, *paths):
+ return self.filesystem.exists(self.join_path(*paths))
+
+ def read(self, *paths):
+ return self.filesystem.read(self.join_path(*paths))
+
+ def realpath(self, *paths):
+ return self.filesystem.realpath(self.join_path(*paths))
+
+ def write(self, *args):
+ data = args[-1]
+ paths = args[:-1]
+ self.filesystem.write(self.join_path(*paths), data)
+
+ def listdir(self, *paths, full_path=False):
+ return self.filesystem.listdir(self.join_path(*paths),
+ full_path=full_path)
+
+ def glob(self, *paths):
+ for file_path in self.filesystem.glob(self.join_path(*paths)):
+ yield file_path
+
+
+class SysFS(DeviceFS):
+ '''Класс для работы с sysfs.'''
+ BASE_DIRECTORY = '/sys'
+ PATH = {'firmware': 'firmware',
+ 'efi': 'firmware/efi',
+ 'efivars': 'firmware/efi/efivars',
+ 'classnet': 'class/net',
+ 'input': 'class/input',
+ 'block': 'block',
+ 'dmi': 'class/dmi/id',
+ 'module': 'module',
+ 'block_scheduler': 'queue/scheduler'}
+
+ def join_path(self, *paths):
+ output = os.path.join('/', *(_path[1:] if _path.startswith('/')
+ else _path for _path in paths))
+ if output.startswith(self.BASE_DIRECTORY):
+ return output
+ else:
+ return os.path.join(self.BASE_DIRECTORY, output[1:])
+
+ def glob(self, *paths):
+ for _path in self.filesystem.glob(self.join_path(*paths)):
+ yield _path[len(self.BASE_DIRECTORY):]
+
+
+class DevFS(DeviceFS):
+ '''Класс для работы с /dev.'''
+ BASE_DIRECTORY = '/dev'
+
+ def join_path(self, *paths):
+ output = os.path.join('/', *(_path[1:] if _path.startswith('/')
+ else _path for _path in paths))
+ if output.startswith(self.BASE_DIRECTORY):
+ return output
+ else:
+ return os.path.join(self.BASE_DIRECTORY, output[1:])
+
+ def glob(self, *paths):
+ for _path in self.filesystem.glob(self.join_path(*paths)):
+ yield _path[len(self.BASE_DIRECTORY):]
+
+
+class UdevAdmNull:
+ def info_property(self, path=None, name=None):
+ return {}
+
+ def info_export(self):
+ return ''
+
+ def settle(self, timeout=15):
+ pass
+
+ def trigger(self, subsystem=None):
+ pass
+
+ @property
+ def broken(self):
+ return False
+
+
+class UdevAdmCommand(UdevAdmNull):
+ def __init__(self):
+ self.first_run = True
+
+ @property
+ def udevadm_cmd(self):
+ return files.get_program_path('/sbin/udevadm')
+
+ @property
+ def broken(self):
+ return not bool(self.udevadm_cmd)
+
+ def info_property(self, path=None, name=None):
+ if self.first_run:
+ self.trigger("block")
+ self.settle()
+ self.first_run = False
+
+ if name is not None:
+ type_query = "--name"
+ value = name
+ else:
+ type_query = "--path"
+ value = path
+ udevadm_output = files.Process(self.udevadm_cmd, "info",
+ "--query", "property",
+ type_query, value).read_lines()
+ output_items = []
+ for line in udevadm_output:
+ if '=' in line:
+ output_items.append(line.partition('=')[0::2])
+ return dict(output_items)
+
+ def info_export(self):
+ return files.Process(self.udevadm_cmd, 'info', '-e').read().strip()
+
+ def trigger(self, subsystem=None):
+ if subsystem:
+ files.Process(self.udevadm_cmd, 'trigger', '--subsystem-match',
+ subsystem).success()
+ else:
+ files.Process(self.udevadm_cmd, 'trigger').success()
+
+ def settle(self, timeout=15):
+ files.Process(self.udevadm_cmd, 'settle', '--timeout={}'.
+ format(timeout)).success()
+
+
+class Udev:
+ '''Класс возвращающий преобразованную или кэшированную информацию о системе
+ из udev.'''
+ def __init__(self, udevadm=None):
+ self.path_cache = {}
+ self.name_cache = {}
+ if isinstance(udevadm, UdevAdmCommand):
+ self.udevadm = udevadm
+ else:
+ self.udevadm = UdevAdmCommand()
+ if self.udevadm.broken:
+ self.udevadm = UdevAdmNull()
+
+ def clear_cache(self):
+ self.path_cache = {}
+ self.name_cache = {}
+ self.udevadm = UdevAdmCommand()
+
+ def get_device_info(self, path=None, name=None):
+ if name is not None:
+ cache = self.name_cache
+ value = devfs.realpath(name)
+ name = value
+ else:
+ cache = self.path_cache
+ value = sysfs.realpath(path)
+ path = value
+
+ if value not in cache:
+ data = self.udevadm.info_property(path, name)
+ devname = data.get('DEVNAME', '')
+ devpath = data.get('DEVPATH', '')
+ if devname:
+ self.name_cache[devname] = data
+ if devpath:
+ devpath = '/sys{}'.format(devpath)
+ self.path_cache[devname] = data
+ return data
+ else:
+ return cache[value]
+
+ def get_block_devices(self):
+ for block in sysfs.glob(sysfs.PATH['block'], '*'):
+ yield block
+ blockname = os.path.basename(block)
+ for part in sysfs.glob(block, '{}*'.format(blockname)):
+ yield part
+
+ def syspath_to_devname(self, devices, drop_empty=True):
+ for device_path in devices:
+ info = self.get_device_info(path=device_path)
+ if 'DEVNAME' in info:
+ yield info['DEVNAME']
+ elif not drop_empty:
+ yield ''
+
+ def devname_to_syspath(self, devices, drop_empty=True):
+ for device_name in devices:
+ info = self.get_device_info(name=device_name)
+ if 'DEVPATH' in info:
+ yield info['DEVPATH']
+ elif not drop_empty:
+ yield ''
+
+ def get_devname(self, path=None, name=None, fallback=None):
+ if fallback is None:
+ if name:
+ fallback = name
+ else:
+ fallback = ''
+ return self.get_device_info(path, name).get('DEVNAME', fallback)
+
+ def get_syspath(self, path=None, name=None, fallback=None):
+ if fallback is None:
+ if path:
+ fallback = path
+ else:
+ fallback = ''
+ return self.get_device_info(path, name).get('DEVPATH', fallback)
+
+ def refresh(self, trigger_only=False):
+ if not trigger_only:
+ self.clear_cache()
+ files.quite_unlink('/etc/blkid.tab')
+ if not os.environ.get('EBUILD_PHASE'):
+ self.udevadm.trigger(subsystem='block')
+ self.udevadm.settle(15)
+
+ def is_cdrom(self, udev_data):
+ return udev_data.get('ID_CDROM', '') == '1'
+
+ def is_device(self, udev_data):
+ if 'DEVPATH' not in udev_data:
+ return False
+ return (sysfs.exists(udev_data.get('DEVPATH', ''), 'device') and
+ udev_data.get('DEVTYPE', '') == 'disk')
+
+ def is_raid(self, udev_data):
+ return (udev_data.get('MD_LEVEL', '').startswith('raid') and
+ udev_data.get('DEVTYPE', '') == 'disk')
+
+ def is_raid_partition(self, udev_data):
+ return (udev_data.get('MD_LEVEL', '').startswith('raid') and
+ udev_data.get('DEVTYPE', '') == 'partition')
+
+ def is_lvm(self, udev_data):
+ return 'DM_LV_NAME' in udev_data and 'DM_VG_NAME' in udev_data
+
+ def is_partition(self, udev_data):
+ return udev_data.get('DEVTYPE', '') == 'partition'
+
+ def is_block_device(self, udev_data):
+ return udev_data.get('SUBSYSTEM', '') == 'block'
+
+ def get_device_type(self, path=None, name=None):
+ info = self.get_device_info(path, name)
+ syspath = info.get('DEVPATH', '')
+ if self.is_cdrom(info):
+ return 'cdrom'
+ if self.is_device(info):
+ return 'disk'
+ if self.is_partition(info):
+ return '{}-partition'.format(
+ self.get_device_type(os.path.dirname(syspath)))
+ if self.is_raid(info):
+ for raid_device in raid.get_devices_syspath(syspath):
+ return '{}-{}'.format(self.get_device_type(raid_device),
+ info['MD_LEVEL'])
+ else:
+ return 'loop'
+ if self.is_lvm(info):
+ for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
+ info['DM_LV_NAME']):
+ return '{}-lvm'.format(self.get_device_type(name=lv_device))
+ if self.is_block_device(info):
+ return 'loop'
+ return ''
+
+ def get_partition_type(self, path=None, name=None):
+ info = self.get_device_info(path, name)
+ if info.get('ID_PART_ENTRY_SCHEME') == 'dos':
+ part_id = info.get('ID_PART_ENTRY_TYPE', '')
+ part_number = info.get('ID_PART_ENTRY_NUMBER', '')
+ if part_id and part_number:
+ if part_id == '0x5':
+ return 'extended'
+ elif int(part_number) > 4:
+ return 'logical'
+ else:
+ return 'primary'
+ return info.get('ID_PART_TABLE_TYPE', '')
+
+ def _get_disk_devices(self, path=None, name=None):
+ info = udev.get_device_info(path=path, name=name)
+ syspath = info.get('DEVPATH', '')
+ if syspath:
+ # real device
+ if self.is_device(info):
+ yield True, info.get('DEVNAME', '')
+ # partition
+ elif self.is_partition(info):
+ for device in self._get_disk_devices(
+ path=os.path.dirname(syspath)):
+ yield device
+ # md raid
+ elif udev.is_raid(info):
+ yield False, info.get('DEVNAME', '')
+ for raid_device in sorted(raid.get_devices_syspath(syspath)):
+ for device in self._get_disk_devices(path=raid_device):
+ yield device
+ # lvm
+ elif udev.is_lvm(info):
+ yield False, info.get('DEVNAME', '')
+ for lv_device in lvm.get_used_partitions(info['DM_VG_NAME'],
+ info['DM_LV_NAME']):
+ for device in self._get_disk_devices(name=lv_device):
+ yield device
+
+ def get_disk_devices(self, path=None, name=None):
+ return sorted({
+ device for real_device, device in
+ self._get_disk_devices(path, name) if real_device
+ })
+
+ def get_all_base_devices(self, path=None, name=None):
+ try:
+ devices = (device for real_device, device in
+ self._get_disk_devices(path, name)
+ if not device.startswith('/dev/loop'))
+ if not self.is_partition(self.get_device_info(path, name)):
+ next(devices)
+ return list(unique(devices))
+ except StopIteration:
+ return []
+
+
+sysfs = SysFS()
+devfs = DevFS()
+
+udev = Udev(UdevAdmCommand())
+lvm = Lvm(LvmCommand())
+raid = RAID(MdadmCommand())
+
+
+if __name__ == '__main__':
+ print('FIND DEVICE BY PARTITION:')
+ print(find_device_by_partition('/dev/nvme0n1p4'))
+ print('LSPCI TEST:')
+ pprint(get_lspci_output())
+ print('GET PHYSICAL EXTENT SIZE:')
+ lvm_obj = Lvm(LvmCommand())
+ print(lvm_obj.get_physical_extent_size())
+ print('GET RUN COMMANDS:')
+ for command in files.get_run_commands(with_pid=True):
+ print(command)
diff --git a/calculate/utils/io_module.py b/calculate/utils/io_module.py
new file mode 100644
index 0000000..9ec582d
--- /dev/null
+++ b/calculate/utils/io_module.py
@@ -0,0 +1,161 @@
+import sys
+import os
+
+
+class ColorPrint:
+ def __init__(self, output=sys.stdout):
+ self.system_output = output
+
+ def get_console_width(self):
+ system_output_fd = self.system_output.fileno()
+ try:
+ width, hight = os.get_terminal_size(system_output_fd)
+ except OSError:
+ return 80
+
+ if width is None:
+ return 80
+ else:
+ return width
+
+ def print_right(self, left_offset, right_offset):
+ console_width = self.get_console_width()
+ number_of_spaces = console_width - left_offset - right_offset
+ self.system_output.write(' ' * number_of_spaces)
+
+ def print_colored(self, string, attribute='0', foreground='37',
+ background='40'):
+ print_parameters = []
+ if attribute:
+ print_parameters.append(attribute)
+ if foreground:
+ print_parameters.append(foreground)
+ if background:
+ print_parameters.append(background)
+ self.system_output.write('\033[{0}m{1}\033[0m'.
+ format(';'.join(print_parameters), string))
+
+ def print_bright_red(self, string):
+ self.print_colored(string,
+ attribute='1',
+ foreground='31')
+
+ def print_bright_green(self, string):
+ self.print_colored(string,
+ attribute='1',
+ foreground='32')
+
+ def print_bright_yellow(self, string):
+ self.print_colored(string,
+ attribute='1',
+ foreground='33')
+
+ def print_bright_blue(self, string):
+ self.print_colored(string,
+ attribute='1',
+ foreground='34')
+
+ def print_default(self, string):
+ try:
+ self.system_output.write(string)
+ except UnicodeError:
+ self.system_output.write(string.encode('utf-8'))
+ try:
+ self.system_output.flush()
+ except IOError:
+ exit(1)
+
+ def print_line(self, left_arg, right_arg, left_offset=0,
+ printBR=True):
+ COLORS = {'default': self.print_default,
+ 'green': self.print_bright_green,
+ 'red': self.print_bright_red,
+ 'yellow': self.print_bright_yellow,
+ 'blue': self.print_bright_blue}
+
+ for color, left_string in left_arg:
+ left_offset += len(left_string)
+ COLORS.get(color, self.print_default)(left_string)
+
+ right_offset = 0
+ for color, right_string in right_arg:
+ right_offset += len(right_string)
+ if right_offset:
+ self.print_right(left_offset, right_offset)
+ for color, right_string in right_arg:
+ COLORS.get(color, self.print_default)(right_string)
+ if printBR:
+ self.system_output.write('\n')
+ try:
+ self.system_output.flush()
+ except IOError:
+ exit(1)
+
+ def print_ok(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stdout
+ self.print_line((('green', ' * '), ('', string)),
+ (('blue', '['), ('green', ' ok '), ('blue', ']')),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_only_ok(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stdout
+ self.print_line((('', string),),
+ (('blue', '['), ('green', ' ok '), ('blue', ']')),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_not_ok(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stderr
+ self.print_line((('green', ' * '), ('', string)),
+ (('blue', '['), ('red', ' !! '), ('blue', ']')),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_only_not_ok(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stderr
+ self.print_line((('', string),),
+ (('blue', '['), ('red', ' !! '), ('blue', ']')),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_warning(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stdout
+ self.print_line((('yellow', ' * '), ('', string)),
+ (('', ''),),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_error(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stderr
+ self.print_line((('red', ' * '), ('', string)),
+ (('', ''),),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_success(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stdout
+ self.print_line((('green', ' * '), ('', string)),
+ (('', ''),),
+ left_offset=left_offset, printBR=printBR)
+
+ def print_info(self, string, left_offset=0, printBR=True):
+ self.system_output = sys.stdout
+ self.print_line((('blue', ' * '), ('', string)),
+ (('', ''),),
+ left_offset=left_offset, printBR=printBR)
+
+
+# Заглушка вместо модуля вывода.
+class IOModule:
+ def __init__(self):
+ self.console_output = ColorPrint()
+
+ def set_error(self, message):
+ self.console_output.print_error(message)
+
+ def set_warning(self, message):
+ self.console_output.print_warning(message)
+
+ def set_info(self, message):
+ self.console_output.print_info(message)
+
+ def set_success(self, message):
+ self.console_output.print_ok(message)
+
+ def set_failure(self, message):
+ self.console_output.print_not_ok(message)
diff --git a/calculate/utils/mount.py b/calculate/utils/mount.py
new file mode 100644
index 0000000..61c6516
--- /dev/null
+++ b/calculate/utils/mount.py
@@ -0,0 +1,330 @@
+import os
+import re
+from . import device
+from . import files
+import xattr
+import errno
+from contextlib import contextmanager
+from .tools import SingletonParam, flat_iterable
+import tempfile
+
+
+def is_mount(device_name):
+ '''Возвращает путь к точке монтирования, если устройство примонтировано
+ или '''
+ def find_names(old_device_name):
+ device_name = os.path.abspath(old_device_name)
+ yield device_name
+ if device_name.startswith('/dev'):
+ info = device.udev.get_device_info(name=device_name)
+ if 'DM_VG_NAME' in info and 'DM_LV_NAME' in info:
+ yield '/dev/mapper/{vg}-{lv}'.format(vg=info['DM_VG_NAME'],
+ lv=info['DM_LV_NAME'])
+
+ def get_overlay_mounts(line):
+ mounts = line.split(' ')
+ yield mounts[1]
+ for device_name in re.findall(
+ '(?:lowerdir=|upperdir=|workdir=)([^,]+)',
+ mounts[3]):
+ yield device_name
+
+ find_data = set(find_names(device_name))
+ output = ''
+ for mtab_line in files.read_file_lines('/etc/mtab'):
+ if 'overlay' not in mtab_line:
+ if ' ' in mtab_line:
+ mounts = set(mtab_line.split(' ')[:2])
+ if mounts & find_data:
+ output = (mounts - find_data).pop()
+ else:
+ mounts = set(get_overlay_mounts(mtab_line))
+ dest = mtab_line.split(' ')[1]
+ if mounts & find_data:
+ if dest in find_data:
+ output = 'overlay'
+ else:
+ return dest
+ return output
+
+
+class MountHelperError(Exception):
+ pass
+
+
+class MountHelperNotFound(Exception):
+ pass
+
+
+class MountHelper:
+ '''Базовый класс для чтения файлов /etc/fstab и /etc/mtab.'''
+ DATA_FILE = '/etc/fstab'
+ NAME, DIR, TYPE, OPTS, FREQ, PASSNO = range(0, 6)
+
+ def __init__(self, data_file=None, devices=()):
+ from device import get_uuid_dict
+ if data_file:
+ self.DATA_FILE = data_file
+ self.cache = []
+ self.rotated_cache = []
+ self.uuid_dictionary = get_uuid_dict(devices=devices)
+ self.update_cache()
+
+ def _read_data(self):
+ with open(self.DATA_FILE, 'r') as data_file:
+ return data_file.read()
+
+ def update_cache(self):
+ def setlen(ar):
+ return ar[:6] + [''] * (6 - len(ar))
+
+ self.cache = []
+ for line in self._read_data().split('\n'):
+ line = line.strip()
+ if line and not line.startswith('#'):
+ separated_line = []
+ for part in line.split():
+ part = part.strip()
+ separated_line.append(part)
+ self.cache.append(separated_line)
+
+ for data in self.cache:
+ device_name = self.uuid_dictionary.get(data[0], data[0])
+
+ if device_name.startswith('/'):
+ device_name = os.path.realpath(device_name)
+
+ data[0] = device.udev.get_device_info(name=device_name).get(
+ 'DEVNAME', data[0])
+ data[1] = data[1] if data[2] != 'swap' else 'swap'
+ self.rotated_cache = list(zip(*self.cache))
+
+ def get_from_fstab(self, what=DIR, where=NAME, is_in=None, is_equal=None,
+ contains=None, is_not_equal=None, all_entry=True):
+ if is_equal is not None:
+ def filter_function(tab_line):
+ return tab_line[where] == is_equal
+ elif is_in is not None:
+ def filter_function(tab_line):
+ return tab_line[where] in is_in
+ elif contains is not None:
+ def filter_function(tab_line):
+ return contains in tab_line[where]
+ else:
+ def filter_function(tab_line):
+ return tab_line[where] != is_not_equal
+ output = []
+ for line in filter(filter_function, self.cache):
+ output.append(line[what])
+ if all_entry:
+ return output
+ else:
+ return '' if not output else output[-1]
+
+ def get_fields(self, *fields):
+ fstab_fields = [self.rotated_cache[field] for field in fields]
+ return list(zip(*fstab_fields))
+
+ def is_read_only(self, what=DIR, is_equal=None):
+ tab_to_check = list(filter(lambda fstab_tab:
+ fstab_tab[what] == is_equal, self.cache))
+ if tab_to_check:
+ for data in tab_to_check:
+ options = data[self.OPTS].split(',')
+ if 'ro' in options:
+ return True
+ else:
+ return False
+ else:
+ raise MountHelperNotFound(is_equal)
+
+ def is_exist(self, what=DIR, is_equal=None, is_not_equal=None):
+ if is_equal is not None:
+ def filter_function(tab_line):
+ tab_line[what] == is_equal
+ else:
+ def filter_function(tab_line):
+ tab_line[what] != is_not_equal
+ return bool(filter(filter_function, self.cache))
+
+ @property
+ def writable(self, directory_path):
+ return not self.is_read_only(is_equal=directory_path)
+
+ @property
+ def read_only(self, directory_path):
+ return self.is_read_only(is_equal=directory_path)
+
+ @property
+ def exists(self, directory_path):
+ return self.is_exist(is_equal=directory_path)\
+ or self.is_exist(what=self.NAME,
+ is_equal=directory_path)
+
+
+class FStab(MountHelper, metaclass=SingletonParam):
+ '''Класс для чтения содержимого /etc/fstab и его кеширования.'''
+ DATA_FILE = '/etc/fstab'
+
+
+class Mounts(MountHelper):
+ '''Класс для чтения содержимого /etc/mtab и его кеширования.'''
+ DATA_FILE = '/etc/mtab'
+
+
+class DiskSpaceError(Exception):
+ pass
+
+
+class DiskSpace:
+ def __init__(self):
+ self.df_command = files.get_program_path('/bin/df')
+
+ def get_free(self, device=None, device_path=None):
+ if device:
+ mount_path = is_mount(device)
+ if not mount_path:
+ raise DiskSpaceError('Device {} must be mounted.'.
+ format(device))
+ device_path = device
+ df_process = files.Process(self.df_command, device_path, '-B1')
+ if df_process.success():
+ df_data = df_process.read().strip()
+ df_lines = df_data.split('\n')
+ if len(df_lines) >= 2:
+ columns = df_lines[1].split()
+ if len(columns) == 6:
+ return int(columns[3])
+ raise DiskSpaceError('Wrong df output:\n{}'.format(df_data))
+ else:
+ raise DiskSpaceError(str(df_process.read_error()))
+
+
+def get_child_mounts(path_name):
+ '''Получить все точки монтирования, содержащиеся в пути.'''
+ mtab_file_path = '/etc/mtab'
+ if not os.access(mtab_file_path, os.R_OK):
+ return ''
+
+ with open(mtab_file_path) as mtab_file:
+ output = []
+ mtab = list(map(lambda line: line.split(' '), mtab_file))
+ mtab = [[line[0], line[1]] for line in mtab]
+
+ if path_name != 'none':
+ abs_path = os.path.abspath(path_name)
+ for tab_line in mtab:
+ if os.path.commonpath([abs_path, tab_line[1]]) == abs_path:
+ output.append(tab_line)
+ else:
+ abs_path = path_name
+ for tab_line in mtab:
+ if tab_line[0] == abs_path:
+ output.append(tab_line)
+ return output
+
+
+class MountError(Exception):
+ pass
+
+
+def mount(source, target, fstype=None, options=None):
+ parameters = [source, target]
+ if options is not None:
+ if isinstance(options, list) or isinstance(options, tuple):
+ options = ','.join(flat_iterable(options))
+ parameters.insert(0, '-o {}'.format(options))
+ if fstype is not None:
+ parameters.insert(0, '-t {}'.format(fstype))
+ mount_process = files.Process('/bin/mount', *parameters)
+ if mount_process.success():
+ return True
+ else:
+ raise MountError('Failed to mount {source} to {target}: {stderr}'
+ .format(source=source, target=target,
+ stderr=str(mount_process.read_error())))
+
+
+def umount(target):
+ umount_process = files.Process('/bin/umount', target)
+ if umount_process.success():
+ return True
+ else:
+ raise MountError('Failed to umount {target}: {stderr}'
+ .format(target=target,
+ stderr=umount_process.read_error()))
+
+
+class BtrfsError(Exception):
+ pass
+
+
+class Btrfs:
+ check_path = None
+
+ def __init__(self, block_device):
+ self.block_device = block_device
+ if not os.path.exists(block_device):
+ raise BtrfsError('Device is not found.')
+
+ @contextmanager
+ def mount(self):
+ tempfile_path = None
+ try:
+ files.make_directory(self.check_path)
+ tempfile_path = tempfile.mkdtemp(prefix='btrfscheck-',
+ dir=self.check_path)
+ mount(self.block_device, tempfile_path, 'btrfs')
+ yield tempfile_path
+ except KeyboardInterrupt:
+ raise
+ except MountError as error:
+ if 'wrong fs type' in str(error):
+ raise BtrfsError('{} is not btrfs'.format(self.block))
+ else:
+ raise BtrfsError(str(error))
+ finally:
+ if tempfile_path:
+ if os.path.ismount(tempfile_path):
+ umount(tempfile_path)
+ os.rmdir(tempfile_path)
+
+ def get_compression(self, relative_path):
+ relative_path = relative_path.lstrip('/')
+ with self.mount() as device_path:
+ try:
+ absolute_path = os.path.join(device_path, relative_path)
+ if os.path.exists(absolute_path):
+ return xattr.get(absolute_path, 'btrfs.compression')
+ else:
+ return ''
+ except IOError as error:
+ if error.errno == errno.ENODATA:
+ return ''
+ raise BtrfsError('Failed to get btrfs compression.')
+
+ @property
+ def compression(self):
+ return self.get_compression('')
+
+ def set_compression(self, relative_path, value):
+ relative_path = relative_path.lstrip('/')
+ with self.mount() as device_path:
+ try:
+ absolute_path = os.path.join(device_path, relative_path)
+ if not os.path.exists(absolute_path):
+ files.make_directory(absolute_path)
+ return xattr.set(absolute_path, 'btrfs.compression', value)
+ except IOError as error:
+ if error.errno == errno.ENODATA:
+ return ''
+ raise BtrfsError('Failed to set btrfs compression.')
+
+ @compression.setter
+ def compression(self, value):
+ self.set_compression('', value)
+
+
+if __name__ == '__main__':
+ print('GET CHILD MOUNTS TEST:')
+ print(get_child_mounts('/home'))
diff --git a/calculate/utils/tools.py b/calculate/utils/tools.py
new file mode 100644
index 0000000..47e8d17
--- /dev/null
+++ b/calculate/utils/tools.py
@@ -0,0 +1,150 @@
+import os
+from functools import wraps
+from abc import ABCMeta, abstractmethod
+from inspect import getcallargs
+
+
+class Cachable:
+ '''Базовый класс для создания классов, кэширующих вывод своих методов.
+ Декоратор @Cachable.method_cached() предназначен для указания методов
+ с кэшируемым выводом.'''
+ def __init__(self):
+ self.clear_method_cache()
+
+ def clear_method_cache(self):
+ self._method_cache = {}
+
+ @staticmethod
+ def method_cached(key=lambda *args, **kwargs: hash(args)):
+ def decorator(function):
+ function_name = function.__name__
+
+ @wraps(function)
+ def wrapper(self, *args, **kwargs):
+ keyval = key(*args, **kwargs)
+ assert isinstance(self, Cachable)
+ if function_name not in self._method_cache:
+ self._method_cache[function_name] = {}
+ cache = self._method_cache[function_name]
+ if keyval not in cache:
+ cache[keyval] = function(self, *args, **kwargs)
+ return cache[keyval]
+
+ @wraps(function)
+ def null_wrapper(self):
+ assert isinstance(self, Cachable)
+ if function_name not in self._method_cache:
+ self._method_cache[function_name] = function(self)
+ return self._method_cache[function_name]
+
+ if len(function.__code__.co_varnames) > 1:
+ return wrapper
+ else:
+ return null_wrapper
+
+ return decorator
+
+
+class Singleton(type):
+ '''Метакласс для создания синглтонов.'''
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super().__call__(*args, **kwargs)
+ return cls._instances[cls]
+
+
+class SingletonParam(type):
+ '''Метакласс для создания синглтонов по параметрам.'''
+ _instances = {}
+ _init = {}
+
+ def __init__(cls, class_name, base_classes, class_dict):
+ cls._init[cls] = class_dict.get('__init__', None)
+
+ def __call__(cls, *args, **kwargs):
+ init = cls._init[cls]
+ if init is not None:
+ key_value = (cls, frozenset(getcallargs(init, None, *args,
+ **kwargs).items()))
+ else:
+ key_value = cls
+
+ if key_value not in cls._instances:
+ cls._instances[key_value] = super().__call__(*args, **kwargs)
+ return cls._instances[key_value]
+
+
+class GenericFS(metaclass=ABCMeta):
+ '''Абстрактный класс для работы с файловыми системами.'''
+ @abstractmethod
+ def exists(self, path):
+ pass
+
+ @abstractmethod
+ def read(self, path):
+ pass
+
+ @abstractmethod
+ def glob(self, path):
+ pass
+
+ @abstractmethod
+ def realpath(self, path):
+ pass
+
+ @abstractmethod
+ def write(self, path, data):
+ pass
+
+ @abstractmethod
+ def listdir(self, path, fullpath=False):
+ pass
+
+
+def get_traceback_caller(exception_type, exception_object,
+ exception_traceback):
+ '''Возвращает имя модуля, в котором было сгенерировано исключение,
+ и соответствующий номер строки.'''
+ while exception_traceback.tb_next:
+ exception_traceback = exception_traceback.tb_next
+
+ line_number = exception_traceback.tb_lineno
+
+ module_path, module_name = os.path.split(exception_traceback.tb_frame.
+ f_code.co_filename)
+ if module_name.endswith('.py'):
+ module_name = module_name[:-3]
+ full_module_name = [module_name]
+ while (module_path and module_path != '/' and not
+ module_path.endswith('site-packages')):
+ module_path, package_name = os.path.split(module_path)
+ full_module_name.insert(0, package_name)
+ if module_path.endswith('site-packages'):
+ module_name = '.'.join(full_module_name)
+ return module_name, line_number
+
+
+def unique(iterable):
+ '''Возвращает итерируемый объект, содержащий только уникальные элементы
+ входного объекта, сохраняя их порядок.
+ '''
+ output = []
+ for element in iterable:
+ if element not in output:
+ output.append(element)
+ return output
+
+
+def flat_iterable(iterable, types=(list, tuple, map, zip, filter)):
+ '''Распаковывает все вложенные итерируемые объекты во входном объекте.
+ Например:
+ [1, 2, [3, 4, [5, 6], 7], [8, 9], 10] -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ '''
+ if isinstance(iterable, types):
+ for it in iterable:
+ for sub_iterable in flat_iterable(it, types=types):
+ yield sub_iterable
+ else:
+ yield iterable
diff --git a/calculate/vars/os/__init__.py b/calculate/vars/os/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/templates/format/testfiles/__init__.py b/tests/templates/format/testfiles/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/templates/test_directory_processor.py b/tests/templates/test_directory_processor.py
new file mode 100644
index 0000000..d424bf7
--- /dev/null
+++ b/tests/templates/test_directory_processor.py
@@ -0,0 +1,52 @@
+import pytest
+from calculate.templates.template_processor import DirectoryProcessor
+from calculate.templates.template_engine import Variables
+
+
+# Вместо модуля переменных.
+group = Variables({'bool': True,
+ 'list': [1, 2, 3]})
+
+variables = Variables({'variable_1': 'value_1',
+ 'variable_2': 'value_2',
+ 'group': group})
+
+install = Variables({'os_disk_dev': 'os_disk_dev_value',
+ 'version': 1.5,
+ 'number': 128,
+ 'boolean': False,
+ 'type': 'static',
+ 'path': '/usr/sbin'})
+
+merge = Variables({'var_1': 674,
+ 'var_2': 48,
+ 'version': 1.0,
+ 'calculate_domains': 'lists.calculate-linux.org',
+ 'ip_value': '127.0.0.0/8'})
+
+cl_template = Variables({
+ 'path':
+ 'tests/templates/testfiles/test_dir_1,tests/templates/testfiles/test_dir_2'
+ })
+
+cl_chroot = Variables({'path': '/etc'})
+
+main = Variables({'cl_template': cl_template,
+ 'cl_chroot': cl_chroot})
+
+datavars = Variables({'install': install,
+ 'merge': merge,
+ 'variables': variables,
+ 'main': main,
+ 'custom': Variables()})
+
+
+@pytest.mark.directory_processor
+class TestDirectoryProcessor:
+ def test_just_for_debug(self):
+ try:
+ dir_processor = DirectoryProcessor('install', datavars_module=datavars,
+ package='package_1')
+ dir_processor.process_template_directories()
+ except Exception as error:
+ pytest.fail('Unexpected exception: {}'.format(str(error)))
diff --git a/tests/templates/test_template_action.py b/tests/templates/test_template_action.py
new file mode 100644
index 0000000..ec25bed
--- /dev/null
+++ b/tests/templates/test_template_action.py
@@ -0,0 +1,8 @@
+import pytest
+from calculate.templates.template_processor import TemplateAction
+from calculate.templates.template_engine import Variables
+
+
+@pytest.mark.template_action
+class TestTemplateAction:
+ pass
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/.calculate_directory
new file mode 100644
index 0000000..5280952
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/.calculate_directory
@@ -0,0 +1 @@
+{% calculate append="skip" %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/file.conf b/tests/templates/testfiles/test_dir_1/test_dir/file.conf
new file mode 100644
index 0000000..3129485
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/file.conf
@@ -0,0 +1,11 @@
+{% calculate format = 'kde', action = 'install', package = 'kde' -%}
+[section][parts][of][section name]
+parameter 1 = {{ variables.variable_1 }}
+
+parameter 2 = {{ variables.variable_2 }}
+
+# Random comment.
+parameter 3 = very important and veery interesting value
+{% for num in variables.group.list -%}
+statement {{ num }} = {{ num * 2 - 1 }}
+{% endfor -%}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/.calculate_directory
new file mode 100644
index 0000000..aaa8da8
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/.calculate_directory
@@ -0,0 +1,3 @@
+{% calculate name = 'subdir_1_folder', env='merge', chmod = 'rwxr-xr-x', action = 'install' -%}
+{% calculate package = 'xfce', merge = 'package_2' -%}
+{% calculate not install.boolean -%}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/.calculate_directory
new file mode 100644
index 0000000..fe62b3d
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/.calculate_directory
@@ -0,0 +1 @@
+{% calculate env = 'install', name = 'template_1_folder' %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/template_1 b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/template_1
new file mode 100644
index 0000000..f8e7b89
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template1/template_1
@@ -0,0 +1,5 @@
+{% calculate format = 'json', append = 'join', force -%}
+{
+ "!parameter_1": "important_value",
+ "parameter_2": {{ os_disk_dev }}
+}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/.calculate_directory
new file mode 100644
index 0000000..401b906
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = 'template_2' -%}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/a_template_2.xml b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/a_template_2.xml
new file mode 100644
index 0000000..f9ff917
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/a_template_2.xml
@@ -0,0 +1,10 @@
+{% calculate format='xml_xfce', mirror -%}
+{% save custom.group.parameter = 'DoubleClickTime' -%}
+
+
+
+
+
+
+
+
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/b_template_2.xml b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/b_template_2.xml
new file mode 100644
index 0000000..ea514b4
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir1/template2/b_template_2.xml
@@ -0,0 +1,10 @@
+{% calculate format='xml_xfce', force -%}
+
+
+
+
+
+
+
+
+
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir2/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/.calculate_directory
new file mode 100644
index 0000000..23a941f
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/.calculate_directory
@@ -0,0 +1,4 @@
+{% calculate name = 'important_dir', path = '/etc/folder/in_folder', action = 'install' -%}
+{% calculate install.version > 2.0 %}
+{% calculate package = 'kde' -%}
+{% calculate merge.version < 1.2 -%}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/.calculate_directory
new file mode 100644
index 0000000..84b5e9a
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = "template_3_folder", append='skip', autoupdate %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/template_3.conf b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/template_3.conf
new file mode 100644
index 0000000..bfc6cc9
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir2/template3/template_3.conf
@@ -0,0 +1,11 @@
+{% calculate format = 'kde', path = '/etc/folder/in_folder', name = 'filename.conf' -%}
+{% calculate merge = 'any, xfce' -%}
+# KDE or Plasma config file.
+# Part from Plasma
+[PlasmaViews][Panel 69][Horizontal1024]
+alignment={{ install.number }}
+length={{ merge.var_1 }}
+maxLength={{ merge.var_1 }}
+minLength={{ merge.var_1 }}
+panelVisibility=1
+thickness={{ merge.var_2 }}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir3/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/.calculate_directory
new file mode 100644
index 0000000..5ea524d
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/.calculate_directory
@@ -0,0 +1,2 @@
+{% calculate name = 'directory', path = '/etc/important_dir', package = 'any' %}
+{% calculate action = 'install' %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/.calculate_directory
new file mode 100644
index 0000000..243ba04
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = 'first_dir' %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/template_file.conf b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/template_file.conf
new file mode 100644
index 0000000..1a4a26f
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_4/template_file.conf
@@ -0,0 +1,4 @@
+{% calculate name = 'settings.conf', mirror, format = 'postfix' -%}
+queue_directory = /var/spool/postfix
+
+command_directory = {{ install.path }}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/.calculate_directory b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/.calculate_directory
new file mode 100644
index 0000000..1b21640
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = 'second_dir', force %}
diff --git a/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/template_file.conf b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/template_file.conf
new file mode 100644
index 0000000..54b4fb4
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_1/test_dir/subdir3/template_5/template_file.conf
@@ -0,0 +1,6 @@
+{% calculate name = 'important.conf', merge = 'kde', autoupdate -%}
+relay_domains = {{ merge.calculate_domains }}
+
+#Для создания базы используется postmap
+transport_maps = hash:/etc/postfix/transport_maps
+relay_recipient_maps = hash:/etc/postfix/valid_recipients
diff --git a/tests/templates/testfiles/test_dir_2/test_dir/.calculate_directory b/tests/templates/testfiles/test_dir_2/test_dir/.calculate_directory
new file mode 100644
index 0000000..7ae6970
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_2/test_dir/.calculate_directory
@@ -0,0 +1 @@
+{% calculate append = 'skip', action = 'install' %}
diff --git a/tests/templates/testfiles/test_dir_2/test_dir/template_1/.calculate_directory b/tests/templates/testfiles/test_dir_2/test_dir/template_1/.calculate_directory
new file mode 100644
index 0000000..4530c35
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_2/test_dir/template_1/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = 'configuration_1', path = '/etc', package = 'package_1' %}
diff --git a/tests/templates/testfiles/test_dir_2/test_dir/template_1/template_1.conf b/tests/templates/testfiles/test_dir_2/test_dir/template_1/template_1.conf
new file mode 100644
index 0000000..d849d27
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_2/test_dir/template_1/template_1.conf
@@ -0,0 +1,8 @@
+{% calculate name = 'template', format = 'bind', append = 'before' -%}
+{% calculate merge = 'package_2' -%}
+acl "trusted" {
+ {{ merge.ip_value }};
+ 10.0.0.0/8;
+ 192.168.1.0/24;
+ ::1/128;
+};
diff --git a/tests/templates/testfiles/test_dir_2/test_dir/template_2/.calculate_directory b/tests/templates/testfiles/test_dir_2/test_dir/template_2/.calculate_directory
new file mode 100644
index 0000000..0726732
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_2/test_dir/template_2/.calculate_directory
@@ -0,0 +1 @@
+{% calculate name = 'config.d', path = '/etc/dir', package = 'package_2' %}
diff --git a/tests/templates/testfiles/test_dir_2/test_dir/template_2/template_2.conf b/tests/templates/testfiles/test_dir_2/test_dir/template_2/template_2.conf
new file mode 100644
index 0000000..8451433
--- /dev/null
+++ b/tests/templates/testfiles/test_dir_2/test_dir/template_2/template_2.conf
@@ -0,0 +1,9 @@
+{% calculate name = 'wow_file.conf', force, format = 'samba', merge = 'kde' -%}
+[global]
+ server role = standalone server
+ hosts allow = 192.168.1. 192.168.2. 127.
+ log file = /var/log/samba/log.%m
+ workgroup = {{ variables.variable_1 }}
+ netbios name = {{ variables.variable_2 }}
+ server string = Calculate Directory Server
+ directory mask = 0755
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 0000000..e69de29