You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3080 lines
152 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. from pprint import pprint
  4. from ..utils.package import (
  5. PackageAtomParser,
  6. PackageCreator,
  7. Package,
  8. PackageNotFound,
  9. PackageAtomName,
  10. Version,
  11. NonePackage,
  12. )
  13. from ..utils.files import (
  14. join_paths,
  15. write_file,
  16. read_file_lines,
  17. FilesError,
  18. check_directory_link,
  19. read_link,
  20. Process,
  21. get_target_from_link,
  22. get_directory_contents,
  23. )
  24. from .template_engine import (
  25. TemplateEngine,
  26. Variables,
  27. ConditionFailed,
  28. ParametersProcessor,
  29. DIR, FILE,
  30. ParametersContainer,
  31. )
  32. from calculate.variables.datavars import (
  33. StringType,
  34. ListType,
  35. NamespaceNode,
  36. VariableNode,
  37. TableType,
  38. VariableNotFoundError
  39. )
  40. from typing import (
  41. Union,
  42. Dict,
  43. List,
  44. Tuple,
  45. Iterator,
  46. NoReturn,
  47. Optional,
  48. Callable
  49. )
  50. from calculate.variables.loader import Datavars
  51. from .format.base_format import Format, FormatError
  52. from ..utils.io_module import IOModule
  53. from collections import OrderedDict, abc
  54. from contextlib import contextmanager
  55. from ..utils.mount import Mounts
  56. import hashlib
  57. import fnmatch
  58. import shutil
  59. import errno
  60. import stat
  61. import glob
  62. import copy
  63. import os
  64. # Наверное временно.
  65. CALCULATE_VERSION = Version('4.0')
  66. class TemplateExecutorError(Exception):
  67. pass
  68. class TemplateTypeConflict(Exception):
  69. pass
  70. class TemplateCollisionError(Exception):
  71. pass
  72. class CalculateConfigFile:
  73. '''Класс для работы с файлом /var/lib/calculate/config.'''
  74. def __init__(self, cl_config_path: str = '/var/lib/calculate/config',
  75. cl_chroot_path: str = '/'):
  76. self.chroot_path: str = cl_chroot_path
  77. self.cl_config_path: str = cl_config_path
  78. self._config_dictionary: OrderedDict = self._get_cl_config_dictionary()
  79. self._unsaved_changes: bool = False
  80. def __contains__(self, file_path: str) -> bool:
  81. file_path = self._remove_chroot(file_path)
  82. return file_path in self._config_dictionary
  83. def _get_cl_config_dictionary(self) -> OrderedDict:
  84. '''Метод для загрузки словаря файла /var/lib/calculate/config.'''
  85. config_dictionary = OrderedDict()
  86. if os.path.exists(self.cl_config_path):
  87. if os.path.isdir(self.cl_config_path):
  88. raise TemplateExecutorError(
  89. "directory instead calculate config file in: {}".
  90. format(self.cl_config_path))
  91. else:
  92. write_file(self.cl_config_path).close()
  93. return config_dictionary
  94. try:
  95. config_file_lines = read_file_lines(self.cl_config_path)
  96. except FilesError as error:
  97. raise TemplateExecutorError(
  98. "cannot read calculate config file in: {0}. Reason: {1}".
  99. format(self.cl_config_path, str(error)))
  100. # TODO Продумать проверку корректности найденного файла.
  101. for file_line in config_file_lines:
  102. filename, md5_sum = file_line.split(' ')
  103. config_dictionary.update({filename: md5_sum})
  104. self._unsaved_changes = False
  105. return config_dictionary
  106. def set_files_md5(self, file_path: str, file_md5: str) -> NoReturn:
  107. '''Метод для установки в config соответствия файла некоторой
  108. контрольной сумме.'''
  109. file_path = self._remove_chroot(file_path)
  110. self._config_dictionary[file_path] = file_md5
  111. self._unsaved_changes = True
  112. def remove_file(self, file_path: str) -> NoReturn:
  113. '''Метод для удаления файла из config.'''
  114. file_path = self._remove_chroot(file_path)
  115. if file_path in self._config_dictionary:
  116. self._config_dictionary.pop(file_path)
  117. self._unsaved_changes = True
  118. def compare_md5(self, file_path: str, file_md5: str) -> NoReturn:
  119. '''Метод для сравнения хэш-суммы из config и некоторой заданной.'''
  120. file_path = self._remove_chroot(file_path)
  121. if file_path in self._config_dictionary:
  122. return self._config_dictionary[file_path] == file_md5
  123. else:
  124. return False
  125. def save_changes(self) -> NoReturn:
  126. '''Метод для записи изменений, внессенных в файл config.'''
  127. if not self._unsaved_changes:
  128. return
  129. config_file = write_file(self.cl_config_path)
  130. for file_name, file_md5 in self._config_dictionary.items():
  131. config_file.write('{} {}\n'.format(file_name, file_md5))
  132. config_file.close()
  133. self._unsaved_changes = False
  134. def _remove_chroot(self, file_path: str) -> str:
  135. '''Метод для удаления корневого пути из указанного пути.'''
  136. if self.chroot_path != '/' and file_path.startswith(self.chroot_path):
  137. file_path = file_path[len(self.chroot_path):]
  138. return file_path
  139. class TemplateWrapper:
  140. '''Класс связывающий шаблон с целевым файлом и определяющий параметры
  141. наложения шаблона, обусловленные состоянием целевого файла.'''
  142. type_checks: Dict[int,
  143. Callable[[str], bool]] = {DIR: os.path.isdir,
  144. FILE: os.path.isfile}
  145. _protected_is_set: bool = False
  146. _protected_set: set = set()
  147. _unprotected_set: set = set()
  148. def __new__(cls, *args, **kwargs):
  149. if not cls._protected_is_set:
  150. # Устанавливаем значения PROTECTED, если не заданы.
  151. if 'chroot_path' in kwargs:
  152. chroot_path = kwargs['chroot_path']
  153. else:
  154. chroot_path = '/'
  155. cls._set_protected(chroot_path)
  156. return super().__new__(cls)
  157. def __init__(
  158. self, target_file_path: str,
  159. parameters: ParametersContainer,
  160. template_type: int,
  161. template_path: str,
  162. template_text: str = '',
  163. target_package: Optional[Package] = None,
  164. chroot_path: str = '/',
  165. config_archive_path: str = '/var/lib/calculate/config-archive',
  166. dbpkg: bool = True,
  167. pkg_autosave: bool = False):
  168. self.target_path: str = target_file_path
  169. self.template_path: str = template_path
  170. self.chroot_path: str = chroot_path
  171. self.config_archive_path: str = config_archive_path
  172. self.target_package_name: Union[PackageAtomName, None] = None
  173. self.package_atom_parser: PackageAtomParser = PackageAtomParser(
  174. chroot_path=self.chroot_path)
  175. self._pkg_autosave: bool = pkg_autosave
  176. # Вспомогательный флаг, включается, если по целевому пути лежит файл,
  177. # для которого не определился никакой пакет.
  178. self.target_without_package: bool = False
  179. self.parameters: ParametersContainer = parameters
  180. self.output_path: str = self.target_path
  181. self.input_path: Union[str, None] = None
  182. self.template_type: int = template_type
  183. self.template_text: str = template_text
  184. self.contents_matching: bool = True
  185. self.ca_is_missed: bool = False
  186. # Флаг, указывающий, что нужно удалить файл из target_path перед
  187. # применением шаблона.
  188. self.remove_original: bool = False
  189. # Флаг, указывающий, что целевой путь был изменен.
  190. self.target_path_is_changed: bool = False
  191. # Флаг, указывающий, что файл по целевому пути является ссылкой.
  192. self.target_is_link: bool = False
  193. # Пакет, к которому относится файл.
  194. self.target_package: Package = target_package
  195. # Флаг, разрешающий работу с CONTENTS. Если False, то выключает
  196. # protected для всех файлов блокирует все операции с CONTENTS и ._cfg.
  197. self.dbpkg: bool = dbpkg
  198. # Флаг, указывающий, что файл является PROTECTED.
  199. self.protected: bool = False
  200. # Временный флаг для определения того, является ли шаблон userspace.
  201. self.is_userspace: bool = False
  202. self.format_class: Union[Format, None] = None
  203. if self.parameters.run or self.parameters.exec:
  204. # Если есть параметр run или exec, то кроме текста шаблона ничего
  205. # не нужно.
  206. return
  207. if self.parameters.append in {'join', 'before', 'after', 'replace'}:
  208. # Получаем класс соответствующего формата файла.
  209. if self.parameters.format:
  210. self.format_class = ParametersProcessor.\
  211. available_formats[self.parameters.format]
  212. elif self.template_type is FILE:
  213. # TODO Здесь будет детектор форматов. Когда-нибудь.
  214. raise TemplateExecutorError("'format' parameter is not set"
  215. " file template.")
  216. # Если по этому пути что-то есть -- проверяем конфликты.
  217. if os.path.exists(target_file_path):
  218. for file_type, checker in self.type_checks.items():
  219. if checker(target_file_path):
  220. self.target_type = file_type
  221. break
  222. self.target_is_link = os.path.islink(target_file_path)
  223. # Если установлен параметр mirror и есть параметр source,
  224. # содержащий несуществующий путь -- удаляем целевой файл.
  225. if (self.parameters.source is True and self.parameters.mirror and
  226. not self.format_class.EXECUTABLE):
  227. self.remove_original = True
  228. else:
  229. self.target_type = None
  230. if self.format_class is not None and self.format_class.EXECUTABLE:
  231. # Если формат исполняемый -- проверяем, существует ли директория,
  232. # из которой будет выполняться шаблон.
  233. if self.target_type is None:
  234. # Если не существует -- создаем ее.
  235. if not os.path.exists(os.path.dirname(self.target_path)):
  236. os.makedirs(os.path.dirname(self.target_path))
  237. # Если есть параметр package, определяем по нему пакет.
  238. if self.parameters.package:
  239. self.target_package_name = self.parameters.package
  240. if (self.target_package is None or
  241. self.target_package.package_name !=
  242. self.target_package_name):
  243. self.target_package = Package(self.parameters.package,
  244. chroot_path=self.chroot_path,
  245. autosave=self._pkg_autosave)
  246. return
  247. self._check_type_conflicts()
  248. self._check_package_collision()
  249. self._check_user_changes()
  250. # if self.target_type is not None and self.contents_matching:
  251. # # Удаляем целевой файл, если append = 'replace'
  252. # if (self.parameters.append and
  253. # self.parameters.append == "replace"):
  254. # self.remove_original = True
  255. def _check_type_conflicts(self) -> NoReturn:
  256. '''Метод для проверки конфликтов типов.'''
  257. if self.parameters.append == 'link':
  258. if self.parameters.force:
  259. if os.path.exists(self.parameters.source):
  260. self.remove_original = True
  261. elif self.target_is_link:
  262. if self.template_type != self.target_type:
  263. raise TemplateTypeConflict(
  264. "the target is a link to {} while the template"
  265. "is {} and has append = 'link'".
  266. format('directory' if self.template_type ==
  267. DIR else 'file',
  268. 'file' if self.template_type ==
  269. DIR else 'directory'))
  270. else:
  271. self.remove_original = True
  272. elif self.target_type == DIR:
  273. raise TemplateTypeConflict("the target is a directory while "
  274. "the template has append = 'link'")
  275. elif self.target_type == FILE:
  276. raise TemplateTypeConflict("the target is a file while the"
  277. " template has append = 'link'")
  278. elif self.template_type == DIR:
  279. if self.target_type == FILE:
  280. if self.parameters.force:
  281. self.remove_original = True
  282. else:
  283. raise TemplateTypeConflict("the target is a file while the"
  284. " template is a directory")
  285. elif self.target_is_link:
  286. if self.parameters.force:
  287. self.remove_original = True
  288. elif not self.parameters.append == "remove":
  289. try:
  290. link_source = check_directory_link(
  291. self.target_path,
  292. chroot_path=self.chroot_path)
  293. self.target_path = link_source
  294. self.target_path_is_changed = True
  295. except FilesError as error:
  296. raise TemplateExecutorError("files error: {}".
  297. format(str(error)))
  298. elif self.template_type == FILE:
  299. if self.parameters.force:
  300. if self.target_type == DIR:
  301. self.remove_original = True
  302. elif self.target_is_link and self.target_type == FILE:
  303. try:
  304. link_source = read_link(self.target_path)
  305. self.target_path = get_target_from_link(
  306. self.target_path,
  307. link_source,
  308. chroot_path=self.chroot_path)
  309. self.target_path_is_changed = True
  310. except FilesError as error:
  311. raise TemplateExecutorError("files error: {}".
  312. format(str(error)))
  313. elif self.target_is_link:
  314. if not self.parameters.append == "remove":
  315. if self.target_type == DIR:
  316. raise TemplateTypeConflict("the target file is a link"
  317. " to a directory while the"
  318. " template is a file")
  319. else:
  320. raise TemplateTypeConflict("the target file is a link"
  321. " to a file while the"
  322. " template is a file")
  323. elif self.target_type == DIR:
  324. raise TemplateTypeConflict("the target file is a directory"
  325. " while the template is a file")
  326. def _check_package_collision(self) -> NoReturn:
  327. '''Метод для проверки на предмет коллизии, то есть конфликта пакета
  328. шаблона и целевого файла.'''
  329. if self.parameters.package:
  330. parameter_package = self.parameters.package
  331. else:
  332. parameter_package = None
  333. if self.target_type is not None:
  334. try:
  335. file_package = self.package_atom_parser.get_file_package(
  336. self.target_path)
  337. except PackageNotFound:
  338. file_package = None
  339. self.target_without_package = True
  340. else:
  341. file_package = None
  342. # Если для шаблона и целевого файла никаким образом не удается
  343. # определить пакет и есть параметр append -- шаблон пропускаем.
  344. if parameter_package is None and file_package is None:
  345. if (self.parameters.append and self.parameters.append != 'skip'
  346. and not self.parameters.handler):
  347. raise TemplateCollisionError(
  348. "'package' parameter is not defined for"
  349. " template with 'append' parameter")
  350. else:
  351. return
  352. elif parameter_package is None:
  353. self.target_package_name = file_package
  354. elif file_package is None:
  355. if self.parameters.handler:
  356. raise TemplateCollisionError((
  357. "The template is a handler while target"
  358. " file belongs {} package").format(
  359. file_package.atom
  360. ))
  361. self.target_package_name = parameter_package
  362. elif file_package != parameter_package and self.template_type != DIR:
  363. target_name = self._compare_packages(parameter_package,
  364. file_package)
  365. if (target_name is not None and self.target_package is not None
  366. and self.target_package.package_name == target_name):
  367. target_name = self.target_package.package_name
  368. if target_name is None or target_name.slot_specified:
  369. raise TemplateCollisionError((
  370. "The template package is {0} while target"
  371. " file package is {1}").format(
  372. parameter_package.atom,
  373. file_package.atom
  374. ))
  375. if (self.target_package is None or
  376. self.target_package_name != self.target_package.package_name):
  377. self.target_package = Package(self.target_package_name,
  378. chroot_path=self.chroot_path,
  379. autosave=True)
  380. # Теперь перемещаем файл в пакет со старшей версией.
  381. self.source_package = Package(file_package,
  382. chroot_path=self.chroot_path,
  383. autosave=self._pkg_autosave)
  384. removed = self.source_package.remove_file(self.target_path)
  385. for file_path, file_info in removed.items():
  386. if file_info['type'] == 'dir':
  387. self.target_package.add_dir(file_path)
  388. elif file_info['type'] == 'obj':
  389. self.target_package.add_obj(file_path,
  390. file_md5=file_info['md5'],
  391. mtime=file_info['mtime'])
  392. elif file_info['type'] == 'sym':
  393. self.target_package.add_sym(
  394. file_path,
  395. target_path=file_info['target'],
  396. mtime=file_info['mtime'])
  397. else:
  398. self.target_package_name = parameter_package
  399. if (self.target_package is None or
  400. self.target_package_name != self.target_package.package_name):
  401. self.target_package = Package(self.target_package_name,
  402. chroot_path=self.chroot_path,
  403. autosave=self._pkg_autosave)
  404. def _compare_packages(self, lpackage: PackageAtomName,
  405. rpackage: PackageAtomName
  406. ) -> Union[None, PackageAtomName]:
  407. '''Метод, сравнивающий пакеты по их именам, возвращает старший.'''
  408. if lpackage.category != rpackage.category:
  409. return None
  410. if lpackage.name != rpackage.name:
  411. return None
  412. if lpackage.version > rpackage.version:
  413. return lpackage
  414. else:
  415. return rpackage
  416. def _check_user_changes(self) -> NoReturn:
  417. '''Метод для проверки наличия пользовательских изменений в
  418. конфигурационных файлах.'''
  419. # Эта проверка только для файлов.
  420. if self.template_type != FILE:
  421. return
  422. # Проверим, является ли файл защищенным.
  423. # Сначала проверяем по переменной CONFIG_PROTECT.
  424. if self.dbpkg and not self.parameters.handler:
  425. for protected_path in self._protected_set:
  426. if self.target_path.startswith(protected_path):
  427. self.protected = True
  428. break
  429. # Затем по переменной CONFIG_PROTECT_MASK.
  430. for unprotected_path in self._unprotected_set:
  431. if self.target_path.startswith(unprotected_path):
  432. self.protected = False
  433. break
  434. else:
  435. self.protected = False
  436. # Собираем список имеющихся ._cfg файлов.
  437. cfg_pattern = os.path.join(os.path.dirname(self.target_path),
  438. "._cfg????_{}".format(
  439. os.path.basename(self.target_path)))
  440. self.cfg_list = glob.glob(cfg_pattern)
  441. self.cfg_list.sort()
  442. # Путь к архивной версии файла.
  443. self.archive_path = self._get_archive_path(self.target_path)
  444. self.contents_matching = (self.parameters.autoupdate
  445. or self.parameters.force)
  446. if not self.protected:
  447. self.contents_matching = True
  448. elif self.parameters.unbound:
  449. # Если присутствует unbound, то просто модифицируем файл и
  450. # удаляем его из CONTENTS.
  451. self.contents_matching = True
  452. elif self.target_type is None:
  453. # Если целевой файл отсутствует.
  454. if (self.target_path in self.target_package and
  455. not self.parameters.autoupdate):
  456. # Проверка -- был ли файл удален.
  457. self.contents_matching = False
  458. pass
  459. else:
  460. self.contents_matching = True
  461. elif self.target_without_package:
  462. # Если файл по целевому пути не относится к какому-либо пакету.
  463. # self.contents_matching = False
  464. pass
  465. elif not self.contents_matching:
  466. # Если файл есть и он относится к текущему пакету.
  467. # Если по каким-то причинам уже нужно считать, что хэш-суммы
  468. # совпадают -- в дальнейшей проверке нет необходимости.
  469. # target_md5 = self.target_package.get_md5(self.target_path)
  470. self.contents_matching = self.target_package.check_contents_data(
  471. self.target_path,
  472. symlink=self.target_is_link)
  473. # Если по целевому пути файл не относящийся к какому-либо пакету и
  474. # присутствует параметр autoupdate -- удаляем этот файл.
  475. if (self.target_without_package and
  476. (self.parameters.autoupdate or self.parameters.force)):
  477. self.remove_original = True
  478. # Определяем пути входных и выходных файлов.
  479. if self.contents_matching:
  480. # Приоритет отдаем пути из параметра source.
  481. if self.parameters.source:
  482. self.input_path = self.parameters.source
  483. elif self.cfg_list and not self.parameters.unbound:
  484. if os.path.exists(self.archive_path):
  485. self.input_path = self.archive_path
  486. else:
  487. self.input_path = self.target_path
  488. self.ca_is_missed = True
  489. else:
  490. self.input_path = self.target_path
  491. self.output_path = self.target_path
  492. else:
  493. # Приоритет отдаем пути из параметра source.
  494. if self.parameters.source:
  495. self.input_path = self.parameters.source
  496. else:
  497. if os.path.exists(self.archive_path):
  498. self.input_path = self.archive_path
  499. else:
  500. self.input_path = self.target_path
  501. self.ca_is_missed = True
  502. self.output_path = self._get_cfg_path(self.target_path)
  503. def _get_archive_path(self, file_path: str) -> str:
  504. '''Метод для получения пути к архивной версии указанного файла.'''
  505. if self.chroot_path != "/" and file_path.startswith(self.chroot_path):
  506. file_path = file_path[len(self.chroot_path):]
  507. return join_paths(self.config_archive_path, file_path)
  508. def _get_cfg_path(self, file_path: str) -> str:
  509. '''Метод для получения пути для создания нового ._cfg????_ файла.'''
  510. if self.cfg_list:
  511. last_cfg_name = os.path.basename(self.cfg_list[-1])
  512. slice_value = len('._cfg')
  513. cfg_number = int(last_cfg_name[slice_value: slice_value + 4]) + 1
  514. cfg_number = str(cfg_number)
  515. else:
  516. cfg_number = '0'
  517. if len(cfg_number) < 4:
  518. cfg_number = '0' * (4 - len(cfg_number)) + cfg_number
  519. new_cfg_name = "._cfg{}_{}".format(cfg_number,
  520. os.path.basename(file_path))
  521. new_cfg_path = os.path.join(os.path.dirname(file_path), new_cfg_name)
  522. return new_cfg_path
  523. def remove_from_contents(self) -> NoReturn:
  524. '''Метод для удаления целевого файла из CONTENTS.'''
  525. if self.target_package is None:
  526. return
  527. if self.template_type == DIR:
  528. self.target_package.remove_dir(self.target_path)
  529. elif self.template_type == FILE:
  530. self.target_package.remove_obj(self.target_path)
  531. def clear_dir_contents(self) -> NoReturn:
  532. '''Метод для удаления из CONTENTS всего содержимого директории после
  533. применения append = "clear".'''
  534. if self.template_type == DIR and self.target_package is not None:
  535. self.target_package.clear_dir(self.target_path)
  536. def add_to_contents(self, file_md5: Optional[str] = None) -> NoReturn:
  537. '''Метод для добавления целевого файла в CONTENTS.'''
  538. if self.target_package is None:
  539. return
  540. # В подавляющем большинстве случаев берем хэш-сумму из выходного файла,
  541. # но если по какой-то причине выходного файла нет -- пытаемся
  542. # по целевому. Такое поведение маловероятно, но, наверное, стоит
  543. # учесть возможность такой ситуации.
  544. if os.path.exists(self.output_path):
  545. source_path = self.output_path
  546. else:
  547. source_path = self.target_path
  548. if self.parameters.append == 'link':
  549. self.target_package.add_sym(source_path,
  550. self.parameters.source)
  551. elif self.template_type == DIR:
  552. self.target_package.add_dir(source_path)
  553. elif self.template_type == FILE:
  554. self.target_package.add_obj(source_path, file_md5=file_md5)
  555. def update_contents_from_list(self, changed_list: dict) -> NoReturn:
  556. '''Метод для изменения CONTENTS по списку измененных файлов.'''
  557. print("UPDATE CONTENTS FROM LIST")
  558. if self.target_package is None:
  559. return
  560. for file_path, mode in changed_list.items():
  561. if mode in {"M", "N"}:
  562. if os.path.islink(file_path):
  563. self.target_package.add_sym(file_path)
  564. elif os.path.isdir(file_path):
  565. self.target_package.add_dir(file_path)
  566. elif os.path.isfile(file_path):
  567. self.target_package.add_obj(file_path)
  568. elif mode == "D":
  569. if os.path.islink(file_path) or os.path.isfile(file_path):
  570. self.target_package.remove_obj(file_path)
  571. elif os.path.isdir(file_path):
  572. self.target_package.add_dir(file_path)
  573. @classmethod
  574. def _set_protected(cls, chroot_path: str) -> NoReturn:
  575. '''Метод для получения множества защищенных директорий.'''
  576. cls._protected_set = set()
  577. cls._unprotected_set = set()
  578. cls._protected_set.add(join_paths(chroot_path, '/etc'))
  579. config_protect_env = os.environ.get('CONFIG_PROTECT', False)
  580. if config_protect_env:
  581. for protected_path in config_protect_env.split():
  582. protected_path = join_paths(chroot_path,
  583. protected_path.strip())
  584. cls._protected_set.add(protected_path)
  585. config_protect_mask_env = os.environ.get('CONFIG_PROTECT_MASK', False)
  586. if config_protect_mask_env:
  587. for unprotected_path in config_protect_mask_env.split():
  588. unprotected_path = join_paths(chroot_path,
  589. unprotected_path.strip())
  590. cls._unprotected_set.add(unprotected_path)
  591. cls._protected_is_set = True
  592. def save_changes(self) -> NoReturn:
  593. '''Метод для сохранения изменений внесенных в CONTENTS.'''
  594. if self.target_package:
  595. self.target_package.remove_empty_directories()
  596. self.target_package.write_contents_file()
  597. class TemplateExecutor:
  598. '''Класс исполнительного модуля.'''
  599. def __init__(self,
  600. datavars_module: Union[Datavars, Variables] = Variables(),
  601. chroot_path: str = '/',
  602. cl_config_archive: str = '/var/lib/calculate/config-archive',
  603. cl_config_path: str = '/var/lib/calculate/config',
  604. execute_archive_path: str = '/var/lib/calculate/.execute/',
  605. dbpkg: bool = True,
  606. pkg_autosave: bool = False):
  607. # TODO добавить список измененных файлов.
  608. self.datavars_module: Union[Datavars, Variables] = datavars_module
  609. self.chroot_path: str = chroot_path
  610. # Объект для проверки файловых систем. Пока не инициализируем.
  611. self.mounts: Union[Mounts, None] = None
  612. # Директория для хранения полученных при обработке exec скриптов.
  613. self.execute_archive_path: str = execute_archive_path
  614. self.execute_files: OrderedDict = OrderedDict()
  615. self.dbpkg: bool = dbpkg
  616. self._pkg_autosave: bool = pkg_autosave
  617. # Словарь с измененными файлами и статусами их изменений.
  618. self.changed_files: dict = {}
  619. # Список целевых путей измененных файлов. Нужен для корректного
  620. # формирования calculate-заголовка.
  621. self.processed_targets: list = []
  622. # TODO разобраться с этим.
  623. # Значения параметров по умолчанию, пока не используются.
  624. self.directory_default_parameters: dict =\
  625. ParametersProcessor.directory_default_parameters
  626. self.file_default_parameters: dict =\
  627. ParametersProcessor.file_default_parameters
  628. # Отображение имен действий для директорий на методы их реализующие.
  629. self.directory_appends: dict = {
  630. 'join': self._append_join_directory,
  631. 'remove': self._append_remove_directory,
  632. 'skip': self._append_skip_directory,
  633. 'clear': self._append_clear_directory,
  634. 'link': self._append_link_directory,
  635. 'replace': self._append_replace_directory}
  636. # Отображение имен действий для файлов на методы их реализующие.
  637. self.file_appends: dict = {'join': self._append_join_file,
  638. 'after': self._append_after_file,
  639. 'before': self._append_before_file,
  640. 'replace': self._append_replace_file,
  641. 'remove': self._append_remove_file,
  642. 'skip': self._append_skip_file,
  643. 'clear': self._append_clear_file,
  644. 'link': self._append_link_file}
  645. self.formats_classes: set = ParametersProcessor.available_formats
  646. self.calculate_config_file: CalculateConfigFile = CalculateConfigFile(
  647. cl_config_path=cl_config_path,
  648. cl_chroot_path=chroot_path)
  649. self.cl_config_archive_path: str = cl_config_archive
  650. Format.CALCULATE_VERSION = CALCULATE_VERSION
  651. @property
  652. def available_appends(self) -> set:
  653. '''Метод для получения множества возможных значений append.'''
  654. appends_set = set(self.directory_appends.keys()).union(
  655. set(self.file_appends.keys()))
  656. return appends_set
  657. def execute_template(self, target_path: str,
  658. parameters: ParametersContainer, template_type: int,
  659. template_path: str, template_text: str = '',
  660. save_changes: bool = True,
  661. target_package: Optional[Package] = None) -> dict:
  662. '''Метод для запуска выполнения шаблонов.'''
  663. # Словарь с данными о результате работы исполнительного метода.
  664. self.executor_output = {'target_path': None,
  665. 'stdout': None,
  666. 'stderr': None,
  667. 'warnings': []}
  668. if parameters.append == 'skip':
  669. return self.executor_output
  670. try:
  671. template_object = TemplateWrapper(
  672. target_path, parameters,
  673. template_type,
  674. template_path,
  675. template_text=template_text,
  676. target_package=target_package,
  677. chroot_path=self.chroot_path,
  678. config_archive_path=self.cl_config_archive_path,
  679. dbpkg=self.dbpkg,
  680. pkg_autosave=self._pkg_autosave)
  681. except TemplateTypeConflict as error:
  682. raise TemplateExecutorError("type conflict: {}".format(str(error)))
  683. except TemplateCollisionError as error:
  684. raise TemplateExecutorError("collision: {}".format(str(error)))
  685. # Удаляем оригинал, если это необходимо из-за наличия force или по
  686. # другим причинам.
  687. if template_object.remove_original:
  688. if template_object.target_type == DIR:
  689. self._remove_directory(template_object.target_path)
  690. else:
  691. self._remove_file(template_object.target_path)
  692. if self.dbpkg:
  693. template_object.remove_from_contents()
  694. template_object.target_type = None
  695. # Если был включен mirror, то после удаления файла завершаем
  696. # выполнение шаблона.
  697. if template_object.parameters.mirror:
  698. # if save_changes:
  699. # template_object.save_changes()
  700. return self.executor_output
  701. template_object.target_type = None
  702. if template_object.parameters.run:
  703. # Если есть параметр run -- запускаем текст шаблона.
  704. self._run_template(template_object)
  705. elif template_object.parameters.exec:
  706. # Если есть параметр exec -- запускаем текст шаблона после
  707. # обработки всех шаблонов.
  708. self._exec_template(template_object)
  709. elif template_object.parameters.append:
  710. if template_object.template_type == DIR:
  711. self.directory_appends[template_object.parameters.append](
  712. template_object)
  713. elif template_object.template_type == FILE:
  714. self.file_appends[template_object.parameters.append](
  715. template_object)
  716. # Сохраняем изменения в CONTENTS внесенные согласно шаблону.
  717. # if save_changes:
  718. # template_object.save_changes()
  719. # Возвращаем целевой путь, если он был изменен, или
  720. # None если не был изменен.
  721. if template_object.target_path_is_changed:
  722. self.executor_output['target_path'] =\
  723. template_object.target_path
  724. if template_object.ca_is_missed:
  725. self.executor_output['warnings'].append(
  726. "archive file is missed,"
  727. " target file was used instead")
  728. return self.executor_output
  729. def save_changes(self) -> NoReturn:
  730. '''Метод для сохранения чего-нибудь после выполнения всех шаблонов.'''
  731. # Пока сохраняем только получившееся содержимое config-файла.
  732. self.calculate_config_file.save_changes()
  733. def _append_join_directory(self, template_object: TemplateWrapper) -> None:
  734. '''Метод описывающий действия для append = "join", если шаблон --
  735. директория. Создает директорию, если ее нет.'''
  736. if template_object.target_type is None:
  737. self._create_directory(template_object)
  738. # Корректируем список измененных файлов.
  739. if template_object.target_path in self.changed_files:
  740. self.changed_files[template_object.target_path] = 'M'
  741. else:
  742. self.changed_files[template_object.target_path] = 'N'
  743. else:
  744. if template_object.parameters.chmod:
  745. self._chmod_directory(template_object.target_path,
  746. template_object.parameters.chmod)
  747. if template_object.parameters.chown:
  748. self._chown_directory(template_object.target_path,
  749. template_object.parameters.chown)
  750. if self.dbpkg:
  751. template_object.add_to_contents()
  752. def _append_remove_directory(self, template_object: TemplateWrapper
  753. ) -> NoReturn:
  754. '''Метод описывающий действия для append = "remove", если шаблон --
  755. директория. Удаляет директорию со всем содержимым, если она есть.'''
  756. if template_object.target_type is not None:
  757. changed = [template_object.target_path]
  758. changed.extend(get_directory_contents(template_object.target_path))
  759. self._remove_directory(template_object.target_path)
  760. # Добавляем все содержимое директории в список измененных файлов.
  761. for file_path in changed:
  762. if file_path in self.changed_files:
  763. if self.changed_files[file_path] == 'N':
  764. self.changed_files.pop(file_path)
  765. else:
  766. self.changed_files[file_path] = 'D'
  767. else:
  768. self.changed_files[file_path] = 'D'
  769. if self.dbpkg:
  770. template_object.remove_from_contents()
  771. def _append_skip_directory(self,
  772. template_object: TemplateWrapper) -> NoReturn:
  773. pass
  774. def _append_clear_directory(self,
  775. template_object: TemplateWrapper) -> NoReturn:
  776. '''Метод описывающий действия для append = "clear", если шаблон --
  777. директория. Удаляет все содержимое директории, если она есть.'''
  778. if template_object.target_type is not None:
  779. dir_contents = get_directory_contents(template_object.target_path)
  780. self._clear_directory(template_object.target_path)
  781. # Добавляем все содержимое директории в список изменных файлов.
  782. for file_path in dir_contents:
  783. if file_path in self.changed_files:
  784. if self.changed_files[file_path] == 'N':
  785. self.changed_files.pop(file_path)
  786. else:
  787. self.changed_files[file_path] = 'D'
  788. else:
  789. self.changed_files[file_path] = 'D'
  790. # Меняем права и владельца очищенной директории, если это
  791. # необходимо.
  792. if template_object.parameters.chmod:
  793. self._chmod_directory(template_object.target_path,
  794. template_object.parameters.chmod)
  795. if template_object.parameters.chown:
  796. self._chown_directory(template_object.target_path,
  797. template_object.parameters.chown)
  798. if self.dbpkg:
  799. template_object.clear_dir_contents()
  800. def _append_link_directory(self, template_object: TemplateWrapper
  801. ) -> NoReturn:
  802. '''Метод описывающий действия для append = "link", если шаблон --
  803. директория. Создает ссылку на директорию, если она есть.'''
  804. self._link_directory(template_object.parameters.source,
  805. template_object.target_path)
  806. # Корректируем список измененных файлов.
  807. if template_object.target_path in self.changed_files:
  808. self.changed_files[template_object.target_path] = 'M'
  809. else:
  810. self.changed_files[template_object.target_path] = 'N'
  811. # Меняем права и владельца файла, на который указывает ссылка.
  812. if template_object.parameters.chmod:
  813. self._chmod_directory(template_object.parameters.source,
  814. template_object.parameters.chmod)
  815. if template_object.parameters.chown:
  816. self._chown_directory(template_object.parameters.source,
  817. template_object.parameters.chown)
  818. if self.dbpkg:
  819. template_object.add_to_contents()
  820. def _append_replace_directory(self, template_object: TemplateWrapper
  821. ) -> NoReturn:
  822. '''Метод описывающий действия для append = "replace", если шаблон --
  823. директория. Очищает директорию или создает, если ее нет.'''
  824. if template_object.target_type is None:
  825. self._create_directory(template_object)
  826. if self.dbpkg:
  827. template_object.add_to_contents()
  828. # Корректируем список измененных файлов.
  829. if template_object.target_path in self.changed_files:
  830. self.changed_files[template_object.target_path] = 'M'
  831. else:
  832. self.changed_files[template_object.target_path] = 'N'
  833. else:
  834. dir_contents = get_directory_contents(template_object.target_path)
  835. self._clear_directory(template_object.target_path)
  836. # Добавляем все содержимое директории в список измененных файлов.
  837. for file_path in dir_contents:
  838. if file_path in self.changed_files:
  839. if self.changed_files[file_path] == 'N':
  840. self.changed_files.pop(file_path)
  841. else:
  842. self.changed_files[file_path] = 'D'
  843. else:
  844. self.changed_files[file_path] = 'D'
  845. if self.dbpkg:
  846. template_object.clear_dir_contents()
  847. def _append_join_file(self, template_object: TemplateWrapper,
  848. join_before: bool = False, replace: bool = False
  849. ) -> NoReturn:
  850. '''Метод описывающий действия при append = "join", если шаблон -- файл.
  851. Объединяет шаблон с целевым файлом.'''
  852. output_path = template_object.output_path
  853. input_file_md5 = None
  854. output_file_md5 = None
  855. template_format = template_object.format_class
  856. # Задаемся значениями chmod и chown в зависимости от наличия или
  857. # отсутствия файла, принадлежности его пакету и наличия значений
  858. # параметров по умолчанию.
  859. chmod = self._get_chmod(template_object)
  860. chown = self._get_chown(template_object)
  861. if template_format.EXECUTABLE or template_object.contents_matching:
  862. # Действия, если целевой файл не имеет пользовательских изменений
  863. # или если он исполнительный.
  864. # Парсим текст шаблона используя его формат.
  865. if (not template_object.template_text.strip()
  866. and template_object.parameters.source
  867. and template_object.parameters.append == "replace"
  868. and template_object.parameters.format == "raw"):
  869. # Если шаблон пуст, параметром source задан входной файл,
  870. # и при этом формат шаблона raw и append = 'replace'
  871. # значит этот шаблон предназначен для копирования.
  872. if template_object.target_type is not None:
  873. self._remove_file(template_object.target_path)
  874. output_file_md5 = self._copy_from_source(template_object,
  875. chown=chown,
  876. chmod=chmod)
  877. elif not template_object.format_class.EXECUTABLE:
  878. parsed_template = template_format(
  879. template_object.template_text,
  880. template_object.template_path,
  881. ignore_comments=True)
  882. # Действия для шаблонов не являющихся исполнительными.
  883. output_paths = [output_path]
  884. # Если целевой файл защищен, а шаблон не userspace.
  885. if (template_object.protected
  886. and not template_object.is_userspace):
  887. # Тогда также обновляем архив.
  888. output_paths.append(template_object.archive_path)
  889. input_text = self._get_input_text(template_object,
  890. replace=replace)
  891. if (replace and template_object.target_type is not None and
  892. os.path.exists(output_path)):
  893. self._clear_file(output_path)
  894. # Если шаблон не исполнительный разбираем входной текст.
  895. parsed_input = template_format(
  896. input_text,
  897. template_object.template_path,
  898. add_header=True,
  899. join_before=join_before,
  900. already_changed=(template_object.target_path
  901. in self.processed_targets),
  902. parameters=template_object.parameters)
  903. parsed_input.join_template(parsed_template)
  904. # Результат наложения шаблона.
  905. output_text = parsed_input.document_text
  906. # Удаляем форматный объект входного файла.
  907. # del(parsed_input)
  908. output_file_md5 = hashlib.md5(output_text.encode()).hexdigest()
  909. if input_text:
  910. input_file_md5 = hashlib.md5(
  911. input_text.encode()).hexdigest()
  912. for save_path in output_paths:
  913. if not os.path.exists(os.path.dirname(save_path)):
  914. self._create_directory(
  915. template_object,
  916. path_to_create=os.path.dirname(save_path))
  917. with open(save_path, 'w') as output_file:
  918. output_file.write(output_text)
  919. # Меняем права доступа и владельца всех сохраняемых файлов,
  920. # если это необходимо.
  921. if chown:
  922. self._chown_file(save_path, chown)
  923. if chmod:
  924. self._chmod_file(save_path, chmod)
  925. elif template_object.format_class.EXECUTABLE:
  926. parsed_template = template_format(
  927. template_object.template_text,
  928. template_object.template_path,
  929. template_object.parameters,
  930. self.datavars_module)
  931. changed_files = parsed_template.execute_format(
  932. template_object.target_path,
  933. chroot_path=self.chroot_path)
  934. # Удаляем форматный объект входного файла.
  935. # del(parsed_template)
  936. # Если исполняемый формат выдал список измененных файлов для
  937. # изменения CONTENTS и при этом задан пакет -- обновляем
  938. # CONTENTS.
  939. for file_path, status in changed_files.items():
  940. if status == 'M':
  941. if file_path in self.changed_files:
  942. if self.changed_files[file_path] == 'D':
  943. self.changed_files[file_path] == 'N'
  944. else:
  945. self.changed_files[file_path] = 'M'
  946. elif status == 'D':
  947. if file_path in self.changed_files:
  948. if self.changed_files[file_path] == 'M':
  949. self.changed_files[file_path] == 'D'
  950. if self.changed_files[file_path] == 'N':
  951. self.changed_files.pop(file_path)
  952. else:
  953. self.changed_files[file_path] = 'D'
  954. # Если в ходе работы формат выкидывал предупреждения --
  955. # добавляем их в список предупреждений.
  956. if parsed_template.warnings:
  957. self.executor_output['warnings'].extend(
  958. parsed_template.warnings)
  959. if (self.dbpkg and changed_files and
  960. template_object.target_package and
  961. template_object.format_class.FORMAT != 'contents'):
  962. template_object.update_contents_from_list(changed_files)
  963. return
  964. if input_file_md5 != output_file_md5:
  965. if template_object.target_type is None:
  966. self.changed_files[
  967. template_object.target_path] = 'N'
  968. else:
  969. self.changed_files[
  970. template_object.target_path] = 'M'
  971. if self.dbpkg:
  972. self._remove_cfg_files(template_object)
  973. if template_object.parameters.unbound:
  974. template_object.remove_from_contents()
  975. else:
  976. template_object.add_to_contents(
  977. file_md5=output_file_md5)
  978. else:
  979. input_text = self._get_input_text(template_object,
  980. replace=replace)
  981. parsed_input = template_format(
  982. input_text,
  983. template_object.template_path,
  984. add_header=True,
  985. join_before=join_before,
  986. already_changed=False,
  987. parameters=template_object.parameters)
  988. parsed_template = template_format(template_object.template_text,
  989. template_object.template_path,
  990. ignore_comments=True)
  991. parsed_input.join_template(parsed_template)
  992. # Результат наложения шаблона.
  993. output_text = parsed_input.document_text
  994. # Удаляем форматный объект входного файла.
  995. del(parsed_input)
  996. output_file_md5 = hashlib.md5(output_text.encode()).hexdigest()
  997. if not self.calculate_config_file.compare_md5(
  998. template_object.target_path,
  999. output_file_md5):
  1000. if not os.path.exists(os.path.dirname(output_path)):
  1001. self._create_directory(
  1002. template_object,
  1003. path_to_create=os.path.dirname(output_path))
  1004. with open(output_path, 'w') as output_file:
  1005. output_file.write(output_text)
  1006. # Меняем права доступа и владельца ._cfg????_ файлов, если
  1007. # это необходимо.
  1008. if chown:
  1009. self._chown_file(output_path, chown)
  1010. if chmod:
  1011. self._chmod_file(output_path, chmod)
  1012. # Обновляем CL.
  1013. self.calculate_config_file.set_files_md5(
  1014. template_object.target_path,
  1015. output_file_md5)
  1016. self.changed_files[template_object.target_path] = 'C'
  1017. # Обновляем CONTENTS.
  1018. template_object.add_to_contents(file_md5=output_file_md5)
  1019. else:
  1020. # Действия если CL совпало. Hичего не делаем.
  1021. pass
  1022. def _get_input_text(self, template_object: TemplateWrapper,
  1023. replace: bool = False) -> str:
  1024. '''Метод для получения текста входного файла.'''
  1025. # TODO Сделать это как-нибудь получше
  1026. input_path = template_object.input_path
  1027. if (not replace and (template_object.target_type is not None
  1028. or template_object.parameters.source)):
  1029. # Если целевой файл есть и нет параметра replace --
  1030. # используем текст целевого файла.
  1031. if (not input_path.startswith(self.cl_config_archive_path)
  1032. or os.path.exists(input_path)):
  1033. # Если входной файл просто не из архива, или из архива
  1034. # и при этом существует -- используем его
  1035. try:
  1036. with open(input_path, 'r') as input_file:
  1037. input_text = input_file.read()
  1038. return input_text
  1039. except UnicodeDecodeError:
  1040. raise TemplateExecutorError("can not join binary files.")
  1041. return ''
  1042. def _remove_cfg_files(self, template_object: TemplateWrapper) -> None:
  1043. '''Метод для удаления всех ._cfg-файлов и хэш-сумм добавленных в
  1044. config-файл.'''
  1045. # Убираем все ._cfg файлы.
  1046. if template_object.cfg_list:
  1047. for cfg_file_path in template_object.cfg_list:
  1048. self._remove_file(cfg_file_path)
  1049. # Убираем целевой файл из CL.
  1050. self.calculate_config_file.remove_file(template_object.target_path)
  1051. def _copy_from_source(self, template_object: TemplateWrapper,
  1052. chown: Optional[dict] = None,
  1053. chmod: Optional[int] = None) -> str:
  1054. '''Метод для копирования файла, указанного в source.'''
  1055. output_path = template_object.output_path
  1056. source_path = template_object.input_path
  1057. output_directory = os.path.dirname(output_path)
  1058. if not os.path.exists(output_directory):
  1059. self._create_directory(template_object,
  1060. path_to_create=output_directory)
  1061. shutil.copy(source_path, output_path)
  1062. if chown:
  1063. self._chown_file(template_object.output_path, chown)
  1064. if chmod:
  1065. self._chmod_file(template_object.output_path, chmod)
  1066. with open(output_path, "rb") as output_file:
  1067. output_file_data = output_file.read()
  1068. output_file_md5 = hashlib.md5(output_file_data).hexdigest()
  1069. return output_file_md5
  1070. def _get_chmod(self, template_object: TemplateWrapper) -> int:
  1071. chmod = template_object.parameters.chmod
  1072. if not chmod:
  1073. if (template_object.target_type is not None and
  1074. not template_object.target_without_package):
  1075. chmod = self._get_file_mode(template_object.target_path)
  1076. else:
  1077. chmod = self.file_default_parameters.get('chmod', False)
  1078. return chmod
  1079. def _get_chown(self, template_object: TemplateWrapper) -> dict:
  1080. chown = template_object.parameters.chown
  1081. if not chown:
  1082. if (template_object.target_type is not None and
  1083. not template_object.target_without_package):
  1084. chown = self._get_file_owner(template_object.target_path)
  1085. else:
  1086. chown = self.file_default_parameters.get('chown', False)
  1087. return chown
  1088. def _append_after_file(self, template_object: TemplateWrapper) -> NoReturn:
  1089. '''Метод описывающий действия при append = "after", если шаблон --
  1090. файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
  1091. в конец файла и в конец каждой секции файла.'''
  1092. self._append_join_file(template_object, join_before=False)
  1093. def _append_before_file(self, template_object: TemplateWrapper
  1094. ) -> NoReturn:
  1095. '''Метод описывающий действия при append = "after", если шаблон --
  1096. файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
  1097. в начало файла и в начало каждой секции файла.'''
  1098. self._append_join_file(template_object, join_before=True)
  1099. def _append_skip_file(self, template_object: TemplateWrapper) -> NoReturn:
  1100. '''Метод описывающий действия при append = "skip". Пока никаких
  1101. действий.'''
  1102. pass
  1103. def _append_replace_file(self, template_object: TemplateWrapper
  1104. ) -> NoReturn:
  1105. '''Метод описывающий действия при append = "replace", если шаблон --
  1106. файл. Очищает файл и затем накладывает на него шаблон.'''
  1107. self._append_join_file(template_object, replace=True)
  1108. def _append_remove_file(self, template_object: TemplateWrapper
  1109. ) -> NoReturn:
  1110. '''Метод описывающий действия при append = "remove", если шаблон --
  1111. файл. Удаляет файл.'''
  1112. if template_object.target_type is not None:
  1113. self._remove_file(template_object.target_path)
  1114. if template_object.target_path in self.changed_files:
  1115. if self.changed_files[template_object.target_path] == 'N':
  1116. self.changed_files.pop(template_object.target_path)
  1117. elif self.changed_files[template_object.target_path] == 'M':
  1118. self.changed_files[template_object.target_path] = 'D'
  1119. else:
  1120. self.changed_files[template_object.target_path] = 'D'
  1121. if self.dbpkg:
  1122. template_object.remove_from_contents()
  1123. def _append_clear_file(self, template_object: TemplateWrapper) -> NoReturn:
  1124. '''Метод описывающий действия при append = "clear", если шаблон --
  1125. файл. Очищает файл.'''
  1126. if template_object.target_type is not None:
  1127. self._clear_file(template_object.target_path)
  1128. # Меняем владельца и права доступа к очищенному файлу, если нужно.
  1129. if template_object.parameters.chown:
  1130. self._chown_file(template_object.target_path,
  1131. template_object.parameters.chown)
  1132. if template_object.parameters.chmod:
  1133. self._chmod_file(template_object.target_path,
  1134. template_object.parameters.chmod)
  1135. # Корректируем список измененных файлов.
  1136. if template_object.target_path not in self.changed_files:
  1137. self.changed_files[template_object.target_path] = 'M'
  1138. if self.dbpkg:
  1139. template_object.add_to_contents()
  1140. def _append_link_file(self, template_object: TemplateWrapper) -> NoReturn:
  1141. '''Метод описывающий действия при append = "link", если шаблон --
  1142. файл. Создает ссылку на файл, указанный в параметре source.'''
  1143. input_path = template_object.input_path
  1144. output_path = template_object.output_path
  1145. if template_object.contents_matching:
  1146. output_paths = [output_path]
  1147. # Если целевой файл защищен, а шаблон не userspace,
  1148. # тогда также обновляем архив.
  1149. if (template_object.protected
  1150. and not template_object.is_userspace):
  1151. output_paths.append(template_object.archive_path)
  1152. for link_path in output_paths:
  1153. if not os.path.exists(os.path.dirname(link_path)):
  1154. self._create_directory(
  1155. template_object,
  1156. path_to_create=os.path.dirname(link_path))
  1157. elif os.path.exists(link_path):
  1158. os.unlink(link_path)
  1159. self._link_file(input_path, link_path)
  1160. # Корректируем список измененных файлов.
  1161. if template_object.target_path not in self.changed_files:
  1162. self.changed_files[template_object.target_path] = 'N'
  1163. else:
  1164. self.changed_files[template_object.target_path] = 'M'
  1165. if self.dbpkg:
  1166. # Убираем целевой файл из CL.
  1167. self.calculate_config_file.remove_file(
  1168. template_object.target_path)
  1169. if template_object.parameters.unbound:
  1170. template_object.remove_from_contents()
  1171. else:
  1172. template_object.add_to_contents()
  1173. else:
  1174. source_path_md5 = self._get_source_hash(input_path)
  1175. if not self.calculate_config_file.compare_md5(
  1176. template_object.target_path,
  1177. source_path_md5
  1178. ):
  1179. if not os.path.exists(os.path.dirname(output_path)):
  1180. self._create_directory(
  1181. template_object,
  1182. path_to_create=os.path.dirname(output_path))
  1183. elif os.path.exists(output_path):
  1184. os.unlink(output_path)
  1185. self._link_file(input_path, output_path)
  1186. self.changed_files[template_object.target_path] = 'C'
  1187. if self.dbpkg:
  1188. template_object.add_to_contents()
  1189. # Обновляем CL.
  1190. self.calculate_config_file.set_files_md5(
  1191. template_object.target_path,
  1192. source_path_md5)
  1193. self.changed_files[template_object.target_path] = 'C'
  1194. # Обновляем CONTENTS.
  1195. template_object.add_to_contents()
  1196. else:
  1197. # Действия если CL совпало. Hичего не делаем.
  1198. pass
  1199. # Меняем права и владельца файла, на который указывает ссылка.
  1200. if template_object.parameters.chmod:
  1201. self._chmod_file(input_path, template_object.parameters.chmod)
  1202. if template_object.parameters.chown:
  1203. self._chown_file(input_path, template_object.parameters.chown)
  1204. def _get_source_hash(self, source_path: str) -> str:
  1205. if (self.chroot_path != "/" and
  1206. source_path.startswith(self.chroot_path)):
  1207. source_path = source_path[len(self.chroot_path):]
  1208. if not source_path.startswith("/"):
  1209. source_path = "/" + source_path
  1210. return hashlib.md5(source_path.encode()).hexdigest()
  1211. def _create_directory(self, template_object: TemplateWrapper,
  1212. path_to_create: Optional[str] = None) -> NoReturn:
  1213. '''Метод для создания директории и, при необходимости, изменения
  1214. владельца и доступа все директорий на пути к целевой.'''
  1215. if path_to_create is None:
  1216. target_path = template_object.target_path
  1217. else:
  1218. target_path = path_to_create
  1219. template_parameters = template_object.parameters
  1220. # Если файл есть, но указан chmod или chown -- используем их.
  1221. if os.access(target_path, os.F_OK):
  1222. if template_parameters.chmod:
  1223. self._chmod_directory(target_path, template_parameters.chmod)
  1224. if template_parameters.chown:
  1225. self._chown_directory(target_path, template_parameters.chown)
  1226. return
  1227. directories_to_create = [target_path]
  1228. directory_path = os.path.dirname(target_path)
  1229. # Составляем список путей к директориям, которые нужно создать.
  1230. while not os.access(directory_path, os.F_OK) and directory_path:
  1231. directories_to_create.append(directory_path)
  1232. directory_path = os.path.dirname(directory_path)
  1233. # получаем информацию о владельце и правах доступа ближайшей
  1234. # существующей директории.
  1235. chmod = template_parameters.chmod
  1236. if not chmod:
  1237. chmod = self._get_file_mode(directory_path)
  1238. chown = template_parameters.chown
  1239. if not chown:
  1240. chown = self._get_file_owner(directory_path)
  1241. directories_to_create.reverse()
  1242. # создаем директории.
  1243. for create_path in directories_to_create:
  1244. try:
  1245. os.mkdir(create_path)
  1246. # Для каждой созданной директории меняем права и владельца
  1247. # если это необходимо.
  1248. if chmod:
  1249. self._chmod_directory(create_path, chmod)
  1250. if chown:
  1251. self._chown_directory(create_path, chown)
  1252. except OSError as error:
  1253. raise TemplateExecutorError(
  1254. 'Failed to create directory: {}, reason: {}'.
  1255. format(create_path, str(error)))
  1256. def _remove_directory(self, target_path: str) -> NoReturn:
  1257. '''Метод для удаления директории.'''
  1258. if os.path.exists(target_path):
  1259. if os.path.isdir(target_path):
  1260. try:
  1261. if os.path.islink(target_path):
  1262. os.unlink(target_path)
  1263. else:
  1264. shutil.rmtree(target_path)
  1265. return
  1266. except Exception as error:
  1267. raise TemplateExecutorError(
  1268. ("Failed to delete the directory: {0},"
  1269. " reason: {1}").format(target_path,
  1270. str(error)))
  1271. else:
  1272. error_message = "target file is not directory"
  1273. else:
  1274. error_message = "target file does not exist"
  1275. raise TemplateExecutorError(("Failed to delete the directory: {0},"
  1276. "reason: {1}").format(target_path,
  1277. error_message))
  1278. def _clear_directory(self, target_path: str) -> NoReturn:
  1279. '''Метод для очистки содержимого целевой директории.'''
  1280. if os.path.exists(target_path):
  1281. if os.path.isdir(target_path):
  1282. # Удаляем все содержимое директории.
  1283. for node in os.scandir(target_path):
  1284. if node.is_dir():
  1285. self._remove_directory(node.path)
  1286. else:
  1287. self._remove_file(node.path)
  1288. return
  1289. else:
  1290. error_message = "target file is not directory"
  1291. else:
  1292. error_message = "target directory does not exist"
  1293. raise TemplateExecutorError(("failed to clear directory: {},"
  1294. " reason: {}").format(target_path,
  1295. error_message))
  1296. def _link_directory(self, source: str, target_path: str,
  1297. force: bool = False) -> NoReturn:
  1298. '''Метод для создания по целевому пути ссылки на директорию
  1299. расположенную на пути, указанному в source.'''
  1300. try:
  1301. os.symlink(source, target_path, target_is_directory=True)
  1302. except OSError as error:
  1303. raise TemplateExecutorError(
  1304. "failed to create symlink: {0} -> {1}, reason: {2}".
  1305. format(target_path, source, str(error)))
  1306. def _remove_file(self, target_path: str) -> NoReturn:
  1307. '''Метод для удаления файлов.'''
  1308. if os.path.exists(target_path):
  1309. if os.path.isfile(target_path):
  1310. if os.path.islink(target_path):
  1311. try:
  1312. os.unlink(target_path)
  1313. return
  1314. except OSError as error:
  1315. error_message = str(error)
  1316. try:
  1317. os.remove(target_path)
  1318. return
  1319. except OSError as error:
  1320. error_message = str(error)
  1321. else:
  1322. error_message = 'target is not a file'
  1323. elif os.path.islink(target_path):
  1324. try:
  1325. os.unlink(target_path)
  1326. return
  1327. except OSError as error:
  1328. error_message = str(error)
  1329. else:
  1330. error_message = 'target file does not exist'
  1331. raise TemplateExecutorError(("failed to remove the file: {0},"
  1332. "reason: {1}").format(target_path,
  1333. error_message))
  1334. def _clear_file(self, target_path: str) -> NoReturn:
  1335. '''Метод для очистки файлов.'''
  1336. if os.path.exists(target_path):
  1337. if os.path.isfile(target_path):
  1338. try:
  1339. with open(target_path, 'w') as f:
  1340. f.truncate(0)
  1341. return
  1342. except IOError as error:
  1343. error_message = str(error)
  1344. else:
  1345. error_message = 'target is not a file'
  1346. else:
  1347. error_message = 'target file does not exist'
  1348. raise TemplateExecutorError(("failed to clear the file: {0},"
  1349. "reason: {1}").format(target_path,
  1350. error_message))
  1351. def _link_file(self, source: str, target_path: str) -> NoReturn:
  1352. '''Метод для создания по целевому пути ссылки на файл расположенный на
  1353. пути, указанному в source.'''
  1354. try:
  1355. os.symlink(source, target_path)
  1356. except OSError as error:
  1357. raise TemplateExecutorError(
  1358. "failed to create symlink to the file: {0} -> {1}, reason: {2}".
  1359. format(target_path, source, str(error)))
  1360. def _run_template(self, template_object: TemplateWrapper) -> NoReturn:
  1361. '''Метод для сохранения текста шаблонов, который должен быть исполнен
  1362. интерпретатором указанным в run прямо во время обработки шаблонов.'''
  1363. text_to_run = template_object.template_text
  1364. interpreter = template_object.parameters.run
  1365. if template_object.template_type == FILE:
  1366. cwd_path = os.path.dirname(template_object.target_path)
  1367. else:
  1368. cwd_path = template_object.target_path
  1369. if not os.path.exists(cwd_path):
  1370. raise TemplateExecutorError(("can not run template, directory from"
  1371. " target path does not exist: {}").
  1372. format(template_object.target_path))
  1373. elif not os.path.isdir(cwd_path):
  1374. raise TemplateExecutorError(("can not exec template, {} is not a"
  1375. " directory.").format(cwd_path))
  1376. try:
  1377. run_process = Process(interpreter, cwd=cwd_path)
  1378. run_process.write(text_to_run)
  1379. if run_process.readable:
  1380. stdout = run_process.read()
  1381. if stdout:
  1382. self.executor_output['stdout'] = stdout
  1383. if run_process.readable_errors:
  1384. stderr = run_process.read_error()
  1385. if stderr:
  1386. self.executor_output['stderr'] = stderr
  1387. except FilesError as error:
  1388. raise TemplateExecutorError(("can not run template using the"
  1389. " interpreter '{}', reason: {}").
  1390. format(interpreter, str(error)))
  1391. def _exec_template(self, template_object: TemplateWrapper) -> NoReturn:
  1392. '''Метод для сохранения текста шаблонов, который должен быть исполнен
  1393. интерпретатором указанным в exec после выполнения всех прочих шаблонов.
  1394. '''
  1395. text_to_run = template_object.template_text
  1396. interpreter = template_object.parameters.exec
  1397. if template_object.template_type == FILE:
  1398. cwd_path = os.path.dirname(template_object.target_path)
  1399. else:
  1400. cwd_path = template_object.target_path
  1401. if not os.path.exists(cwd_path):
  1402. raise TemplateExecutorError(
  1403. ("can not exec template, directory from"
  1404. " target path does not exist: {}").
  1405. format(cwd_path))
  1406. elif not os.path.isdir(cwd_path):
  1407. raise TemplateExecutorError(("can not exec template, {} is not a"
  1408. " directory.").format(cwd_path))
  1409. # Получаем путь к директории для хранения файлов .execute.
  1410. if (self.chroot_path != '/' and not
  1411. self.execute_archive_path.startswith(self.chroot_path)):
  1412. self.execute_archive_path = join_paths(
  1413. self.chroot_path,
  1414. self.execute_archive_path)
  1415. # Если директория уже существует получаем номер очередного файла для
  1416. # exec по номеру последнего.
  1417. exec_number = 0
  1418. if not self.execute_files:
  1419. if os.path.exists(self.execute_archive_path):
  1420. exec_files_list = os.listdir(self.execute_archive_path)
  1421. if exec_files_list:
  1422. exec_number = int(exec_files_list[-1][-4:])
  1423. exec_number = str(exec_number + 1)
  1424. else:
  1425. exec_number = str(len(self.execute_files) + 1)
  1426. # Получаем название нового exec_???? файла.
  1427. if len(exec_number) < 4:
  1428. exec_number = '0' * (4 - len(exec_number)) + exec_number
  1429. exec_file_name = 'exec_{}'.format(exec_number)
  1430. exec_file_path = join_paths(self.execute_archive_path,
  1431. exec_file_name)
  1432. exec_file = write_file(exec_file_path)
  1433. exec_file.write(text_to_run)
  1434. exec_file.close()
  1435. self.execute_files[exec_file_path] = {'interpreter': interpreter,
  1436. 'cwd_path': cwd_path,
  1437. 'template_path':
  1438. template_object.template_path}
  1439. def execute_file(self, interpreter: str, exec_path: str,
  1440. cwd_path: str) -> dict:
  1441. """Метод для выполнения скриптов сохраненных в результате обработки
  1442. шаблонов с параметром exec. Скрипт всегда удаляется вне зависимости
  1443. от успешности его выполнения."""
  1444. exec_output = {'stdout': None, 'stderr': None}
  1445. with open(exec_path, 'r') as exec_file:
  1446. script_text = exec_file.read()
  1447. os.remove(exec_path)
  1448. try:
  1449. run_process = Process(interpreter, cwd=cwd_path)
  1450. run_process.write(script_text)
  1451. if run_process.readable:
  1452. stdout = run_process.read()
  1453. if stdout:
  1454. exec_output['stdout'] = stdout
  1455. if run_process.readable_errors:
  1456. stderr = run_process.read_error()
  1457. if stderr:
  1458. exec_output['stderr'] = stderr
  1459. return exec_output
  1460. except FilesError as error:
  1461. raise TemplateExecutorError(("can not run template using the"
  1462. " interpreter '{}', reason: {}").
  1463. format(interpreter, str(error)))
  1464. def _chown_directory(self, target_path: str, chown_value: dict
  1465. ) -> NoReturn:
  1466. """Метод для смены владельца директории."""
  1467. try:
  1468. if os.path.exists(target_path):
  1469. os.chown(target_path, chown_value['uid'], chown_value['gid'])
  1470. else:
  1471. raise TemplateExecutorError(
  1472. 'The target directory does not exist: {0}'.
  1473. format(target_path))
  1474. except (OSError, Exception) as error:
  1475. if not self._check_os_error(error, target_path):
  1476. raise TemplateExecutorError(
  1477. 'Can not chown file: {0} to {1}, reason: {2}'.
  1478. format(target_path, self._translate_uid_gid(
  1479. chown_value['uid'],
  1480. chown_value['gid']),
  1481. str(error)))
  1482. def _chmod_directory(self, target_path: str, chmod_value: int) -> NoReturn:
  1483. '''Метод для смены прав доступа к директории.'''
  1484. if isinstance(chmod_value, tuple) and not chmod_value[1]:
  1485. chmod_value = chmod_value[0]
  1486. try:
  1487. if os.path.exists(target_path):
  1488. if isinstance(chmod_value, int):
  1489. os.chmod(target_path, chmod_value)
  1490. else:
  1491. chmod_value = self._use_chmod_x_mask(chmod_value)
  1492. os.chmod(target_path, chmod_value)
  1493. else:
  1494. raise TemplateExecutorError(
  1495. 'The target directory does not exist: {0}'.
  1496. format(target_path))
  1497. except (OSError, Exception) as error:
  1498. if not self._check_os_error(error, target_path):
  1499. raise TemplateExecutorError(
  1500. 'Can not chmod directory: {0}, reason: {1}'.
  1501. format(target_path, str(error)))
  1502. def _chown_file(self, target_path: str, chown_value: dict) -> NoReturn:
  1503. '''Метод для смены владельца файла.'''
  1504. try:
  1505. if os.path.exists(target_path):
  1506. os.lchown(target_path, chown_value['uid'], chown_value['gid'])
  1507. else:
  1508. raise TemplateExecutorError(
  1509. 'The target file does not exist: {0}'.
  1510. format(target_path))
  1511. except (OSError, Exception) as error:
  1512. if not self._check_os_error(error, target_path):
  1513. raise TemplateExecutorError(
  1514. 'Can not chown file: {0} to {1}, reason: {2}'.
  1515. format(target_path, self._translate_uid_gid(
  1516. chown_value['uid'],
  1517. chown_value['gid']),
  1518. str(error)))
  1519. def _chmod_file(self, target_path: str, chmod_value: int) -> NoReturn:
  1520. '''Метод для смены прав доступа к директории.'''
  1521. try:
  1522. if not os.path.exists(target_path):
  1523. raise TemplateExecutorError(
  1524. 'The target file does not exist: {0}'.
  1525. format(target_path))
  1526. if isinstance(chmod_value, int):
  1527. os.chmod(target_path, chmod_value)
  1528. else:
  1529. chmod_value = self._use_chmod_x_mask(
  1530. chmod_value,
  1531. current_mode=self._get_file_mode(
  1532. target_path))
  1533. os.chmod(target_path, chmod_value)
  1534. except (OSError, Exception) as error:
  1535. if not self._check_os_error(error, target_path):
  1536. raise TemplateExecutorError(
  1537. 'Can not chmod file: {0}, reason: {1}'.
  1538. format(target_path, str(error)))
  1539. def _use_chmod_x_mask(self, chmod: Tuple[int, int],
  1540. current_mode: Optional[int] = None) -> int:
  1541. '''Метод для наложения X-маски, необходимой для получения значения
  1542. chmod, c учетом возможности наличия в нем значения "X".'''
  1543. if not chmod[1]:
  1544. return chmod[0]
  1545. if current_mode is None:
  1546. return chmod[0] ^ chmod[1]
  1547. else:
  1548. return chmod[0] ^ (current_mode & chmod[1])
  1549. def _get_file_mode(self, file_path: str) -> int:
  1550. '''Метод для получения прав доступа для указанного файла.'''
  1551. if not os.path.exists(file_path):
  1552. raise TemplateExecutorError(
  1553. 'The file to get mode does not exist: {0}'.
  1554. format(file_path))
  1555. file_stat = os.stat(file_path)
  1556. return stat.S_IMODE(file_stat.st_mode)
  1557. def _get_file_owner(self, file_path: str) -> dict:
  1558. '''Метод для получения uid и gid значений для владельца указанного
  1559. файла.'''
  1560. if not os.path.exists(file_path):
  1561. raise TemplateExecutorError(
  1562. 'The file to get owner does not exist: {0}'.
  1563. format(file_path))
  1564. file_stat = os.stat(file_path)
  1565. return {'uid': file_stat.st_uid, 'gid': file_stat.st_gid}
  1566. def _translate_uid_gid(self, uid: int, gid: int) -> str:
  1567. '''Метод для получения из uid и gid имен пользователя и группы при,
  1568. необходимых для выдачи сообщения об ошибке при попытке chown.'''
  1569. import pwd
  1570. import grp
  1571. try:
  1572. if self.chroot_path == '/':
  1573. user_name = pwd.getpwuid(uid).pw_name
  1574. else:
  1575. user_name = self._get_user_name_from_uid(uid)
  1576. except (TypeError, KeyError):
  1577. user_name = str(uid)
  1578. try:
  1579. if self.chroot_path == '/':
  1580. group_name = grp.getgrgid(gid).gr_name
  1581. else:
  1582. group_name = self._get_group_name_form_gid(gid)
  1583. except (TypeError, KeyError):
  1584. group_name = str(gid)
  1585. return '{0}:{1}'.format(user_name, group_name)
  1586. def _get_user_name_from_uid(self, uid: int) -> str:
  1587. '''Метод для получения имени пользователя по его uid.'''
  1588. passwd_file_path = os.path.join(self.chroot_path, 'etc/passwd')
  1589. passwd_dictionary = dict()
  1590. if os.path.exists(passwd_file_path):
  1591. with open(passwd_file_path, 'r') as passwd_file:
  1592. for line in passwd_file:
  1593. if line.startswith('#'):
  1594. continue
  1595. passwd_line = line.split(':')
  1596. line_uid = int(passwd_line[2])
  1597. line_username = passwd_line[0]
  1598. if line_uid and line_username:
  1599. passwd_dictionary[line_uid] = line_username
  1600. if uid in passwd_dictionary:
  1601. return passwd_dictionary[uid]
  1602. else:
  1603. return str(uid)
  1604. def _get_group_name_form_gid(self, gid: int) -> str:
  1605. '''Метод для получения названия группы по его gid.'''
  1606. group_file_path = os.path.join(self.chroot_path, 'etc/group')
  1607. group_dictionary = dict()
  1608. if os.path.exists(group_file_path):
  1609. with open(group_file_path, 'r') as group_file:
  1610. for line in group_file:
  1611. if line.startswith('#'):
  1612. continue
  1613. group_line = line.split(':')
  1614. line_gid = int(group_line[2])
  1615. line_group = group_line[0]
  1616. if line_gid and line_group:
  1617. group_dictionary[line_gid] = line_group
  1618. if gid in group_dictionary:
  1619. return group_dictionary[gid]
  1620. else:
  1621. return str(gid)
  1622. def _check_os_error(self, error: Exception, path_to_check: str) -> bool:
  1623. '''Метод для проверки причины, по которой не удалось изменить владельца
  1624. или права доступа файла.'''
  1625. if hasattr(error, 'errno') and error.errno == errno.EPERM:
  1626. if self._is_vfat(path_to_check):
  1627. return True
  1628. return hasattr(error, 'errno') and error.errno == errno.EACCES and\
  1629. 'var/calculate/remote' in path_to_check
  1630. def _is_vfat(self, path_to_check: str) -> bool:
  1631. '''Метод, проверяющий является ли файловая система vfat. Нужно для того,
  1632. чтобы знать о возможности применения chown, chmod и т.д.'''
  1633. # Инициализируем объект для проверки примонтированных файловых систем.
  1634. if self.mounts is None:
  1635. self.mounts = Mounts()
  1636. # Проверяем файловую систему на пути.
  1637. fstab_info = self.mounts.get_from_fstab(what=self.mounts.TYPE,
  1638. where=self.mounts.DIR,
  1639. is_in=path_to_check)[0]
  1640. return fstab_info in {'vfat', 'ntfs-3g', 'ntfs'}
  1641. class DirectoryTree:
  1642. '''Класс реализующий дерево каталогов для пакета.'''
  1643. def __init__(self, base_directory: str):
  1644. self.base_directory = base_directory
  1645. self._tree = {}
  1646. def update_tree(self, tree: dict) -> NoReturn:
  1647. '''Метод, инициирующий наложение заданного дерева каталогов на данный
  1648. экземпляр дерева.'''
  1649. self._update(self._tree, tree)
  1650. def _update(self, original_tree: dict, tree: dict) -> dict:
  1651. '''Метод для рекурсивного наложения одного дерева на другое.'''
  1652. for parent, child in tree.items():
  1653. if isinstance(child, abc.Mapping):
  1654. original_tree[parent] = self._update(original_tree.get(parent,
  1655. dict()),
  1656. child)
  1657. else:
  1658. original_tree[parent] = child
  1659. return original_tree
  1660. def show_tree(self) -> NoReturn:
  1661. pprint(self._tree)
  1662. def get_directory_tree(self, directory: str) -> "DirectoryTree":
  1663. '''Метод для получения нового дерева из ветви данного дерева,
  1664. соответствующей некоторому каталогу, содержащемуся в корне данного
  1665. дерева.'''
  1666. directory_tree = DirectoryTree(os.path.join(self.base_directory,
  1667. directory))
  1668. if directory in self._tree:
  1669. directory_tree._tree = self._tree[directory]
  1670. return directory_tree
  1671. def __getitem__(self, name: str) -> dict:
  1672. if name in self._tree:
  1673. return self._tree[name]
  1674. else:
  1675. return None
  1676. def __setitem__(self, name: str, value: Union[None, dict]) -> NoReturn:
  1677. self._tree[name] = value
  1678. def __iter__(self) -> Iterator[str]:
  1679. if self._tree is not None:
  1680. return iter(self._tree.keys())
  1681. else:
  1682. return iter([])
  1683. def __repr__(self) -> str:
  1684. return '<DirectoryTree: {}>'.format(self._tree)
  1685. def __bool__(self) -> bool:
  1686. return bool(self._tree)
  1687. class DirectoryProcessor:
  1688. '''Класс обработчика директорий шаблонов.'''
  1689. def __init__(self, action: str,
  1690. datavars_module: Union[Datavars, Variables] = Variables(),
  1691. install: Union[str, PackageAtomName] = '',
  1692. output_module: IOModule = IOModule(),
  1693. dbpkg: bool = True, pkg_autosave: bool = True,
  1694. namespace: Optional[NamespaceNode] = None, **groups: dict):
  1695. if isinstance(action, list):
  1696. self.action = action
  1697. else:
  1698. self.action = [action]
  1699. self.output: IOModule = output_module
  1700. self.datavars_module: Variables = datavars_module
  1701. self._namespace: NamespaceNode = namespace
  1702. self._pkg_autosave: bool = pkg_autosave
  1703. # Корневая директория.
  1704. self.cl_chroot_path: str
  1705. if 'cl_chroot_path' in datavars_module.main:
  1706. self.cl_chroot_path = datavars_module.main.cl_chroot_path
  1707. else:
  1708. self.cl_chroot_path = '/'
  1709. # Корневой путь шаблонов.
  1710. self.templates_root: str
  1711. if 'cl_root_path' in datavars_module.main:
  1712. self.templates_root = join_paths(self.cl_chroot_path,
  1713. datavars_module.main.cl_root_path)
  1714. else:
  1715. self.templates_root = self.cl_chroot_path
  1716. self.cl_ignore_files: List[str] = self._get_cl_ignore_files()
  1717. # Путь к файлу config с хэш-суммами файлов, для которых уже
  1718. # предлагались изменения.
  1719. self.cl_config_path: str
  1720. if 'cl_config_path' in datavars_module.main:
  1721. self.cl_config_path = self._add_chroot_path(
  1722. self.datavars_module.main.cl_config_path)
  1723. else:
  1724. self.cl_config_path = self._add_chroot_path(
  1725. '/var/lib/calculate/config')
  1726. # Путь к директории config-archive для хранения оригинальной ветки
  1727. # конфигурационных файлов.
  1728. self.cl_config_archive: str
  1729. if 'cl_config_archive' in datavars_module.main:
  1730. self.cl_config_archive = self._add_chroot_path(
  1731. self.datavars_module.main.cl_config_archive)
  1732. else:
  1733. self.cl_config_archive = self._add_chroot_path(
  1734. '/var/lib/calculate/config-archive')
  1735. # Путь к директории .execute для хранения хранения файлов скриптов,
  1736. # полученных из шаблонов с параметром exec.
  1737. self.cl_exec_dir_path: str
  1738. if 'cl_exec_dir_path' in datavars_module.main:
  1739. self.cl_exec_dir_path = self._add_chroot_path(
  1740. self.datavars_module.main.cl_exec_dir_path)
  1741. else:
  1742. self.cl_exec_dir_path = self._add_chroot_path(
  1743. '/var/lib/calculate/.execute/')
  1744. # Инициализируем исполнительный модуль.
  1745. self.template_executor: TemplateExecutor = TemplateExecutor(
  1746. datavars_module=self.datavars_module,
  1747. chroot_path=self.cl_chroot_path,
  1748. cl_config_archive=self.cl_config_archive,
  1749. cl_config_path=self.cl_config_path,
  1750. execute_archive_path=self.cl_exec_dir_path,
  1751. dbpkg=dbpkg,
  1752. pkg_autosave=self._pkg_autosave)
  1753. # Разбираем atom имена пакетов, указанных для групп пакетов.
  1754. if groups:
  1755. for group_name, package_name in groups.items():
  1756. if isinstance(package_name, list):
  1757. for atom in package_name:
  1758. self._add_package_to_group(group_name, atom)
  1759. else:
  1760. self._add_package_to_group(group_name, package_name)
  1761. # Создаем переменную для указания текущих шаблонов, если ее еще нет.
  1762. self._make_current_template_var()
  1763. # Инициализируем шаблонизатор.
  1764. self.template_engine: TemplateEngine = TemplateEngine(
  1765. datavars_module=self.datavars_module,
  1766. chroot_path=self.cl_chroot_path,
  1767. appends_set=self.template_executor.available_appends)
  1768. # Разбираем atom имя пакета, для которого накладываем шаблоны.
  1769. self.for_package: Union[PackageAtomName, None] = None
  1770. if install:
  1771. if isinstance(install, PackageAtomName):
  1772. self.for_package = install
  1773. elif isinstance(install, str):
  1774. try:
  1775. self.for_package = self.template_engine.\
  1776. parameters_processor.check_postparse_package(
  1777. install)
  1778. except ConditionFailed as error:
  1779. # ConditionFailed потому что для проверки значения пакета,
  1780. # используется тот же метод, что проверяет параметр package
  1781. # в шаблонах, а в них этот параметр играет роль условия.
  1782. self.output.set_error(str(error))
  1783. return
  1784. self.template_engine.for_package = self.for_package
  1785. # Получаем список директорий шаблонов.
  1786. # TODO переменная список.
  1787. self.template_paths: List[str]
  1788. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  1789. var_type = self.datavars_module.main[
  1790. 'cl_template_path'].variable_type
  1791. else:
  1792. var_type = StringType
  1793. if var_type is StringType:
  1794. self.template_paths = (self.datavars_module.
  1795. main.cl_template_path.split(','))
  1796. elif var_type is ListType:
  1797. self.template_paths = self.datavars_module.main.cl_template_path
  1798. # Список обработанных пакетов.
  1799. self.processed_packages = set()
  1800. # Список пакетов, взятый из значений параметра merge.
  1801. self.packages_to_merge = set()
  1802. # Словарь для хранения деревьев директорий для различных пакетов.
  1803. self.packages_file_trees = OrderedDict()
  1804. # Список обработчиков.
  1805. self._handlers: Dict[tuple] = {}
  1806. self._handlers_queue: List[str] = []
  1807. self._handling: Union[str, None] = None
  1808. def _get_cl_ignore_files(self) -> List[str]:
  1809. '''Метод для получения из соответствующей переменной списка паттернов
  1810. для обнаружения игнорируемых в ходе обработки шаблонов файлов.'''
  1811. if 'cl_ignore_files' in self.datavars_module.main:
  1812. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  1813. var_type = self.datavars_module.main[
  1814. 'cl_ignore_files'].variable_type
  1815. else:
  1816. var_type = StringType
  1817. cl_ignore_files = self.datavars_module.main.cl_ignore_files
  1818. cl_ignore_files_list = []
  1819. if var_type is StringType:
  1820. for pattern in cl_ignore_files.split(','):
  1821. cl_ignore_files_list.append(pattern.strip())
  1822. elif var_type is ListType:
  1823. cl_ignore_files_list = cl_ignore_files
  1824. return cl_ignore_files_list
  1825. else:
  1826. return []
  1827. def _add_chroot_path(self, path_to_add: str) -> str:
  1828. '''Метод для добавления корневого пути к заданному пути, если таковой
  1829. задан и отсутствует в заданном пути.'''
  1830. if (self.cl_chroot_path != '/' and
  1831. not path_to_add.startswith(self.cl_chroot_path)):
  1832. return join_paths(self.cl_chroot_path, path_to_add)
  1833. else:
  1834. return path_to_add
  1835. def _add_package_to_group(self, group_name: str,
  1836. package_atom: str) -> NoReturn:
  1837. try:
  1838. groups_namespace = self.datavars_module.main.cl.groups
  1839. except (VariableNotFoundError, AttributeError):
  1840. namespaces = ['cl', 'groups']
  1841. groups_namespace = self.datavars_module.main
  1842. for namespace in namespaces:
  1843. if namespace in groups_namespace:
  1844. groups_namespace = groups_namespace[namespace]
  1845. else:
  1846. if isinstance(groups_namespace, NamespaceNode):
  1847. groups_namespace = NamespaceNode(
  1848. namespace,
  1849. parent=groups_namespace)
  1850. else:
  1851. groups_namespace[namespace] = Variables()
  1852. groups_namespace = groups_namespace[namespace]
  1853. atom_dict = PackageAtomParser.parse_atom_name(package_atom)
  1854. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  1855. if group_name not in groups_namespace:
  1856. VariableNode(group_name, groups_namespace,
  1857. variable_type=TableType,
  1858. source=[atom_dict])
  1859. else:
  1860. group_var = groups_namespace[group_name]
  1861. group_table = group_var.get_value().get_table()
  1862. check_result = self.check_existance_in_group(atom_dict,
  1863. group_table)
  1864. if check_result == -2:
  1865. group_table.append(atom_dict)
  1866. elif check_result >= 0:
  1867. group_table[check_result] = atom_dict
  1868. if check_result != -1:
  1869. group_var.source = group_table
  1870. else:
  1871. if group_name not in groups_namespace:
  1872. groups_namespace[group_name] = [atom_dict]
  1873. else:
  1874. group_table = groups_namespace[group_name]
  1875. check_result = self.check_existance_in_group(atom_dict,
  1876. group_table)
  1877. if check_result == -2:
  1878. group_table.append(atom_dict)
  1879. elif check_result >= 0:
  1880. group_table[check_result] = atom_dict
  1881. if check_result != -1:
  1882. groups_namespace[group_name] = group_table
  1883. def check_existance_in_group(self, atom_dict: dict, group: list) -> bool:
  1884. '''Метод для проверки наличия в таблице групп указанного пакета, а
  1885. также для сравнения данных о пакете из таблицы и из атом словаря.
  1886. Возвращает:
  1887. -2 -- если пакета нет;
  1888. -1 -- если присутствует и полностью совпадает;
  1889. >=0 -- если имеющиеся в таблице данные о пакете должены быть заменены
  1890. на указанные. В этом случае возвращается индекс старых данных в
  1891. таблице. '''
  1892. index = 0
  1893. for group_atom in group:
  1894. if (atom_dict['category'] != group_atom['category'] and
  1895. atom_dict['name'] != group_atom['name']):
  1896. index += 1
  1897. continue
  1898. if (atom_dict['slot'] is not None and
  1899. group_atom['slot'] is not None):
  1900. if atom_dict['slot'] != group_atom['slot']:
  1901. return -2
  1902. if atom_dict['version'] is not None:
  1903. if (group_atom['version'] is None or
  1904. atom_dict['version'] != group_atom['version']):
  1905. return index
  1906. return -1
  1907. return -2
  1908. def _make_current_template_var(self) -> NoReturn:
  1909. var_path = ['main', 'cl']
  1910. namespace = self.datavars_module
  1911. for field in var_path:
  1912. if field not in namespace:
  1913. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  1914. namespace = NamespaceNode(field, parent=namespace)
  1915. else:
  1916. namespace[field] = Variables()
  1917. namespace = namespace[field]
  1918. else:
  1919. namespace = namespace[field]
  1920. if 'current_template' not in namespace:
  1921. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  1922. VariableNode('current_template', namespace,
  1923. variable_type=StringType, source="")
  1924. else:
  1925. namespace['current_template'] = ""
  1926. def process_template_directories(self) -> NoReturn:
  1927. '''Метод для обхода шаблонов, содержащихся в каталогах из
  1928. main.cl_template.path.'''
  1929. # Режим заполнения очередей директорий пакетов, необходимых для более
  1930. # быстрой обработки параметра merge.
  1931. self.fill_trees = bool(self.for_package)
  1932. if self.for_package:
  1933. if self.for_package is NonePackage:
  1934. package = self.for_package
  1935. else:
  1936. package = Package(self.for_package,
  1937. chroot_path=self.cl_chroot_path,
  1938. autosave=self._pkg_autosave)
  1939. else:
  1940. package = None
  1941. self.base_directory: str
  1942. for directory_path in self.template_paths:
  1943. self.base_directory = directory_path.strip()
  1944. entries = os.scandir(self.base_directory)
  1945. files: list = []
  1946. directories: list = []
  1947. for node in entries:
  1948. if node.is_file():
  1949. if (node.name != ".calculate_directory" and
  1950. self._check_file_name(node.name)):
  1951. files.append(node.name)
  1952. elif node.is_dir():
  1953. directories.append(node.path)
  1954. self.directory_tree = {}
  1955. self._run_template_from_base_directory(
  1956. files, self.base_directory,
  1957. package=package,
  1958. directory_tree=self.directory_tree)
  1959. for directory_path in directories:
  1960. self.directory_tree = {}
  1961. parameters = ParametersContainer()
  1962. if self._namespace:
  1963. parameters.set_inheritable({'env': {self._namespace}})
  1964. self._walk_directory_tree(directory_path,
  1965. self.templates_root,
  1966. parameters,
  1967. directory_tree=self.directory_tree,
  1968. package=package)
  1969. # Теперь когда дерево заполнено, можно выключить этот режим.
  1970. self.fill_trees = False
  1971. if self.for_package:
  1972. self.output.set_info('Processing packages from merge parameter...')
  1973. self.processed_packages.add(self.for_package)
  1974. self._merge_packages()
  1975. if self._handlers_queue:
  1976. self._execute_handlers()
  1977. if self.template_executor.execute_files:
  1978. self._run_exec_files()
  1979. self.template_executor.save_changes()
  1980. PackageCreator.save_all()
  1981. return self.template_executor.changed_files
  1982. def _run_template_from_base_directory(
  1983. self, template_names: List[str],
  1984. base_directory: str,
  1985. package: Optional[Package] = None,
  1986. directory_tree: Optional[dict] = None
  1987. ) -> NoReturn:
  1988. '''Метод для запуска шаблонов файлов находящихся в базовой директории.
  1989. '''
  1990. self.template_engine.change_directory(base_directory)
  1991. for template_name in template_names:
  1992. template_path = join_paths(base_directory, template_name)
  1993. target_path = join_paths(self.templates_root, template_name)
  1994. parameters = ParametersContainer()
  1995. if self._namespace:
  1996. parameters.set_inheritable({'env': {self._namespace}})
  1997. template_text = self._parse_template(parameters,
  1998. template_name,
  1999. FILE, base_directory)
  2000. if template_text is False:
  2001. continue
  2002. if not self._check_package_and_action(
  2003. parameters,
  2004. template_path,
  2005. directory_tree=directory_tree):
  2006. if parameters.handler:
  2007. # Если директория шаблонов является обработчиком --
  2008. # добавляем ее в словарь обработчиков и пропускаем ее.
  2009. self._handlers.update({parameters.handler:
  2010. (FILE, template_path)})
  2011. self._update_package_tree(parameters.package,
  2012. directory_tree)
  2013. continue
  2014. # Если есть параметр merge добавляем его содержимое в список
  2015. # пакетов для последующей обработки.
  2016. if self.for_package and parameters.merge:
  2017. for pkg in parameters.merge:
  2018. if pkg not in self.processed_packages:
  2019. self.packages_to_merge.add(pkg)
  2020. # Если присутствует параметр notify, в котором указаны хэндлеры для
  2021. # последующего выполнения -- добавляем их в очередь.
  2022. if parameters.notify:
  2023. for handler_id in parameters.notify:
  2024. if handler_id not in self._handlers_queue:
  2025. self._handlers_queue.append(handler_id)
  2026. # Корректируем путь к целевому файлу.
  2027. target_path = self._make_target_path(target_path,
  2028. template_name,
  2029. parameters)
  2030. if parameters.package:
  2031. template_package = Package(parameters.package,
  2032. chroot_path=self.cl_chroot_path,
  2033. autosave=self._pkg_autosave)
  2034. else:
  2035. template_package = package
  2036. if not parameters.run and not parameters.exec:
  2037. if not parameters.format:
  2038. parameters.set_parameter({'format': 'raw'})
  2039. if not parameters.append:
  2040. if parameters.format == "raw":
  2041. parameters.set_parameter({'append': 'replace'})
  2042. else:
  2043. parameters.set_parameter({'append': 'join'})
  2044. if parameters.append != 'skip':
  2045. # Выполняем действия, указанные в шаблоне.
  2046. self._execute_template(target_path, parameters,
  2047. FILE, template_path,
  2048. template_text=template_text,
  2049. package=template_package)
  2050. def _execute_handlers(self) -> NoReturn:
  2051. '''Метод для запуска обработчиков добавленных в очередь обработчиков
  2052. с помощью параметра notify.'''
  2053. self.output.set_info('Processing handlers...')
  2054. index = 0
  2055. while index < len(self._handlers_queue):
  2056. handler = self._handlers_queue[index]
  2057. index += 1
  2058. if handler in self._handlers:
  2059. handler_type, handler_path = self._handlers[handler]
  2060. else:
  2061. self.output.set_warning(
  2062. f"Handler '{handler}' is not found.")
  2063. continue
  2064. with self._start_handling(handler):
  2065. if handler_type is DIR:
  2066. self.directory_tree = {}
  2067. parameters = ParametersContainer()
  2068. if self._namespace:
  2069. parameters.set_inheritable({'env': {self._namespace}})
  2070. self._walk_directory_tree(
  2071. handler_path,
  2072. self.templates_root,
  2073. parameters,
  2074. directory_tree=self.directory_tree)
  2075. elif handler_type is FILE:
  2076. handler_dir, handler_name = os.path.split(handler_path)
  2077. self.template_engine.change_directory(handler_dir)
  2078. parameters = ParametersContainer()
  2079. if self._namespace:
  2080. parameters.set_inheritable({'env': {self._namespace}})
  2081. handler_text = self._parse_template(parameters,
  2082. handler_name,
  2083. FILE, handler_dir)
  2084. if handler_text is False:
  2085. continue
  2086. if parameters.notify:
  2087. for handler_id in parameters.notify:
  2088. if handler_id not in self._handlers_queue:
  2089. self._handlers_queue.append(handler_id)
  2090. # Корректируем путь к целевому файлу.
  2091. target_file_path = self._make_target_path(
  2092. self.templates_root,
  2093. handler_name,
  2094. parameters)
  2095. if not parameters.run and not parameters.exec:
  2096. if not parameters.format:
  2097. parameters.set_parameter({'format': 'raw'})
  2098. if not parameters.append:
  2099. if parameters.format == "raw":
  2100. parameters.set_parameter({'append': 'replace'})
  2101. else:
  2102. parameters.set_parameter({'append': 'join'})
  2103. # Выполняем действия, указанные в обработчике.
  2104. self._execute_template(target_file_path, parameters,
  2105. FILE, handler_path,
  2106. template_text=handler_text)
  2107. def _merge_packages(self) -> NoReturn:
  2108. '''Метод для выполнения шаблонов относящихся к пакетам, указанным во
  2109. всех встреченных значениях параметра merge.'''
  2110. not_merged_packages = []
  2111. self.for_package = None
  2112. while self.packages_to_merge:
  2113. if self.for_package is None:
  2114. # Если список найденных пакетов пройден, но пакеты для
  2115. # настройки еще есть -- заново проходимся по списку.
  2116. for package in self.packages_to_merge.difference(
  2117. self.packages_file_trees):
  2118. self.output.set_warning(
  2119. ("Warning: package '{0}' not found for"
  2120. " action{1} '{2}'.").format(
  2121. str(package),
  2122. 's' if len(self.action) > 1 else '',
  2123. ', '.join(self.action)))
  2124. self.packages_to_merge.remove(package)
  2125. if not self.packages_to_merge:
  2126. break
  2127. founded_packages = iter(self.packages_file_trees.keys())
  2128. self.for_package = next(founded_packages, None)
  2129. if (self.for_package is None
  2130. or self.for_package not in self.packages_to_merge):
  2131. continue
  2132. self.template_engine.for_package = self.for_package
  2133. if self.for_package not in self.packages_file_trees:
  2134. self.output.set_error(
  2135. "Error: package '{0}' not found for action{1} '{2}'.".
  2136. format(self.for_package,
  2137. 's' if len(self.action) > 1 else '',
  2138. ', '.join(self.action)))
  2139. not_merged_packages.append(self.for_package)
  2140. continue
  2141. package = Package(self.for_package,
  2142. chroot_path=self.cl_chroot_path,
  2143. autosave=self._pkg_autosave)
  2144. for directory_name in self.packages_file_trees[self.for_package]:
  2145. directory_tree = self.packages_file_trees[self.for_package].\
  2146. get_directory_tree(directory_name)
  2147. parameters = ParametersContainer()
  2148. if self._namespace:
  2149. parameters.set_inheritable({'env': {self._namespace}})
  2150. self._walk_directory_tree(directory_tree.base_directory,
  2151. self.templates_root,
  2152. parameters,
  2153. directory_tree=directory_tree,
  2154. package=package)
  2155. self.processed_packages.add(self.for_package)
  2156. self.packages_to_merge.remove(self.for_package)
  2157. if not_merged_packages:
  2158. self.output.set_error('Packages {} is not merged.'.
  2159. format(','.join(not_merged_packages)))
  2160. else:
  2161. self.output.set_success('All packages are merged.')
  2162. def _run_exec_files(self) -> NoReturn:
  2163. '''Метод для выполнения скриптов, полученных в результате обработки
  2164. шаблонов с параметром exec.'''
  2165. for exec_file_path, exec_info in\
  2166. self.template_executor.execute_files.items():
  2167. try:
  2168. output = self.template_executor.execute_file(
  2169. exec_info['interpreter'],
  2170. exec_file_path,
  2171. exec_info['cwd_path'])
  2172. if output['stdout'] is not None:
  2173. self.output.set_info("stdout from template: {}:\n{}\n".
  2174. format(exec_info['template_path'],
  2175. output['stdout']))
  2176. if output['stderr'] is not None:
  2177. self.output.set_error("stderr from template: {}:\n{}\n".
  2178. format(exec_info['template_path'],
  2179. output['stderr']))
  2180. except TemplateExecutorError as error:
  2181. self.output.set_error(str(error))
  2182. def _walk_directory_tree(self, current_directory_path: str,
  2183. current_target_path: str,
  2184. directory_parameters: ParametersContainer,
  2185. directory_tree: Union[dict, DirectoryTree] = {},
  2186. package: Optional[Package] = None) -> NoReturn:
  2187. '''Метод для рекурсивного обхода директорий с шаблонами, а также, при
  2188. необходимости, заполнения деревьев директорий шаблонов, с помощью
  2189. которых далее выполняются шаблоны пакетов из merge.'''
  2190. directory_name = os.path.basename(current_directory_path)
  2191. # Если включено заполнение дерева создаем пустой словарь для сбора
  2192. # содержимого текущей директории.
  2193. if self.fill_trees:
  2194. directory_tree[directory_name] = {}
  2195. self.template_engine.change_directory(current_directory_path)
  2196. template_directories, template_files = self._scan_directory(
  2197. current_directory_path)
  2198. template_files = sorted(template_files)
  2199. # обрабатываем в первую очередь шаблон директории.
  2200. if '.calculate_directory' in template_files:
  2201. template_files.remove('.calculate_directory')
  2202. template_text = self._parse_template(directory_parameters,
  2203. '.calculate_directory',
  2204. DIR, current_directory_path)
  2205. if template_text is False:
  2206. directory_tree = {}
  2207. return
  2208. # directory_parameters.print_parameters_for_debug()
  2209. # Корректируем путь к целевой директории.
  2210. current_target_path = self._make_target_path(current_target_path,
  2211. directory_name,
  2212. directory_parameters)
  2213. # Если нужно заполнять дерево директорий, отправляем в метод для
  2214. # проверки параметров package и action текущее дерево.
  2215. if (self._handling is None
  2216. and not self._check_package_and_action(
  2217. directory_parameters,
  2218. current_directory_path,
  2219. directory_tree=(directory_tree if
  2220. self.fill_trees else None))):
  2221. if directory_parameters.handler:
  2222. # Если директория шаблонов является обработчиком и параметр
  2223. # handler -- не унаследован, что свидетельствует о том,
  2224. # что директория не расположена внутри другой
  2225. # директории-обработчика добавляем ее в словарь
  2226. # обработчиков и пропускаем ее.
  2227. self._handlers.update({directory_parameters.handler:
  2228. (DIR, current_directory_path)})
  2229. if self.fill_trees:
  2230. directory_tree = {}
  2231. elif self.fill_trees:
  2232. # Если проверка не пройдена и включено заполнение дерева,
  2233. # то, используя нынешнее состояние дерева директорий,
  2234. # обновляем дерево пакета текущего шаблона директории.
  2235. self._update_package_tree(directory_parameters.package,
  2236. directory_tree[directory_name])
  2237. # Перед выходом из директории очищаем текущий уровень
  2238. # дерева.
  2239. directory_tree = {}
  2240. return
  2241. # Хэндлеры, вложенные в другие хэндлеры не разрешены.
  2242. if self._handling is not None and directory_parameters.handler:
  2243. if directory_parameters.handler != self._handling:
  2244. self.output.set_error("'handler' parameter is not"
  2245. " available in other handler"
  2246. f" '{self._handling}'")
  2247. return
  2248. # Если есть параметр merge -- сохраняем присутствующие в нем пакеты
  2249. # для последующей обработки.
  2250. if self.for_package and directory_parameters.merge:
  2251. for pkg in directory_parameters.merge:
  2252. if pkg not in self.processed_packages:
  2253. self.packages_to_merge.add(pkg)
  2254. # Если присутствует параметр notify, в котором указаны хэндлеры для
  2255. # последующего выполнения -- добавляем их в очередь.
  2256. if directory_parameters.notify:
  2257. for handler_id in directory_parameters.notify:
  2258. if handler_id not in self._handlers_queue:
  2259. self._handlers_queue.append(handler_id)
  2260. # Если присутствует параметр package -- проверяем, изменился ли он
  2261. # и был ли задан до этого. Если не был задан или изменился, меняем
  2262. # текущий пакет ветки шаблонов на этот.
  2263. if directory_parameters.package:
  2264. if (package is None or
  2265. package.package_name != directory_parameters.package):
  2266. package = Package(directory_parameters.package,
  2267. chroot_path=self.cl_chroot_path,
  2268. autosave=self._pkg_autosave)
  2269. else:
  2270. # Если .calculate_directory отсутствует -- создаем директорию,
  2271. # используя унаследованные параметры и имя самой директории.
  2272. # Для того чтобы директория была создана, просто добавляем параметр
  2273. # append = join.
  2274. template_text = ''
  2275. current_target_path = os.path.join(current_target_path,
  2276. directory_name)
  2277. # Для директорий по умолчанию append = join.
  2278. if not directory_parameters.append:
  2279. directory_parameters.set_parameter({'append': 'join'})
  2280. # Выполняем наложение шаблона.
  2281. current_target_path = self._execute_template(
  2282. current_target_path,
  2283. directory_parameters, DIR,
  2284. current_directory_path,
  2285. template_text=template_text,
  2286. package=package)
  2287. if not current_target_path:
  2288. directory_tree = {}
  2289. return
  2290. # Далее обрабатываем файлы шаблонов хранящихся в директории.
  2291. # Если в данный момент обходим дерево -- берем список файлов и
  2292. # директорий из него.
  2293. if not self.fill_trees and directory_tree:
  2294. template_directories, template_files =\
  2295. self._get_files_and_dirs_from_tree(template_files,
  2296. template_directories,
  2297. directory_tree)
  2298. # Просто псевдоним, чтобы меньше путаницы было далее.
  2299. template_parameters = directory_parameters
  2300. # Обрабатываем файлы шаблонов.
  2301. for template_name in template_files:
  2302. # Удаляем все параметры, которые не наследуются и используем
  2303. # полученный контейнер для сбора параметров файлов шаблонов.
  2304. template_parameters.remove_not_inheritable()
  2305. template_path = os.path.join(current_directory_path,
  2306. template_name)
  2307. # Обрабатываем файл шаблона шаблонизатором.
  2308. template_text = self._parse_template(template_parameters,
  2309. template_name,
  2310. FILE, current_directory_path)
  2311. if template_text is False:
  2312. continue
  2313. # template_parameters.print_parameters_for_debug()
  2314. # Если находимся на стадии заполнения дерева директорий --
  2315. # проверяем параметры package и action с заполнением дерева.
  2316. if (self._handling is None
  2317. and not self._check_package_and_action(
  2318. template_parameters,
  2319. template_path,
  2320. directory_tree=(directory_tree[directory_name] if
  2321. self.fill_trees else None))):
  2322. if template_parameters.handler:
  2323. # Если директория шаблонов является обработчиком --
  2324. # добавляем ее в словарь обработчиков и пропускаем ее.
  2325. self._handlers.update({template_parameters.handler:
  2326. (FILE, template_path)})
  2327. # * * * ПРИДУМАТЬ ОПТИМИЗАЦИЮ * * *
  2328. # TODO Потому что накладывать дерево каждый раз, когда
  2329. # обнаружены файлы какого-то пакета не рационально.
  2330. # Обновляем дерево директорий для данного пакета, если
  2331. # происходит его заполнение.
  2332. if self.fill_trees:
  2333. self._update_package_tree(template_parameters.package,
  2334. directory_tree[directory_name])
  2335. directory_tree[directory_name] = {}
  2336. continue
  2337. if self._handling is not None and template_parameters.handler:
  2338. if template_parameters.handler != self._handling:
  2339. self.output.set_error("'handler' parameter is not"
  2340. " available in other handler"
  2341. f" '{self._handling}'")
  2342. continue
  2343. # Если есть параметр merge добавляем его содержимое в список
  2344. # пакетов для последующей обработки.
  2345. if self.for_package and template_parameters.merge:
  2346. for pkg in template_parameters.merge:
  2347. if pkg not in self.processed_packages:
  2348. self.packages_to_merge.add(pkg)
  2349. # Если присутствует параметр notify, в котором указаны хэндлеры для
  2350. # последующего выполнения -- добавляем их в очередь.
  2351. if template_parameters.notify:
  2352. for handler_id in template_parameters.notify:
  2353. if handler_id not in self._handlers_queue:
  2354. self._handlers_queue.append(handler_id)
  2355. # Корректируем путь к целевому файлу.
  2356. target_file_path = self._make_target_path(current_target_path,
  2357. template_name,
  2358. template_parameters)
  2359. # Создаем объект пакета для файлов шаблонов этой директории.
  2360. template_package = package
  2361. if template_parameters.package:
  2362. if (template_package is None or
  2363. package.package_name != template_parameters.package):
  2364. template_package = Package(template_parameters.package,
  2365. chroot_path=self.cl_chroot_path,
  2366. autosave=self._pkg_autosave)
  2367. if not template_parameters.run and not template_parameters.exec:
  2368. if not template_parameters.format:
  2369. template_parameters.set_parameter({'format': 'raw'})
  2370. if not template_parameters.append:
  2371. if template_parameters.format == "raw":
  2372. template_parameters.set_parameter(
  2373. {'append': 'replace'})
  2374. else:
  2375. template_parameters.set_parameter({'append': 'join'})
  2376. if template_parameters.append != 'skip':
  2377. # Выполняем действия, указанные в шаблоне.
  2378. target_file_path = self._execute_template(
  2379. target_file_path,
  2380. template_parameters,
  2381. FILE, template_path,
  2382. template_text=template_text,
  2383. package=template_package)
  2384. if not target_file_path:
  2385. continue
  2386. # Проходимся далее по директориям.
  2387. for directory in template_directories:
  2388. if self.fill_trees:
  2389. self._walk_directory_tree(
  2390. directory, current_target_path,
  2391. directory_parameters.get_inheritables(),
  2392. directory_tree=directory_tree[directory_name],
  2393. package=package)
  2394. directory_tree[directory_name] = {}
  2395. else:
  2396. if isinstance(directory, DirectoryTree):
  2397. # Если директории взяты из дерева -- путь к директории
  2398. # соответствует корню каждой взятой ветви дерева.
  2399. directory_path = directory.base_directory
  2400. else:
  2401. directory_path = directory
  2402. self._walk_directory_tree(
  2403. directory_path, current_target_path,
  2404. directory_parameters.get_inheritables(),
  2405. package=package)
  2406. if self.fill_trees:
  2407. directory_tree = {}
  2408. return
  2409. def _scan_directory(self, directory_path: str
  2410. ) -> Tuple[List[str], List[str]]:
  2411. '''Метод для получения и фильтрования списка файлов и директорий,
  2412. содержащихся в директории шаблонов.'''
  2413. template_files = []
  2414. template_directories = []
  2415. entries = os.scandir(directory_path)
  2416. for node in entries:
  2417. if not self._check_file_name(node.name):
  2418. continue
  2419. if node.is_symlink():
  2420. self.output.set_warning(
  2421. 'symlink: {0} is ignored in the template directory: {1}'.
  2422. format(node.path, directory_path))
  2423. continue
  2424. elif node.is_dir():
  2425. template_directories.append(node.path)
  2426. elif node.is_file():
  2427. template_files.append(node.name)
  2428. return template_directories, template_files
  2429. def _check_file_name(self, filename: str) -> bool:
  2430. '''Метод для проверки соответствия имени файла содержимому переменной
  2431. main.cl_ignore_files.'''
  2432. for pattern in self.cl_ignore_files:
  2433. if fnmatch.fnmatch(filename, pattern):
  2434. return False
  2435. return True
  2436. def _get_files_and_dirs_from_tree(self, template_files: List[str],
  2437. template_directories: List[str],
  2438. directory_tree: DirectoryTree
  2439. ) -> Tuple[List[str],
  2440. List[DirectoryTree]]:
  2441. '''Метод для получения списков файлов и директорий из дерева
  2442. директорий.'''
  2443. tree_files = []
  2444. tree_directories = []
  2445. for template in directory_tree:
  2446. if template in template_files:
  2447. tree_files.append(template)
  2448. else:
  2449. next_directory_tree =\
  2450. directory_tree.get_directory_tree(template)
  2451. tree_directories.append(next_directory_tree)
  2452. return tree_directories, tree_files
  2453. def _make_target_path(self, target_path: str, template_name: str,
  2454. parameters: ParametersContainer) -> str:
  2455. '''Метод для получения пути к целевому файлу с учетом наличия
  2456. параметров name, path и append = skip.'''
  2457. # Если есть непустой параметр name -- меняем имя шаблона.
  2458. if parameters.name is not False and parameters.name != '':
  2459. template_name = parameters.name
  2460. # Если для шаблона задан путь -- меняем путь к директории шаблона.
  2461. if parameters.path:
  2462. target_path = join_paths(self.templates_root,
  2463. parameters.path)
  2464. # Если параметр append не равен skip -- добавляем имя шаблона к
  2465. # целевому пути.
  2466. if not parameters.append == 'skip':
  2467. target_path = os.path.join(target_path,
  2468. template_name)
  2469. return target_path
  2470. def _parse_template(self, parameters: ParametersContainer,
  2471. template_name: str,
  2472. template_type: int,
  2473. template_directory: str) -> Union[str, bool]:
  2474. '''Метод для разбора шаблонов, получения значений их параметров и их
  2475. текста после отработки шаблонизитора.'''
  2476. if template_type == DIR:
  2477. template_path = template_directory
  2478. else:
  2479. template_path = join_paths(template_directory, template_name)
  2480. if isinstance(self.datavars_module, (Datavars, NamespaceNode)):
  2481. self.datavars_module.main.cl['current_template'].set(template_path)
  2482. else:
  2483. self.datavars_module.main.cl['current_template'] = template_path
  2484. try:
  2485. self.template_engine.process_template(template_name,
  2486. template_type,
  2487. parameters=parameters)
  2488. return self.template_engine.template_text
  2489. except ConditionFailed as error:
  2490. self.output.set_warning('{0}. Template: {1}'.
  2491. format(str(error),
  2492. template_path))
  2493. return False
  2494. except Exception as error:
  2495. self.output.set_error('Template error: {0}. Template: {1}'.
  2496. format(str(error),
  2497. template_path))
  2498. return False
  2499. def _execute_template(self, target_path: str,
  2500. parameters: ParametersContainer,
  2501. template_type: int,
  2502. template_path: str,
  2503. template_text: str = '',
  2504. package: Optional[Package] = None
  2505. ) -> Union[bool, str]:
  2506. '''Метод для наложения шаблонов и обработки информации полученной после
  2507. наложения.'''
  2508. try:
  2509. output = self.template_executor.execute_template(
  2510. target_path,
  2511. parameters,
  2512. template_type,
  2513. template_path,
  2514. template_text=template_text,
  2515. target_package=package)
  2516. # Если во время выполнения шаблона был изменен целевой путь,
  2517. # например, из-за ссылки на директорию в source -- обновляем
  2518. # целевой путь.
  2519. if output['target_path'] is not None:
  2520. target_path = output['target_path']
  2521. if output['warnings']:
  2522. if not isinstance(output['warnings'], list):
  2523. output['warnings'] = [output['warnings']]
  2524. for warning in output['warnings']:
  2525. self.output.set_warning(f"{warning}."
  2526. f" Template: {template_path}")
  2527. # Если есть вывод от параметра run -- выводим как info.
  2528. if output['stdout'] is not None:
  2529. self.output.set_info("stdout from template: {}:\n{}\n".format(
  2530. template_path,
  2531. output['stdout']))
  2532. # Если есть ошибки от параметра run -- выводим их как error.
  2533. if output['stderr'] is not None:
  2534. self.output.set_error("stderr from template: {}:\n{}\n".
  2535. format(template_path,
  2536. output['stderr']))
  2537. # Если run выполнен с ошибками -- пропускаем директорию.
  2538. return False
  2539. except TemplateExecutorError as error:
  2540. self.output.set_error('Template execution error: {}. Template: {}'.
  2541. format(str(error),
  2542. template_path))
  2543. return False
  2544. except FormatError as error:
  2545. if error.executable:
  2546. msg = 'Format execution error: {}. Template: {}'
  2547. else:
  2548. msg = 'Format joining error: {}. Template: {}'
  2549. self.output.set_error(msg.format(str(error), template_path))
  2550. return False
  2551. if template_type == DIR:
  2552. self.output.set_success('Processed directory: {}'.
  2553. format(template_path))
  2554. else:
  2555. self.output.set_success('Processed template: {}'.
  2556. format(template_path))
  2557. return target_path
  2558. def _update_package_tree(self, package: Package,
  2559. current_level_tree: Union[None, dict]) -> None:
  2560. '''Метод для обновления деревьев директорий пакетов, необходимых для
  2561. обработки шаблонов пакетов из значения параметра merge.'''
  2562. # Если текущему уровню соответствует заглушка None или он содержит
  2563. # файлы, то есть не пустой -- тогда есть смысл обновлять.
  2564. if current_level_tree is None or current_level_tree:
  2565. if package in self.packages_file_trees:
  2566. # Если для данного пакета уже есть дерево --
  2567. # накладываем на него текущее.
  2568. self.packages_file_trees[package].update_tree(
  2569. copy.deepcopy(self.directory_tree)
  2570. )
  2571. else:
  2572. # Если для данного пакета еще нет дерева --
  2573. # копируем для него текущее.
  2574. directory_tree = DirectoryTree(self.base_directory)
  2575. directory_tree.update_tree(copy.deepcopy(self.directory_tree))
  2576. self.packages_file_trees[package] = directory_tree
  2577. def _check_package_and_action(self, parameters: ParametersContainer,
  2578. template_path: str,
  2579. directory_tree: Optional[dict] = None
  2580. ) -> bool:
  2581. '''Метод для проверки параметров action и package во время обработки
  2582. каталогов с шаблонами. Если среди аргументов указано также
  2583. дерево каталогов, то в случае несовпадения значений package для файла
  2584. или директории, им в дереве присваивается значение None.'''
  2585. if parameters.handler:
  2586. return False
  2587. if parameters.append != 'skip' or parameters.action:
  2588. if not parameters.action:
  2589. self.output.set_warning(
  2590. ("Action parameter is not set for template:"
  2591. " {0}").format(template_path))
  2592. return False
  2593. elif parameters.action.startswith('!'):
  2594. action_matching = (parameters.action[1:].strip() not in
  2595. self.action)
  2596. elif parameters.action not in self.action:
  2597. action_matching = False
  2598. else:
  2599. action_matching = True
  2600. if not action_matching:
  2601. self.output.set_warning(
  2602. ("Action parameter value '{0}' does not match its"
  2603. " current value{1} '{2}'. Template: {3}").format(
  2604. parameters.action,
  2605. 's' if len(self.action) > 1
  2606. else '',
  2607. ', '.join(self.action),
  2608. template_path))
  2609. return False
  2610. if self.for_package:
  2611. # if not parameters.package:
  2612. # if self.for_package is not NonePackage:
  2613. # self.output.set_warning(
  2614. # "'package' parameter is not defined. Template: {}".
  2615. # format(template_path))
  2616. if parameters.package != self.for_package:
  2617. if directory_tree is not None:
  2618. # Если есть дерево, которое собирается для текущих шаблонов
  2619. # и параметр package шаблона не совпадает с текущим,
  2620. # ставим заглушку, в качестве которой используется None.
  2621. template_name = os.path.basename(template_path)
  2622. directory_tree[template_name] = None
  2623. self.output.set_warning(
  2624. ("'package' parameter value '{0}' does not "
  2625. "match its current target package '{1}'. "
  2626. "Template: {2}").
  2627. format(parameters.package.atom,
  2628. self.for_package.atom,
  2629. template_path)
  2630. )
  2631. return False
  2632. return True
  2633. @contextmanager
  2634. def _start_handling(self, handler_id: str):
  2635. '''Метод для перевода обработчика каталогов в режим обработки
  2636. хэндлеров.'''
  2637. try:
  2638. self._handling = handler_id
  2639. yield self
  2640. finally:
  2641. self._handling = None
  2642. @contextmanager
  2643. def _set_current_package(self, package: Package):
  2644. '''Метод для указания в шаблонизаторе пакета, для которого в данный
  2645. момент проводим настройку. Пока не используется.'''
  2646. try:
  2647. last_package = self.template_engine.for_package
  2648. self.template_engine.for_package = package
  2649. yield self
  2650. finally:
  2651. self.template_engine.for_package = last_package